diff --git a/.agents/skills/pretty-console-expert/SKILL.md b/.agents/skills/pretty-console-expert/SKILL.md index 900c4f6..9d95512 100644 --- a/.agents/skills/pretty-console-expert/SKILL.md +++ b/.agents/skills/pretty-console-expert/SKILL.md @@ -21,11 +21,16 @@ using static System.Console; // optional 3. Choose APIs by intent. - Styled output: `Console.WriteInterpolated`, `Console.WriteLineInterpolated`. - Inputs/prompts: `Console.TryReadLine`, `Console.ReadLine`, `Console.Confirm`, `Console.RequestAnyInput`. -- Dynamic rendering: `Console.Overwrite`, `Console.ClearNextLines`, `Console.SkipLines`. +- Dynamic rendering and line control: `Console.Overwrite`, `Console.ClearNextLines`, `Console.SkipLines`, `Console.NewLine`. - Progress UI: `ProgressBar.Update`, `ProgressBar.Render`, `Spinner.RunAsync`. - Menus/tables: `Console.Selection`, `Console.MultiSelection`, `Console.TreeMenu`, `Console.Table`. - Low-level override only: use `Console.Write(...)` / `Console.WriteLine(...)` span+`ISpanFormattable` overloads only when you intentionally bypass the handler for a custom formatting pipeline. +4. Route output deliberately. +- Keep normal prompts, menus, tables, durable user-facing output, and machine-readable non-error output on `OutputPipe.Out` unless there is a specific reason not to. +- Use `OutputPipe.Error` for transient live UI and for actual errors/diagnostics/warnings so stdout stays pipe-friendly and error output remains distinguishable. +- Do not bounce a single interaction between `Out` and `Error` unless you intentionally want that split; mixed-pipe prompts and retry messages are usually awkward in consumer CLIs. + ## Handler Special Formats - Use `:duration` with `TimeSpan` to render compact elapsed time text from the handler: @@ -39,16 +44,26 @@ using static System.Console; // optional - Prefer interpolated-handler APIs over manually concatenated strings. - Avoid span/formattable `Write`/`WriteLine` overloads in normal app code; reserve them for rare advanced/manual formatting scenarios. +- If the intent is only to end the current line or emit a blank line, use `Console.NewLine(pipe)` instead of `WriteLineInterpolated($"")` or reset-only interpolations such as `$"{ConsoleColor.Default}"`. - Keep ANSI/decorations inside interpolation holes (for example, `$"{Markup.Bold}..."`) instead of literal escape codes inside string literals. -- Route transient UI (spinner/progress/overwrite loops) to `OutputPipe.Error` to keep stdout pipe-friendly. -- For very frequent concurrent status updates, prefer a single-reader bounded `Channel` that owns `Console.Overwrite(...)`; let workers `TryWrite` snapshots into a capacity-1 channel with `FullMode = DropWrite` so producers stay non-blocking and intermediate frames can be skipped. -- After the last overwrite/progress frame, clear the UI region once with `Console.ClearNextLines(totalLines, pipe)` or intentionally keep it with `Console.SkipLines`. +- Route transient UI (spinner/progress/overwrite loops) to `OutputPipe.Error` to keep stdout pipe-friendly, and use `OutputPipe.Error` for genuine errors/diagnostics. Keep ordinary non-error interaction flow on `OutputPipe.Out`. +- Spinner/progress/overwrite output is caller-owned after rendering completes. Explicitly remove it with `Console.ClearNextLines(totalLines, pipe)` or intentionally keep the region with `Console.SkipLines(totalLines)`. +- Only use the bounded `Channel` snapshot pattern when multiple producers must update the same live region at high frequency. For single-producer or modest-rate updates, keep the rendering loop simple. ## Practical Patterns -- For wizard-like flows, wrap `Console.Selection(...)` / `Console.MultiSelection(...)` in retrying `Console.Overwrite(...)` loops so each step reuses one screen region instead of scrolling. +- For wizard-like flows, wrap `Console.Selection(...)` / `Console.MultiSelection(...)` in retrying `Console.Overwrite(...)` loops so each step reuses one screen region instead of scrolling. Keep the whole prompt/retry exchange on `OutputPipe.Out` unless the message is genuinely diagnostic. - Prefer `Console.Overwrite(state, static ...)` for fixed-height live regions such as `status + progress`; it avoids closure captures and keeps the rendered surface explicit through `lines`. -- For dynamic spinner headers tied to concurrent work, keep the mutable step/progress state outside the spinner and read it with `Volatile.Read` / `Interlocked` inside the handler factory. +- For dynamic spinner/progress headers tied to concurrent work, keep the mutable step/progress state outside the renderer and read it with `Volatile.Read` / `Interlocked` inside the handler factory. +- If a live region should disappear after completion, pair the last render with an explicit `ClearNextLines(...)`. If it should remain visible as completed output, advance past it with `SkipLines(...)`. + +## Testing CLI Code + +- When a CLI already routes its behavior through callable command handlers or functions that use PrettyConsole directly, prefer in-process tests over spawning the whole app with `Process`. +- Inject `ConsoleContext.Out`, `ConsoleContext.Error`, and `ConsoleContext.In` with `StringWriter`/`StringReader`, invoke the same handler the CLI entrypoint uses internally, and assert on writers plus returned exit codes/results. +- Keep separate writers for `Out` and `Error` so pipe routing remains testable. +- Save and restore the original `ConsoleContext` streams in `try/finally` or a scoped helper. +- Reserve `Process` for true end-to-end coverage such as entrypoint wiring, shell integration, environment/current-directory behavior, published-binary checks, or argument parsing that is only exercised at the process boundary. ## API Guardrails (Current Surface) @@ -57,6 +72,7 @@ using static System.Console; // optional - Use `ProgressBar.Render(...)`, not `ProgressBar.WriteProgressBar(...)`. - Use `ConsoleContext`, not `PrettyConsoleExtensions`. - Use `ConsoleColor` helpers/tuples (for example `ConsoleColor.Red / ConsoleColor.White`), not removed `ColoredOutput`/`Color` types. +- Use `Console.NewLine(pipe)` when you only need a newline or blank line; do not use `WriteLineInterpolated` with empty/reset-only payloads just to move the cursor. - Use `Confirm(ReadOnlySpan trueValues, ref PrettyConsoleInterpolatedStringHandler handler, bool emptyIsTrue = true)` (boolean parameter is last). - Use handler factory overloads for dynamic spinner/progress headers: `(builder, out handler) => handler = builder.Build(OutputPipe.Error, $"...")`. @@ -66,6 +82,7 @@ using static System.Console; // optional ```csharp // Colored/status output Console.WriteLineInterpolated($"{ConsoleColor.Green / ConsoleColor.DefaultBackground}OK{ConsoleColor.Default}"); +Console.NewLine(); // Typed input if (!Console.TryReadLine(out int port, $"Port ({ConsoleColor.Cyan}5000{ConsoleColor.Default}): ")) @@ -78,6 +95,7 @@ bool deploy = Console.Confirm(["y", "yes", "deploy"], $"Deploy now? ", emptyIsTr var spinner = new Spinner(); await spinner.RunAsync(workTask, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"Syncing...")); +Console.ClearNextLines(1, OutputPipe.Error); // or Console.SkipLines(1) to keep the final row // Progress rendering var bar = new ProgressBar { ProgressColor = ConsoleColor.Green }; @@ -88,5 +106,6 @@ ProgressBar.Render(OutputPipe.Error, 65, ConsoleColor.Green); ## Reference File Read [references/v5-api-map.md](references/v5-api-map.md) when you need exact usage snippets, migration mapping from old APIs, or a compile-fix checklist. +Read [references/testing-with-consolecontext.md](references/testing-with-consolecontext.md) when the task involves testing a PrettyConsole-based CLI or command handler. If public API usage changes in the edited project, ask whether to update `README.md` and changelog/release-notes files. diff --git a/.agents/skills/pretty-console-expert/agents/openai.yaml b/.agents/skills/pretty-console-expert/agents/openai.yaml index 98b4e8b..c17fa88 100644 --- a/.agents/skills/pretty-console-expert/agents/openai.yaml +++ b/.agents/skills/pretty-console-expert/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "PrettyConsole Expert" short_description: "Use PrettyConsole APIs correctly for fast console UIs" - default_prompt: "Implement PrettyConsole features with current APIs, migration-safe names, and allocation-conscious patterns." + default_prompt: "Implement PrettyConsole features with current APIs, migration-safe names, allocation-conscious patterns, deliberate stdout/stderr routing for live UI and diagnostics, and explicit cleanup for live console UI." diff --git a/.agents/skills/pretty-console-expert/references/testing-with-consolecontext.md b/.agents/skills/pretty-console-expert/references/testing-with-consolecontext.md new file mode 100644 index 0000000..84a7944 --- /dev/null +++ b/.agents/skills/pretty-console-expert/references/testing-with-consolecontext.md @@ -0,0 +1,119 @@ +# Testing PrettyConsole CLIs With `ConsoleContext` + +Use this file when a task involves testing a CLI or command layer that already uses PrettyConsole internally. + +## When This Applies + +Prefer this approach when: + +- the CLI logic already lives in callable commands, handlers, or functions +- those commands write via PrettyConsole APIs such as `Console.WriteInterpolated(...)`, `Console.WriteLineInterpolated(...)`, `Console.NewLine(...)`, `Console.TryReadLine(...)`, or `Console.Confirm(...)` +- you want fast, deterministic tests for stdout/stderr routing, prompts, parsed input, and returned exit codes/results + +Do not default to `Process` in those cases. PrettyConsole already gives you an in-process seam through `ConsoleContext.Out`, `ConsoleContext.Error`, and `ConsoleContext.In`. + +## Default Testing Pattern + +1. Save the current `ConsoleContext.Out`, `ConsoleContext.Error`, and `ConsoleContext.In`. +2. Replace them with test doubles such as `StringWriter` and `StringReader`. +3. Invoke the same command handler or function the CLI entrypoint uses internally. +4. Assert on: + - returned exit code or result + - stdout text from `ConsoleContext.Out` + - stderr text from `ConsoleContext.Error` +5. Restore the original streams in `finally`. + +## Output Assertions + +- Use separate writers for `Out` and `Error`. Do not merge them unless the test intentionally treats both pipes as one stream. +- Assert on the selected pipe instead of parsing combined process output. +- If the test only cares about visible content, assert on the text written to the injected `StringWriter`. +- When testing pipe routing, assert that expected content landed on the correct writer and the other one stayed empty. + +Example: + +```csharp +var originalOut = ConsoleContext.Out; +var originalError = ConsoleContext.Error; +var stdout = new StringWriter(); +var stderr = new StringWriter(); + +try { + ConsoleContext.Out = stdout; + ConsoleContext.Error = stderr; + + int exitCode = await RunCommandAsync(options, cancellationToken); + + await Assert.That(exitCode).IsEqualTo(0); + await Assert.That(stdout.ToString()).Contains("Completed"); + await Assert.That(stderr.ToString()).IsEmpty(); +} finally { + ConsoleContext.Out = originalOut; + ConsoleContext.Error = originalError; +} +``` + +## Input Assertions + +- Feed stdin with `ConsoleContext.In = new StringReader("value" + Environment.NewLine)`. +- Call the real command/handler code directly. +- Assert on both the parsed result and the prompt written to the selected output pipe. + +Example: + +```csharp +var originalOut = ConsoleContext.Out; +var originalIn = ConsoleContext.In; +var stdout = new StringWriter(); + +try { + ConsoleContext.Out = stdout; + ConsoleContext.In = new StringReader("42" + Environment.NewLine); + + int value = ReadPortFromUser(); + + await Assert.That(value).IsEqualTo(42); + await Assert.That(stdout.ToString()).Contains("Port"); +} finally { + ConsoleContext.Out = originalOut; + ConsoleContext.In = originalIn; +} +``` + +## Suggested Shape For Consumer CLIs + +Prefer this structure when a consumer CLI is testable but currently shells out to itself: + +- thin entrypoint: parse args, compose dependencies, call a command handler/function +- command handler/function: returns an `int`, `Task`, result object, or domain value +- all console I/O stays inside PrettyConsole calls, so tests can intercept it through `ConsoleContext` + +This lets tests call the command handler directly instead of launching a child process and scraping console output. + +## When `Process` Is Still Appropriate + +Use `Process` only when the behavior truly exists at the process boundary, for example: + +- verifying entrypoint wiring or host bootstrapping +- testing the real argument parser if it only runs in `Main` +- checking environment variables, current directory, exit codes from the actual executable, or shell integration +- validating published-binary behavior or packaging/install flows + +Even then, keep the number of process-level tests small and focused. Most behavioral coverage should stay in-process. + +## Anti-Pattern To Avoid + +Avoid this default approach for PrettyConsole-based CLIs: + +- launch the full executable with `Process` +- pass arguments +- capture combined output +- scrape strings to infer stdout/stderr behavior that the code already exposes directly through `ConsoleContext` + +That style is slower, less precise, and makes it harder to verify separate pipes or inject interactive input. + +## Extra Notes + +- If the command uses both `Out` and `Error`, always keep two writers so routing bugs stay visible. +- If the command uses `Console.TryReadLine(...)`, `Console.ReadLine(...)`, or `Console.Confirm(...)`, inject `ConsoleContext.In` instead of trying to fake terminal input through a spawned process. +- If you need to verify transient UI behavior at a lower level, prefer calling the underlying render/update methods directly and asserting on the injected writers before reaching for process tests. diff --git a/.agents/skills/pretty-console-expert/references/v5-api-map.md b/.agents/skills/pretty-console-expert/references/v5-api-map.md index 7d05a02..dc71729 100644 --- a/.agents/skills/pretty-console-expert/references/v5-api-map.md +++ b/.agents/skills/pretty-console-expert/references/v5-api-map.md @@ -38,6 +38,7 @@ PrettyConsole methods are extension members on `System.Console`. - `Console.Overwrite(...)` - `Console.ClearNextLines(...)` - `Console.SkipLines(...)` + - `Console.NewLine(...)` - Progress: - `ProgressBar.Update(...)` - `ProgressBar.Render(...)` @@ -48,6 +49,12 @@ PrettyConsole methods are extension members on `System.Console`. - `Console.TreeMenu(...)` - `Console.Table(...)` +### Output routing + +- Keep prompts, menus, tables, final user-facing output, and machine-readable non-error output on `OutputPipe.Out` unless you intentionally need a different split. +- Use `OutputPipe.Error` for transient live UI and for actual errors/diagnostics/warnings so stdout remains pipe-friendly and error output stays distinct. +- Avoid mixing a single interactive exchange across `Out` and `Error` unless the split is intentional. + ### Interpolated-handler special formats - `TimeSpan` with `:duration`: @@ -74,6 +81,12 @@ Use these only when intentionally bypassing the interpolated handler for a custo - `Console.Write(ReadOnlySpan ...)` - `Console.WriteLine(...)` +### New lines and blank lines + +- Use `Console.NewLine(pipe)` when the intent is only to end the current line or emit a blank line. +- Do not use `Console.WriteLineInterpolated($"")` or payloads like `$"{ConsoleColor.Default}"` just to force a newline. +- Use `WriteLineInterpolated(...)` when you are actually writing content and also want the trailing newline. + ## 4. Old -> New Migration Table - `IndeterminateProgressBar` -> `Spinner` @@ -88,7 +101,8 @@ Use these only when intentionally bypassing the interpolated handler for a custo ```csharp Console.WriteInterpolated($"[{ConsoleColor.Cyan}info{ConsoleColor.Default}] {message}"); -Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Yellow}warn{ConsoleColor.Default}"); +Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Red}error{ConsoleColor.Default}: {message}"); +Console.NewLine(); ``` ### Typed input @@ -167,9 +181,14 @@ Console.Overwrite(() => { Console.ClearNextLines(2, OutputPipe.Error); ``` +`Spinner.RunAsync(...)`, `ProgressBar.Update(...)`, and overwrite-based regions do not clean up the final area for you. Choose one of these explicitly after the last frame: + +- `Console.ClearNextLines(totalLines, pipe)` to remove the live UI +- `Console.SkipLines(totalLines)` to keep the final rendered rows and continue below them + ### High-frequency concurrent status updates -Use one reader task to own all `Console.Overwrite(...)` calls and let concurrent workers publish snapshots through a bounded channel: +Use one reader task to own all `Console.Overwrite(...)` calls and let concurrent workers publish snapshots through a bounded channel only when multiple producers need to update the same live region at high frequency: ```csharp using System.Threading.Channels; @@ -200,10 +219,14 @@ Why this works: - capacity `1` + `DropWrite` avoids backpressure on workers during high-frequency updates - this pattern is best when dropped intermediate states are acceptable and only recent snapshots matter +For single-producer or modest-rate updates, prefer a simpler render loop without the channel. + ## 6. Performance Checklist - Prefer interpolated handlers over string concatenation. - Treat span/formattable `Write`/`WriteLine` overloads as advanced escape hatches, not default app-level APIs. +- Use `Console.NewLine(pipe)` for bare line breaks instead of empty/reset-only `WriteLineInterpolated(...)` calls. - Keep ANSI/decorations in interpolation holes, not raw literal spans. -- Use `OutputPipe.Error` for transient rendering. +- Use `OutputPipe.Error` for transient rendering and genuine errors/diagnostics, but keep ordinary non-error interaction flow on `OutputPipe.Out`. +- Clean up live UI explicitly after the last frame with `ClearNextLines(...)` or keep it intentionally with `SkipLines(...)`. - Avoid introducing wrapper abstractions when direct PrettyConsole APIs already solve the task. diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 2c18bd3..9054ddb 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -26,7 +26,7 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 5.4.1 + 5.4.2 enable MIT True @@ -43,6 +43,8 @@ + - Improve perf of ReadOnlySpan based overloads of Write and WriteLine. + - Skill improvements diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 29e6d9f..cb689e0 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -114,7 +114,7 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, /// /// The output pipe to use public static void Write(ReadOnlySpan span, OutputPipe pipe) { - Write(span, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground); + ConsoleContext.GetPipeTarget(pipe).Write(span); } /// @@ -136,7 +136,7 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor /// background color public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { Console.SetColors(foreground, background); - ConsoleContext.GetPipeTarget(pipe).Write(span); + Write(span, pipe); Console.ResetColor(); } } diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index db491b2..5feefba 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -100,7 +100,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// /// The output pipe to use public static void WriteLine(ReadOnlySpan span, OutputPipe pipe) { - Console.WriteLine(span, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground); + ConsoleContext.GetPipeTarget(pipe).WriteLine(span); } /// @@ -121,8 +121,9 @@ public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleCo /// foreground color /// background color public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { - Console.Write(span, pipe, foreground, background); - Console.NewLine(pipe); + Console.SetColors(foreground, background); + WriteLine(span, pipe); + Console.ResetColor(); } } } \ No newline at end of file diff --git a/Versions.md b/Versions.md index 880a342..115db5f 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,12 @@ # Versions +# v5.4.2 + +- Improve perf of `ReadOnlySpan` based overloads of `Write` and `WriteLine`. +- `SKILL` improvements + - Highlight some points that correct invalid API assumptions made by agents. + - Add testing section and teach agents to about `ConsoleContext`. + ## v5.4.1 - This version now ships with a specialized AI agent skill `PrettyConsoleExpert` that will be copied to consumers on build.