diff --git a/AGENTS.md b/AGENTS.md index 0c526d2..b51ba3e 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.0.0 (November 2025) removed the legacy `ColoredOutput`/`Color` types; color composition now flows through `ConsoleColor` helpers and tuples exposed by the library. +- v5.1.0 (current) renames `PrettyConsoleExtensions` to `ConsoleContext`, adds `Console.WriteWhiteSpaces(length, pipe)`, and makes `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. Commands you’ll use often @@ -44,19 +44,19 @@ Repo-specific agent rules and conventions High-level architecture and key concepts - Console facade - - Extension members declared via `extension(Console)` attach directly to `System.Console`, so APIs such as `Console.WriteInterpolated`, `Console.TryReadLine`, `Console.Overwrite`, etc. light up once `using PrettyConsole;` (optionally with `using static System.Console;`) is in scope. `PrettyConsoleExtensions` still exposes the live `In`, `Out`, and `Error` streams plus helpers like `GetWidthOrDefault`. + - Extension members declared via `extension(Console)` attach directly to `System.Console`, so APIs such as `Console.WriteInterpolated`, `Console.TryReadLine`, `Console.Overwrite`, etc. light up once `using PrettyConsole;` (optionally with `using static System.Console;`) is in scope. `ConsoleContext` exposes the live `In`, `Out`, and `Error` streams (all now settable for testing) plus helpers like `GetWidthOrDefault`. - Output routing - - `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `PrettyConsoleExtensions.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly. + - `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` enables zero-allocation `$"..."` calls for `WriteInterpolated`, `WriteLineInterpolated`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors automatically reset after each invocation, and handlers respect the selected pipe and optional `IFormatProvider`. Recent changes ensure any `object` argument that implements `ISpanFormattable` is emitted through the span-based path before falling back to `IFormattable`/string allocations. + - `PrettyConsoleInterpolatedStringHandler` enables zero-allocation `$"..."` calls for `WriteInterpolated`, `WriteLineInterpolated`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors automatically reset after each invocation, and handlers respect the selected pipe and optional `IFormatProvider`. Recent changes ensure any `object` argument that implements `ISpanFormattable` is emitted through the span-based path before falling back to `IFormattable`/string allocations. `Console.WriteInterpolated`/`WriteLineInterpolated` now return the rendered character count (escape sequences emitted by the handler are excluded), which can be used for padding/layout math. - 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. - 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 - - `WriteInterpolated`/`WriteLineInterpolated` host the interpolated-string handler; `Write`/`WriteLine` overloads target `ISpanFormattable` values (including `ref struct`s) and raw `ReadOnlySpan` spans with optional foreground/background overrides. Implementations rent buffers via `BufferPool` to avoid allocation spikes and always reset colors. + - `WriteInterpolated`/`WriteLineInterpolated` host the interpolated-string handler; `Write`/`WriteLine` overloads target `ISpanFormattable` values (including `ref struct`s) and raw `ReadOnlySpan` spans with optional foreground/background overrides. Implementations rent buffers from `ArrayPool.Shared` to avoid allocation spikes and always reset colors. - TextWriter helpers - - `PrettyConsoleExtensions` surfaces a `TextWriter.WriteWhiteSpaces(int)` extension (available via `PrettyConsoleExtensions.Out/Error`) for allocation-free padding. Use it instead of creating temporary `string`s when building menus, tables, or progress output. + - `ConsoleContext` surfaces the live `Out`/`Error` writers (now with public setters for test doubles) and keeps helpers like `GetWidthOrDefault`. Use `Console.WriteWhiteSpaces(int length, OutputPipe pipe = OutputPipe.Out)` for direct padding from call sites; `TextWriter.WriteWhiteSpaces(int)` remains available on the writers if you already have them on hand. - 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 diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index d8400f6..01ea1f1 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -9,9 +9,9 @@ using BenchmarkDotNet.Order; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.NativeAot; using Perfolizer.Horology; - using Perfolizer.Mathematics.OutlierDetection; namespace Benchmarks; @@ -21,12 +21,23 @@ public Config() { UnionRule = ConfigUnionRule.AlwaysUseLocal; SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); AddDiagnoser(MemoryDiagnoser.Default); - AddJob(Job.Default + var baseJob = Job.Default + .WithId("PGO1") .WithOutlierMode(OutlierMode.RemoveAll) .WithLaunchCount(3) .WithWarmupCount(5) .WithIterationCount(30) - .WithIterationTime(TimeInterval.FromMilliseconds(100))); + .WithIterationTime(TimeInterval.FromMilliseconds(100)) + .WithEnvironmentVariable("DOTNET_TieredPGO", "1"); // default, explicit for clarity + + AddJob(baseJob); + AddJob(baseJob + .WithId("PGO2") + .WithEnvironmentVariable("DOTNET_TieredPGO", "2")); + AddJob(baseJob + .WithId("NativeAOT") + .WithEnvironmentVariable("DOTNET_TieredPGO", "0") // not applicable, but keep deterministic + .WithToolchain(NativeAotToolchain.Net10_0)); AddColumnProvider(DefaultColumnProviders.Instance); HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); WithOrderer(new GroupByTypeOrderer()); @@ -48,7 +59,8 @@ public IEnumerable GetExecutionOrder( // Sort rows in the summary: first by Type, then Method, then Params public IEnumerable GetSummaryOrder( ImmutableArray cases, Summary summary) => - cases.OrderBy(c => c.Descriptor.Type.FullName) + cases.OrderBy(c => c.Job.Id) + .ThenBy(c => c.Descriptor.Type.FullName) .ThenBy(c => c.Parameters.DisplayInfo); // We don’t use highlight groups diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index 6237f20..d8cb3d4 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -23,7 +23,7 @@ public class StyledOutputBenchmarks { [GlobalSetup] public void GlobalSetup() { _outputWriter = Console.Out; - PrettyConsoleExtensions.Out = TextWriter.Null; + ConsoleContext.Out = TextWriter.Null; _ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(TextWriter.Null) }); @@ -32,7 +32,7 @@ public void GlobalSetup() { [GlobalCleanup] public void GlobalCleanup() { - PrettyConsoleExtensions.Out = _outputWriter; + ConsoleContext.Out = _outputWriter; _ansiConsole = AnsiConsole.Console; Console.SetOut(_outputWriter); } diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index 3d73b08..f0d05ea 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -1,4 +1,4 @@ global using Xunit; global using static System.ConsoleColor; -global using static PrettyConsole.PrettyConsoleExtensions; \ No newline at end of file +global using static PrettyConsole.ConsoleContext; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs index 0428e76..72507b7 100644 --- a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -41,6 +41,91 @@ public void AppendFormatted_DoubleBytes_WritesExpected(double value) { Assert.Equal($"Size {FormatBytes(value)}", _writer.ToStringAndFlush()); } + [Fact] + public void CharsWritten_IgnoresAnsiColorAndMarkupSequences() { + int chars = Console.WriteInterpolated($"{ConsoleColor.Red}{Markup.Bold}Hi{Markup.Reset}"); + + var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); + + Assert.Equal("Hi", written); + Assert.Equal(2, chars); + } + + [Theory] + [InlineData(5, " OK")] + [InlineData(-5, "OK ")] + public void Alignment_UsesVisibleLengthWhenMarkupPresent(int alignment, string expected) { + int chars = alignment > 0 + ? Console.WriteInterpolated($"{Markup.Bold}{"OK",5}{Markup.Reset}") + : Console.WriteInterpolated($"{Markup.Bold}{"OK",-5}{Markup.Reset}"); + + var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); + + Assert.Equal(expected, written); + Assert.Equal(expected.Length, chars); + } + + [Fact] + public void WriteLineInterpolated_ReturnsCharsWithoutNewline() { + int chars = Console.WriteLineInterpolated($"Hi"); + + var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); + + Assert.Equal("Hi" + Environment.NewLine, written); + Assert.Equal(2, chars); + } + + [Fact] + public void WriteInterpolated_MixedPrimitivesAndFormats_ReturnsVisibleCount() { + var duration = TimeSpan.FromMinutes(5) + TimeSpan.FromSeconds(7); + + int chars = Console.WriteInterpolated($"Id:{123} Ok:{true} Pi:{3.14159:F2} Char:{'X'} Elapsed:{duration:duration}"); + + var expected = $"Id:123 Ok:True Pi:3.14 Char:X Elapsed:0h 5m 7s"; + Assert.Equal(expected, _writer.ToStringAndFlush()); + Assert.Equal(expected.Length, chars); + } + + [Fact] + public void WriteInterpolated_ColorTuple_DoesNotAffectVisibleCount() { + int chars = Console.WriteInterpolated($"{(ConsoleColor.Red, ConsoleColor.White)}ERR{ConsoleColor.Default} done"); + + var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); + + Assert.Equal("ERR done", written); + Assert.Equal("ERR done".Length, chars); + } + + [Theory] + [InlineData(6)] + [InlineData(-6)] + public void WriteInterpolated_ColorTupleAlignment_UsesWidthOnly(int alignment) { + int chars = alignment > 0 + ? Console.WriteInterpolated($"{(ConsoleColor.Blue, ConsoleColor.White),6}") + : Console.WriteInterpolated($"{(ConsoleColor.Blue, ConsoleColor.White),-6}"); + + var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); + var expected = new string(' ', 6); + + Assert.Equal(expected, written); + Assert.Equal(6, chars); + } + + [Fact] + public void WriteLineInterpolated_WithColorsAndPrimitives_CountExcludesNewline() { + var duration = TimeSpan.FromSeconds(42); + var writer = new StringWriter(); + Error = writer; + + int chars = Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Yellow}[{duration:duration}] {Markup.Bold}done{Markup.Reset}"); + + var stripped = Utilities.StripAnsiSequences(writer.ToString()); + var expected = $"[0h 0m 42s] done{Environment.NewLine}"; + + Assert.Equal(expected, stripped); + Assert.Equal("[0h 0m 42s] done".Length, chars); + } + private static string FormatBytes(double value) { const double formatBytesKb = 1024d; var suffix = 0; diff --git a/PrettyConsole/ConsoleContext.cs b/PrettyConsole/ConsoleContext.cs new file mode 100755 index 0000000..713766a --- /dev/null +++ b/PrettyConsole/ConsoleContext.cs @@ -0,0 +1,66 @@ +using System.Runtime.Versioning; + +namespace PrettyConsole; + +/// +/// The static class the provides the abstraction over and other extensions. +/// +[UnsupportedOSPlatform("android")] +[UnsupportedOSPlatform("browser")] +[UnsupportedOSPlatform("ios")] +[UnsupportedOSPlatform("tvos")] +public static class ConsoleContext { + /// + /// The standard output stream. + /// + public static TextWriter Out { get; set; } = Console.Out; + + /// + /// The standard error stream. + /// + public static TextWriter Error { get; set; } = Console.Error; + + /// + /// The standard input stream. + /// + public static TextReader In { get; set; } = Console.In; + + /// + /// Gets the appropriate based on + /// + /// + internal static TextWriter GetWriter(OutputPipe pipe) + => pipe switch { + OutputPipe.Error => Error, + _ => Out + }; + + /// + /// Returns the current console buffer width or if + /// + /// + internal static int GetWidthOrDefault(int defaultWidth = 120) { + if (Console.IsOutputRedirected) { + return defaultWidth; + } + return Console.BufferWidth; + } + + extension(TextWriter @this) { + /// + /// Writes whitespace to this up to length by chucks + /// + /// + public void WriteWhiteSpaces(int length) { + ReadOnlySpan whiteSpaces = WhiteSpaces; + + while (length > 0) { + int curLength = Math.Min(length, 256); + @this.Write(whiteSpaces.Slice(0, curLength)); + length -= curLength; + } + } + } + + private static readonly string WhiteSpaces = new(' ', 256); +} \ No newline at end of file diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs deleted file mode 100755 index ce2eada..0000000 --- a/PrettyConsole/ConsolePipes.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace PrettyConsole; - -public static partial class PrettyConsoleExtensions { - /// - /// The standard input stream. - /// - public static TextWriter Out { get; internal set; } = Console.Out; - - /// - /// The error output stream. - /// - public static TextWriter Error { get; internal set; } = Console.Error; - - /// - /// The standard input stream. - /// - public static TextReader In { get; internal set; } = Console.In; - - /// - /// Gets the appropriate based on - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - internal static TextWriter GetWriter(OutputPipe pipe) - => pipe switch { - OutputPipe.Error => Error, - _ => Out - }; - - /// - /// Returns the current console buffer width or if - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - internal static int GetWidthOrDefault(int defaultWidth = 120) { - if (Console.IsOutputRedirected) { - return defaultWidth; - } - return Console.BufferWidth; - } -} \ No newline at end of file diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index d98a3e1..6f03156 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -106,7 +106,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d while (!task.IsCompleted && !token.IsCancellationRequested) { try { Console.ForegroundColor = ForegroundColor; - PrettyConsoleExtensions.Error.Write(AnimationSequence[seqIndex]); + ConsoleContext.Error.Write(AnimationSequence[seqIndex]); } finally { Console.ForegroundColor = originalColor; } @@ -164,7 +164,6 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d Console.ResetColor(); } - [MethodImpl(MethodImplOptions.NoInlining)] private Task RunAsyncNonGeneric(Task task, string header, CancellationToken token) => RunAsync(task, header, token); /// diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index b873f71..a4266c5 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -36,9 +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) { - return Confirm(DefaultConfirmValues, true, handler); - } + public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) => Confirm(DefaultConfirmValues, true, handler); /// /// Used to get user confirmation @@ -51,7 +49,7 @@ public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInte /// public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = true, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); - var input = PrettyConsoleExtensions.In.ReadLine(); + var input = ConsoleContext.In.ReadLine(); if (input is null or { Length: 0 }) { return emptyIsTrue; } diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index 114f4a0..320a5f6 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -106,7 +106,7 @@ public static (string option, string subOption) TreeMenu(Dictionary x.Length) + 10; // Used to make sub-tree prefix spaces uniform var pool = ArrayPool.Shared; - var width = PrettyConsoleExtensions.GetWidthOrDefault(); + var width = ConsoleContext.GetWidthOrDefault(); var array = pool.Rent(width); try { var span = new Span(array); @@ -117,20 +117,20 @@ public static (string option, string subOption) TreeMenu(Dictionary 0) { - PrettyConsoleExtensions.Out.WriteWhiteSpaces(remainingLength); + ConsoleContext.Out.WriteWhiteSpaces(remainingLength); } for (int j = 0; j < subChoices.Count; j++) { if (j is not 0) { - PrettyConsoleExtensions.Out.WriteWhiteSpaces(maxMainOption); + ConsoleContext.Out.WriteWhiteSpaces(maxMainOption); } span.TryWrite($" {j + 1}) {subChoices[j]}", out written); - PrettyConsoleExtensions.Out.WriteLine(span.Slice(0, written)); + ConsoleContext.Out.WriteLine(span.Slice(0, written)); } Console.NewLine(); @@ -203,8 +203,8 @@ public static void Table(TList headers, ReadOnlySpan columns) wher Span rowSeparation = stackalloc char[header.Length]; rowSeparation.Fill(rowSeparator); - PrettyConsoleExtensions.Out.WriteLine(header); - PrettyConsoleExtensions.Out.WriteLine(rowSeparation); + ConsoleContext.Out.WriteLine(header); + ConsoleContext.Out.WriteLine(rowSeparation); for (int row = 0; row < height; row++) { for (int i = 0; i < columnsLength; i++) { buffer.Add(columns[i][row].PadRight(lengths[i])); @@ -212,10 +212,10 @@ public static void Table(TList headers, ReadOnlySpan columns) wher var line = string.Join(columnSeparator, buffer); buffer.Clear(); - PrettyConsoleExtensions.Out.WriteLine(line); + ConsoleContext.Out.WriteLine(line); } - PrettyConsoleExtensions.Out.WriteLine(rowSeparation); + ConsoleContext.Out.WriteLine(rowSeparation); } } } \ No newline at end of file diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 2695024..4fcf505 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.0.0 + 5.1.0 enable MIT True @@ -43,10 +43,10 @@ - - v5.0.0 rewrites the API surface as extension members on `System.Console`, introduces `WriteInterpolated`/`WriteLineInterpolated`, and exposes helpers such as `Selection`, `Menu`, `Table`, `ReadLine`, and `TryReadLine` directly on the console. - - `ColoredOutput` and the `Color` struct were removed; styling now flows through the enhanced `ConsoleColor` helpers (`DefaultForeground`, `DefaultBackground`, `Default`, and `/` tuples) together with the interpolated string handler to keep colored output allocation-free. - - `ProgressBar` and `IndeterminateProgressBar` are standalone types, and `ProgressBar.WriteProgressBar` plus the instance helper now accept an optional `maxLineWidth` for constrained layouts. - - New utilities include `TextWriter.WriteWhiteSpaces(int)` for allocation-free padding, `Markup` for ANSI decorations that collapse when output is redirected, and formatting shortcuts such as `duration` (for `TimeSpan`) and `bytes` (for `double`) on `PrettyConsoleInterpolatedStringHandler`. + - v5.1.0 adds return values to `Console.WriteInterpolated`/`WriteLineInterpolated` so callers can reuse the rendered character count for padding/width calculations (escape sequences emitted by the handler are excluded). + - `PrettyConsoleExtensions` is now `ConsoleContext`; the exposed `Out`/`Error`/`In` streams include public setters to simplify testing and redirection scenarios. + - New convenience API `Console.WriteWhiteSpaces(int length, OutputPipe pipe = OutputPipe.Out)` reduces boilerplate when padding output; the `TextWriter.WriteWhiteSpaces` helper remains available on the writers. + - Existing features continue to flow through the span-first, allocation-conscious pipeline introduced in v5.x, including `ConsoleColor` tuple helpers, ANSI `Markup`, and the progress bar helpers. @@ -66,9 +66,5 @@ <_Parameter1>PrettyConsole.Tests.Unit - - <_Parameter1>Benchmarks - - diff --git a/PrettyConsole/PrettyConsoleExtensions.cs b/PrettyConsole/PrettyConsoleExtensions.cs deleted file mode 100755 index f5a27b9..0000000 --- a/PrettyConsole/PrettyConsoleExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Runtime.Versioning; - -namespace PrettyConsole; - -/// -/// The static class the provides the abstraction over and other extensions. -/// -[UnsupportedOSPlatform("android")] -[UnsupportedOSPlatform("browser")] -[UnsupportedOSPlatform("ios")] -[UnsupportedOSPlatform("tvos")] -public static partial class PrettyConsoleExtensions { - extension(TextWriter @this) { - /// - /// Writes whitespace to this up to length by chucks - /// - /// - public void WriteWhiteSpaces(int length) { - ReadOnlySpan whiteSpaces = WhiteSpaces; - - while (length > 0) { - int curLength = Math.Min(length, 256); - @this.Write(whiteSpaces.Slice(0, curLength)); - length -= curLength; - } - } - } - - private static readonly string WhiteSpaces = new(' ', 256); -} \ No newline at end of file diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 8036fea..afe90d4 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -14,6 +14,11 @@ public struct PrettyConsoleInterpolatedStringHandler { private ConsoleColor _currentForeground; private ConsoleColor _currentBackground; + /// + /// The number of characters written in this instance of . + /// + public int CharsWritten { get; private set; } + static PrettyConsoleInterpolatedStringHandler() { if (AnsiColors.Enabled) { ChangeFg = static (writer, color) => writer.Write(AnsiColors.Foreground(color)); @@ -56,7 +61,7 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, OutputPipe pipe, IFormatProvider? provider, out bool shouldAppend) { _currentForeground = ConsoleColor.DefaultForeground; _currentBackground = ConsoleColor.DefaultBackground; - _writer = PrettyConsoleExtensions.GetWriter(pipe); + _writer = ConsoleContext.GetWriter(pipe); _provider = provider; shouldAppend = true; } @@ -64,8 +69,9 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo /// /// Appends a literal segment supplied by the compiler. /// - public readonly void AppendLiteral(string value) { + public void AppendLiteral(string value) { _writer.Write(value); + CharsWritten += GetVisibleLength(value.AsSpan()); } /// @@ -74,7 +80,7 @@ public readonly void AppendLiteral(string value) { /// Formatted string. /// Optional alignment as provided by the interpolation. /// Unused string format specifier. - public readonly void AppendFormatted(string? value, int alignment = 0, string? format = null) { + public void AppendFormatted(string? value, int alignment = 0, string? format = null) { AppendString(value, alignment); } @@ -83,7 +89,7 @@ public readonly void AppendFormatted(string? value, int alignment = 0, string? f /// /// Characters to write. /// Optional alignment as provided by the interpolation. - public readonly void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { + public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { AppendSpan(value, alignment); } @@ -92,7 +98,7 @@ public readonly void AppendFormatted(scoped ReadOnlySpan value, int alignm /// /// Character to write. /// Optional alignment as provided by the interpolation. - public readonly void AppendFormatted(char value, int alignment = 0) { + public void AppendFormatted(char value, int alignment = 0) { Span buffer = [value]; AppendSpan(buffer, alignment); } @@ -137,7 +143,7 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c /// /// /// - public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) + public void AppendFormatted(TimeSpan timeSpan, string? format = null) => AppendFormatted(timeSpan, alignment: 0, format); /// @@ -146,7 +152,7 @@ public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) /// /// /// - public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? format = null) { + public void AppendFormatted(TimeSpan timeSpan, int alignment, string? format = null) { if (format != "duration") { AppendSpanFormattable(timeSpan, alignment, format); return; @@ -165,7 +171,7 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f /// /// /// - public readonly void AppendFormatted(double num, string? format = null) + public void AppendFormatted(double num, string? format = null) => AppendFormatted(num, alignment: 0, format); /// @@ -174,7 +180,7 @@ public readonly void AppendFormatted(double num, string? format = null) /// /// /// - public readonly void AppendFormatted(double num, int alignment, string? format = null) { + public void AppendFormatted(double num, int alignment, string? format = null) { if (format != "bytes") { AppendSpanFormattable(num, alignment, format); return; @@ -202,28 +208,28 @@ public readonly void AppendFormatted(double num, int alignment, string? format = /// /// Appends a value type that implements without boxing. /// - public readonly void AppendFormatted(T value) where T : ISpanFormattable { + public void AppendFormatted(T value) where T : ISpanFormattable { AppendSpanFormattable(value, alignment: 0, format: null); } /// /// Appends a value type that implements without boxing while respecting alignment. /// - public readonly void AppendFormatted(T value, int alignment) where T : ISpanFormattable { + public void AppendFormatted(T value, int alignment) where T : ISpanFormattable { AppendSpanFormattable(value, alignment, format: null); } /// /// Appends a value type that implements without boxing using the provided format string. /// - public readonly void AppendFormatted(T value, string? format) where T : ISpanFormattable { + 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 readonly void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { + public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { AppendSpanFormattable(value, alignment, format); } @@ -233,7 +239,7 @@ public readonly void AppendFormatted(T value, int alignment, string? format) /// Value to write. /// Optional alignment as provided by the interpolation. /// Optional format specifier. - public readonly void AppendFormatted(object? value, int alignment = 0, string? format = null) { + public void AppendFormatted(object? value, int alignment = 0, string? format = null) { switch (value) { case null: { AppendSpan(ReadOnlySpan.Empty, alignment); @@ -258,7 +264,7 @@ public readonly void AppendFormatted(object? value, int alignment = 0, string? f } } - private readonly void AppendSpanFormattable(T value, int alignment, string? format) + private void AppendSpanFormattable(T value, int alignment, string? format) where T : ISpanFormattable { ReadOnlySpan formatSpan = format.AsSpan(); Span buffer = stackalloc char[128]; @@ -285,32 +291,37 @@ private readonly void AppendSpanFormattable(T value, int alignment, string? f } } - private readonly void AppendString(string? value, int alignment) { + private void AppendString(string? value, int alignment) { // AppendSpan handles null and empty spans AppendSpan(value.AsSpan(), alignment); } - private readonly void AppendSpan(scoped ReadOnlySpan span, int alignment) { + private void AppendSpan(scoped ReadOnlySpan span, int alignment) { + int visibleLength = GetVisibleLength(span); if (alignment == 0) { if (!span.IsEmpty) { _writer.Write(span); } + CharsWritten += visibleLength; return; } bool leftAlign = alignment < 0; int width = Math.Abs(alignment); - int padding = width - span.Length; + int padding = width - visibleLength; if (padding > 0 && !leftAlign) { WritePadding(padding); + CharsWritten += padding; } if (!span.IsEmpty) { _writer.Write(span); } + CharsWritten += visibleLength; if (padding > 0 && leftAlign) { WritePadding(padding); + CharsWritten += padding; } } @@ -336,4 +347,10 @@ public void ResetColors() { /// Writes a new line to the used internally. /// public readonly void AppendNewLine() => _writer.WriteLine(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetVisibleLength(ReadOnlySpan span) + => span.Length > 0 && span[0] == '\e' + ? 0 + : span.Length; } \ No newline at end of file diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 755220c..8980f22 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -46,7 +46,6 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty, true); /// @@ -56,7 +55,6 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty, true); /// @@ -68,7 +66,6 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) => Update((int)percentage, status, sameLine); @@ -88,7 +85,7 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr Console.ClearNextLines(1, OutputPipe.Error); if (status.Length > 0) { Console.Write(status, OutputPipe.Error, ForegroundColor); - PrettyConsoleExtensions.GetWriter(OutputPipe.Error).WriteWhiteSpaces(1); + ConsoleContext.GetWriter(OutputPipe.Error).WriteWhiteSpaces(1); WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); } } else { @@ -110,7 +107,6 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. /// Optional total line length (including brackets and percentage). When provided, the rendered output will not exceed this width unless the decorations already require more characters. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar, maxLineWidth); @@ -122,12 +118,11 @@ public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleC /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. /// Optional total line length (including brackets and percentage). When provided, the rendered output will not exceed this width unless the decorations already require more characters. - [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) { Console.ResetColor(); int p = Math.Clamp(percentage, 0, 100); - int bufferWidth = Math.Max(0, PrettyConsoleExtensions.GetWidthOrDefault() - Console.CursorLeft); + int bufferWidth = Math.Max(0, ConsoleContext.GetWidthOrDefault() - Console.CursorLeft); const int bracketsAndSpacing = 3; // '[' + ']' + ' ' const int percentageWidth = 3; // numeric portion width @@ -141,7 +136,7 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo int barLength = Math.Max(0, constrainedWidth - decorationWidth); - var writer = PrettyConsoleExtensions.GetWriter(pipe); + var writer = ConsoleContext.GetWriter(pipe); Console.Write('[', pipe); if (barLength > 0) { diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs index 44c83a0..dd341f8 100755 --- a/PrettyConsole/ReadLineExtensions.cs +++ b/PrettyConsole/ReadLineExtensions.cs @@ -16,7 +16,7 @@ public static class ReadLineExtensions { /// True if the parsing was successful, false otherwise public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { handler.ResetColors(); - var input = PrettyConsoleExtensions.In.ReadLine(); + var input = ConsoleContext.In.ReadLine(); return T.TryParse(input, CultureInfo.CurrentCulture, out result); } @@ -61,7 +61,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, [Interp /// 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 { handler.ResetColors(); - var input = PrettyConsoleExtensions.In.ReadLine(); + var input = ConsoleContext.In.ReadLine(); var res = Enum.TryParse(input, ignoreCase, out result); if (!res) { result = @default; diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index 88f6ae7..3a832b7 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -21,6 +21,15 @@ internal static void ConfigureCursorAccessors(Func? cursorTopAccessor, Acti } extension(Console) { + /// + /// Write white spaces to + /// + /// + /// + public static void WriteWhiteSpaces(int length, OutputPipe pipe = OutputPipe.Out) { + ConsoleContext.GetWriter(pipe).WriteWhiteSpaces(length); + } + /// /// Clears the next . /// @@ -30,8 +39,8 @@ internal static void ConfigureCursorAccessors(Func? cursorTopAccessor, Acti /// Useful for clearing output of overriding functions, like the ProgressBar /// public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { - var textWriter = PrettyConsoleExtensions.GetWriter(pipe); - var lineLength = PrettyConsoleExtensions.GetWidthOrDefault(); + var textWriter = ConsoleContext.GetWriter(pipe); + var lineLength = ConsoleContext.GetWidthOrDefault(); var currentLine = GetCurrentLine(); GoToLine(currentLine); for (int i = 0; i < lines; i++) { @@ -44,7 +53,7 @@ public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) /// Used to end current line or write an empty one, depends whether the current line has any text. /// public static void NewLine(OutputPipe pipe = OutputPipe.Out) { - PrettyConsoleExtensions.GetWriter(pipe).WriteLine(); + ConsoleContext.GetWriter(pipe).WriteLine(); } /// diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index ca8115f..438cf58 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -11,8 +11,10 @@ public static class WriteExtensions { /// Writes interpolated content using to . /// /// Interpolated string handler that streams the content. - public static void WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + /// The number of characters written by the handler. + public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); + return handler.CharsWritten; } /// @@ -20,8 +22,10 @@ public static void WriteInterpolated([InterpolatedStringHandlerArgument] PrettyC /// /// Destination pipe. Defaults to . /// Interpolated string handler that streams the content. - public static void WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + /// The number of characters written by the handler. + public static int WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); + return handler.CharsWritten; } /// @@ -131,7 +135,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); - PrettyConsoleExtensions.GetWriter(pipe).Write(span); + ConsoleContext.GetWriter(pipe).Write(span); Console.ResetColor(); } } diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index 97bd2ff..b454670 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -9,10 +9,12 @@ public static class WriteLineExtensions { /// Writes interpolated content using to . /// /// Interpolated string handler that streams the content. + /// The number of characters written by the handler. [OverloadResolutionPriority(int.MaxValue)] - public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); handler.AppendNewLine(); + return handler.CharsWritten; } /// @@ -20,9 +22,11 @@ public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] Pre /// /// Destination pipe. Defaults to . /// Interpolated string handler that streams the content. - public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + /// The number of characters written by the handler. + public static int WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); handler.AppendNewLine(); + return handler.CharsWritten; } /// diff --git a/README.md b/README.md index d5f5fd0..e065df1 100755 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ PrettyConsole is a high-performance, ultra-low-latency, allocation-free extensio - 🎨 Inline color composition with `ConsoleColor` tuples and helpers (`DefaultForeground`, `DefaultBackground`, `Default`) - 🔁 Advanced rendering primitives (`Overwrite`, `ClearNextLines`, `GoToLine`, progress bars) that respect console pipes - 🧰 Rich input helpers (`TryReadLine`, `Confirm`, `RequestAnyInput`) with `IParsable` and enum support -- ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `TextWriter.WriteWhiteSpaces`) +- ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `Console.WriteWhiteSpaces` / `TextWriter.WriteWhiteSpaces`) - ⛓ Output routing through `OutputPipe.Out` and `OutputPipe.Error` so piping/redirects continue to work ## Performance @@ -47,7 +47,7 @@ This setup lets you call `Console.WriteInterpolated`, `Console.Overwrite`, `Cons ### Interpolated strings & inline colors -`PrettyConsoleInterpolatedStringHandler` streams interpolated content directly to the selected pipe without allocating. Colors auto-reset at the end of each call. +`PrettyConsoleInterpolatedStringHandler` streams interpolated content directly to the selected pipe without allocating. 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}!"); @@ -111,7 +111,7 @@ Console.NewLine(); // writes newline to the default output pipe Console.Write(percentage, OutputPipe.Out, ConsoleColor.Cyan, ConsoleColor.DefaultBackground, format: "F2", formatProvider: null); ``` -Behind the scenes these overloads rent buffers via `BufferPool` and route output to the correct pipe through `PrettyConsoleExtensions.GetWriter`. +Behind the scenes these overloads rent buffers from the shared `ArrayPool` and route output to the correct pipe through `ConsoleContext.GetWriter`. ### Basic inputs @@ -154,10 +154,11 @@ Console.SetColors(ConsoleColor.White, ConsoleColor.DarkBlue); Console.ResetColors(); ``` -`PrettyConsoleExtensions.Out`/`Error` expose the live writers. Each writer now has `WriteWhiteSpaces(int)` for zero-allocation padding: +`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: ```csharp -PrettyConsoleExtensions.Error.WriteWhiteSpaces(8); // pad status blocks +Console.WriteWhiteSpaces(8, OutputPipe.Error); // pad status blocks +ConsoleContext.Error.WriteWhiteSpaces(4); // same via writer ``` ### Advanced outputs @@ -268,15 +269,15 @@ Each producer reports progress over the channel, the consumer loops with `ReadAl ### Pipes & writers -PrettyConsole keeps the original console streams accessible: +PrettyConsole keeps the original console streams accessible (and settable for tests) via `ConsoleContext`: ```csharp -TextWriter @out = PrettyConsoleExtensions.Out; -TextWriter @err = PrettyConsoleExtensions.Error; -TextReader @in = PrettyConsoleExtensions.In; +TextWriter @out = ConsoleContext.Out; +TextWriter @err = ConsoleContext.Error; +TextReader @in = ConsoleContext.In; ``` -Use these when you need direct writer access (custom buffering, `WriteWhiteSpaces`, etc.). In cases where you must call raw `System.Console` APIs (e.g., `Console.ReadKey(true)`), do so explicitly—PrettyConsole never hides the built-in console. +Use these when you need direct writer access (custom buffering, `WriteWhiteSpaces`, etc.) or swap in mocks for testing. In cases where you must call raw `System.Console` APIs (e.g., `Console.ReadKey(true)`), do so explicitly—PrettyConsole never hides the built-in console. ## Contributing diff --git a/Versions.md b/Versions.md index 8cdda06..c564511 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,13 @@ # Versions +## v5.1.0 + +- `Console.WriteInterpolated` and `Console.WriteLineInterpolated` now return a `int` that contains the number of characters written using the handler. This could be used to help calculate paddings or other things when creating structured output. + - It will ignore escape sequences that were added using the handler like `ConsoleColor` or `Markup` but if you hardcode your own they might be taken into account. As such, if you do this, I recommend first checking the length without using `ConsoleColor` or `Markup`, then using this result for the calculation. +- `PrettyConsoleExtensions` that contains the `Out`, `Err`, `In`, etc... was renamed to `ConsoleContext`. +- The standard `Out`, `Err`, `In` streams now have a public setter, so end users could mock it in their own tests. +- `Console.WriteWhiteSpaces(length, OutputPipe)` was added to reduce the complexity of using the `TextWriter` extension. + ## v5.0.0 - .NET 10+ This version contains a lot of breaking changes, but they were necessary to trim legacy and sub-optimal things from the library to ensure it remains the best performing library for stylized console outputs.