Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Summary
- PrettyConsole/ — main library
- PrettyConsole.Tests/ — interactive/demo runner (manually selects visual feature demos)
- PrettyConsole.Tests.Unit/ — xUnit v3 unit tests using Microsoft Testing Platform
- v5.2.0 (current) rewrites `PrettyConsoleInterpolatedStringHandler` to buffer before writing for a major perf bump, recognizes a new `WhiteSpace` struct that expands to the requested padding length, and adds `IndeterminateProgressBar` overloads that take a `Func<PrettyConsoleInterpolatedStringHandler>` (use `PrettyConsoleInterpolatedStringHandler.Build` to bind the right `OutputPipe`). v5.1.0 renamed `PrettyConsoleExtensions` to `ConsoleContext`, added `Console.WriteWhiteSpaces(length, pipe)`, and made `Out`/`Error`/`In` settable for test doubles. v5.0.0 removed the legacy `ColoredOutput`/`Color` types; color composition now flows through `ConsoleColor` helpers and tuples exposed by the library.
- v5.3.0 (current) makes more `PrettyConsoleInterpolatedStringHandler` members public, adds `AppendInline` for nesting handlers, introduces a ctor that takes only `OutputPipe` + optional `IFormatProvider`, and passes handlers by `ref` to callers. It adds `SkipLines` to advance the cursor while keeping overwritten UIs, reorders `Confirm(trueValues, ref handler, bool emptyIsTrue = true)` arguments (the boolean is now last), switches `IndeterminateProgressBar` header factories to `PrettyConsoleInterpolatedStringHandlerFactory` with the singleton `PrettyConsoleInterpolatedStringHandlerBuilder`, and makes `AnsiColors` public. v5.2.0 rewrote the handler to buffer before writing and added `WhiteSpace`; v5.1.0 renamed `PrettyConsoleExtensions` to `ConsoleContext`, added `Console.WriteWhiteSpaces(length, pipe)`, and made `Out`/`Error`/`In` settable; v5.0.0 removed the legacy `ColoredOutput`/`Color` types in favor of `ConsoleColor` helpers and tuples.

Commands you’ll use often

