diff --git a/AGENTS.md b/AGENTS.md index ccafa40..76926b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` (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 @@ -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 @@ -60,13 +61,13 @@ High-level architecture and key concepts - Inputs - `ReadLine`/`TryReadLine` support `IParsable` 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` 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` 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. diff --git a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md index 43174e5..a7a0e44 100644 --- a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -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 @@ -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 | diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index 9eb2182..ae195b0 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -8,7 +8,7 @@ - + diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs index 6b21598..bd013c3 100755 --- a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs @@ -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); } } \ No newline at end of file diff --git a/PrettyConsole.UnitTests/AdvancedInputsTests.cs b/PrettyConsole.UnitTests/AdvancedInputsTests.cs index 83400d8..43f315c 100755 --- a/PrettyConsole.UnitTests/AdvancedInputsTests.cs +++ b/PrettyConsole.UnitTests/AdvancedInputsTests.cs @@ -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(); diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs index 1fe9a1a..39a0aac 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -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); diff --git a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs index c5c4b46..451b8c5 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -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; } @@ -293,4 +293,52 @@ private static string FormatBytes(double value) { } private static ReadOnlySpan 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(); + } } \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index f2b965e..b45bb90 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -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); @@ -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 { @@ -305,6 +305,14 @@ internal sealed class SkipWhenConsoleUnavailableAttribute : SkipAttribute { public override Task 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 ShouldSkip(TestRegisteredContext testContext) => Task.FromResult(!ConsoleAvailability.ColorsSupported()); +} + internal static class ConsoleAvailability { public static bool IsAvailable() { try { @@ -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; + } + } } \ No newline at end of file diff --git a/PrettyConsole.UnitTests/RenderingExtensionsTests.cs b/PrettyConsole.UnitTests/RenderingExtensionsTests.cs new file mode 100644 index 0000000..99fe0bb --- /dev/null +++ b/PrettyConsole.UnitTests/RenderingExtensionsTests.cs @@ -0,0 +1,120 @@ +namespace PrettyConsole.UnitTests; + +public class RenderingExtensionsTests { + [Test] + public async Task WriteWhiteSpaces_WritesExpectedCount() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteWhiteSpaces(5); + + await Assert.That(writer.ToString()).IsEqualTo(new string(' ', 5)); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task NewLine_WritesEnvironmentNewLine() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.NewLine(); + + await Assert.That(writer.ToString()).IsEqualTo(Environment.NewLine); + } finally { + Out = originalOut; + } + } + + [Test] + [SkipWhenConsoleColorsUnavailable] + public async Task SetColors_UpdatesConsoleColors() { + var originalForeground = Console.ForegroundColor; + var originalBackground = Console.BackgroundColor; + + try { + Console.SetColors(Cyan, DarkRed); + + await Assert.That(Console.ForegroundColor).IsEqualTo(Cyan); + await Assert.That(Console.BackgroundColor).IsEqualTo(DarkRed); + } finally { + Console.ForegroundColor = originalForeground; + Console.BackgroundColor = originalBackground; + } + } + + [Test] + public async Task GetCurrentLine_UsesConfiguredAccessor() { + const int expectedLine = 17; + RenderingExtensions.ConfigureCursorAccessors(() => expectedLine, null); + + try { + int currentLine = Console.GetCurrentLine(); + + await Assert.That(currentLine).IsEqualTo(expectedLine); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } + + [Test] + public async Task GoToLine_InvokesConfiguredSetter() { + List<(int Left, int Top)> positions = []; + RenderingExtensions.ConfigureCursorAccessors(null, (left, top) => positions.Add((left, top))); + + try { + Console.GoToLine(12); + + await Assert.That(positions.Count).IsEqualTo(1); + await Assert.That(positions[0]).IsEqualTo((0, 12)); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } + + [Test] + public async Task SkipLines_MovesCursorRelativeToCurrentLine() { + const int startingLine = 4; + List<(int Left, int Top)> positions = []; + RenderingExtensions.ConfigureCursorAccessors(() => startingLine, (left, top) => positions.Add((left, top))); + + try { + Console.SkipLines(3); + + await Assert.That(positions.Count).IsEqualTo(1); + await Assert.That(positions[0]).IsEqualTo((0, startingLine + 3)); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } + + [Test] + public async Task ClearNextLines_WritesSpacesAndRestoresCursor() { + var originalOut = Out; + var originalError = Error; + const int currentLine = 5; + List<(int Left, int Top)> positions = []; + + try { + Out = Utilities.GetWriter(out _); // force width fallback path + Error = Utilities.GetWriter(out var errorWriter); + RenderingExtensions.ConfigureCursorAccessors(() => currentLine, (left, top) => positions.Add((left, top))); + + int width = GetWidthOrDefault(); + + Console.ClearNextLines(3, OutputPipe.Error); + + await Assert.That(errorWriter.ToString()).IsEqualTo(new string(' ', width * 3)); + await Assert.That(positions.Count).IsEqualTo(2); + await Assert.That(positions[0]).IsEqualTo((0, currentLine)); + await Assert.That(positions[1]).IsEqualTo((0, currentLine)); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + Out = originalOut; + Error = originalError; + } + } +} \ No newline at end of file diff --git a/PrettyConsole/AnsiColors.cs b/PrettyConsole/AnsiColors.cs index bccd86d..5f963ca 100644 --- a/PrettyConsole/AnsiColors.cs +++ b/PrettyConsole/AnsiColors.cs @@ -1,8 +1,18 @@ namespace PrettyConsole; -internal static class AnsiColors { - internal const string ForegroundResetSequence = "\e[39m"; - internal const string BackgroundResetSequence = "\e[49m"; +/// +/// Provides ANSI sequences for the common s. +/// +public static class AnsiColors { + /// + /// A sequence to reset foreground color. + /// + public const string ForegroundResetSequence = "\e[39m"; + + /// + /// A sequence to reset background color. + /// + public const string BackgroundResetSequence = "\e[49m"; private static readonly string[] ForegroundCodes; private static readonly string[] BackgroundCodes; @@ -18,7 +28,7 @@ static AnsiColors() { } /// - /// Gets the ANSI sequence for the specified foreground color or an empty string when disabled. + /// Gets the ANSI sequence for the specified foreground color. /// public static string Foreground(ConsoleColor color) { int index = (int)color; @@ -28,7 +38,7 @@ public static string Foreground(ConsoleColor color) { /// - /// Gets the ANSI sequence for the specified background color or an empty string when disabled. + /// Gets the ANSI sequence for the specified background color. /// public static string Background(ConsoleColor color) { int index = (int)color; diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 916e34d..a24a78e 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -58,7 +58,7 @@ public async Task RunAsync(Task task, CancellationToken token = default /// /// public async Task RunAsync(Task task, string header, CancellationToken token) { - await RunAsyncNonGeneric(task, () => WrapHeader(header), token); + await RunAsyncNonGeneric(task, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"{header}"), token); return task.IsCompleted ? task.Result : await task; } @@ -70,7 +70,7 @@ public async Task RunAsync(Task task, string header, CancellationToken /// Factory invoked every frame to render a header with . /// /// The output of the running task. - public async Task RunAsync(Task task, Func? headerFactory, CancellationToken token = default) { + public async Task RunAsync(Task task, PrettyConsoleInterpolatedStringHandlerFactory? headerFactory, CancellationToken token = default) { await RunAsyncNonGeneric(task, headerFactory, token); return task.IsCompleted ? task.Result : await task; @@ -91,7 +91,7 @@ public async Task RunAsync(Task task, Func /// public Task RunAsync(Task task, string header, CancellationToken token) { - return RunAsyncNonGeneric(task, () => WrapHeader(header), token); + return RunAsyncNonGeneric(task, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"{header}"), token); } /// @@ -100,8 +100,7 @@ public Task RunAsync(Task task, string header, CancellationToken token) { /// /// Factory invoked every frame to render a header with . /// - public Task RunAsync(Task task, Func? headerFactory, CancellationToken token) => RunAsyncNonGeneric(task, headerFactory, token); - + public Task RunAsync(Task task, PrettyConsoleInterpolatedStringHandlerFactory? headerFactory, CancellationToken token) => RunAsyncNonGeneric(task, headerFactory, token); /// /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. @@ -109,7 +108,7 @@ public Task RunAsync(Task task, string header, CancellationToken token) { /// /// Factory invoked every frame to render a header with . /// - private async Task RunAsyncNonGeneric(Task task, Func? headerFactory, CancellationToken token) { + private async Task RunAsyncNonGeneric(Task task, PrettyConsoleInterpolatedStringHandlerFactory? headerFactory, CancellationToken token) { try { if (task.Status is not TaskStatus.Running) { task.Start(); @@ -136,7 +135,8 @@ private async Task RunAsyncNonGeneric(Task task, Func PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"{header}"); - /// /// Provides constant animation sequences that can be used for /// diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index ff7f1d5..8aa3b49 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -24,7 +24,7 @@ internal static void ConfigureReadKey(Func? readKey) { /// Used to wait for user input /// /// - public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + public static void RequestAnyInput([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) { handler.Flush(); _ = s_readKey(); } @@ -36,7 +36,7 @@ public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyCon /// /// It does not display a question mark or any other prompt, only the message /// - public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) => Confirm(DefaultConfirmValues, true, handler); + public static bool Confirm([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) => Confirm(DefaultConfirmValues, ref handler, true); /// /// Used to get user confirmation @@ -47,7 +47,7 @@ public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyCon /// /// It does not display a question mark or any other prompt, only the message /// - public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = true, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + public static bool Confirm(ReadOnlySpan trueValues, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler, bool emptyIsTrue = true) { handler.Flush(); var input = ConsoleContext.In.ReadLine(); if (input is null or { Length: 0 }) { diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index 43930a5..0f74388 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -16,7 +16,7 @@ public static class MenuExtensions { /// /// This validates the input for you. /// - public static string Selection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) + public static string Selection(TList choices, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where TList : IList { handler.AppendNewLine(); handler.Flush(); @@ -49,7 +49,7 @@ public static string Selection(TList choices, [InterpolatedStringHandlerA /// /// This validates the input for you. /// - public static string[] MultiSelection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) + public static string[] MultiSelection(TList choices, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where TList : IList { handler.AppendNewLine(); handler.Flush(); @@ -98,7 +98,7 @@ public static string[] MultiSelection(TList choices, [InterpolatedStringH /// /// This validates the input for you. /// - public static (string option, string subOption) TreeMenu(Dictionary menu, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { + public static (string option, string subOption) TreeMenu(Dictionary menu, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where TList : IList { handler.AppendNewLine(); handler.Flush(); diff --git a/PrettyConsole/NamespaceScope.cs b/PrettyConsole/NamespaceScope.cs new file mode 100644 index 0000000..f2fac10 --- /dev/null +++ b/PrettyConsole/NamespaceScope.cs @@ -0,0 +1,9 @@ +namespace PrettyConsole; + +/// +/// A factory function that returns a reference to a PrettyConsoleInterpolatedStringHandler +/// +/// +/// +/// +public delegate void PrettyConsoleInterpolatedStringHandlerFactory(PrettyConsoleInterpolatedStringHandlerBuilder builder, out PrettyConsoleInterpolatedStringHandler handler); \ No newline at end of file diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 3d4110d..008dbde 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.2.0 + 5.3.0 enable MIT True @@ -43,9 +43,11 @@ - - `PrettyConsoleInterpolatedStringHandler` was re-written to buffers interpolated content before emitting it (instead of streaming), along with other optimizations. - - Added a `WhiteSpace` struct to declare padding regions; the handler recognizes it as a special argument and writes that many spaces directly into the buffer. - - `IndeterminateProgressBar` gains overloads that take a `Func{PrettyConsoleInterpolatedStringHandler}`, letting each frame build status text with captured locals. Use `PrettyConsoleInterpolatedStringHandler.Build` to bind the handler to the correct `OutputPipe` inside the factory. + - Exposed more of `PrettyConsoleInterpolatedStringHandler`, including a ctor that takes only `OutputPipe` with optional `IFormatProvider`, public helpers, and `AppendInline`; handlers are now passed by `ref`. + - Added `Console.SkipLines(int)` to advance the cursor while preserving overwritten UIs such as progress bars and spinners. + - Reordered `Confirm(ReadOnlySpan<string> trueValues, ref PrettyConsoleInterpolatedStringHandler handler, bool emptyIsTrue = true)` so `emptyIsTrue` is the last parameter. + - `IndeterminateProgressBar` header factories now use `PrettyConsoleInterpolatedStringHandlerFactory` with the `PrettyConsoleInterpolatedStringHandlerBuilder` singleton to avoid extra struct copies; the old `Build` helper was removed. + - `AnsiColors` is now public for converting `ConsoleColor` to ANSI sequences. diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 8858f5d..19e9fe2 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -10,25 +10,41 @@ public struct PrettyConsoleInterpolatedStringHandler { private static readonly ArrayPool BufferPool = ArrayPool.Shared; private bool _flushed; - private char[] _buffer; - private int _index; - private int _capacity = 4096; - private readonly TextWriter _writer; private readonly bool _isRedirected; private readonly IFormatProvider? _provider; - private ConsoleColor _currentForeground; private ConsoleColor _currentBackground; + private readonly Span Written => new(_buffer, 0, _index); + /// - /// The number of characters written in this instance of . + /// Returns the written portion of the internal buffer as . /// + public readonly ReadOnlySpan WrittenSpan { + get { + ThrowIfFlushed(); + return new(_buffer, 0, _index); + } + } + + /// + /// The number of characters written in this instance of . + /// public int CharsWritten { get; private set; } + /// + /// Creates a new handler that writes to . + /// + /// The pipe to stream the output to. + /// Optional format provider used when formatting values. + public PrettyConsoleInterpolatedStringHandler(OutputPipe pipe, IFormatProvider? provider = null) + : this(0, 0, pipe, provider: provider, out _) { + } + /// /// Creates a new handler that writes to . /// @@ -67,14 +83,6 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo shouldAppend = true; } - /// - /// Creates a instance attached to . - /// - /// - /// - /// - public static PrettyConsoleInterpolatedStringHandler Build(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) => handler; - /// /// Appends a literal segment supplied by the compiler. /// @@ -89,18 +97,16 @@ public void AppendLiteral(string value) { /// Formatted string. /// Optional alignment as provided by the interpolation. /// Unused string format specifier. - public void AppendFormatted(string? value, int alignment = 0, string? format = null) { - AppendString(value, alignment); - } + public void AppendFormatted(string? value, int alignment = 0, string? format = null) + => AppendString(value, alignment); /// /// Appends a span segment /// /// Characters to write. /// Optional alignment as provided by the interpolation. - public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { - AppendSpan(value, alignment); - } + public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) + => AppendSpan(value, alignment); /// /// Appends a single character. @@ -155,13 +161,8 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c /// Appends a region of whitespaces to the buffer. /// /// - public void AppendFormatted(WhiteSpace whiteSpace) { - var length = whiteSpace.Length; - EnsureCapacity(length); - _buffer.AsSpan(_index, length).Fill(' '); - _index += length; - CharsWritten += length; - } + public void AppendFormatted(WhiteSpace whiteSpace) + => WritePadding(whiteSpace.Length); /// /// Append timeSpan with optional formatting. @@ -195,6 +196,8 @@ public void AppendFormatted(TimeSpan timeSpan, int alignment, string? format = n _index += written; CharsWritten += written; } + + AlignLastSection(alignment, written); } private static ReadOnlySpan FileSizeSuffix => new[] { "B", "KB", "MB", "GB", "TB", "PB" }; @@ -242,28 +245,27 @@ public void AppendFormatted(double num, int alignment, string? format = null) { _index += written; CharsWritten += written; } + + AlignLastSection(alignment, written); } /// /// Appends a value type that implements without boxing while respecting alignment. /// - public void AppendFormatted(T value, int alignment = 0) where T : ISpanFormattable { - AppendSpanFormattable(value, alignment, format: null); - } + public void AppendFormatted(T value, int alignment = 0) where T : ISpanFormattable + => AppendSpanFormattable(value, alignment, format: null); /// /// Appends a value type that implements without boxing using the provided format string. /// - public void AppendFormatted(T value, string? format) where T : ISpanFormattable { - AppendSpanFormattable(value, alignment: 0, format); - } + public void AppendFormatted(T value, string? format) where T : ISpanFormattable + => AppendSpanFormattable(value, alignment: 0, format); /// /// Appends a value type that implements without boxing using alignment and format string. /// - public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { - AppendSpanFormattable(value, alignment, format); - } + public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable + => AppendSpanFormattable(value, alignment, format); /// /// Appends an object value when the compiler cannot resolve a more specific overload. @@ -296,13 +298,20 @@ public void AppendFormatted(object? value, int alignment = 0, string? format = n } } + /// + /// Append formatted into the internal buffer with . + /// + /// + /// + /// + /// private void AppendSpanFormattable(T value, int alignment, string? format) where T : ISpanFormattable { ThrowIfFlushed(); ReadOnlySpan formatSpan = format.AsSpan(); int charsWritten; - int start = _index; + // int start = _index; while (true) { Span dest = _buffer.AsSpan(_index); @@ -316,34 +325,51 @@ private void AppendSpanFormattable(T value, int alignment, string? format) Grow(_capacity * 2); } + AlignLastSection(alignment, charsWritten); + } + + /// + /// Align the last written section. + /// + /// + /// Section length + private void AlignLastSection(int alignment, int length) { if (alignment == 0) return; if (alignment > 0) { // shift forward and prefix whitespaces - int padding = alignment - charsWritten; + int padding = alignment - length; if (padding <= 0) return; EnsureCapacity(padding); - var written = _buffer.AsSpan(start, charsWritten); - written.CopyTo(_buffer.AsSpan(start + padding, charsWritten)); + var start = _index - length; + var written = _buffer.AsSpan(start, length); + written.CopyTo(_buffer.AsSpan(start + padding, length)); _buffer.AsSpan(start, padding).Fill(' '); _index += padding; CharsWritten += padding; } else { // suffix whitespaces int targetWidth = -alignment; - int trailing = targetWidth - charsWritten; + int trailing = targetWidth - length; if (trailing > 0) { WritePadding(trailing); - CharsWritten += trailing; } } } - private void AppendString(string? value, int alignment) { - // AppendSpan handles null and empty spans - AppendSpan(value.AsSpan(), alignment); - } + /// + /// Append with . + /// + /// + /// + private void AppendString(string? value, int alignment) + => AppendSpan(value, alignment); - private void AppendSpan(scoped ReadOnlySpan span, int alignment) { + /// + /// Appends with to the internal buffer. + /// + /// + /// + public void AppendSpan(scoped ReadOnlySpan span, int alignment = 0) { ThrowIfFlushed(); if (alignment == 0) { @@ -355,23 +381,23 @@ private void AppendSpan(scoped ReadOnlySpan span, int alignment) { int width = Math.Abs(alignment); int visibleLength = span.Length > 0 && span[0] == '\e' ? 0 : span.Length; int padding = width - visibleLength; - int required = span.Length + padding; - EnsureCapacity(required); if (padding > 0 && !leftAlign) { WritePadding(padding); - CharsWritten += padding; } - AppendSpanCore(span, false); + AppendSpanCore(span); if (padding > 0 && leftAlign) { WritePadding(padding); - CharsWritten += padding; } } - private void AppendSpanCore(scoped ReadOnlySpan span, bool ensureCapacity = true) { + /// + /// Appends to the internal buffer. + /// + /// + private void AppendSpanCore(scoped ReadOnlySpan span) { int length = span.Length; if (length == 0) return; @@ -380,16 +406,39 @@ private void AppendSpanCore(scoped ReadOnlySpan span, bool ensureCapacity if (isEscapeSequence && _isRedirected) return; - if (ensureCapacity) EnsureCapacity(length); + EnsureCapacity(length); span.CopyTo(_buffer.AsSpan(_index, length)); _index += length; if (!isEscapeSequence) CharsWritten += length; } + /// + /// Writes padding according to and advances _index and CharsWritten + /// + /// private void WritePadding(int count) { + EnsureCapacity(count); _buffer.AsSpan(_index, count).Fill(' '); _index += count; + CharsWritten += count; + } + + /// + /// Appends the contents of another to this one. + /// + /// + /// + /// After copying the content, is called on the incoming handler. + public void AppendInline(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] ref PrettyConsoleInterpolatedStringHandler handler) { + ThrowIfFlushed(); + handler.ResetColors(); + ReadOnlySpan other = handler.WrittenSpan; + EnsureCapacity(other.Length); + other.CopyTo(_buffer.AsSpan(_index, other.Length)); + _index += other.Length; + CharsWritten += handler.CharsWritten; + handler.FlushWithoutWrite(); } /// @@ -402,6 +451,10 @@ public void AppendNewLine() { CharsWritten -= newline.Length; } + /// + /// Ensures that the unwritten portion of the buffer is at least as long as . + /// + /// private void EnsureCapacity(int capacity) { int available = _buffer.Length - _index; if (capacity <= available) return; @@ -415,6 +468,10 @@ private void EnsureCapacity(int capacity) { Grow(targetCapacity); } + /// + /// Grows the internal buffer to be at least . + /// + /// private void Grow(int targetCapacity) { char[] temp = _buffer; @@ -426,6 +483,10 @@ private void Grow(int targetCapacity) { BufferPool.Return(temp, false); } + /// + /// Throws an if the buffer was already returned to the backing array pool. + /// + /// private readonly void ThrowIfFlushed() { if (!_flushed) return; @@ -433,12 +494,32 @@ private readonly void ThrowIfFlushed() { } /// - /// Writes the underline buffer to the held . + /// Resets the colors of the contents if they were overwritten. /// - public void Flush() { - ThrowIfFlushed(); + public void ResetColors() { AppendFormatted(ConsoleColor.DefaultForeground); AppendFormattedBackground(ConsoleColor.DefaultBackground); + } + + /// + /// Clears the internal buffer and returns it to the underlying array pool without writing to the to the held . + /// + /// This overload does not reset colors. + public void FlushWithoutWrite() { + ThrowIfFlushed(); + var written = Written; + written.Clear(); + BufferPool.Return(_buffer, false); + _flushed = true; + } + + /// + /// Writes the underline buffer to the held and clears and returns the underlying buffer to the underlying array pool. + /// + /// Whether to reset colors before flushing + public void Flush(bool resetColors = true) { + ThrowIfFlushed(); + if (resetColors) ResetColors(); Span written = new(_buffer, 0, _index); _writer.Write(written); written.Clear(); diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs new file mode 100644 index 0000000..4483809 --- /dev/null +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs @@ -0,0 +1,29 @@ +namespace PrettyConsole; + +#pragma warning disable CA1822 // Mark members as static +/// +/// Provides an api to build a string handler. +/// +public sealed class PrettyConsoleInterpolatedStringHandlerBuilder { + /// + /// A singleton instance of . + /// + /// This instance is stateless and thread-safe + public static readonly PrettyConsoleInterpolatedStringHandlerBuilder Singleton = new(); + + /// + /// Builds a and returns its reference. + /// + /// + /// + public ref PrettyConsoleInterpolatedStringHandler Build([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) => ref handler; + + /// + /// Builds a and returns its reference. + /// + /// + /// + /// + public ref PrettyConsoleInterpolatedStringHandler Build(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] ref PrettyConsoleInterpolatedStringHandler handler) => ref handler; +} +#pragma warning restore CA1822 // Mark members as static \ No newline at end of file diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs index b647546..27be5b1 100755 --- a/PrettyConsole/ReadLineExtensions.cs +++ b/PrettyConsole/ReadLineExtensions.cs @@ -14,7 +14,7 @@ public static class ReadLineExtensions { /// The result of the parsing /// /// True if the parsing was successful, false otherwise - public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where T : IParsable { handler.Flush(); var input = ConsoleContext.In.ReadLine(); return T.TryParse(input, CultureInfo.CurrentCulture, out result); @@ -28,8 +28,8 @@ public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgum /// The default value to return if parsing fails /// /// True if the parsing was successful, false otherwise - public static bool TryReadLine(out T result, T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { - var couldParse = TryReadLine(out T? innerResult, handler); + public static bool TryReadLine(out T result, T @default, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where T : IParsable { + var couldParse = TryReadLine(out T? innerResult, ref handler); if (couldParse) { result = innerResult!; return true; @@ -46,8 +46,8 @@ public static bool TryReadLine(out T result, T @default, [InterpolatedStringH /// Whether to ignore case when parsing /// /// True if the parsing was successful, false otherwise - public static bool TryReadLine(out TEnum result, bool ignoreCase, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TEnum : struct, Enum { - return TryReadLine(out result, ignoreCase, default, handler); + public static bool TryReadLine(out TEnum result, bool ignoreCase, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where TEnum : struct, Enum { + return TryReadLine(out result, ignoreCase, default, ref handler); } /// @@ -59,7 +59,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, [Interp /// The default value to return if parsing fails /// /// True if the parsing was successful, false otherwise - public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TEnum : struct, Enum { + public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @default, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where TEnum : struct, Enum { handler.Flush(); var input = ConsoleContext.In.ReadLine(); var res = Enum.TryParse(input, ignoreCase, out result); @@ -75,8 +75,8 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ /// /// A string if the user entered any, empty string otherwise - never null. [OverloadResolutionPriority(3)] - public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - _ = TryReadLine(out string result, string.Empty, handler); + public static string ReadLine([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) { + _ = TryReadLine(out string result, string.Empty, ref handler); return result; } @@ -87,8 +87,8 @@ public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleI /// /// The result of the parsing [OverloadResolutionPriority(2)] - public static T? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { - _ = TryReadLine(out T? result, handler); + public static T? ReadLine([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where T : IParsable { + _ = TryReadLine(out T? result, ref handler); return result; } @@ -100,8 +100,8 @@ public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleI /// /// The result of the parsing [OverloadResolutionPriority(1)] - public static T ReadLine(T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { - _ = TryReadLine(out T result, @default, handler); + public static T ReadLine(T @default, [InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) where T : IParsable { + _ = TryReadLine(out T result, @default, ref handler); return result; } } diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index 3fd3e3b..ec45b4c 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -49,6 +49,18 @@ public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) GoToLine(currentLine); } + /// + /// Moves the cursor forward by . + /// + /// Amount of lines to skip + /// + /// Useful for keeping overwritten lines, like progress bar or dashboards after the task is done. + /// + public static void SkipLines(int lines) { + var currentLine = GetCurrentLine(); + GoToLine(currentLine + lines); + } + /// /// Used to end current line or write an empty one, depends whether the current line has any text. /// diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 532d8e2..e3293d2 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -12,7 +12,7 @@ public static class WriteExtensions { /// /// /// The number of characters written by the handler. - public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + public static int WriteInterpolated([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) { handler.Flush(); return handler.CharsWritten; } @@ -23,7 +23,7 @@ public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyCo /// Destination pipe. Defaults to . /// /// The number of characters written by the handler. - public static int WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + public static int WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] ref PrettyConsoleInterpolatedStringHandler handler) { handler.Flush(); return handler.CharsWritten; } diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index 5c5cadd..9e90890 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -11,7 +11,7 @@ public static class WriteLineExtensions { /// /// The number of characters written by the handler. [OverloadResolutionPriority(int.MaxValue)] - public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] ref PrettyConsoleInterpolatedStringHandler handler) { handler.AppendNewLine(); handler.Flush(); return handler.CharsWritten; @@ -23,7 +23,7 @@ public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] Pret /// Destination pipe. Defaults to . /// /// The number of characters written by the handler. - public static int WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + public static int WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] ref PrettyConsoleInterpolatedStringHandler handler) { handler.AppendNewLine(); handler.Flush(); return handler.CharsWritten; diff --git a/README.md b/README.md index ef122d0..14bef75 100755 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ PrettyConsole is a high-performance, ultra-low-latency, allocation-free extensio ## Features - 🚀 Zero-allocation interpolated string handler (`PrettyConsoleInterpolatedStringHandler`) for inline colors and formatting -- 🎨 Inline color composition with `ConsoleColor` tuples and helpers (`DefaultForeground`, `DefaultBackground`, `Default`) -- 🔁 Advanced rendering primitives (`Overwrite`, `ClearNextLines`, `GoToLine`, progress bars) that respect console pipes +- 🎨 Inline color composition with `ConsoleColor` tuples and helpers (`DefaultForeground`, `DefaultBackground`, `Default`) plus `AnsiColors` utilities when you need raw ANSI sequences +- 🔁 Advanced rendering primitives (`Overwrite`, `ClearNextLines`, `GoToLine`, `SkipLines`, progress bars) that respect console pipes - 🧱 Handler-aware `WhiteSpace` struct for zero-allocation padding directly inside interpolated strings - 🧰 Rich input helpers (`TryReadLine`, `Confirm`, `RequestAnyInput`) with `IParsable` and enum support - ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `Console.WriteWhiteSpaces` / `TextWriter.WriteWhiteSpaces`) @@ -23,11 +23,11 @@ BenchmarkDotNet measures [styled output performance](Benchmarks/BenchmarkDotNet. | 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 | -PrettyConsole is **the go-to choice for ultra-low-latency, allocation-free console rendering**, running almost ~90× faster than Spectre.Console while allocating nothing and even beating the manual unrolling with the BCL. +PrettyConsole is **the go-to choice for ultra-low-latency, allocation-free console rendering**, running 90X faster than **Spectre.Console** while allocating nothing and even beating the manual unrolling with the BCL. ## Installation @@ -48,7 +48,7 @@ This setup lets you call `Console.WriteInterpolated`, `Console.Overwrite`, `Cons ### Interpolated strings & inline colors -`PrettyConsoleInterpolatedStringHandler` now buffers interpolated content in a pooled buffer before flushing to the selected pipe—a v5.2.0 rewrite that delivered a big perf jump while staying allocation-free. Colors auto-reset at the end of each call. `Console.WriteInterpolated` and `Console.WriteLineInterpolated` return the number of visible characters written (handler-emitted escape sequences are excluded) so you can drive padding/width calculations from the same call sites. +`PrettyConsoleInterpolatedStringHandler` now buffers interpolated content in a pooled buffer before flushing to the selected pipe. Colors auto-reset at the end of each call. `Console.WriteInterpolated` and `Console.WriteLineInterpolated` return the number of visible characters written (handler-emitted escape sequences are excluded) so you can drive padding/width calculations from the same call sites. ```csharp Console.WriteInterpolated($"Hello {ConsoleColor.Green / ConsoleColor.DefaultBackground}world{ConsoleColor.Default}!"); @@ -155,7 +155,7 @@ if (!Console.Confirm($"Deploy to production? ({ConsoleColor.Green}y{ConsoleColor } var customTruths = new[] { "sure", "do it" }; -bool overwrite = Console.Confirm(customTruths, emptyIsTrue: false, $"Overwrite existing files? "); +bool overwrite = Console.Confirm(customTruths, $"Overwrite existing files? ", emptyIsTrue: false); ``` ### Rendering helpers @@ -167,9 +167,10 @@ int line = Console.GetCurrentLine(); Console.GoToLine(line); Console.SetColors(ConsoleColor.White, ConsoleColor.DarkBlue); Console.ResetColors(); +Console.SkipLines(2); // keep multi-line UIs (progress bars, dashboards) and continue writing below them ``` -`ConsoleContext.Out`/`Error` expose the live writers (both are settable if you need to swap in test doubles). Use `Console.WriteWhiteSpaces(int length, OutputPipe pipe)` for convenient padding from call sites, or call `WriteWhiteSpaces(int)` on an existing writer: +`ConsoleContext.Out`/`Error` expose the live writers (both are settable if you need to swap in test doubles). Use `Console.WriteWhiteSpaces(int length, OutputPipe pipe)` for convenient padding from call sites, or call `WriteWhiteSpaces(int)` on an existing writer. `Console.SkipLines(n)` advances the cursor without clearing so you can keep overwritten UI (progress bars, spinners, dashboards) visible after completion: ```csharp Console.WriteWhiteSpaces(8, OutputPipe.Error); // pad status blocks @@ -241,14 +242,15 @@ ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', ma #### Indeterminate progress -`IndeterminateProgressBar` renders animated frames on the error pipe. v5.2.0 adds overloads that accept a `Func` so you can generate per-frame headers with captured locals. Bind the handler to the right pipe via `PrettyConsoleInterpolatedStringHandler.Build`: +`IndeterminateProgressBar` renders animated frames on the error pipe. `PrettyConsoleInterpolatedStringHandlerFactory` overloads all use of a lambda to create a `PrettyConsoleInterpolatedStringHandler` with a builder for a header that will be created when the spinner is re-rendered.: ```csharp var spinner = new IndeterminateProgressBar(); -await spinner.RunAsync(workTask, () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"Syncing {DateTime.Now:T}")); +await spinner.RunAsync(workTask, (builder, out handler) => + handler = builder.Build(OutputPipe.Error, $"Syncing {DateTime.Now:T}")); ``` -The factory runs each frame, letting you inject dynamic status text without allocations. +The factory runs each frame, letting you inject dynamic status text without allocations while avoiding extra struct copies. #### Multiple progress bars with tasks + channels diff --git a/Versions.md b/Versions.md index a865833..951e3f7 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,18 @@ # Versions +## v5.3.0 + +- `PrettyConsoleInterpolatedStringHandler`: + - Turned more of the methods to be `public` to enhance the usability of it in an advance usage (unrolling its creation). To further aid in this. + - Added `AppendInline` which can inline the contents of an another `PrettyConsoleInterpolatedStringHandler` to the source instance and enable nested use cases. + - Added another constructor that accepts just `OutputPipe` and an optional `IFormatProvider`. + - Is now passed by `ref` to accepting methods. +- Added `SkipLines` which can be used to move the cursor `n` amount of lines forward. This can be used to keep the output of overwritten lines, like progress bars, spinners, `OverWrite` and so on and forth. +- `Confirm(trueValues, ref handler, bool emptyIsTrue = true)` parameters were reordered, `emptyIsTrue` is now the last parameter. +- `IndeterminateProgressBar` overloads with the `Func` now use `PrettyConsoleInterpolatedStringHandlerFactory` instead, and usage is now `(builder, out handler) => handler = PrettyConsoleInterpolatedStringHandler.Build(...)`. This was required to reduce compiler created struct copies and increase safety. + - Building custom handlers is now done with `PrettyConsoleInterpolatedStringHandlerBuilder` which contains a thread-safe singleton; `PrettyConsoleInterpolatedStringHandler.Build` was removed in favor of using the builder. +- `AnsiColors` which provides static utilities to convert `ConsoleColor` to `ANSI` sequences is now public (was previously internal) + ## v5.2.0 - `PrettyConsoleInterpolatedStringHandler` was re-written to buffers interpolated content before emitting it (instead of streaming), along with other optimizations.