From eafc3a20cbd390040c3d71fb8b661754d7f09ca3 Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Fri, 24 Jul 2026 20:32:35 -0300 Subject: [PATCH 1/4] Add to project files for build configs Added the property to ConsolePlusFeaturesSamples.csproj, ConsolePlus.csproj, and ConsolePlus.Tests.csproj to specify Debug, Release, and ReleaseDoc build configurations. --- .../ConsolePlusFeaturesSamples/ConsolePlusFeaturesSamples.csproj | 1 + src/ConsolePlus.csproj | 1 + tests/ConsolePlus.Tests/ConsolePlus.Tests.csproj | 1 + 3 files changed, 3 insertions(+) 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/ConsolePlus.csproj b/src/ConsolePlus.csproj index cda97e5..6ad698c 100644 --- a/src/ConsolePlus.csproj +++ b/src/ConsolePlus.csproj @@ -11,6 +11,7 @@ latest-recommended $(DefaultItemExcludes);ExternalAnsiBundleLegacy\**;build\** + Debug;Release;ReleaseDoc 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 From ca048e78b4f861cb5187f966c9fa7e7736c67f4e Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Sat, 25 Jul 2026 14:58:31 -0300 Subject: [PATCH 2/4] Fix ClearLine recursion, cursor-visibility reporting, and driver drift; audit and correct docs Three real bugs found during a docs audit: - ConsolePlusExtends.ClearLine recursed into itself with no base case, causing a StackOverflowException on any call. - AnsiConsoleAdapter/NoAnsiConsoleAdapter's HideCursor/ShowCursor always returned a fixed value regardless of whether the native call succeeded, contradicting their own documented contract; NoAnsiConsoleAdapter's HideCursor was also missing a catch clause ShowCursor already had. - The two repos' physically-duplicated test driver sources had drifted (3 of 8 files); re-synced from PromptPlus's copy. Removed IProfileSetup, a public interface with zero implementers or callers that contradicted the profile-immutability decision; the real override path (ConsoleProfile.json, read before any caching) already covered everything it promised and is now documented. Fixed the ReleaseDoc build configuration, which had never actually been wired to the DefaultDocumentation generator (still gated on Release). Corrected the rest of the docs audit catalog: wrong method names/arity, missing constructor parameters, incorrect Color casing, a fictional Emoji API model, a mostly-wrong markup error-handling table, a fabricated ANSICON injection mechanism (also in ADR0005), and several smaller behavioral gaps across reading-input/markup/widgets/advanced-api. Consolidated ADR0005, and two new ADRs for the IProfileSetup removal and ReleaseDoc fix, as revisions of the existing ADR0005/ADR0002/ADR0015 records rather than new sequence numbers, per project policy. --- ...001V01R01-SplitConsolePlusAndPromptPlus.md | 2 +- ...DR0002V01R01-ImmutableCapabilityProfile.md | 52 ----- ...DR0002V01R02-ImmutableCapabilityProfile.md | 75 +++++++ ...01-AutomaticInitializationOnFirstAccess.md | 2 +- ...0004V01R01-AnsiVsNonAnsiDriverSelection.md | 2 +- ...R0005V01R01-AnsiconLegacyWindowsSupport.md | 52 ----- ...R0005V01R02-AnsiconLegacyWindowsSupport.md | 61 ++++++ docs/adr/ADR0006V01R01-AutoDetectTriState.md | 2 +- ...14V01R01-LowLevelAnsiAndAlternateScreen.md | 2 +- ...ADR0015V01R01-GeneratedApiDocsOffLimits.md | 50 ----- ...ADR0015V01R02-GeneratedApiDocsOffLimits.md | 74 +++++++ docs/adr/README.md | 6 +- docs/advanced-api.md | 11 +- docs/api-documentation-guide.md | 39 ++-- docs/api/ConsolePlusLibrary.md | 1 - docs/api/IProfileSetup.md | 194 ------------------ docs/colors.md | 8 +- docs/emoji.md | 121 ++++++----- docs/environment-detection.md | 54 +++-- docs/getting-started.md | 11 +- docs/markup.md | 79 ++++--- docs/profiles-and-capabilities.md | 16 +- docs/reading-input.md | 17 +- docs/styles.md | 4 +- docs/widgets.md | 19 +- src/ConsoleAbstractions/AnsiConsoleAdapter.cs | 16 +- .../NoAnsiConsoleAdapter.cs | 16 +- src/ConsolePlus.Extends.cs | 1 - src/ConsolePlus.csproj | 6 +- src/Shared/IProfileSetup.cs | 81 -------- tests/_driver-src/AnsiScreenInterpreter.cs | 4 +- tests/_driver-src/VirtualScreen.cs | 6 +- tests/_driver-src/VirtualTerminal.cs | 10 +- 33 files changed, 462 insertions(+), 632 deletions(-) delete mode 100644 docs/adr/ADR0002V01R01-ImmutableCapabilityProfile.md create mode 100644 docs/adr/ADR0002V01R02-ImmutableCapabilityProfile.md delete mode 100644 docs/adr/ADR0005V01R01-AnsiconLegacyWindowsSupport.md create mode 100644 docs/adr/ADR0005V01R02-AnsiconLegacyWindowsSupport.md delete mode 100644 docs/adr/ADR0015V01R01-GeneratedApiDocsOffLimits.md create mode 100644 docs/adr/ADR0015V01R02-GeneratedApiDocsOffLimits.md delete mode 100644 docs/api/IProfileSetup.md delete mode 100644 src/Shared/IProfileSetup.cs 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/src/ConsoleAbstractions/AnsiConsoleAdapter.cs b/src/ConsoleAbstractions/AnsiConsoleAdapter.cs index 433ea19..804911e 100644 --- a/src/ConsoleAbstractions/AnsiConsoleAdapter.cs +++ b/src/ConsoleAbstractions/AnsiConsoleAdapter.cs @@ -1110,12 +1110,10 @@ public bool HideCursor() catch (PlatformNotSupportedException) { // Ignore if the platform doesn't support this operation + return false; } - finally - { - _cursorVisible = false; - } - return _cursorVisible; + _cursorVisible = false; + return true; }); } @@ -1132,12 +1130,10 @@ public bool ShowCursor() catch (PlatformNotSupportedException) { // Ignore if the platform doesn't support this operation + return false; } - finally - { - _cursorVisible = true; - } - return _cursorVisible; + _cursorVisible = true; + return true; }); } diff --git a/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs b/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs index 95bb68d..a1acf8c 100644 --- a/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs +++ b/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs @@ -1098,11 +1098,13 @@ public bool HideCursor() { Console.CursorVisible = false; } - finally + catch (PlatformNotSupportedException) { - _cursorVisible = false; + // Ignore if the platform doesn't support this operation + return false; } - return _cursorVisible; + _cursorVisible = false; + return true; }); } @@ -1119,12 +1121,10 @@ public bool ShowCursor() catch (PlatformNotSupportedException) { // Ignore if the platform doesn't support this operation + 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 6ad698c..1b129bd 100644 --- a/src/ConsolePlus.csproj +++ b/src/ConsolePlus.csproj @@ -94,7 +94,7 @@ - + @@ -108,7 +108,7 @@ - + ..\docs\api Assembly, Namespaces , Classes, Interfaces, Events, Enums, Structs, Delegates NameAndMd5Mix @@ -146,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/_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; } From 687c9b495283d62a33053e71059b72128dbab416 Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Sat, 25 Jul 2026 15:13:02 -0300 Subject: [PATCH 3/4] Make HideCursor/ShowCursor tolerate a console-less environment Console.CursorVisible throws IOException ("The handle is invalid") when no real console is attached (headless processes, some test hosts) rather than PlatformNotSupportedException, so the earlier success/failure fix didn't cover it. Broadened the catch on both adapters to match the "Safe" pattern EnvironmentUtil already uses for the same class of failure. This was blocking ConsolePlusExtends.* from being unit-tested at all, since touching any of its methods forces the ConsolePlus singleton to construct a real console adapter. Restores the ClearLine regression test that previously crashed the whole run. --- src/ConsoleAbstractions/AnsiConsoleAdapter.cs | 12 +++-- .../NoAnsiConsoleAdapter.cs | 12 +++-- .../Unit/ConsolePlusExtendsTests.cs | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs diff --git a/src/ConsoleAbstractions/AnsiConsoleAdapter.cs b/src/ConsoleAbstractions/AnsiConsoleAdapter.cs index 804911e..1f73b54 100644 --- a/src/ConsoleAbstractions/AnsiConsoleAdapter.cs +++ b/src/ConsoleAbstractions/AnsiConsoleAdapter.cs @@ -1107,9 +1107,11 @@ 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 + // 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; } _cursorVisible = false; @@ -1127,9 +1129,11 @@ 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 + // 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; } _cursorVisible = true; diff --git a/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs b/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs index a1acf8c..277a831 100644 --- a/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs +++ b/src/ConsoleAbstractions/NoAnsiConsoleAdapter.cs @@ -1098,9 +1098,11 @@ public bool HideCursor() { Console.CursorVisible = false; } - 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; } _cursorVisible = false; @@ -1118,9 +1120,11 @@ 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; } _cursorVisible = true; diff --git a/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs b/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs new file mode 100644 index 0000000..55da243 --- /dev/null +++ b/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs @@ -0,0 +1,44 @@ +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. + 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); + } + } +} From 01a4ebf4266bcdd081b613680e651f4fc0b35e5b Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Sat, 25 Jul 2026 15:32:26 -0300 Subject: [PATCH 4/4] Fix a CI-detector test race exposed by the new console-singleton test ConsolePlusExtendsTests forces ConsolePlusLibrary.ConsolePlus's real static ctor to run, which populates BaseClassCI's process-wide environment-variable cache via EnrichersCI(). ProfileExtensionsTests manages that same cache by hand (reflection reset per test) to simulate CI providers in isolation, which only worked because nothing else touched it. With xUnit parallelizing different test classes by default, the two could race, intermittently stomping the simulated env-var snapshot with a real one (seen on a macOS CI run: TF_BUILD detection failed because ChangedColorDepth came back false). Added GlobalStateCollection (DisableParallelization) and put both classes in it, matching PromptPlus's SerializedGlobalStateCollection pattern for the same class of shared-static-state risk. --- .../Unit/ConsolePlusExtendsTests.cs | 9 +++++++++ .../Unit/GlobalStateCollection.cs | 15 +++++++++++++++ .../Unit/ProfileExtensionsTests.cs | 7 +++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/ConsolePlus.Tests/Unit/GlobalStateCollection.cs diff --git a/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs b/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs index 55da243..631e0aa 100644 --- a/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs +++ b/tests/ConsolePlus.Tests/Unit/ConsolePlusExtendsTests.cs @@ -13,6 +13,15 @@ namespace ConsolePlus.Tests.Unit // 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] 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