Expand Down Expand Up @@ -48,9 +48,10 @@ High-level architecture and key concepts
- Output routing
- `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `ConsoleContext.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly.
- Interpolated string handler
- `PrettyConsoleInterpolatedStringHandler` buffers the interpolated content before emitting it, yielding a large perf boost while staying allocation-free. It enables `$"..."` calls for `WriteInterpolated`, `WriteLineInterpolated`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors auto-reset, handlers respect the selected pipe/`IFormatProvider`, and `object` arguments that implement `ISpanFormattable` are emitted via the span path before falling back to `IFormattable`/string. `Console.WriteInterpolated`/`WriteLineInterpolated` return the rendered character count (handler-emitted escape sequences excluded). Passing the new `WhiteSpace` struct writes a span of padding directly from the handler without allocations.
- `PrettyConsoleInterpolatedStringHandler` buffers interpolated content before emitting it, stays allocation-free, now exposes additional public helpers (including `AppendInline` for composing handlers) and is constructed/consumed by `ref`. `$"..."` calls light up `WriteInterpolated`, `WriteLineInterpolated`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors auto-reset, handlers respect the selected pipe/`IFormatProvider`, and `object` arguments that implement `ISpanFormattable` are emitted via the span path before falling back to `IFormattable`/string. `Console.WriteInterpolated`/`WriteLineInterpolated` return the rendered character count (handler-emitted escape sequences excluded). Passing the `WhiteSpace` struct writes padding directly from the handler without allocations.
- Mid-span ANSI sequences are intentionally unsupported: every ANSI sequence (from `ConsoleColor` conversions or `Markup`) is only safe when emitted via an interpolated hole, which lets the handler isolate the escape and keep width calculations consistent. Do not try to "account" for mid-span sequences or adjust character counts manually when discussing this repo.
- Coloring model
- `ConsoleColor` now exposes `DefaultForeground`, `DefaultBackground`, and `Default` tuple properties plus `/` operator overloads so you can inline foreground/background tuples (`$"{ConsoleColor.Red / ConsoleColor.White}Error"`). These tuples play nicely with the interpolated string handler and keep color resets allocation-free.
- `ConsoleColor` exposes `DefaultForeground`, `DefaultBackground`, and `Default` tuple properties plus `/` operator overloads so you can inline foreground/background tuples (`$"{ConsoleColor.Red / ConsoleColor.White}Error"`). These tuples play nicely with the interpolated string handler and keep color resets allocation-free. `AnsiColors` is now public if you need raw ANSI sequences from `ConsoleColor`.
- Markup decorations
- The `Markup` static class exposes ANSI sequences for underline, bold, italic, and strikethrough. Fields expand to escape codes only when output/error aren’t redirected; otherwise they collapse to empty strings so callers can safely interpolate them without extra checks.
- Write APIs
Expand All @@ -60,13 +61,13 @@ High-level architecture and key concepts
- Inputs
- `ReadLine`/`TryReadLine` support `IParsable<T>` types, optional defaults, enum parsing with `ignoreCase`, and interpolated prompts. `Confirm` exposes `DefaultConfirmValues`, overloads for custom truthy tokens, and interpolated prompts; `RequestAnyInput` blocks on `ReadKey` with colored prompts if desired.
- Rendering controls
- `ClearNextLines`, `GoToLine`, and `GetCurrentLine` coordinate bounded screen regions; `Clear` wipes the buffer when safe. These helpers underpin progress rendering and overwrite scenarios.
- `ClearNextLines`, `GoToLine`, `GetCurrentLine`, and `SkipLines` coordinate bounded screen regions; `Clear` wipes the buffer when safe. `SkipLines` lets you advance the cursor to preserve overwritten UIs (progress bars, spinners) after completion. These helpers underpin progress rendering and overwrite scenarios.
- Advanced outputs
- `OverwriteCurrentLine`, `Overwrite`, and `Overwrite<TState>` run user actions while clearing a configurable number of lines. Set the `lines` argument to however many rows you emit during the action (e.g., the multi-progress sample uses `lines: 2`) and call `Console.ClearNextLines` once after the last overwrite to remove residual UI. `TypeWrite`/`TypeWriteLine` animate character-by-character output with adjustable delays.
- Menus and tables
- `Selection` returns a single choice or empty string on invalid input; `MultiSelection` parses space-separated indices into string arrays; `TreeMenu` renders two-level hierarchies and validates input (throwing `ArgumentException` when selections are invalid); `Table` renders headers + columns with width calculations.
- Progress bars
- `IndeterminateProgressBar` binds to running `Task` instances, optionally starts tasks, supports cancellable `RunAsync` overloads, exposes `AnimationSequence`, `Patterns`, `ForegroundColor`, `DisplayElapsedTime`, and `UpdateRate`. Frames render on the error pipe and auto-clear. v5.2.0 adds overloads that accept a `Func<PrettyConsoleInterpolatedStringHandler>` so status text can be built per-frame with captured locals (call `PrettyConsoleInterpolatedStringHandler.Build(pipe)` inside the lambda to target the right output pipe).
- `IndeterminateProgressBar` binds to running `Task` instances, optionally starts tasks, supports cancellable `RunAsync` overloads, exposes `AnimationSequence`, `Patterns`, `ForegroundColor`, `DisplayElapsedTime`, and `UpdateRate`. Frames render on the error pipe and auto-clear. v5.3.0 switches header factories to `PrettyConsoleInterpolatedStringHandlerFactory`; use `(builder, out handler) => handler = builder.Build(OutputPipe.Error, $"status")` with the singleton `PrettyConsoleInterpolatedStringHandlerBuilder`.
- `ProgressBar` maintains a single-line bar on the error pipe. `Update` accepts `int`/`double` percentages plus optional status spans, and exposes `ProgressChar`, `ForegroundColor`, and `ProgressColor` for customization. The static `ProgressBar.WriteProgressBar` helper renders one-off segments without moving the cursor (so you can stack multiple bars within an `Overwrite` block).
- Packaging and targets
- `PrettyConsole.csproj` targets net10.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
```

BenchmarkDotNet v0.15.6, macOS 26.1 (25B78) [Darwin 25.1.0]
BenchmarkDotNet v0.15.8, macOS Tahoe 26.1 (25B78) [Darwin 25.1.0]
Apple M2 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK 10.0.100
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a
Expand All @@ -13,6 +13,6 @@ WarmupCount=5
```
| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|--------------- |------------:|--------------:|-------:|----------:|--------------:|
| PrettyConsole | 58.34 ns | 86.94x faster | - | - | NA |
| SpectreConsole | 5,069.69 ns | baseline | 2.1284 | 17840 B | |
| SystemConsole | 71.82 ns | 70.59x faster | 0.0022 | 24 B | 743.333x less |
| PrettyConsole | 55.96 ns | 90.23x faster | - | - | NA |
| SpectreConsole | 5,046.29 ns | baseline | 2.1193 | 17840 B | |
| SystemConsole | 71.64 ns | 70.44x faster | 0.0022 | 24 B | 743.333x less |
2 changes: 1 addition & 1 deletion Benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.6" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Spectre.Console" Version="0.54.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public async ValueTask Implementation() {
// UpdateRate = 120,
DisplayElapsedTime = true
};
await prg.RunAsync(Task.Delay(5_000), () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}..."), CancellationToken.None);
await prg.RunAsync(Task.Delay(5_000), (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}..."), CancellationToken.None);
}
}
2 changes: 1 addition & 1 deletion PrettyConsole.UnitTests/AdvancedInputsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public async Task Confirm_CustomTrueValues_WithInterpolatedPrompt() {
var reader = Utilities.GetReader("ok");
In = reader;

var res = Console.Confirm(["ok", "okay"], false, $"Proceed?");
var res = Console.Confirm(["ok", "okay"], $"Proceed?", false);

await Assert.That(stringWriter.ToStringAndFlush()).IsEqualTo("Proceed?");
await Assert.That(res).IsTrue();
Expand Down
2 changes: 1 addition & 1 deletion PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task ConsoleContext_GetWidthOrDefault_WhenRedirected() {
Console.SetOut(new StringWriter());


int width = ConsoleContext.GetWidthOrDefault(77);
int width = GetWidthOrDefault(77);
Console.SetOut(originalOut);

await Assert.That(width).IsEqualTo(77);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ public async Task AppendFormattedTimeSpan_WithAlignmentAndDurationFormat() {

Console.WriteInterpolated($"{ts,10:duration}");

await Assert.That(writer.ToString()).IsEqualTo("0h 0m 5s");
await Assert.That(writer.ToString()).IsEqualTo(" 0h 0m 5s");
} finally {
Out = originalOut;
}
Expand Down Expand Up @@ -293,4 +293,52 @@ private static string FormatBytes(double value) {
}

private static ReadOnlySpan<string> FileSizeSuffix => new[] { "B", "KB", "MB", "GB", "TB", "PB" };

[Test]
public async Task AppendWhiteSpace() {
var originalOut = Out;
try {
Out = Utilities.GetWriter(out var writer);

Console.WriteInterpolated($"{new WhiteSpace(5)}");

await Assert.That(writer.ToString()).IsEqualTo(new string(' ', 5));
} finally {
Out = originalOut;
}
}

[Test]
public async Task ManualCtor() {
(_, var isRedirected) = GetPipeTargetAndState(OutputPipe.Out);

var handler = new PrettyConsoleInterpolatedStringHandler(OutputPipe.Out);
handler.AppendFormatted(Green);
handler.AppendSpan("Hello");
handler.ResetColors();

if (isRedirected) {
await Assert.That(new string(handler.WrittenSpan)).IsEqualTo("Hello");
} else {
await Assert.That(new string(handler.WrittenSpan)).IsEqualTo($"{AnsiColors.Foreground(Green)}Hello{AnsiColors.ForegroundResetSequence}");
}

handler.FlushWithoutWrite();
}

[Test]
public async Task NestedHandler() {
(_, var isRedirected) = GetPipeTargetAndState(OutputPipe.Out);

var handler = new PrettyConsoleInterpolatedStringHandler(OutputPipe.Out);
handler.AppendInline(OutputPipe.Out, $"{Green}Hello");

if (isRedirected) {
await Assert.That(new string(handler.WrittenSpan)).IsEqualTo("Hello");
} else {
await Assert.That(new string(handler.WrittenSpan)).IsEqualTo($"{AnsiColors.Foreground(Green)}Hello{AnsiColors.ForegroundResetSequence}");
}

handler.FlushWithoutWrite();
}
}
32 changes: 30 additions & 2 deletions PrettyConsole.UnitTests/ProgressBarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult()
int result = await bar.RunAsync(Task.Run(async () => {
await Task.Delay(20, cancellation);
return 42;
}, cancellation), () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"Working"), cancellation);
}, cancellation), (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"Working"), cancellation);

