diff --git a/docs/adr/ADR0001V01R01-SplitConsolePlusAndPromptPlus.md b/docs/adr/ADR0001V01R01-SplitConsolePlusAndPromptPlus.md index 556979e..6816a7c 100644 --- a/docs/adr/ADR0001V01R01-SplitConsolePlusAndPromptPlus.md +++ b/docs/adr/ADR0001V01R01-SplitConsolePlusAndPromptPlus.md @@ -17,7 +17,7 @@ # ADR0001V01R01 -[ADR Index](README.md) • **Next:** [ADR0002 →](ADR0002V01R01-ImmutableCapabilityProfile.md) +[ADR Index](README.md) • **Next:** [ADR0002 →](ADR0002V01R02-ImmutableCapabilityProfile.md) --- diff --git a/docs/adr/ADR0002V01R01-ImmutableCapabilityProfile.md b/docs/adr/ADR0002V01R01-ImmutableCapabilityProfile.md deleted file mode 100644 index 5a69a63..0000000 --- a/docs/adr/ADR0002V01R01-ImmutableCapabilityProfile.md +++ /dev/null @@ -1,52 +0,0 @@ - - - -
- ConsolePlus - - # ADR0002V01R01 -
- -[← ADR0001](ADR0001V01R01-SplitConsolePlusAndPromptPlus.md) • [ADR Index](README.md) • **Next:** [ADR0003 →](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md) - ---- - - - -# ADR0002V01R01 — Immutable capability profile - -- **Status:** Accepted -- **Version:** V01 / Revision R01 -- **Created:** 2026-07-24 - -## Context - -Rendering decisions (ANSI, Unicode, color depth, interactivity) must be -consistent for the whole process lifetime. Re-detecting per call would be slow -and could produce inconsistent output if the environment appears to change. - -## Decision - -Detect the environment **once** and expose it as an **immutable snapshot**, -`ConsolePlus.Profile` typed as `IProfileReadOnly`. The profile describes -`ProfileName`, `IsTerminal`, `Interactive`, `SupportUnicode`, `SupportsAnsi`, -`ColorDepth`, captured culture, default/original colors and encodings. Nothing -in the public API mutates the profile after initialization. - -## Consequences - -- **Positive:** stable, self-consistent rendering; detection cost paid once; - read-only contract prevents accidental capability changes mid-run. -- **Negative / trade-off:** environment changes after startup (e.g. a resize of - color support) are not reflected; acceptable because such changes are rare and - restart-scoped. diff --git a/docs/adr/ADR0002V01R02-ImmutableCapabilityProfile.md b/docs/adr/ADR0002V01R02-ImmutableCapabilityProfile.md new file mode 100644 index 0000000..e5f7153 --- /dev/null +++ b/docs/adr/ADR0002V01R02-ImmutableCapabilityProfile.md @@ -0,0 +1,75 @@ + + + +
+ ConsolePlus + + # ADR0002V01R02 +
+ +[← ADR0001](ADR0001V01R01-SplitConsolePlusAndPromptPlus.md) • [ADR Index](README.md) • **Next:** [ADR0003 →](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md) + +--- + +# ADR0002V01R02 — Immutable capability profile + +- **Status:** Accepted +- **Version:** V01 / Revision R02 +- **Created:** 2026-07-24 +- **Changed:** 2026-07-25 (R02 — documented the real override path; removed a dead API that + contradicted this decision) + +## Context + +Rendering decisions (ANSI, Unicode, color depth, interactivity) must be +consistent for the whole process lifetime. Re-detecting per call would be slow +and could produce inconsistent output if the environment appears to change. + +## Decision + +Detect the environment **once** and expose it as an **immutable snapshot**, +`ConsolePlus.Profile` typed as `IProfileReadOnly`. The profile describes +`ProfileName`, `IsTerminal`, `Interactive`, `SupportUnicode`, `SupportsAnsi`, +`ColorDepth`, captured culture, default/original colors and encodings. Nothing +in the public API mutates the profile after initialization. + +**R02 addendum — the one supported override path, and a correction:** callers who need to +override auto-detected values do so via an optional `ConsoleProfile.json` file next to the +executable, read once in `EnvironmentUtil.CreateProfile` **before** detection and before any +downstream caching (e.g. `ConsoleWriter` caches `Profile.ColorDepth` in a `readonly` field at +construction; the ANSI-vs-NoAnsi adapter choice is also made once, at startup — see +[ADR0004V01R01](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md)). This is why a JSON-file override +works fully while a hypothetical post-init C# mutation API could not — by the time user code could +call it, the values it would change are already cached elsewhere. See +`docs/environment-detection.md` → "Override detection" for the full JSON schema. + +This addendum exists because a 2026-07-25 docs audit found a public interface, `IProfileSetup` +(a fluent builder with `SupportsAnsi(AutoDetect)`, `ColorDepth(ColorSystem)`, `Apply()`, etc.), that +had **zero implementers and zero callers anywhere in the repository** — `ProfileConsole` (the +concrete class backing `IProfileReadOnly`) never implemented it, and one of its members +(`TimeMsResizeChange`) was referenced nowhere else at all. It directly contradicted this ADR's +"nothing... mutates the profile after initialization" line. It has been **deleted outright** — the +JSON mechanism above already covered everything it promised, and does so correctly (unaffected by +the caching problem a post-init API would hit). No consumer depended on it (ConsolePlus is +pre-1.0/Beta, so this is not considered a breaking change). + +## Consequences + +- **Positive:** stable, self-consistent rendering; detection cost paid once; + read-only contract prevents accidental capability changes mid-run; the one real override path + (`ConsoleProfile.json`) is now documented where users would look for it, and no orphaned API + contradicts the immutability guarantee. +- **Negative / trade-off:** environment changes after startup (e.g. a resize of + color support) are not reflected; acceptable because such changes are rare and + restart-scoped. Overriding via `ConsoleProfile.json` requires a restart to take effect (the file + is only read at startup) — there is no live-reload. diff --git a/docs/adr/ADR0003V01R01-AutomaticInitializationOnFirstAccess.md b/docs/adr/ADR0003V01R01-AutomaticInitializationOnFirstAccess.md index a2acbe3..36d0811 100644 --- a/docs/adr/ADR0003V01R01-AutomaticInitializationOnFirstAccess.md +++ b/docs/adr/ADR0003V01R01-AutomaticInitializationOnFirstAccess.md @@ -17,7 +17,7 @@ # ADR0003V01R01 -[← ADR0002](ADR0002V01R01-ImmutableCapabilityProfile.md) • [ADR Index](README.md) • **Next:** [ADR0004 →](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md) +[← ADR0002](ADR0002V01R02-ImmutableCapabilityProfile.md) • [ADR Index](README.md) • **Next:** [ADR0004 →](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md) --- diff --git a/docs/adr/ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md b/docs/adr/ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md index b27f223..8b59fab 100644 --- a/docs/adr/ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md +++ b/docs/adr/ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md @@ -17,7 +17,7 @@ # ADR0004V01R01 -[← ADR0003](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md) • [ADR Index](README.md) • **Next:** [ADR0005 →](ADR0005V01R01-AnsiconLegacyWindowsSupport.md) +[← ADR0003](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md) • [ADR Index](README.md) • **Next:** [ADR0005 →](ADR0005V01R02-AnsiconLegacyWindowsSupport.md) --- diff --git a/docs/adr/ADR0005V01R01-AnsiconLegacyWindowsSupport.md b/docs/adr/ADR0005V01R01-AnsiconLegacyWindowsSupport.md deleted file mode 100644 index 40ba4ac..0000000 --- a/docs/adr/ADR0005V01R01-AnsiconLegacyWindowsSupport.md +++ /dev/null @@ -1,52 +0,0 @@ - - - -
- ConsolePlus - - # ADR0005V01R01 -
- -[← ADR0004](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md) • [ADR Index](README.md) • **Next:** [ADR0006 →](ADR0006V01R01-AutoDetectTriState.md) - ---- - - - -# ADR0005V01R01 — ANSICON injection for legacy Windows - -- **Status:** Accepted -- **Version:** V01 / Revision R01 -- **Created:** 2026-07-24 - -## Context - -Modern Windows (10+) supports ANSI via Virtual Terminal Processing, but -pre-Windows 10 consoles do not. Requiring users to install a third-party tool -would break the "just works" promise on legacy systems. - -## Decision - -Bundle [ANSICON](https://github.com/adoxa/ansicon) and **inject it -automatically** on legacy Windows consoles that lack native ANSI support. The -injection uses the `LdrLoadDll` approach via `CreateRemoteThread` for 64-bit -.NET AnyCPU processes, providing transparent ANSI escape-sequence support with -no manual installation or configuration. - -## Consequences - -- **Positive:** ANSI rendering works transparently on old Windows without user - action. -- **Negative / trade-off:** ships a native dependency and performs process - injection, which some environments/AV tooling may flag; scoped strictly to - legacy Windows where native ANSI is absent. diff --git a/docs/adr/ADR0005V01R02-AnsiconLegacyWindowsSupport.md b/docs/adr/ADR0005V01R02-AnsiconLegacyWindowsSupport.md new file mode 100644 index 0000000..c692a0c --- /dev/null +++ b/docs/adr/ADR0005V01R02-AnsiconLegacyWindowsSupport.md @@ -0,0 +1,61 @@ + + + +
+ ConsolePlus + + # ADR0005V01R02 +
+ +[← ADR0004](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md) • [ADR Index](README.md) • **Next:** [ADR0006 →](ADR0006V01R01-AutoDetectTriState.md) + +--- + +# ADR0005V01R02 — ANSICON launch (not injection) for legacy Windows + +- **Status:** Accepted +- **Version:** V01 / Revision R02 +- **Created:** 2026-07-24 +- **Changed:** 2026-07-25 (R02 — corrected the mechanism description; the original text described + DLL injection, but the real implementation never did that) + +## Context + +Modern Windows (10+) supports ANSI via Virtual Terminal Processing, but +pre-Windows 10 consoles do not. Requiring users to install a third-party tool +would break the "just works" promise on legacy systems. + +**R02 correction:** the original (R01) text of this ADR described the mechanism as DLL injection +(`LdrLoadDll` via `CreateRemoteThread`). A 2026-07-25 docs audit traced the real implementation in +`src/Core/LegacyAnsiBootstrapper.cs` and found no DLL injection or remote-thread APIs anywhere in +this codebase — the R01 description was inaccurate from the start (the same inaccurate claim had +also propagated into 3 hand-written docs, corrected in the same pass). + +## Decision + +Bundle [ANSICON](https://github.com/adoxa/ansicon) and **launch it +automatically** on legacy Windows consoles that lack native ANSI support. +`LegacyAnsiBootstrapper` runs the bundled `ansicon.exe` (matching the process +architecture, x86 or x64) via `Process.Start(..., "-p")` and waits for it to +exit — no DLL injection or remote-thread APIs are involved — providing +transparent ANSI escape-sequence support with no manual installation or +configuration. + +## Consequences + +- **Positive:** ANSI rendering works transparently on old Windows without user + action. +- **Negative / trade-off:** ships a native third-party executable per + architecture, which some environments/AV tooling may flag merely for being an + unfamiliar bundled binary; scoped strictly to legacy Windows where native + ANSI is absent. diff --git a/docs/adr/ADR0006V01R01-AutoDetectTriState.md b/docs/adr/ADR0006V01R01-AutoDetectTriState.md index 8b08d9d..d3f6b5b 100644 --- a/docs/adr/ADR0006V01R01-AutoDetectTriState.md +++ b/docs/adr/ADR0006V01R01-AutoDetectTriState.md @@ -17,7 +17,7 @@ # ADR0006V01R01 -[← ADR0005](ADR0005V01R01-AnsiconLegacyWindowsSupport.md) • [ADR Index](README.md) • **Next:** [ADR0007 →](ADR0007V01R01-GracefulDegradation.md) +[← ADR0005](ADR0005V01R02-AnsiconLegacyWindowsSupport.md) • [ADR Index](README.md) • **Next:** [ADR0007 →](ADR0007V01R01-GracefulDegradation.md) --- diff --git a/docs/adr/ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md b/docs/adr/ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md index 6a2747e..0aae93a 100644 --- a/docs/adr/ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md +++ b/docs/adr/ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md @@ -17,7 +17,7 @@ # ADR0014V01R01 -[← ADR0013](ADR0013V01R01-EmojiShortcodeModel.md) • [ADR Index](README.md) • **Next:** [ADR0015 →](ADR0015V01R01-GeneratedApiDocsOffLimits.md) +[← ADR0013](ADR0013V01R01-EmojiShortcodeModel.md) • [ADR Index](README.md) • **Next:** [ADR0015 →](ADR0015V01R02-GeneratedApiDocsOffLimits.md) --- diff --git a/docs/adr/ADR0015V01R01-GeneratedApiDocsOffLimits.md b/docs/adr/ADR0015V01R01-GeneratedApiDocsOffLimits.md deleted file mode 100644 index a42cbd7..0000000 --- a/docs/adr/ADR0015V01R01-GeneratedApiDocsOffLimits.md +++ /dev/null @@ -1,50 +0,0 @@ - - - -
- ConsolePlus - - # ADR0015V01R01 -
- -[← ADR0014](ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md) • [ADR Index](README.md) - ---- - - - -# ADR0015V01R01 — Generated API docs are off-limits for manual edits - -- **Status:** Accepted -- **Version:** V01 / Revision R01 -- **Created:** 2026-07-24 - -## Context - -The `docs/api/` folder is generated from XML doc comments by the documentation -tooling. Manual edits there are silently overwritten on the next build and give -a false impression of being authoritative. - -## Decision - -`docs/api/` is **generated output** and must never be edited by hand. All API -documentation changes are made in the source XML doc comments. Narrative and -conceptual documentation lives in the hand-written `docs/*.md` files, which are -the only Markdown docs that may be edited directly. See -`docs/api-documentation-guide.md`. - -## Consequences - -- **Positive:** single source of truth for API docs (the code); no lost edits. -- **Negative / trade-off:** correcting API text requires a code change and a - regeneration step rather than a quick Markdown edit. diff --git a/docs/adr/ADR0015V01R02-GeneratedApiDocsOffLimits.md b/docs/adr/ADR0015V01R02-GeneratedApiDocsOffLimits.md new file mode 100644 index 0000000..efdda6e --- /dev/null +++ b/docs/adr/ADR0015V01R02-GeneratedApiDocsOffLimits.md @@ -0,0 +1,74 @@ + + + +
+ ConsolePlus + + # ADR0015V01R02 +
+ +[← ADR0014](ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md) • [ADR Index](README.md) + +--- + +# ADR0015V01R02 — Generated API docs are off-limits for manual edits; regeneration is gated on `ReleaseDoc`, not `Release` + +- **Status:** Accepted +- **Version:** V01 / Revision R02 +- **Created:** 2026-07-24 +- **Changed:** 2026-07-25 (R02 — fixed the build-configuration gate that actually triggers + regeneration) + +## Context + +The `docs/api/` folder is generated from XML doc comments by the documentation +tooling. Manual edits there are silently overwritten on the next build and give +a false impression of being authoritative. + +`src/ConsolePlus.csproj` declares `Debug;Release;ReleaseDoc` — a +dedicated `ReleaseDoc` configuration exists precisely so that regenerating `docs/api/` (via +`DefaultDocumentation`) is a deliberate, separate step from `Release`, which is used to pack and +publish the NuGet package. This mirrors the sibling PromptPlus repository's already-correct setup. + +**R02 finding:** while regenerating `docs/api/` for an unrelated change, `dotnet build -c +ReleaseDoc` produced no `DefaultDocumentation` output at all. The `PackageReference`/ +`PropertyGroup`/`AddDocIconHeader` target conditions in the csproj all still checked +`'$(Configuration)' == 'Release'` — a leftover from before the `ReleaseDoc` configuration was +introduced into the `Configurations` list. `ReleaseDoc` existed as a buildable configuration name +but nothing was actually gated on it; `Release` builds were silently paying the +`DefaultDocumentation` cost that the dedicated configuration was supposed to isolate. + +## Decision + +`docs/api/` is **generated output** and must never be edited by hand. All API +documentation changes are made in the source XML doc comments. Narrative and +conceptual documentation lives in the hand-written `docs/*.md` files, which are +the only Markdown docs that may be edited directly. See +`docs/api-documentation-guide.md`. + +**R02 addendum:** changed all three conditions (`ItemGroup` `PackageReference`, `PropertyGroup` +with the `DefaultDocumentation*` properties, and the `AddDocIconHeader` target) from +`'$(Configuration)' == 'Release'` to `'$(Configuration)' == 'ReleaseDoc'`. `docs/api/` regeneration +now requires `dotnet build src/ConsolePlus.csproj -c ReleaseDoc -f net10.0`; `-c Release` no longer +touches `docs/api/` at all. `docs/api-documentation-guide.md` updated to match. + +## Consequences + +- **Positive:** single source of truth for API docs (the code); no lost edits. `Release` builds + (used for NuGet packing) no longer carry the `DefaultDocumentation` package/analysis cost; + regeneration is now an explicit, intentional step, matching the sibling PromptPlus repo. +- **Negative / trade-off:** correcting API text requires a code change and a + regeneration step rather than a quick Markdown edit. Anyone with a local habit of running + `-c Release` to pick up doc changes must switch to `-c ReleaseDoc`. No CI workflow depended on + the old behavior (verified: neither `ci.yml` nor `publish-nuget.yml` reference `docs/api` or + `DefaultDocumentation`), so this has no automation impact. diff --git a/docs/adr/README.md b/docs/adr/README.md index f5f2e0d..9513187 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -46,10 +46,10 @@ projects (ConsolePlus + PromptPlus). | ADR | Title | Version | Status | | --- | --- | --- | --- | | [ADR0001V01R01](ADR0001V01R01-SplitConsolePlusAndPromptPlus.md) | Split PromptPlus 5.x into two projects (ConsolePlus + PromptPlus) | V01 | Accepted | -| [ADR0002V01R01](ADR0002V01R01-ImmutableCapabilityProfile.md) | Immutable capability profile | V01 | Accepted | +| [ADR0002V01R02](ADR0002V01R02-ImmutableCapabilityProfile.md) | Immutable capability profile | V01 (R02) | Accepted | | [ADR0003V01R01](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md) | Automatic initialization on first access | V01 | Accepted | | [ADR0004V01R01](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md) | ANSI vs non-ANSI driver selection | V01 | Accepted | -| [ADR0005V01R01](ADR0005V01R01-AnsiconLegacyWindowsSupport.md) | ANSICON injection for legacy Windows | V01 | Accepted | +| [ADR0005V01R02](ADR0005V01R02-AnsiconLegacyWindowsSupport.md) | ANSICON launch (not injection) for legacy Windows | V01 (R02) | Accepted | | [ADR0006V01R01](ADR0006V01R01-AutoDetectTriState.md) | `AutoDetect` tri-state for capabilities | V01 | Accepted | | [ADR0007V01R01](ADR0007V01R01-GracefulDegradation.md) | Profile-driven graceful degradation | V01 | Accepted | | [ADR0008V01R01](ADR0008V01R01-ColorDepthModel.md) | Color depth model (`ColorSystem`) | V01 | Accepted | @@ -59,4 +59,4 @@ projects (ConsolePlus + PromptPlus). | [ADR0012V01R01](ADR0012V01R01-ShutdownStateRestoration.md) | Shutdown state restoration | V01 | Accepted | | [ADR0013V01R01](ADR0013V01R01-EmojiShortcodeModel.md) | Emoji shortcode model with fallback | V01 | Accepted | | [ADR0014V01R01](ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md) | Low-level ANSI and alternate-screen API | V01 | Accepted | -| [ADR0015V01R01](ADR0015V01R01-GeneratedApiDocsOffLimits.md) | Generated API docs are off-limits for manual edits | V01 | Accepted | +| [ADR0015V01R02](ADR0015V01R02-GeneratedApiDocsOffLimits.md) | Generated API docs are off-limits for manual edits; regeneration gated on `ReleaseDoc` | V01 (R02) | Accepted | diff --git a/docs/advanced-api.md b/docs/advanced-api.md index d744777..0c2409a 100644 --- a/docs/advanced-api.md +++ b/docs/advanced-api.md @@ -132,21 +132,24 @@ Emacs-style editing semantics (cursor movement, word operations, kill/yank, case in-memory string. Expose it directly when you are building a **custom input loop** and want the same editing behavior ConsolePlus uses. -Create one with three options: +Create one with four options: - `isreadlonly` — when `true`, the buffer rejects edits (useful for read-only display). - `caseOption` — a [`CaseOptions`](reading-input.md#case-transformation-and-length-limits) value (`Any`, `Uppercase`, or `Lowercase`) applied to typed characters. +- `enableEmacsKeys` — when `true`, Emacs-style key bindings (Ctrl+A/E/B/F/K/…, Alt+B/F/…) are + active; when `false`, they're no-ops and only plain character entry/navigation works. - `validate` — an optional `Func` that filters which characters are accepted. ```csharp using ConsolePlusLibrary; using System; -// A digit-only, upper-case buffer +// A digit-only, upper-case buffer with Emacs key bindings enabled var buffer = new EmacsConsoleBuffer( isreadlonly: false, caseOption: CaseOptions.Uppercase, + enableEmacsKeys: true, validate: char.IsLetterOrDigit); // Feed keys from your own read loop @@ -189,6 +192,8 @@ does — for example when aligning columns that may contain wide (CJK) character | `NormalizeNewLines()` | `string` | Converts all line endings to `Environment.NewLine` | | `SplitLines()` | `string[]` | Splits text into lines after normalizing line endings | | `GetDisplayLength()` | `int[]` | Display width of each line, counting wide characters as 2 columns | +| `TruncateToDisplayWidth(int maxWidth)` | `string` | Truncates text to fit within `maxWidth` display columns (not character count) | +| `GetRuneWidth()` (on `Rune`) | `int` | Display width, in columns, of a single `Rune` | ```csharp using ConsolePlusLibrary; @@ -280,7 +285,7 @@ ConsolePlus.RunAtomic(() => ConsolePlus.WriteLine("one uninterrupted unit")); Fragment[] parts = Fragment.FromText("[Red]hi[/]", ConsolePlus.CurrentStyle); // Custom Emacs input buffer -var buffer = new EmacsConsoleBuffer(false, CaseOptions.Any, char.IsLetterOrDigit); +var buffer = new EmacsConsoleBuffer(false, CaseOptions.Any, true, char.IsLetterOrDigit); // String measuring/normalizing (wide-char aware) int[] widths = "Hello\n世界".GetDisplayLength(); // [5, 4] diff --git a/docs/api-documentation-guide.md b/docs/api-documentation-guide.md index 821b0e7..dc15880 100644 --- a/docs/api-documentation-guide.md +++ b/docs/api-documentation-guide.md @@ -31,17 +31,18 @@ A configuração do DefaultDocumentation está no arquivo `src/ConsolePlus.cspro True - - + + - all - runtime; build; native; contentfiles; analyzers; buildtransitive + - + ..\docs\api - Assembly, Namespaces , Types , Members + Assembly, Namespaces, Classes, Interfaces, Events, Enums, Structs, Delegates + NameAndMd5Mix Public ConsolePlus https://raw.githubusercontent.com/FRACerqueira/ConsolePlus/main/icon.png @@ -50,40 +51,44 @@ A configuração do DefaultDocumentation está no arquivo `src/ConsolePlus.cspro ``` > ℹ️ Após a geração, uma task MSBuild (`PrependDocIconHeader`) adiciona o cabeçalho com o ícone -> (`DocIconUrl` / `DocIconWidth`) no topo de cada arquivo `.md` gerado em `docs/api`. +> (`DocIconUrl` / `DocIconWidth`) no topo de cada arquivo `.md` gerado em `docs/api`. Note que o +> `PrivateAssets`/`IncludeAssets` do `PackageReference` está comentado no projeto real — deixado como +> referência caso seja necessário reativar, não como configuração ativa. ### Opções de Configuração | Propriedade | Valor | Descrição | |-------------|-------|-----------| -| `Condition` | `Release` + `net10.0` | **Documentação gerada APENAS em builds Release do target net10.0** | +| `Condition` | `ReleaseDoc` + `net10.0` | **Documentação gerada APENAS em builds da configuração `ReleaseDoc` no target net10.0** (não em `Release`, que é usado para empacotar o NuGet sem regenerar docs) | | `DefaultDocumentationFolder` | `../docs/api` | Pasta de saída para os arquivos Markdown | -| `DefaultDocumentationGeneratedPages` | `Assembly, Namespaces, Types, Members` | Páginas geradas | +| `DefaultDocumentationGeneratedPages` | `Assembly, Namespaces, Classes, Interfaces, Events, Enums, Structs, Delegates` | Páginas geradas | +| `DefaultDocumentationFileNameFactory` | `NameAndMd5Mix` | Estratégia de nomeação dos arquivos gerados | | `DefaultDocumentationGeneratedAccessModifiers` | `Public` | Documenta apenas membros públicos | | `DefaultDocumentationAssemblyPageName` | `ConsolePlus` | Nome da página principal do assembly (`ConsolePlus.md`) | | `DocIconUrl` / `DocIconWidth` | icon.png / `120` | Cabeçalho com ícone adicionado a cada `.md` gerado | ## 🔄 Regenerando a Documentação -A documentação é regenerada automaticamente toda vez que você compila o projeto em **Release** -para o target **net10.0**: +A documentação é regenerada automaticamente toda vez que você compila o projeto na configuração +**ReleaseDoc** para o target **net10.0**: ### Via Visual Studio 1. Abra a solução no Visual Studio -2. Mude para configuração **Release** +2. Mude para configuração **ReleaseDoc** 3. Build → Build Solution (Ctrl+Shift+B) 4. Os arquivos Markdown serão atualizados em `docs/api/` -**Nota**: Em builds **Debug** (ou em targets diferentes de net10.0), a documentação **não** é -gerada, para acelerar o desenvolvimento. +**Nota**: Em builds **Debug** ou **Release** (ou em targets diferentes de net10.0), a documentação +**não** é gerada — `Release` é usado apenas para empacotar o NuGet, sem o custo do DefaultDocumentation. ### Via Linha de Comando ```bash -# Na raiz do repositório - APENAS Release gera documentação (target net10.0) -dotnet build src/ConsolePlus.csproj -c Release -f net10.0 +# Na raiz do repositório - APENAS ReleaseDoc gera documentação (target net10.0) +dotnet build src/ConsolePlus.csproj -c ReleaseDoc -f net10.0 -# Build Debug NÃO gera documentação +# Build Debug ou Release NÃO gera documentação dotnet build src/ConsolePlus.csproj -c Debug +dotnet build src/ConsolePlus.csproj -c Release ``` ### Verificando os arquivos gerados diff --git a/docs/api/ConsolePlusLibrary.md b/docs/api/ConsolePlusLibrary.md index e8bfa98..49e092c 100644 --- a/docs/api/ConsolePlusLibrary.md +++ b/docs/api/ConsolePlusLibrary.md @@ -40,7 +40,6 @@ | [IBanner](IBanner.md 'ConsolePlusLibrary\.IBanner') | Represents a banner that can be customized and displayed\. | | [IConsole](IConsole.md 'ConsolePlusLibrary\.IConsole') | Interface for abstracting console operations\. | | [IProfileReadOnly](IProfileReadOnly.md 'ConsolePlusLibrary\.IProfileReadOnly') | Defines a console profile describing capabilities, dimensions, colors and display behavior for the current console/terminal session\. | -| [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') | Interface for configuring console profiles, allowing for the setup of various console settings such as encoding, color, and interaction capabilities\. | | [IStringDash](IStringDash.md 'ConsolePlusLibrary\.IStringDash') | Represents a banner that can be customized and displayed\. | | [IWidgets](IWidgets.md 'ConsolePlusLibrary\.IWidgets') | Represents a collection of widgets that can be used in the console application\. | diff --git a/docs/api/IProfileSetup.md b/docs/api/IProfileSetup.md deleted file mode 100644 index 54038bd..0000000 --- a/docs/api/IProfileSetup.md +++ /dev/null @@ -1,194 +0,0 @@ -ConsolePlus - -#### [ConsolePlus\.net](ConsolePlus.md 'ConsolePlus') -### [ConsolePlusLibrary](ConsolePlusLibrary.md 'ConsolePlusLibrary') - -## IProfileSetup Interface - -Interface for configuring console profiles, allowing for the setup of various console settings such as encoding, color, and interaction capabilities\. - -```csharp -public interface IProfileSetup -``` -### Methods - - - -## IProfileSetup\.Apply\(\) Method - -Applies the configured profile settings to the console environment\. -This method should be called after all desired settings have been configured using the fluent interface methods\. - -```csharp -void Apply(); -``` - - - -## IProfileSetup\.ColorDepth\(ColorSystem\) Method - -Color depth \(capability\) of the console\. - -```csharp -ConsolePlusLibrary.IProfileSetup ColorDepth(ConsolePlusLibrary.ColorSystem value); -``` -#### Parameters - - - -`value` [ColorSystem](ColorSystem.md 'ConsolePlusLibrary\.ColorSystem') - -The color depth to set for the console\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.DefaultBackgroundColor\(Color\) Method - -Default background [Color](Color.md 'ConsolePlusLibrary\.Color') used when no explicit color is specified\. - -```csharp -ConsolePlusLibrary.IProfileSetup DefaultBackgroundColor(ConsolePlusLibrary.Color value); -``` -#### Parameters - - - -`value` [Color](Color.md 'ConsolePlusLibrary\.Color') - -The color to set as the default background color\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.DefaultInputEncoding\(Encoding\) Method - -Default input encoding of the console at the time the profile was created\. - -```csharp -ConsolePlusLibrary.IProfileSetup DefaultInputEncoding(System.Text.Encoding value); -``` -#### Parameters - - - -`value` [System\.Text\.Encoding](https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding 'System\.Text\.Encoding') - -The encoding to set as the default input encoding\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.DefaultOutputEncoding\(Encoding\) Method - -Default output encoding of the console at the time the profile was created\. - -```csharp -ConsolePlusLibrary.IProfileSetup DefaultOutputEncoding(System.Text.Encoding value); -``` -#### Parameters - - - -`value` [System\.Text\.Encoding](https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding 'System\.Text\.Encoding') - -The encoding to set as the default output encoding\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.Interactive\(bool\) Method - -Indicating whether or not the console supports interaction\. - -```csharp -ConsolePlusLibrary.IProfileSetup Interactive(bool value); -``` -#### Parameters - - - -`value` [System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') - -A boolean value indicating whether the console supports interaction\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.SupportsAnsi\(AutoDetect\) Method - -Gets a value indicating whether ANSI escape sequences are supported for styling/output\. - -```csharp -ConsolePlusLibrary.IProfileSetup SupportsAnsi(ConsolePlusLibrary.AutoDetect value); -``` -#### Parameters - - - -`value` [AutoDetect](AutoDetect.md 'ConsolePlusLibrary\.AutoDetect') - -An [AutoDetect](AutoDetect.md 'ConsolePlusLibrary\.AutoDetect') value indicating whether ANSI escape sequences are supported\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.SupportUnicode\(AutoDetect\) Method - -Indicates whether Unicode output is fully supported\. - -```csharp -ConsolePlusLibrary.IProfileSetup SupportUnicode(ConsolePlusLibrary.AutoDetect value); -``` -#### Parameters - - - -`value` [AutoDetect](AutoDetect.md 'ConsolePlusLibrary\.AutoDetect') - -An [AutoDetect](AutoDetect.md 'ConsolePlusLibrary\.AutoDetect') value indicating whether Unicode output is supported\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. - - - -## IProfileSetup\.TimeMsResizeChange\(int\) Method - -Time taken for the console to trigger a resize event after the console window has been resized\. -This can be used to optimize performance by reducing the frequency of resize events, especially in scenarios where rapid resizing may occur\. -Default value is 300 milliseconds, Range valid 100\-1000 milliseconds\. - -```csharp -ConsolePlusLibrary.IProfileSetup TimeMsResizeChange(int value); -``` -#### Parameters - - - -`value` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') - -The time in milliseconds to set for the resize event delay\. - -#### Returns -[IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') -The current instance of [IProfileSetup](IProfileSetup.md 'ConsolePlusLibrary\.IProfileSetup') to allow for fluent configuration\. \ No newline at end of file diff --git a/docs/colors.md b/docs/colors.md index 29e3b21..5fa70b0 100644 --- a/docs/colors.md +++ b/docs/colors.md @@ -59,7 +59,7 @@ using ConsolePlusLibrary; var c1 = new Color(30, 144, 255); // From a named CSS color (static properties) -var c2 = Color.DodgerBlue; +var c2 = Color.Dodgerblue; // From HEX (throws on invalid input) var c3 = Color.FromHex("#1E90FF"); @@ -101,8 +101,8 @@ All 148 CSS colors are available as static `Color` properties (case-insensitive | `Color.Blue` | ![](https://placehold.co/13x13/0000FF/0000FF.png) | `#0000FF` | | `Color.Teal` | ![](https://placehold.co/13x13/008080/008080.png) | `#008080` | | `Color.Gold` | ![](https://placehold.co/13x13/FFD700/FFD700.png) | `#FFD700` | -| `Color.HotPink` | ![](https://placehold.co/13x13/FF69B4/FF69B4.png) | `#FF69B4` | -| `Color.RebeccaPurple` | ![](https://placehold.co/13x13/663399/663399.png) | `#663399` | +| `Color.Hotpink` | ![](https://placehold.co/13x13/FF69B4/FF69B4.png) | `#FF69B4` | +| `Color.Rebeccapurple` | ![](https://placehold.co/13x13/663399/663399.png) | `#663399` | 👉 Jump to the [Full color reference](#full-color-reference-visual) for all 148 with swatches. @@ -194,7 +194,7 @@ double ratio = Color.White.GetContrast(Color.Navy); Color fg = Color.GetContrastForegroundColor(Color.Gold); // -> black // A quick "readable inverse" (black or white) based on brightness -Color inv = Color.SteelBlue.GetInvertedColor(); +Color inv = Color.Steelblue.GetInvertedColor(); // Nudge a foreground color only if it fails a minimum contrast ratio Color safe = Color.Yellow.AdjustForegroundColorForContrast( diff --git a/docs/emoji.md b/docs/emoji.md index b11a4b6..7bace3b 100644 --- a/docs/emoji.md +++ b/docs/emoji.md @@ -10,9 +10,9 @@ ConsolePlus ships with the full Unicode emoji catalog — **1,426** emoji across the **10 official Unicode groups**, plus a few legacy aliases. You can drop them into any output using `:shortcode:` -tokens (just like [markup](markup.md#emoji)), an optional `:group/shortcode:` form, reference them -as strongly-typed constants on the static `Emoji` class, or use the group-typed -`Emoji.Group.Name` form. +tokens (just like [markup](markup.md#emoji)), an optional `:group/shortcode:` form, or reference +them as strongly-typed constants — one public static class per Unicode group (`EmojiActivities`, +`EmojiSymbols`, `EmojiTravelAndPlaces`, …). ## Table of contents - [Compatibility considerations](#compatibility-considerations) @@ -95,7 +95,7 @@ When using emoji in your application: where your app is deployed 2. **Provide text fallbacks** — consider showing both emoji and text for critical information: ```csharp - ConsolePlus.WriteLine($"{Emoji.Warning} Warning: Low disk space"); + ConsolePlus.WriteLine($"{EmojiSymbols.Warning} Warning: Low disk space"); // Fallback: "⚠️ Warning: Low disk space" or just "Warning: Low disk space" ``` 3. **Use semantic color markup** — combine emoji with [markup colors](markup.md) for emphasis even @@ -118,15 +118,15 @@ ConsolePlus.WriteLine(":rocket: Launching..."); ConsolePlus.WriteLine("[Green]:check_mark_button: Success[/]"); // 2) Strongly-typed constants — great for building strings in code -ConsolePlus.WriteLine($"{Emoji.Rocket} Launching..."); -ConsolePlus.WriteLine($"{Emoji.Fire} {Emoji.ThumbsUp} {Emoji.RedHeart}"); +ConsolePlus.WriteLine($"{EmojiTravelAndPlaces.Rocket} Launching..."); +ConsolePlus.WriteLine($"{EmojiTravelAndPlaces.Fire} {EmojiPeopleAndBody.ThumbsUp} {EmojiSmileysAndEmotion.RedHeart}"); ``` Shortcodes are recognized by any method that parses markup — `Write`, `WriteLine`, `WriteFormat`, `Banner`, and `Dash`. See the [Markup guide](markup.md#emoji) for how emoji combine with color tags. -> Prefer discovering emoji by group? The same constants are also grouped as `Emoji.Group.Name` — -> see [Group-typed constants](#group-typed-constants). +> See [Strongly-typed constants](#strongly-typed-constants) below for the full list of per-group +> classes. --- @@ -183,71 +183,64 @@ accepted, so all of these are equivalent: ## Strongly-typed constants -Every emoji is a `public const string` on the static `Emoji` class, so you get IntelliSense, -compile-time safety, and no parsing overhead: +Every emoji is a `public const string`, organized into **one public static class per Unicode +group** — there is no single flat "all emoji" class; the type that backs all of them internally +(`Emoji`) is an implementation detail and isn't part of the public API. You get IntelliSense, +compile-time safety, and no parsing overhead by referencing the group class directly: ```csharp using ConsolePlusLibrary; -string status = $"{Emoji.CheckMarkButton} Done"; -string alert = $"{Emoji.Warning} Low disk space"; -string love = Emoji.RedHeart; +string status = $"{EmojiSymbols.CheckMarkButton} Done"; +string alert = $"{EmojiSymbols.Warning} Low disk space"; +string love = EmojiSmileysAndEmotion.RedHeart; ``` -The `Emoji` type is a single `public static partial class`; the constants are simply organized into -one source file per Unicode group for maintainability. Each constant carries an XML-doc summary and -its canonical `Lookup:` shortcode, so hovering a constant shows both the glyph and the shortcode. +Each constant carries an XML-doc summary and its canonical `Lookup:` shortcode, so hovering a +constant shows both the glyph and the shortcode. | Shortcode | Constant | Glyph | |-----------|----------|-------| -| `:rocket:` | `Emoji.Rocket` | 🚀 | -| `:fire:` | `Emoji.Fire` | 🔥 | -| `:thumbs_up:` | `Emoji.ThumbsUp` | 👍 | -| `:red_heart:` | `Emoji.RedHeart` | ❤️ | -| `:check_mark_button:` | `Emoji.CheckMarkButton` | ✅ | -| `:cross_mark:` | `Emoji.CrossMark` | ❌ | -| `:warning:` | `Emoji.Warning` | ⚠️ | +| `:rocket:` | `EmojiTravelAndPlaces.Rocket` | 🚀 | +| `:fire:` | `EmojiTravelAndPlaces.Fire` | 🔥 | +| `:thumbs_up:` | `EmojiPeopleAndBody.ThumbsUp` | 👍 | +| `:red_heart:` | `EmojiSmileysAndEmotion.RedHeart` | ❤️ | +| `:check_mark_button:` | `EmojiSymbols.CheckMarkButton` | ✅ | +| `:cross_mark:` | `EmojiSymbols.CrossMark` | ❌ | +| `:warning:` | `EmojiSymbols.Warning` | ⚠️ | --- ## Group-typed constants -For discoverability, every emoji is **also** exposed as a constant on a nested class named after its -Unicode group, so you can write `Emoji.Group.Name`. This is the compile-time mirror of the -[group-qualified shortcode](#group-qualified-shortcodes) form `:group/name:`: +The per-group class name mirrors the [group-qualified shortcode](#group-qualified-shortcodes) form +`:group/name:` — pick the class matching the group, then the constant matching the name: ```csharp using ConsolePlusLibrary; -// Group-typed constant ↔ group-qualified shortcode -ConsolePlus.WriteLine($"{Emoji.Activities.Balloon}"); // :activities/balloon: → 🎈 -ConsolePlus.WriteLine($"{Emoji.Symbols.CheckMarkButton}"); // :symbols/check_mark_button: → ✅ -ConsolePlus.WriteLine($"{Emoji.TravelAndPlaces.Rocket}"); // :travel-and-places/rocket: → 🚀 +// Group class constant ↔ group-qualified shortcode +ConsolePlus.WriteLine($"{EmojiActivities.Balloon}"); // :activities/balloon: → 🎈 +ConsolePlus.WriteLine($"{EmojiSymbols.CheckMarkButton}"); // :symbols/check_mark_button: → ✅ +ConsolePlus.WriteLine($"{EmojiTravelAndPlaces.Rocket}"); // :travel-and-places/rocket: → 🚀 ``` -Each nested constant is a **compile-time alias** of the flat constant (`Emoji.Activities.Balloon` -literally equals `Emoji.Balloon`), so both forms are interchangeable and there is a single source of -truth for every glyph. Type `Emoji.` then a group name to let IntelliSense narrow the catalog one -group at a time. +Type `Emoji` then the group name to let IntelliSense narrow the catalog to that class. -The ten group classes use the PascalCase names of the official Unicode groups: +The ten group classes use the PascalCase names of the official Unicode groups, prefixed with `Emoji`: | Group class | Example | |-------------|---------| -| `Emoji.SmileysAndEmotion` | `Emoji.SmileysAndEmotion.GrinningFace` 😀 | -| `Emoji.PeopleAndBody` | `Emoji.PeopleAndBody.ThumbsUp` 👍 | -| `Emoji.Component` | `Emoji.Component.LightSkinTone` 🏻 | -| `Emoji.AnimalsAndNature` | `Emoji.AnimalsAndNature.Dog` 🐶 | -| `Emoji.FoodAndDrink` | `Emoji.FoodAndDrink.Pizza` 🍕 | -| `Emoji.TravelAndPlaces` | `Emoji.TravelAndPlaces.Rocket` 🚀 | -| `Emoji.Activities` | `Emoji.Activities.Balloon` 🎈 | -| `Emoji.Objects` | `Emoji.Objects.Laptop` 💻 | -| `Emoji.Symbols` | `Emoji.Symbols.CheckMarkButton` ✅ | -| `Emoji.Flags` | `Emoji.Flags.ChequeredFlag` 🏁 | - -> The flat form (`Emoji.Rocket`) and the group-typed form (`Emoji.TravelAndPlaces.Rocket`) always -> resolve to the same value; pick whichever reads better in your code. Legacy aliases remain on the -> flat `Emoji` class only. +| `EmojiSmileysAndEmotion` | `EmojiSmileysAndEmotion.GrinningFace` 😀 | +| `EmojiPeopleAndBody` | `EmojiPeopleAndBody.ThumbsUp` 👍 | +| `EmojiComponent` | `EmojiComponent.LightSkinTone` 🏻 | +| `EmojiAnimalsAndNature` | `EmojiAnimalsAndNature.Dog` 🐶 | +| `EmojiFoodAndDrink` | `EmojiFoodAndDrink.Pizza` 🍕 | +| `EmojiTravelAndPlaces` | `EmojiTravelAndPlaces.Rocket` 🚀 | +| `EmojiActivities` | `EmojiActivities.Balloon` 🎈 | +| `EmojiObjects` | `EmojiObjects.Laptop` 💻 | +| `EmojiSymbols` | `EmojiSymbols.CheckMarkButton` ✅ | +| `EmojiFlags` | `EmojiFlags.ChequeredFlag` 🏁 | --- @@ -277,14 +270,20 @@ Each group has its own reference page listing every glyph, constant, and shortco ## Legacy aliases -A few older names are preserved as aliases so existing code keeps working. They resolve to the -current canonical emoji: +A few older names are preserved as **shortcode** aliases so existing text using them keeps +resolving to the current canonical emoji when parsed as markup: -| Alias | Shortcode | Resolves to | -|-------|-----------|-------------| -| `Emoji.HuggingFace` | `:hugging_face:` | `Emoji.SmilingFaceWithOpenHands` 🤗 | -| `Emoji.KnockedOutFace` | `:knocked_out_face:` | `Emoji.FaceWithCrossedOutEyes` 😵 | -| `Emoji.PoutingFace` | `:pouting_face:` | `Emoji.EnragedFace` 😡 | +| Alias shortcode | Resolves to | Glyph | +|-------|-----------|-------| +| `:hugging_face:` | `SmilingFaceWithOpenHands` | 🤗 | +| `:knocked_out_face:` | `FaceWithCrossedOutEyes` | 😵 | +| `:pouting_face:` | `EnragedFace` | 😡 | + +> These aliases only exist as shortcodes today — the constants backing them live on the internal +> `Emoji` type, not on any of the public per-group classes above, so there is currently no +> strongly-typed way to reference `HuggingFace`/`KnockedOutFace`/`PoutingFace` in code. Use the +> canonical name's public constant instead (e.g. `EmojiSmileysAndEmotion.SmilingFaceWithOpenHands`), +> or the `:hugging_face:` shortcode form. --- @@ -309,15 +308,13 @@ for detection details and how to override the profile if needed. // Group-qualified shortcode (prefix validated against the 10 groups) ":activities/balloon:" ":symbols/check_mark_button:" -// Strongly-typed constant -Emoji.Rocket Emoji.RedHeart Emoji.CheckMarkButton - -// Group-typed constant (Emoji.Group.Name) -Emoji.Activities.Balloon Emoji.Symbols.CheckMarkButton Emoji.TravelAndPlaces.Rocket +// Strongly-typed constant — one public class per group +EmojiTravelAndPlaces.Rocket EmojiSmileysAndEmotion.RedHeart EmojiSymbols.CheckMarkButton +EmojiActivities.Balloon // Combine with markup "[Green]:check_mark_button: Success[/]" -$"[Red]{Emoji.CrossMark} Failed[/]" +$"[Red]{EmojiSymbols.CrossMark} Failed[/]" ``` Jump to a group: [Smileys & Emotion](emoji/smileys-and-emotion.md) • diff --git a/docs/environment-detection.md b/docs/environment-detection.md index 816c4ab..c77bf6c 100644 --- a/docs/environment-detection.md +++ b/docs/environment-detection.md @@ -171,9 +171,10 @@ detects this and enables: - Text styling (bold, underline, etc.) - Screen buffer control -On **legacy Windows** (pre-Windows 10), ConsolePlus automatically injects -**[ANSICON](https://github.com/adoxa/ansicon)** (bundled) using `LdrLoadDll` via `CreateRemoteThread` -to provide transparent ANSI support. +On **legacy Windows** (pre-Windows 10), ConsolePlus automatically launches the bundled +**[ANSICON](https://github.com/adoxa/ansicon)** executable (`ansicon.exe -p`, matching the process +architecture) via `Process.Start` — no DLL injection involved — to provide transparent ANSI +support. ### Unicode support @@ -201,25 +202,40 @@ Colors are automatically down-sampled if the terminal doesn't support the reques ## Override detection -You can override the automatic detection if needed by modifying the profile after initialization: +`ConsolePlus.Profile` is a **read-only snapshot** — there is no supported way to mutate it in code +after initialization. To override auto-detection, drop a `ConsoleProfile.json` file next to your +application's executable (same folder as `AppContext.BaseDirectory`); ConsolePlus reads it once, +**before** any detection runs, so overridden values take full effect (unlike a hypothetical +post-init mutation, which would arrive too late — several detected values are already cached +elsewhere in the console pipeline by the time your own code could run): -```csharp -using ConsolePlusLibrary; -using ConsolePlusLibrary.Core; - -// Force disable ANSI even if detected -ConsolePlus.Profile.SupportsAnsi = AutoDetect.No; - -// Force specific color depth -ConsolePlus.Profile.ColorDepth = ColorSystem.FourBit; - -// Force Unicode support -ConsolePlus.Profile.SupportUnicode = AutoDetect.Yes; - -// Mark as interactive -ConsolePlus.Profile.Interactive = true; +```json +{ + "SupportsAnsi": "No", + "ColorDepth": "FourBit", + "SupportUnicode": "Yes", + "Interactive": "Detect", + "OriginalCulture": "Detect", + "DefaultInputEncoding": "Detect", + "DefaultOutputEncoding": "Detect", + "DefaultBackgroundColor": "Detect", + "DefaultForegroundColor": "Detect", + "ProfileName": "Detect", + "IsTerminal": "Detect" +} ``` +- `SupportsAnsi` / `SupportUnicode` — `"Yes"`, `"No"`, or `"Detect"` (keeps auto-detection). +- `ColorDepth` — `"NoColors"`, `"FourBit"`, `"Standard"`, or `"TrueColor"` (any other value keeps + auto-detection). +- `Interactive` / `IsTerminal` — `"true"`/`"false"`, or `"Detect"`. +- `DefaultBackgroundColor` / `DefaultForegroundColor` — a hex string (e.g. `"#1E90FF"`), or + `"Detect"`. +- `DefaultInputEncoding` / `DefaultOutputEncoding` — a .NET encoding name (e.g. `"utf-8"`), or + `"Detect"`. +- `OriginalCulture` / `ProfileName` — any string, or `"Detect"`. +- Only include the keys you actually want to override; omit or use `"Detect"` for the rest. + > ⚠️ **Warning:** Overriding detection should be done carefully, as incorrect settings may result > in garbled output or missing features. Use this only when you have specific knowledge about your > deployment environment that the automatic detection cannot infer. diff --git a/docs/getting-started.md b/docs/getting-started.md index 0c94c1a..bbd343b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -129,8 +129,8 @@ ConsolePlus initializes **automatically** the first time you touch the `ConsoleP 1. **Attempts to enable ANSI** on legacy Windows versions (using a bundled helper when needed). - On legacy Windows systems (pre-Windows 10) that lack native ANSI support, ConsolePlus automatically uses **[ANSICON](https://github.com/adoxa/ansicon)** (bundled with the library) - - The injection uses the `LdrLoadDll` approach via `CreateRemoteThread` for 64-bit .NET AnyCPU - processes + - It does this by launching the bundled `ansicon.exe` (matching the process architecture, + x86 or x64) via `Process.Start(..., "-p")` — no DLL injection involved - This provides transparent ANSI escape sequence support without requiring manual installation or configuration 2. **Captures the original console state** — culture, foreground/background colors, and input/output @@ -159,11 +159,12 @@ ConsolePlus registers process-exit handlers so your terminal is **left in a clea original colors, culture, and encodings are restored automatically, and streams are flushed. You can also register your own callback to run just before the process exits — for example, to -print a farewell message or persist state. The callback receives the console instance and a flag -indicating whether the exit was triggered by Ctrl+C: +print a farewell message or persist state. The callback receives the console instance, any +exception that caused the exit (`null` on a normal exit), and a flag indicating whether the exit +was triggered by Ctrl+C: ```csharp -ConsolePlus.ActionBeforeExist((console, ctrlCPressed) => +ConsolePlus.ActionBeforeExit((console, exception, ctrlCPressed) => { console.WriteLine(ctrlCPressed ? "[Red]Cancelled by user.[/]" diff --git a/docs/markup.md b/docs/markup.md index 6f99df1..4f49cba 100644 --- a/docs/markup.md +++ b/docs/markup.md @@ -186,8 +186,8 @@ You can mix explicit `[/]` closings with automatic ones: ConsolePlus.WriteLine("[Green]Start [Red]Middle[/] End"); // Multiple levels with partial explicit closing -ConsolePlus.WriteLine("[A]Level1 [B]Level2 [C]Level3[/] Back to B"); -// Level3 closed explicitly, Level2 and Level1 close automatically +ConsolePlus.WriteLine("[Red]Level1 [Green]Level2 [Blue]Level3[/] Back to Green"); +// Level3 (Blue) closed explicitly, Level2 (Green) and Level1 (Red) close automatically ``` > 💡 **Best practice:** Use explicit `[/]` when you need precise control over where colors change, @@ -230,15 +230,16 @@ ConsolePlus.WriteLine("[Red:Blue:Green]Invalid format"); // Output: "[Red:Blue:Green]Invalid format" (literal text, no colors) ConsolePlus.WriteLine("[NotAColor]Unknown color name"); -// Output: "[NotAColor]Unknown color name" (literal text, no colors) - -ConsolePlus.WriteLine("Text [/] with unexpected closing"); -// Output: "Text [/] with unexpected closing" (literal text, no colors) - -ConsolePlus.WriteLine("[RGB(999,999,999)]Out of range"); -// Output: "[RGB(999,999,999)]Out of range" (literal text, no colors) +// Output: "[NotAColor]Unknown color name" — only this unresolved tag becomes literal text; +// text after it keeps parsing normally under the surrounding/inherited style, if any. ``` +> ⚠️ Two cases that look like errors but are **not**: an unmatched `[/]` (no open tag left on the +> stack) is silently dropped rather than shown as literal text — `"Text [/] with no opener"` renders +> as `"Text with no opener"` (the tag just vanishes). And out-of-range RGB components +> (`[RGB(999,999,999)]`) do **not** raise an error or fall back to raw text — they're cast to `byte` +> unchecked, so `999` silently wraps to `231` and a valid (if unexpected) color is applied. + ### Why raw output? Rendering as raw text when errors occur has several benefits: @@ -253,24 +254,30 @@ Rendering as raw text when errors occur has several benefits: | Error Type | Example | Result | |------------|---------|--------| | **Invalid color format** | `[Red:Blue:Green]` | Raw text (too many colors) | -| **Unknown color name** | `[NotARealColor]` | Raw text (color doesn't exist) | -| **Malformed RGB** | `[RGB(256,300,500)]` | Raw text (values out of range) | +| **Unknown color name** | `[NotARealColor]` | The unresolved tag is shown as literal text; surrounding text is unaffected | | **Invalid HEX** | `[#GGGGGG]` | Raw text (invalid hex digits) | -| **Unbalanced brackets** | `[Red][Green]text[/]` | Raw text (mismatched tags) | | **Empty tags** | `[]text[/]` | Raw text (no color specified) | +> `[Red][Green]text[/]` is **not** an error — it's valid nested markup (`Green` closes explicitly, +> `Red` auto-closes at the end); see [Nesting and hierarchy](#nesting-and-hierarchy) above. +> Out-of-range RGB (`[RGB(256,300,500)]`) also does **not** trigger raw output — see the warning +> above this table's section. + ### Fault-tolerant features -Despite the raw output fallback, ConsolePlus markup is designed to be forgiving: +Despite the raw output fallback, ConsolePlus markup is designed to be forgiving in some cases — +but not as broadly as it may first appear: ```csharp // These work fine - parser is lenient ConsolePlus.WriteLine("[red]Case insensitive[/]"); // ✅ Works -ConsolePlus.WriteLine("[Red ]Extra space[/]"); // ✅ Works -ConsolePlus.WriteLine("[ Red ]Extra spaces[/]"); // ✅ Works ConsolePlus.WriteLine("[Red]Auto-close"); // ✅ Works (auto-closes) ConsolePlus.WriteLine("[Red on White]Background[/]"); // ✅ Works ConsolePlus.WriteLine("[Red:White]Shorter syntax[/]"); // ✅ Works + +// These do NOT work despite looking like reasonable whitespace tolerance +ConsolePlus.WriteLine("[Red ]Extra space[/]"); // ❌ Falls back to raw text +ConsolePlus.WriteLine("[ Red ]Extra spaces[/]"); // ❌ Tag rendered literally, not colored ``` ### Testing markup before use @@ -294,14 +301,18 @@ ConsolePlus.WriteLine($"User said: {safe}"); ### Best practices for robust markup 1. **Validate dynamic colors** — If building markup from variables, validate color values first -2. **Use constants** — Prefer the `Color` class constants over string literals +2. **Use constants** — Prefer the `Color` class constants over string literals, and interpolate + them with `.ToMarkup()`, not `.ToString()` — for a color with no exact CSS name (e.g. a custom + `new Color(r,g,b)`), `ToString()` returns `"#RRGGBB (RGB=r,g,b)"` (with a space and parentheses), + which breaks the tag when interpolated; `ToMarkup()` always returns a tag-safe form + (`"#RRGGBB"` or the CSS name) 3. **Test in development** — Run your output once to verify markup renders correctly 4. **Escape user input** — Always `.EscapeMarkup()` on any user-provided strings 5. **Keep it simple** — Complex nested markup is harder to debug; consider using `Style` objects instead ```csharp -// Good - using Color constants -ConsolePlus.WriteLine($"[{Color.Red}]Error[/]"); +// Good - using Color.ToMarkup(), not ToString() +ConsolePlus.WriteLine($"[{Color.Red.ToMarkup()}]Error[/]"); // Good - escaping user input string fileName = userInput.EscapeMarkup(); @@ -330,27 +341,26 @@ ConsolePlus.WriteLine("[Green]:check_mark_button: Success[/]"); ConsolePlus.WriteLine(":fire: :thumbs_up: :red_heart:"); ``` -You can also reference emoji as **strongly-typed constants** from the `Emoji` class: +You can also reference emoji as **strongly-typed constants**, grouped into one public static class +per Unicode emoji group (`EmojiActivities`, `EmojiSymbols`, `EmojiTravelAndPlaces`, …): ```csharp using ConsolePlusLibrary; -ConsolePlus.WriteLine($"{Emoji.Rocket} Launching..."); -ConsolePlus.WriteLine($"{Emoji.Fire} {Emoji.ThumbsUp} {Emoji.RedHeart}"); - -// Or discover them by group with Emoji.Group.Name -ConsolePlus.WriteLine($"{Emoji.TravelAndPlaces.Rocket} {Emoji.Symbols.CheckMarkButton}"); +ConsolePlus.WriteLine($"{EmojiTravelAndPlaces.Rocket} Launching..."); +ConsolePlus.WriteLine($"{EmojiTravelAndPlaces.Fire} {EmojiPeopleAndBody.ThumbsUp} {EmojiSmileysAndEmotion.RedHeart}"); +ConsolePlus.WriteLine($"{EmojiSymbols.CheckMarkButton} Success"); ``` | Shortcode | Constant | Glyph | |-----------|----------|-------| -| `:rocket:` | `Emoji.Rocket` | 🚀 | -| `:fire:` | `Emoji.Fire` | 🔥 | -| `:thumbs_up:` | `Emoji.ThumbsUp` | 👍 | -| `:red_heart:` | `Emoji.RedHeart` | ❤️ | -| `:check_mark_button:` | `Emoji.CheckMarkButton` | ✅ | -| `:cross_mark:` | `Emoji.CrossMark` | ❌ | -| `:warning:` | `Emoji.Warning` | ⚠️ | +| `:rocket:` | `EmojiTravelAndPlaces.Rocket` | 🚀 | +| `:fire:` | `EmojiTravelAndPlaces.Fire` | 🔥 | +| `:thumbs_up:` | `EmojiPeopleAndBody.ThumbsUp` | 👍 | +| `:red_heart:` | `EmojiSmileysAndEmotion.RedHeart` | ❤️ | +| `:check_mark_button:` | `EmojiSymbols.CheckMarkButton` | ✅ | +| `:cross_mark:` | `EmojiSymbols.CrossMark` | ❌ | +| `:warning:` | `EmojiSymbols.Warning` | ⚠️ | > ⚠️ **Important:** Emoji require Unicode support to display correctly. On terminals without Unicode > or emoji-capable fonts, emoji will appear as monochrome symbols, placeholder boxes, or be silently @@ -360,8 +370,9 @@ ConsolePlus.WriteLine($"{Emoji.TravelAndPlaces.Rocket} {Emoji.Symbols.CheckMarkB You can optionally prefix a shortcode with its Unicode group, for example `:activities/balloon:`. See [Emoji → Group-qualified shortcodes](emoji.md#group-qualified-shortcodes) -for the full list of groups and the complete catalog. The same grouping is available on the `Emoji` -class as [group-typed constants](emoji.md#group-typed-constants) (`Emoji.Activities.Balloon`). +for the full list of groups and the complete catalog. The same grouping is available as +[group-typed constants](emoji.md#group-typed-constants) — one public class per group +(`EmojiActivities.Balloon`, not a nested type). --- @@ -416,7 +427,7 @@ rather than the raw string length. value.EscapeMarkup() // Emoji -":rocket:" Emoji.Rocket Emoji.TravelAndPlaces.Rocket +":rocket:" EmojiTravelAndPlaces.Rocket // Helpers text.RemoveMarkup() text.LengthMarkup() text.EscapeMarkup() diff --git a/docs/profiles-and-capabilities.md b/docs/profiles-and-capabilities.md index 3f43d07..551edeb 100644 --- a/docs/profiles-and-capabilities.md +++ b/docs/profiles-and-capabilities.md @@ -35,8 +35,9 @@ The static initializer runs **once**, the first time you touch `ConsolePlus`, an - Modern Windows (Windows 10+) has native ANSI support via Virtual Terminal Processing - On legacy Windows systems (pre-Windows 10) that lack native ANSI support, ConsolePlus automatically uses **[ANSICON](https://github.com/adoxa/ansicon)** (bundled with the library) - - The ANSICON injection uses the `LdrLoadDll` approach via `CreateRemoteThread` for 64-bit - .NET AnyCPU processes, providing transparent ANSI escape sequence support + - It does this by launching the bundled `ansicon.exe` (matching the process architecture, + x86 or x64) via `Process.Start(..., "-p")` — no DLL injection involved, and it provides + transparent ANSI escape sequence support - This happens automatically during initialization without requiring manual installation or configuration 2. **Captures the original culture, colors, and input/output encodings** (so they can be restored on @@ -81,6 +82,10 @@ ConsolePlus.WriteLine($"ColorDepth : {p.ColorDepth}"); // ColorSystem | `DefaultInputEncoding` / `DefaultOutputEncoding` | `Encoding` | Default encodings | | `OriginalInputEncoding` / `OriginalOutputEncoding` | `Encoding` | Encodings captured at startup | +`IProfileReadOnly` is genuinely **read-only** — there is no fluent setter API on it or on +`ConsolePlus.Profile`. To override any of these values, use a `ConsoleProfile.json` file, read once +before detection runs; see [Environment Detection → Override detection](environment-detection.md#override-detection). + --- ## Capability quick-checks @@ -167,7 +172,7 @@ always restored to its original culture, colors, and encodings when your app end shutdown to run final logic: ```csharp -ConsolePlus.ActionBeforeExist((console, ctrlCPressed) => +ConsolePlus.ActionBeforeExit((console, exception, ctrlCPressed) => { console.WriteLine(ctrlCPressed ? "[Yellow]Cancelled by user (Ctrl+C).[/]" @@ -175,7 +180,8 @@ ConsolePlus.ActionBeforeExist((console, ctrlCPressed) => }); ``` -The callback receives the console instance and a `bool` indicating whether Ctrl+C triggered the exit. +The callback receives the console instance, any exception that caused the exit (`null` on a +normal exit), and a `bool` indicating whether Ctrl+C triggered the exit. --- @@ -222,7 +228,7 @@ bool outRedirected = ConsolePlus.IsOutputRedirected; bool inRedirected = ConsolePlus.IsInputRedirected; // Graceful shutdown hook -ConsolePlus.ActionBeforeExist((console, ctrlC) => console.WriteLine("Bye!")); +ConsolePlus.ActionBeforeExit((console, exception, ctrlC) => console.WriteLine("Bye!")); ``` --- diff --git a/docs/reading-input.md b/docs/reading-input.md index bd94602..829b37c 100644 --- a/docs/reading-input.md +++ b/docs/reading-input.md @@ -103,8 +103,9 @@ string ReadLineEmacs( ### Key bindings While editing, the following Emacs-style shortcuts are available. Most of them also have an -equivalent standard editing key. Case-changing, deletion, and clearing commands only take effect -when there is text in the buffer. +equivalent standard editing key. **Every shortcut below — including plain cursor movement, not just +editing/deletion/case-changing — only takes effect when there is text in the buffer** (an empty +buffer ignores `Home`/`End`/arrows/`Ctrl+A`/`Ctrl+E`/etc. entirely, not just deletion commands). **Cursor movement** @@ -211,6 +212,13 @@ string code = ConsolePlus.ReadLineEmacs( | `Uppercase` | Converts accepted characters to uppercase | | `Lowercase` | Converts accepted characters to lowercase | +> ⚠️ **Order matters when combining `caseOptions` with `acceptInput`.** The case transform runs +> **before** `acceptInput` sees the character — `acceptInput` receives the already-transformed +> character, not the one the user actually typed. A predicate that only accepts one case (e.g. +> `caseOptions: CaseOptions.Uppercase` combined with `acceptInput: char.IsLower`) will silently +> reject every character, since it never sees a lowercase char to accept. Write `acceptInput` +> predicates that are case-insensitive (or that expect the target case) when using `caseOptions`. + ### Async and cancellation The async variants accept a `CancellationToken` and return `null` if canceled: @@ -250,6 +258,11 @@ while (!ConsolePlus.KeyAvailable) var key = ConsolePlus.ReadKey(intercept: true); ``` +> ⚠️ `ReadKey`/`ReadKeyAsync` throw `InvalidOperationException` ("Console is not interactive.") +> when `ConsolePlus.Profile.Interactive` is `false` — for example, when input is redirected. Check +> `IsInputRedirected` (or `Profile.Interactive`) before calling them in code that might run with +> redirected/piped input. + --- ## Cheat sheet diff --git a/docs/styles.md b/docs/styles.md index fc4a122..b7dbc8e 100644 --- a/docs/styles.md +++ b/docs/styles.md @@ -58,7 +58,7 @@ Use them anywhere output is written: ```csharp ConsolePlus.WriteLine("Hello", new Style(Color.Black, Color.Gold)); -ConsolePlus.Banner("Title", new Style(Color.White, Color.DarkBlue), DashOptions.SingleBorder); +ConsolePlus.Banner("Title", new Style(Color.White, Color.Darkblue), DashOptions.SingleBorder); ``` --- @@ -152,7 +152,7 @@ void Log(string level, Color color, string message) ConsolePlus.WriteLine(message, ConsolePlus.CurrentStyle.Overflow(Overflow.Ellipsis)); } -Log("INFO", Color.SteelBlue, "Service started"); +Log("INFO", Color.Steelblue, "Service started"); Log("WARN", Color.Gold, "Cache miss rate is high"); Log("ERR ", Color.Firebrick, "Connection reset by peer"); ``` diff --git a/docs/widgets.md b/docs/widgets.md index 4fe3bc5..0e39f93 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -107,10 +107,12 @@ ConsolePlus.Dash( ## Banners -A **banner** draws large, attention-grabbing text. Without a FIGlet font it renders the text on a -single line (optionally bordered); with a font it renders multi-line ASCII-art letters. +A **banner** draws large, attention-grabbing text as multi-line [FIGlet](http://www.figlet.org/) +ASCII-art letters. Every overload renders FIGlet art, even without an explicit font argument — the +library embeds the **Standard** FIGlet font as the default, so there is no "plain single-line text" +banner mode. -### Simple banners +### Banner with the default (embedded) font ```csharp using ConsolePlusLibrary; @@ -128,10 +130,10 @@ ConsolePlus.Banner( DashOptions dashOptions = DashOptions.None); ``` -### FIGlet banners +### Banner with a custom font -Provide a [FIGlet](http://www.figlet.org/) font (a `.flf` file path or a `Stream`) to render -stylized letter art: +Provide a [FIGlet](http://www.figlet.org/) font (a `.flf` file path or a `Stream`) to render with a +different letter style than the embedded default: ```csharp // From a font file on disk @@ -152,9 +154,8 @@ Example (FIGlet "Standard" font): |_| |_|\___|_|_|\___/ ``` -> The library embeds the **Standard** FIGlet font, which is used for simple banners. For custom -> looks, supply any `.flf` font file or stream. Invalid/missing font paths throw an -> `ArgumentException`. +> The library embeds this **Standard** FIGlet font as the default. For a different look, supply any +> `.flf` font file or stream. Invalid/missing font paths throw an `ArgumentException`. --- diff --git a/samples/ConsolePlusFeaturesSamples/ConsolePlusFeaturesSamples.csproj b/samples/ConsolePlusFeaturesSamples/ConsolePlusFeaturesSamples.csproj index d97eff3..2fcea18 100644 --- a/samples/ConsolePlusFeaturesSamples/ConsolePlusFeaturesSamples.csproj +++ b/samples/ConsolePlusFeaturesSamples/ConsolePlusFeaturesSamples.csproj @@ -6,6 +6,7 @@ enable enable 1.0.0 + Debug;Release;ReleaseDoc diff --git a/src/ConsoleAbstractions/AnsiConsoleAdapter.cs b/src/ConsoleAbstractions/AnsiConsoleAdapter.cs index 433ea19..1f73b54 100644 --- a/src/ConsoleAbstractions/AnsiConsoleAdapter.cs +++ b/src/ConsoleAbstractions/AnsiConsoleAdapter.cs @@ -1107,15 +1107,15 @@ public bool HideCursor() { _writer.Ansi.HideCursor(); } - catch (PlatformNotSupportedException) + catch (Exception ex) when (ex is PlatformNotSupportedException or IOException) { - // Ignore if the platform doesn't support this operation - } - finally - { - _cursorVisible = false; + // Ignore if the platform doesn't support this operation, or there is no real + // console attached (e.g. headless/redirected process) — same "Safe" pattern + // used by EnvironmentUtil.GetSafeWidth/GetSafeHeight/GetSafeTopCursor/etc. + return false; } - return _cursorVisible; + _cursorVisible = false; + return true; }); } @@ -1129,15 +1129,15 @@ public bool ShowCursor() { _writer.Ansi.ShowCursor(); } - catch (PlatformNotSupportedException) + catch (Exception ex) when (ex is PlatformNotSupportedException or IOException) { - // Ignore if the platform doesn't support this operation - } - finally - { - _cursorVisible = true; + // Ignore if the platform doesn't support this operation, or there is no real + // console attached (e.g. headless/redirected process) — same "Safe" pattern + // used by EnvironmentUtil.GetSafeWidth/GetSafeHeight/GetSafeTopCursor/etc. + return false; } - return _cursorVisible; + _cursorVisible = true; + return true; }); } diff --git a/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs b/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs index 95bb68d..277a831 100644 --- a/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs +++ b/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs @@ -1098,11 +1098,15 @@ public bool HideCursor() { Console.CursorVisible = false; } - finally + catch (Exception ex) when (ex is PlatformNotSupportedException or IOException) { - _cursorVisible = false; + // Ignore if the platform doesn't support this operation, or there is no real + // console attached (e.g. headless/redirected process) — same "Safe" pattern + // used by EnvironmentUtil.GetSafeWidth/GetSafeHeight/GetSafeTopCursor/etc. + return false; } - return _cursorVisible; + _cursorVisible = false; + return true; }); } @@ -1116,15 +1120,15 @@ public bool ShowCursor() { Console.CursorVisible = true; } - catch (PlatformNotSupportedException) + catch (Exception ex) when (ex is PlatformNotSupportedException or IOException) { - // Ignore if the platform doesn't support this operation + // Ignore if the platform doesn't support this operation, or there is no real + // console attached (e.g. headless/redirected process) — same "Safe" pattern + // used by EnvironmentUtil.GetSafeWidth/GetSafeHeight/GetSafeTopCursor/etc. + return false; } - finally - { - _cursorVisible = true; - } - return _cursorVisible; + _cursorVisible = true; + return true; }); } diff --git a/src/ConsolePlus.Extends.cs b/src/ConsolePlus.Extends.cs index a159da6..8f93c6e 100644 --- a/src/ConsolePlus.Extends.cs +++ b/src/ConsolePlus.Extends.cs @@ -67,7 +67,6 @@ public static void ClearLine(this IConsole console, int? row = null, Style? styl { ConsolePlus.Envlock.Run(() => { - console.ClearLine(row, style); row ??= console.CursorTop; var currentstyle = console.CurrentStyle; var effectiveStyle = style ?? currentstyle; diff --git a/src/ConsolePlus.csproj b/src/ConsolePlus.csproj index cda97e5..1b129bd 100644 --- a/src/ConsolePlus.csproj +++ b/src/ConsolePlus.csproj @@ -11,6 +11,7 @@ latest-recommended $(DefaultItemExcludes);ExternalAnsiBundleLegacy\**;build\** + Debug;Release;ReleaseDoc @@ -93,7 +94,7 @@ - + @@ -107,7 +108,7 @@ - + ..\docs\api Assembly, Namespaces , Classes, Interfaces, Events, Enums, Structs, Delegates NameAndMd5Mix @@ -145,7 +146,7 @@ - + <_DocIconFolder>$([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(DefaultDocumentationFolder)')))) diff --git a/src/Shared/IProfileSetup.cs b/src/Shared/IProfileSetup.cs deleted file mode 100644 index d2aa8ec..0000000 --- a/src/Shared/IProfileSetup.cs +++ /dev/null @@ -1,81 +0,0 @@ -// *************************************************************************************** -// MIT LICENCE -// The maintenance and evolution is maintained by the ConsolePlus project under MIT license -// *************************************************************************************** - -using System.Text; -#pragma warning disable IDE0130 // Namespace does not match folder structure -namespace ConsolePlusLibrary -#pragma warning restore IDE0130 // Namespace does not match folder structure -{ - /// - /// Interface for configuring console profiles, allowing for the setup of various console settings such as encoding, color, and interaction capabilities. - /// - public interface IProfileSetup - { - /// - /// Default input encoding of the console at the time the profile was created. - /// - /// The encoding to set as the default input encoding. - /// The current instance of to allow for fluent configuration. - IProfileSetup DefaultInputEncoding(Encoding value); - - /// - /// Default output encoding of the console at the time the profile was created. - /// - /// The encoding to set as the default output encoding. - /// The current instance of to allow for fluent configuration. - IProfileSetup DefaultOutputEncoding (Encoding value); - - /// - /// Default background used when no explicit color is specified. - /// - /// The color to set as the default background color. - /// The current instance of to allow for fluent configuration. - IProfileSetup DefaultBackgroundColor(Color value); - - /// - ///Indicating whether or not the console supports interaction. - /// - /// A boolean value indicating whether the console supports interaction. - /// The current instance of to allow for fluent configuration. - IProfileSetup Interactive(bool value); - - /// - /// Indicates whether Unicode output is fully supported. - /// - /// An value indicating whether Unicode output is supported. - /// The current instance of to allow for fluent configuration. - IProfileSetup SupportUnicode(AutoDetect value); - - /// - /// Gets a value indicating whether ANSI escape sequences are supported for styling/output. - /// - /// An value indicating whether ANSI escape sequences are supported. - /// The current instance of to allow for fluent configuration. - IProfileSetup SupportsAnsi(AutoDetect value); - - /// - /// Color depth (capability) of the console. - /// - /// The color depth to set for the console. - /// The current instance of to allow for fluent configuration. - IProfileSetup ColorDepth(ColorSystem value); - - - /// - /// Time taken for the console to trigger a resize event after the console window has been resized. - /// This can be used to optimize performance by reducing the frequency of resize events, especially in scenarios where rapid resizing may occur. - /// Default value is 300 milliseconds, Range valid 100-1000 milliseconds. - /// - /// The time in milliseconds to set for the resize event delay. - /// The current instance of to allow for fluent configuration. - IProfileSetup TimeMsResizeChange(int value); - - /// - /// Applies the configured profile settings to the console environment. - /// This method should be called after all desired settings have been configured using the fluent interface methods. - /// - void Apply(); - } -} diff --git a/tests/ConsolePlus.Tests/ConsolePlus.Tests.csproj b/tests/ConsolePlus.Tests/ConsolePlus.Tests.csproj index e0e4df0..eb57b6d 100644 --- a/tests/ConsolePlus.Tests/ConsolePlus.Tests.csproj +++ b/tests/ConsolePlus.Tests/ConsolePlus.Tests.csproj @@ -5,6 +5,7 @@ enable latest false + Debug;Release;ReleaseDoc diff --git a/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs b/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs new file mode 100644 index 0000000..631e0aa --- /dev/null +++ b/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs @@ -0,0 +1,53 @@ +using ConsolePlusLibrary; +using ConsolePlusLibrary.Testing; +using FluentAssertions; +using Xunit; + +namespace ConsolePlus.Tests.Unit +{ + // ConsolePlusExtends.cs — extension methods on IConsole. Ground truth: ClearLine had a real + // bug (self-recursive call with no base case, StackOverflowException on any real use) found + // during a docs audit; this covers the fix. Touching any method here forces the static + // ConsolePlusLibrary.ConsolePlus singleton to initialize for real (Envlock.Run is a static + // member access) — that used to crash the whole test host with IOException ("The handle is + // invalid") from NoAnsiConsoleAdapter's ctor calling ShowCursor() against a console-less test + // process; fixed by making HideCursor/ShowCursor swallow IOException the same way + // EnvironmentUtil's other Safe* console helpers already do. + // + // Real singleton init also runs EnvironmentUtil.CreateProfile -> EnrichersCI(), which populates + // the process-wide static BaseClassCI._environmentVariables cache that ProfileExtensionsTests + // manages by hand via reflection. Since xUnit parallelizes different test classes by default, + // this class must share GlobalStateCollection with ProfileExtensionsTests or the two race on + // that cache (confirmed: intermittent CI failure on macOS, 2026-07-25 — ProfileExtensionsTests + // saw a stale/real-environment snapshot mid-reset because this class's singleton init ran + // concurrently and repopulated the shared cache). + [Collection(GlobalStateCollection.Name)] + public class ConsolePlusExtendsTests + { + [Fact] + public void ClearLine_blanks_the_current_row_without_recursing() + { + var vt = VirtualTerminal.Create(); + vt.SetCursorPosition(0, 0); + vt.Write("hello"); + + vt.ClearLine(); + + vt.TextAt(0, 0, 5).Trim().Should().BeEmpty(); + } + + [Fact] + public void ClearLine_targets_the_given_row_and_restores_the_cursor_style() + { + var vt = VirtualTerminal.Create(); + vt.SetCursorPosition(0, 2); + vt.Write("world"); + var styleBefore = vt.CurrentStyle; + + vt.ClearLine(row: 2); + + vt.TextAt(2, 0, 5).Trim().Should().BeEmpty(); + vt.CurrentStyle.Should().Be(styleBefore); + } + } +} diff --git a/tests/ConsolePlus.Tests/Unit/GlobalStateCollection.cs b/tests/ConsolePlus.Tests/Unit/GlobalStateCollection.cs new file mode 100644 index 0000000..0627da0 --- /dev/null +++ b/tests/ConsolePlus.Tests/Unit/GlobalStateCollection.cs @@ -0,0 +1,15 @@ +using Xunit; + +namespace ConsolePlus.Tests.Unit +{ + // Serializes test classes that touch process-wide static state that isn't otherwise isolated + // per-instance: BaseClassCI's environment-variable cache (ProfileExtensionsTests) and the real + // ConsolePlusLibrary.ConsolePlus singleton, whose static ctor populates that same cache + // (ConsolePlusExtendsTests). xUnit parallelizes different test classes by default; classes in + // this collection run sequentially relative to each other instead. + [CollectionDefinition(Name, DisableParallelization = true)] + public class GlobalStateCollection + { + public const string Name = "ConsolePlus global state (BaseClassCI env-var cache / ConsolePlus singleton)"; + } +} diff --git a/tests/ConsolePlus.Tests/Unit/ProfileExtensionsTests.cs b/tests/ConsolePlus.Tests/Unit/ProfileExtensionsTests.cs index 2341e69..ffd6048 100644 --- a/tests/ConsolePlus.Tests/Unit/ProfileExtensionsTests.cs +++ b/tests/ConsolePlus.Tests/Unit/ProfileExtensionsTests.cs @@ -20,6 +20,13 @@ namespace ConsolePlus.Tests.Unit // de teste sem resetar o cache manualmente — daqui o uso de reflection (mesmo padrão já usado // para métodos privados estáticos, ex. MaskEditControl.NormalizeStringMask) para zerar o campo // antes de cada cenário, sem alterar o código de produção. + // + // Precisa do GlobalStateCollection (DisableParallelization) porque ConsolePlusExtendsTests + // também toca esse mesmo cache indiretamente (via inicialização real do singleton + // ConsolePlusLibrary.ConsolePlus) — sem isolamento as duas classes competem pelo mesmo campo + // static entre threads paralelas do xUnit (achado real, flake intermitente em CI no macOS, + // 2026-07-25). + [Collection(GlobalStateCollection.Name)] public class ProfileExtensionsTests : IDisposable { // Todas as env vars conhecidas pelos 14 detectores, para isolar cada teste do ambiente real diff --git a/tests/_driver-src/AnsiScreenInterpreter.cs b/tests/_driver-src/AnsiScreenInterpreter.cs index 42970f4..c0728d2 100644 --- a/tests/_driver-src/AnsiScreenInterpreter.cs +++ b/tests/_driver-src/AnsiScreenInterpreter.cs @@ -62,12 +62,12 @@ private void Feed(char ch) return; case State.Esc: - if (ch == '[') { _state = State.Csi; _private = false; _params.Clear(); return; } + if (ch == '[') { _state = State.Csi; _private = false; _ = _params.Clear(); return; } throw new NotSupportedException($"ESC {ch} is not supported by the test interpreter."); case State.Csi: if (ch == '?' && _params.Length == 0) { _private = true; return; } - if ((ch >= '0' && ch <= '9') || ch == ';') { _params.Append(ch); return; } + if ((ch >= '0' && ch <= '9') || ch == ';') { _ = _params.Append(ch); return; } Dispatch(ch, _params.ToString()); _state = State.Normal; return; diff --git a/tests/_driver-src/VirtualScreen.cs b/tests/_driver-src/VirtualScreen.cs index fa5879d..cf564b4 100644 --- a/tests/_driver-src/VirtualScreen.cs +++ b/tests/_driver-src/VirtualScreen.cs @@ -187,7 +187,7 @@ public string TextAt(int row, int col, int len) var sb = new StringBuilder(); for (int c = col; c < col + len && c < Width; c++) { - sb.Append(_cells[row, c].Glyph.ToString()); + _ = sb.Append(_cells[row, c].Glyph.ToString()); } return sb.ToString(); } @@ -201,9 +201,9 @@ public string Snapshot() var line = new StringBuilder(); for (int c = 0; c < Width; c++) { - line.Append(_cells[r, c].Glyph.ToString()); + _ = line.Append(_cells[r, c].Glyph.ToString()); } - sb.AppendLine(line.ToString().TrimEnd()); + _ = sb.AppendLine(line.ToString().TrimEnd()); } return sb.ToString(); } diff --git a/tests/_driver-src/VirtualTerminal.cs b/tests/_driver-src/VirtualTerminal.cs index 5db6950..ba5769c 100644 --- a/tests/_driver-src/VirtualTerminal.cs +++ b/tests/_driver-src/VirtualTerminal.cs @@ -117,25 +117,25 @@ public void RaiseResize(int newWidth, int newHeight) public ConsoleColor ForegroundColor { get => Color.ToConsoleColor(_fg); - set { _fg = value; _writer.ApplyStyle(new Style(_fg, _bg)); } + set { _fg = value; _ = _writer.ApplyStyle(new Style(_fg, _bg)); } } public ConsoleColor BackgroundColor { get => Color.ToConsoleColor(_bg); - set { _bg = value; _writer.ApplyStyle(new Style(_fg, _bg)); } + set { _bg = value; _ = _writer.ApplyStyle(new Style(_fg, _bg)); } } public Color ForegroundRgbColor { get => _fg; - set { _fg = value; _writer.ApplyStyle(new Style(_fg, _bg)); } + set { _fg = value; _ = _writer.ApplyStyle(new Style(_fg, _bg)); } } public Color BackgroundRgbColor { get => _bg; - set { _bg = value; _writer.ApplyStyle(new Style(_fg, _bg)); } + set { _bg = value; _ = _writer.ApplyStyle(new Style(_fg, _bg)); } } public void ResetColor() @@ -195,7 +195,7 @@ public void Clear(Color? backgroundcolor = null) public bool CursorVisible { get => _cursorVisible; - set { if (value) { ShowCursor(); } else { HideCursor(); } } + set { if (value) { _ = ShowCursor(); } else { _ = HideCursor(); } } } public bool HideCursor() { _writer.Ansi.HideCursor(); _cursorVisible = false; return true; }