await Assert.That(result).IsEqualTo(42);
await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty);
Expand Down Expand Up @@ -261,7 +261,7 @@ public async Task IndeterminateProgressBar_RunAsync_Generic_TaskAlreadyCompleted
};

var completed = Task.FromResult(5);
var result = await bar.RunAsync(completed, () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"done"));
var result = await bar.RunAsync(completed, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"done"));

await Assert.That(result).IsEqualTo(5);
} finally {
Expand Down Expand Up @@ -305,6 +305,14 @@ internal sealed class SkipWhenConsoleUnavailableAttribute : SkipAttribute {
public override Task<bool> ShouldSkip(TestRegisteredContext testContext) => Task.FromResult(!ConsoleAvailability.IsAvailable());
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
internal sealed class SkipWhenConsoleColorsUnavailableAttribute : SkipAttribute {
public SkipWhenConsoleColorsUnavailableAttribute() : base("Console color APIs unavailable for this environment.") {
}

public override Task<bool> ShouldSkip(TestRegisteredContext testContext) => Task.FromResult(!ConsoleAvailability.ColorsSupported());
}

internal static class ConsoleAvailability {
public static bool IsAvailable() {
try {
Expand All @@ -315,4 +323,24 @@ public static bool IsAvailable() {
return false;
}
}

public static bool ColorsSupported() {
var originalForeground = Console.ForegroundColor;
var originalBackground = Console.BackgroundColor;

try {
Console.ForegroundColor = ConsoleColor.Cyan;
Console.BackgroundColor = ConsoleColor.DarkRed;

return Console.ForegroundColor == ConsoleColor.Cyan
&& Console.BackgroundColor == ConsoleColor.DarkRed;
} catch (IOException) {
return false;
} catch (PlatformNotSupportedException) {
return false;
} finally {
Console.ForegroundColor = originalForeground;
Console.BackgroundColor = originalBackground;
}
}
}
Loading