From cb772c1149875cccb4350539ed4fbcd87d62abfe Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 25 Nov 2025 12:05:54 +0200 Subject: [PATCH 01/23] Show only base by default --- Benchmarks/Config.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index 01ea1f1..989f9ea 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -31,13 +31,12 @@ public Config() { .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)); + // AddJob(baseJob + // .WithId("PGO2") + // .WithEnvironmentVariable("DOTNET_TieredPGO", "2")); + // AddJob(baseJob + // .WithId("NativeAOT") + // .WithToolchain(NativeAotToolchain.Net10_0)); AddColumnProvider(DefaultColumnProviders.Instance); HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); WithOrderer(new GroupByTypeOrderer()); From bddcf97fe80d94fb1ce8aa3140200fd60da9c98c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 13:06:34 +0200 Subject: [PATCH 02/23] Improve string handler --- PrettyConsole.Tests.Unit/ConsoleColorTests.cs | 12 +- PrettyConsole.Tests.Unit/MarkupTests.cs | 11 +- .../PrettyConsole.Tests.Unit.csproj | 4 +- PrettyConsole/AnsiColors.cs | 12 +- PrettyConsole/ConsoleContext.cs | 12 + PrettyConsole/InputRequestExtensions.cs | 4 +- PrettyConsole/Markup.cs | 38 +-- PrettyConsole/MenuExtensions.cs | 6 +- PrettyConsole/PrettyConsole.csproj | 2 +- .../PrettyConsoleInterpolatedStringHandler.cs | 229 +++++++++++------- PrettyConsole/ReadLineExtensions.cs | 4 +- PrettyConsole/WriteExtensions.cs | 4 +- PrettyConsole/WriteLineExtensions.cs | 4 +- Versions.md | 4 + 14 files changed, 195 insertions(+), 151 deletions(-) diff --git a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs index b575a75..86c2a71 100644 --- a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs +++ b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs @@ -24,17 +24,13 @@ public void ConsoleColor_DefaultColors() { [Fact] public void AnsiColors_DefaultForeground_UsesResetSequence() { - if (AnsiColors.Enabled) { - var sequence = AnsiColors.Foreground((ConsoleColor)(-1)); - Assert.Equal("\e[39m", sequence); - } + var sequence = AnsiColors.Foreground((ConsoleColor)(-1)); + Assert.Equal("\e[39m", sequence); } [Fact] public void AnsiColors_DefaultBackground_UsesResetSequence() { - if (AnsiColors.Enabled) { - var sequence = AnsiColors.Background((ConsoleColor)(-1)); - Assert.Equal("\e[49m", sequence); - } + var sequence = AnsiColors.Background((ConsoleColor)(-1)); + Assert.Equal("\e[49m", sequence); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/MarkupTests.cs b/PrettyConsole.Tests.Unit/MarkupTests.cs index eea4b9c..1b163b4 100644 --- a/PrettyConsole.Tests.Unit/MarkupTests.cs +++ b/PrettyConsole.Tests.Unit/MarkupTests.cs @@ -3,14 +3,7 @@ namespace PrettyConsole.Tests.Unit; public class MarkupTests { [Fact] public void Markup_Enabled_MatchesConsoleRedirectionState() { - var expectedEnabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; - Assert.Equal(expectedEnabled, Markup.Enabled); - if (expectedEnabled) { - Assert.Equal("\e[4m", Markup.Underline); - Assert.Equal("\e[0m", Markup.Reset); - } else { - Assert.Equal(string.Empty, Markup.Underline); - Assert.Equal(string.Empty, Markup.Reset); - } + Assert.Equal("\e[4m", Markup.Underline); + Assert.Equal("\e[0m", Markup.Reset); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj index 4137967..34a06f1 100755 --- a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj +++ b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj @@ -18,8 +18,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/PrettyConsole/AnsiColors.cs b/PrettyConsole/AnsiColors.cs index 4c2dc21..d2f67b7 100644 --- a/PrettyConsole/AnsiColors.cs +++ b/PrettyConsole/AnsiColors.cs @@ -4,18 +4,10 @@ internal static class AnsiColors { private const string ForegroundResetSequence = "\e[39m"; private const string BackgroundResetSequence = "\e[49m"; - private static readonly string[] ForegroundCodes = null!; - private static readonly string[] BackgroundCodes = null!; - - /// - /// Gets a value indicating whether ANSI color sequences are emitted. - /// - public static readonly bool Enabled; + private static readonly string[] ForegroundCodes; + private static readonly string[] BackgroundCodes; static AnsiColors() { - Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; - if (!Enabled) return; - ForegroundCodes = new string[16]; BackgroundCodes = new string[16]; foreach (var color in Enum.GetValues()) { diff --git a/PrettyConsole/ConsoleContext.cs b/PrettyConsole/ConsoleContext.cs index 713766a..b8b8873 100755 --- a/PrettyConsole/ConsoleContext.cs +++ b/PrettyConsole/ConsoleContext.cs @@ -35,6 +35,18 @@ internal static TextWriter GetWriter(OutputPipe pipe) _ => Out }; + /// + /// Gets the appropriate based on + /// + /// + internal static (TextWriter Writer, bool IsRedirected) GetWriterAndRedirection(OutputPipe pipe) { + return pipe switch { + OutputPipe.Out => (Out, Console.IsOutputRedirected), + OutputPipe.Error => (Error, Console.IsErrorRedirected), + _ => throw new InvalidOperationException("A pipe that isn't Out or Error is not supported."), + }; + } + /// /// Returns the current console buffer width or if /// diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index a4266c5..f5ecd05 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -25,7 +25,7 @@ internal static void ConfigureReadKey(Func? readKey) { /// /// Interpolated string handler that streams the content. public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - handler.ResetColors(); + handler.Flush(); _ = s_readKey(); } @@ -48,7 +48,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) { - handler.ResetColors(); + handler.Flush(); var input = ConsoleContext.In.ReadLine(); if (input is null or { Length: 0 }) { return emptyIsTrue; diff --git a/PrettyConsole/Markup.cs b/PrettyConsole/Markup.cs index 8de7863..31af8c1 100644 --- a/PrettyConsole/Markup.cs +++ b/PrettyConsole/Markup.cs @@ -4,68 +4,48 @@ namespace PrettyConsole; /// Provides ANSI escape sequences for simple inline decorations. /// public static class Markup { - /// - /// Gets a value indicating whether markup sequences are emitted. - /// - public static readonly bool Enabled; - /// /// Resets all decorations and colors. /// - public static readonly string Reset = string.Empty; + public const string Reset = "\e[0m"; /// /// Enables underlined text. /// - public static readonly string Underline = string.Empty; + public const string Underline = "\e[4m"; /// /// Disables underlined text. /// - public static readonly string ResetUnderline = string.Empty; + public const string ResetUnderline = "\e[24m"; /// /// Enables bold text. /// - public static readonly string Bold = string.Empty; + public const string Bold = "\e[1m"; /// /// Disables bold text. /// - public static readonly string ResetBold = string.Empty; + public const string ResetBold = "\e[22m"; /// /// Enables italic text. /// - public static readonly string Italic = string.Empty; + public const string Italic = "\e[3m"; /// /// Disables italic text. /// - public static readonly string ResetItalic = string.Empty; + public const string ResetItalic = "\e[23m"; /// /// Enables strikethrough text. /// - public static readonly string Strikethrough = string.Empty; + public const string Strikethrough = "\e[9m"; /// /// Disables strikethrough text. /// - public static readonly string ResetStrikethrough = string.Empty; - - static Markup() { - Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; - if (Enabled) { - Reset = "\e[0m"; - Underline = "\e[4m"; - ResetUnderline = "\e[24m"; - Bold = "\e[1m"; - ResetBold = "\e[22m"; - Italic = "\e[3m"; - ResetItalic = "\e[23m"; - Strikethrough = "\e[9m"; - ResetStrikethrough = "\e[29m"; - } - } + public const string ResetStrikethrough = "\e[29m"; } \ No newline at end of file diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index 320a5f6..fc62896 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -18,8 +18,8 @@ public static class MenuExtensions { /// public static string Selection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { - handler.ResetColors(); handler.AppendNewLine(); + handler.Flush(); for (int i = 0; i < choices.Count; i++) { Console.WriteLineInterpolated($" {i + 1}) {choices[i]}"); @@ -51,8 +51,8 @@ public static string Selection(TList choices, [InterpolatedStringHandlerA /// public static string[] MultiSelection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { - handler.ResetColors(); handler.AppendNewLine(); + handler.Flush(); for (int i = 0; i < choices.Count; i++) { Console.WriteLineInterpolated($" {i + 1}) {choices[i]}"); @@ -99,8 +99,8 @@ 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 { - handler.ResetColors(); handler.AppendNewLine(); + handler.Flush(); var menuKeys = menu.Keys.ToArray(); var maxMainOption = menuKeys.Max(static x => x.Length) + 10; // Used to make sub-tree prefix spaces uniform diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 4fcf505..430e972 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.1.0 + 5.2.0 enable MIT True diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index afe90d4..890af6b 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -2,15 +2,27 @@ namespace PrettyConsole; +//TODO: Check if optional values for alignment can reduce overloads + /// -/// Interpolated string handler that streams segments directly to an while allowing inline color changes. +/// Interpolated string handler that handles formatting /// [InterpolatedStringHandler] 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 static readonly Action ChangeFg; - private static readonly Action ChangeBg; + private ConsoleColor _currentForeground; private ConsoleColor _currentBackground; @@ -19,16 +31,6 @@ public struct PrettyConsoleInterpolatedStringHandler { /// public int CharsWritten { get; private set; } - static PrettyConsoleInterpolatedStringHandler() { - if (AnsiColors.Enabled) { - ChangeFg = static (writer, color) => writer.Write(AnsiColors.Foreground(color)); - ChangeBg = static (writer, color) => writer.Write(AnsiColors.Background(color)); - } else { - ChangeFg = static (_, color) => Console.ForegroundColor = color; - ChangeBg = static (_, color) => Console.BackgroundColor = color; - } - } - /// /// Creates a new handler that writes to . /// @@ -59,9 +61,10 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo /// Optional format provider used when formatting values. /// Always ; reserved for future short-circuiting. public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, OutputPipe pipe, IFormatProvider? provider, out bool shouldAppend) { + _buffer = BufferPool.Rent(_capacity); _currentForeground = ConsoleColor.DefaultForeground; _currentBackground = ConsoleColor.DefaultBackground; - _writer = ConsoleContext.GetWriter(pipe); + (_writer, _isRedirected) = ConsoleContext.GetWriterAndRedirection(pipe); _provider = provider; shouldAppend = true; } @@ -70,8 +73,8 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo /// Appends a literal segment supplied by the compiler. /// public void AppendLiteral(string value) { - _writer.Write(value); - CharsWritten += GetVisibleLength(value.AsSpan()); + ThrowIfFlushed(); + AppendSpanCore(value); } /// @@ -85,7 +88,7 @@ public void AppendFormatted(string? value, int alignment = 0, string? format = n } /// - /// Appends a span segment without allocations. + /// Appends a span segment /// /// Characters to write. /// Optional alignment as provided by the interpolation. @@ -103,13 +106,33 @@ public void AppendFormatted(char value, int alignment = 0) { AppendSpan(buffer, alignment); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ChangeForeground(ConsoleColor foreground) => AppendSpanCore(AnsiColors.Foreground(foreground)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ChangeBackground(ConsoleColor background) => AppendSpanCore(AnsiColors.Background(background)); + /// - /// Sets the console foreground color to . + /// Sets the foreground color to . /// public void AppendFormatted(ConsoleColor color) { + if (_isRedirected) return; if (_currentForeground != color) { + ThrowIfFlushed(); _currentForeground = color; - ChangeFg(_writer, _currentForeground); + ChangeForeground(color); + } + } + + /// + /// Sets the background color to . + /// + public void AppendFormattedBackground(ConsoleColor color) { + if (_isRedirected) return; + if (_currentBackground != color) { + ThrowIfFlushed(); + _currentBackground = color; + ChangeBackground(color); } } @@ -118,14 +141,8 @@ public void AppendFormatted(ConsoleColor color) { /// /// public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors) { - if (_currentForeground != colors.Foreground) { - _currentForeground = colors.Foreground; - ChangeFg(_writer, _currentForeground); - } - if (_currentBackground != colors.Background) { - _currentBackground = colors.Background; - ChangeBg(_writer, _currentBackground); - } + AppendFormatted(colors.Foreground); + AppendFormattedBackground(colors.Background); } /// @@ -158,9 +175,17 @@ public void AppendFormatted(TimeSpan timeSpan, int alignment, string? format = n return; } - Span buffer = stackalloc char[32]; - if (buffer.TryWrite($"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m {timeSpan.Seconds}s", out int written)) { - AppendSpan(buffer.Slice(0, written), alignment); + ThrowIfFlushed(); + + const int requiredLength = 32; + + EnsureCapacity(requiredLength); + + Span dest = _buffer.AsSpan(_index); + + if (dest.TryWrite($"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m {timeSpan.Seconds}s", out int written)) { + _index += written; + CharsWritten += written; } } @@ -186,6 +211,8 @@ public void AppendFormatted(double num, int alignment, string? format = null) { return; } + ThrowIfFlushed(); + const double formatBytesKb = 1024d; const double formatBytesDivisor = 1 / formatBytesKb; var suffix = 0; @@ -197,11 +224,15 @@ public void AppendFormatted(double num, int alignment, string? format = null) { const double defaultThreshold = 1e90; - Span buffer = num <= defaultThreshold - ? stackalloc char[128] - : stackalloc char[512]; - if (buffer.TryWrite($"{num:#,##0.##} {unit}", out int written)) { - AppendSpan(buffer.Slice(0, written), alignment); + int requiredLength = num <= defaultThreshold ? 128 : 512; + + EnsureCapacity(requiredLength); + + Span dest = _buffer.AsSpan(_index); + + if (dest.TryWrite($"{num:#,##0.##} {unit}", out int written)) { + _index += written; + CharsWritten += written; } } @@ -266,28 +297,19 @@ public void AppendFormatted(object? value, int alignment = 0, string? format = n private void AppendSpanFormattable(T value, int alignment, string? format) where T : ISpanFormattable { + ThrowIfFlushed(); ReadOnlySpan formatSpan = format.AsSpan(); - Span buffer = stackalloc char[128]; - if (value.TryFormat(buffer, out int charsWritten, formatSpan, _provider)) { - AppendSpan(buffer.Slice(0, charsWritten), alignment); - return; - } - - int lowerBound = 4096; - var pool = ArrayPool.Shared; while (true) { - var array = pool.Rent(lowerBound); - try { - buffer = new Span(array); - if (value.TryFormat(array, out charsWritten, formatSpan, _provider)) { - AppendSpan(buffer.Slice(0, charsWritten), alignment); - return; - } - } finally { - pool.Return(array); + Span dest = _buffer.AsSpan(_index); + + if (value.TryFormat(dest, out int charsWritten, formatSpan, _provider)) { + _index += charsWritten; + CharsWritten += charsWritten; + return; } - lowerBound *= 2; + + Grow(_capacity * 2); } } @@ -297,27 +319,26 @@ private void AppendString(string? value, int alignment) { } private void AppendSpan(scoped ReadOnlySpan span, int alignment) { - int visibleLength = GetVisibleLength(span); + ThrowIfFlushed(); + if (alignment == 0) { - if (!span.IsEmpty) { - _writer.Write(span); - } - CharsWritten += visibleLength; + AppendSpanCore(span); return; } bool leftAlign = alignment < 0; 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; } - if (!span.IsEmpty) { - _writer.Write(span); - } - CharsWritten += visibleLength; + AppendSpanCore(span, false); if (padding > 0 && leftAlign) { WritePadding(padding); @@ -325,32 +346,78 @@ private void AppendSpan(scoped ReadOnlySpan span, int alignment) { } } - private readonly void WritePadding(int count) { - _writer.WriteWhiteSpaces(count); + private void AppendSpanCore(scoped ReadOnlySpan span, bool ensureCapacity = true) { + int length = span.Length; + + if (length == 0) return; + + bool isEscapeSequence = span[0] == '\e'; + + if (isEscapeSequence && _isRedirected) return; + + if (ensureCapacity) EnsureCapacity(length); + span.CopyTo(_buffer.AsSpan(_index, length)); + _index += length; + + if (!isEscapeSequence) CharsWritten += length; + } + + private void WritePadding(int count) { + _buffer.AsSpan(_index, count).Fill(' '); + _index += count; } /// - /// Resets the console colors if they changed. + /// Writes a new line to the internal buffer. /// - public void ResetColors() { - if (_currentForeground != ConsoleColor.DefaultForeground) { - _currentForeground = ConsoleColor.DefaultForeground; - ChangeFg(_writer, _currentForeground); - } - if (_currentBackground != ConsoleColor.DefaultBackground) { - _currentBackground = ConsoleColor.DefaultBackground; - ChangeBg(_writer, _currentBackground); + public void AppendNewLine() { + ThrowIfFlushed(); + string newline = Environment.NewLine; + AppendSpanCore(newline); + CharsWritten -= newline.Length; + } + + private void EnsureCapacity(int capacity) { + int available = _buffer.Length - _index; + if (capacity <= available) return; + + int required = _index + capacity; + int targetCapacity = _capacity; + while (targetCapacity < required) { + targetCapacity *= 2; } + + Grow(targetCapacity); + } + + private void Grow(int targetCapacity) { + char[] temp = _buffer; + + _capacity = targetCapacity; + _buffer = BufferPool.Rent(_capacity); + Span written = temp.AsSpan(0, _index); + written.CopyTo(_buffer); + written.Clear(); + BufferPool.Return(temp, false); + } + + private readonly void ThrowIfFlushed() { + if (!_flushed) return; + + throw new InvalidOperationException("The handler was consumed and its buffer have been freed."); } /// - /// Writes a new line to the used internally. + /// Writes the underline buffer to the held . /// - public readonly void AppendNewLine() => _writer.WriteLine(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetVisibleLength(ReadOnlySpan span) - => span.Length > 0 && span[0] == '\e' - ? 0 - : span.Length; + public void Flush() { + ThrowIfFlushed(); + AppendFormatted(ConsoleColor.DefaultForeground); + AppendFormattedBackground(ConsoleColor.DefaultBackground); + Span written = new(_buffer, 0, _index); + _writer.Write(written); + written.Clear(); + BufferPool.Return(_buffer, false); + _flushed = true; + } } \ No newline at end of file diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs index dd341f8..0b7c64f 100755 --- a/PrettyConsole/ReadLineExtensions.cs +++ b/PrettyConsole/ReadLineExtensions.cs @@ -15,7 +15,7 @@ public static class ReadLineExtensions { /// Interpolated string handler that streams the content. /// True if the parsing was successful, false otherwise public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { - handler.ResetColors(); + handler.Flush(); var input = ConsoleContext.In.ReadLine(); return T.TryParse(input, CultureInfo.CurrentCulture, out result); } @@ -60,7 +60,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, [Interp /// Interpolated string handler that streams the content. /// 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(); + handler.Flush(); var input = ConsoleContext.In.ReadLine(); var res = Enum.TryParse(input, ignoreCase, out result); if (!res) { diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 438cf58..0ccab5e 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -13,7 +13,7 @@ public static class WriteExtensions { /// Interpolated string handler that streams the content. /// The number of characters written by the handler. public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - handler.ResetColors(); + handler.Flush(); return handler.CharsWritten; } @@ -24,7 +24,7 @@ public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyCo /// Interpolated string handler that streams the content. /// The number of characters written by the handler. public static int WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - handler.ResetColors(); + handler.Flush(); return handler.CharsWritten; } diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index b454670..102e0e6 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -12,8 +12,8 @@ public static class WriteLineExtensions { /// The number of characters written by the handler. [OverloadResolutionPriority(int.MaxValue)] public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - handler.ResetColors(); handler.AppendNewLine(); + handler.Flush(); return handler.CharsWritten; } @@ -24,8 +24,8 @@ public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] Pret /// Interpolated string handler that streams the content. /// The number of characters written by the handler. public static int WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - handler.ResetColors(); handler.AppendNewLine(); + handler.Flush(); return handler.CharsWritten; } diff --git a/Versions.md b/Versions.md index c564511..74aac64 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,9 @@ # Versions +## v5.2.0 + +- `PrettyConsoleInterpolatedStringHandler` was rewritten to buffer the contents instead of stream them, along with other optimizations, massively improving performance. + ## 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. From f77468b3b84995086ed3f4bdd8e4bf7346d22d8c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 13:40:01 +0200 Subject: [PATCH 03/23] Fix alignment issue --- .../PrettyConsoleInterpolatedStringHandler.cs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 890af6b..5cb7998 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -300,17 +300,41 @@ private void AppendSpanFormattable(T value, int alignment, string? format) ThrowIfFlushed(); ReadOnlySpan formatSpan = format.AsSpan(); + int charsWritten; + int start = _index; + while (true) { Span dest = _buffer.AsSpan(_index); - if (value.TryFormat(dest, out int charsWritten, formatSpan, _provider)) { + if (value.TryFormat(dest, out charsWritten, formatSpan, _provider)) { _index += charsWritten; CharsWritten += charsWritten; - return; + break; } Grow(_capacity * 2); } + + if (alignment == 0) return; + + if (alignment > 0) { // shift forward and prefix whitespaces + int padding = alignment - charsWritten; + if (padding <= 0) return; + + EnsureCapacity(padding); + var written = _buffer.AsSpan(start, charsWritten); + written.CopyTo(_buffer.AsSpan(start + padding, charsWritten)); + _buffer.AsSpan(start, padding).Fill(' '); + _index += padding; + CharsWritten += padding; + } else { // suffix whitespaces + int targetWidth = -alignment; + int trailing = targetWidth - charsWritten; + if (trailing > 0) { + WritePadding(trailing); + CharsWritten += trailing; + } + } } private void AppendString(string? value, int alignment) { @@ -420,4 +444,4 @@ public void Flush() { BufferPool.Return(_buffer, false); _flushed = true; } -} \ No newline at end of file +} From 8578c556a5d72762c0e4ca5142a46d6e9a1e513c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 14:21:46 +0200 Subject: [PATCH 04/23] Migrate tests to TUnit --- .github/workflows/UnitTests.yaml | 42 ++++--- .../AdvancedInputsTests.cs | 55 --------- PrettyConsole.Tests.Unit/ConsoleColorTests.cs | 36 ------ PrettyConsole.Tests.Unit/GlobalUsings.cs | 4 - PrettyConsole.Tests.Unit/MarkupTests.cs | 9 -- .../PrettyConsole.Tests.Unit.csproj | 37 ------ .../PrettyConsoleExtensionsTests.cs | 16 --- PrettyConsole.Tests.Unit/xunit.runner.json | 7 -- .../AdvancedInputsTests.cs | 55 +++++++++ .../AdvancedOutputsTests.cs | 28 ++--- PrettyConsole.UnitTests/AssemblyInfo.cs | 3 + PrettyConsole.UnitTests/ConsoleColorTests.cs | 36 ++++++ PrettyConsole.UnitTests/GlobalUsings.cs | 6 + .../InputRequestExtensionsTests.cs | 12 +- PrettyConsole.UnitTests/MarkupTests.cs | 9 ++ .../MenusTests.cs | 88 +++++++------- .../PrettyConsole.UnitTests.csproj | 18 +++ .../PrettyConsoleExtensionsTests.cs | 16 +++ ...tyConsoleInterpolatedStringHandlerTests.cs | 108 ++++++++--------- .../ProgressBarTests.cs | 112 +++++++++--------- .../ReadLineExtensionsTests.cs | 54 ++++----- .../Utilities.cs | 2 +- .../WriteExtensionsTests.cs | 58 ++++----- .../WriteLineExtensionsTests.cs | 16 +-- PrettyConsole.slnx | 2 +- PrettyConsole/PrettyConsole.csproj | 3 + global.json | 5 + 27 files changed, 423 insertions(+), 414 deletions(-) delete mode 100755 PrettyConsole.Tests.Unit/AdvancedInputsTests.cs delete mode 100644 PrettyConsole.Tests.Unit/ConsoleColorTests.cs delete mode 100755 PrettyConsole.Tests.Unit/GlobalUsings.cs delete mode 100644 PrettyConsole.Tests.Unit/MarkupTests.cs delete mode 100755 PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj delete mode 100644 PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs delete mode 100644 PrettyConsole.Tests.Unit/xunit.runner.json create mode 100755 PrettyConsole.UnitTests/AdvancedInputsTests.cs rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/AdvancedOutputsTests.cs (70%) create mode 100644 PrettyConsole.UnitTests/AssemblyInfo.cs create mode 100644 PrettyConsole.UnitTests/ConsoleColorTests.cs create mode 100644 PrettyConsole.UnitTests/GlobalUsings.cs rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/InputRequestExtensionsTests.cs (66%) create mode 100644 PrettyConsole.UnitTests/MarkupTests.cs rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/MenusTests.cs (58%) create mode 100644 PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj create mode 100644 PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/PrettyConsoleInterpolatedStringHandlerTests.cs (53%) rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/ProgressBarTests.cs (63%) rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/ReadLineExtensionsTests.cs (52%) rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/Utilities.cs (96%) rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/WriteExtensionsTests.cs (61%) rename {PrettyConsole.Tests.Unit => PrettyConsole.UnitTests}/WriteLineExtensionsTests.cs (62%) create mode 100644 global.json diff --git a/.github/workflows/UnitTests.yaml b/.github/workflows/UnitTests.yaml index 9abb409..eb03f9b 100755 --- a/.github/workflows/UnitTests.yaml +++ b/.github/workflows/UnitTests.yaml @@ -4,22 +4,36 @@ on: pull_request: workflow_dispatch: +env: + TEST_PROJECT: PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj + TEST_ARTIFACTS: artifacts/ + TEST_RUNNER: artifacts/PrettyConsole.UnitTests + jobs: - unit-tests-matrix: + unit-tests: strategy: fail-fast: false matrix: - platform: [ubuntu-latest, windows-latest, macos-latest] - uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main - with: - platform: ${{ matrix.platform }} - dotnet-version: 10.0.x - test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj + include: + - os: ubuntu-latest + - os: macos-latest + - os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Publish native AOT test runner + run: | + dotnet publish ${{ env.TEST_PROJECT }} -c Release -o "${{ env.TEST_ARTIFACTS }}" - unit-tests-debug: - uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main - with: - platform: ubuntu-latest - dotnet-version: 10.0.x - test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj - use-debug: true \ No newline at end of file + - name: Run published tests + shell: bash + run: | + exe="${{ env.TEST_RUNNER }}" + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then exe="$exe.exe"; fi + "$exe" \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/AdvancedInputsTests.cs b/PrettyConsole.Tests.Unit/AdvancedInputsTests.cs deleted file mode 100755 index f9e84bc..0000000 --- a/PrettyConsole.Tests.Unit/AdvancedInputsTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class AdvancedInputsTests { - [Fact] - public void Confirm_Case_Y_Interpolated() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("y"); - In = reader; - var res = Console.Confirm($"Enter y:"); - Assert.Contains("Enter y:", stringWriter.ToString()); - Assert.True(res); - } - - [Fact] - public void Confirm_Case_Yes_Interpolated() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("yes"); - In = reader; - var res = Console.Confirm($"Enter yes:"); - Assert.Contains("Enter yes", stringWriter.ToString()); - Assert.True(res); - } - - [Fact] - public void Confirm_Case_Empty_Interpolated() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader(""); - In = reader; - var res = Console.Confirm($"Enter yes:"); - Assert.Contains("Enter yes", stringWriter.ToString()); - Assert.True(res); - } - - [Fact] - public void Confirm_Case_No_Interpolated() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("no"); - In = reader; - var res = Console.Confirm($"Enter no:"); - Assert.Contains("Enter no", stringWriter.ToString()); - Assert.False(res); - } - - [Fact] - public void Confirm_CustomTrueValues_WithInterpolatedPrompt() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("ok"); - In = reader; - - var res = Console.Confirm(["ok", "okay"], false, $"Proceed?"); - - Assert.Equal("Proceed?", stringWriter.ToStringAndFlush()); - Assert.True(res); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs deleted file mode 100644 index 86c2a71..0000000 --- a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class ConsoleColorTests { - [Fact] - public void ConsoleColor_DivideOperator() { - var (fg, bg) = Red / Blue; - Assert.Equal(Red, fg); - Assert.Equal(Blue, bg); - } - - [Fact] - public void ConsoleColor_DivideTupleOperator() { - var (fg, bg) = Red / (Blue / Green); - Assert.Equal(Red, fg); - Assert.Equal(Green, bg); - } - - [Fact] - public void ConsoleColor_DefaultColors() { - var (fg, bg) = ConsoleColor.Default; - Assert.Equal(ConsoleColor.DefaultForeground, fg); - Assert.Equal(ConsoleColor.DefaultBackground, bg); - } - - [Fact] - public void AnsiColors_DefaultForeground_UsesResetSequence() { - var sequence = AnsiColors.Foreground((ConsoleColor)(-1)); - Assert.Equal("\e[39m", sequence); - } - - [Fact] - public void AnsiColors_DefaultBackground_UsesResetSequence() { - var sequence = AnsiColors.Background((ConsoleColor)(-1)); - Assert.Equal("\e[49m", sequence); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs deleted file mode 100755 index f0d05ea..0000000 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ /dev/null @@ -1,4 +0,0 @@ -global using Xunit; - -global using static System.ConsoleColor; -global using static PrettyConsole.ConsoleContext; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/MarkupTests.cs b/PrettyConsole.Tests.Unit/MarkupTests.cs deleted file mode 100644 index 1b163b4..0000000 --- a/PrettyConsole.Tests.Unit/MarkupTests.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class MarkupTests { - [Fact] - public void Markup_Enabled_MatchesConsoleRedirectionState() { - Assert.Equal("\e[4m", Markup.Underline); - Assert.Equal("\e[0m", Markup.Reset); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj deleted file mode 100755 index 34a06f1..0000000 --- a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - enable - enable - Exe - net10.0 - true - true - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs b/PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs deleted file mode 100644 index 82f263b..0000000 --- a/PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class PrettyConsoleExtensionsTests { - [Fact] - public void WriteWhiteSpaces_WritesRequestedLength() { - var writer = new StringWriter(); - - writer.WriteWhiteSpaces(10); - Assert.Equal(new string(' ', 10), writer.ToString()); - - writer.GetStringBuilder().Clear(); - writer.WriteWhiteSpaces(300); - Assert.Equal(300, writer.ToString().Length); - Assert.True(writer.ToString().All(c => c == ' ')); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/xunit.runner.json b/PrettyConsole.Tests.Unit/xunit.runner.json deleted file mode 100644 index 9acde45..0000000 --- a/PrettyConsole.Tests.Unit/xunit.runner.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "diagnosticMessages": true, - "parallelizeAssembly": false, - "parallelizeTestCollections": false, - "showLiveOutput": true -} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/AdvancedInputsTests.cs b/PrettyConsole.UnitTests/AdvancedInputsTests.cs new file mode 100755 index 0000000..726d7a6 --- /dev/null +++ b/PrettyConsole.UnitTests/AdvancedInputsTests.cs @@ -0,0 +1,55 @@ +namespace PrettyConsole.UnitTests; + +public class AdvancedInputsTests { + [Test] + public async Task Confirm_Case_Y_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("y"); + In = reader; + var res = Console.Confirm($"Enter y:"); + await Assert.That(stringWriter.ToString()).Contains("Enter y:"); + await Assert.That(res).IsTrue(); + } + + [Test] + public async Task Confirm_Case_Yes_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("yes"); + In = reader; + var res = Console.Confirm($"Enter yes:"); + await Assert.That(stringWriter.ToString()).Contains("Enter yes"); + await Assert.That(res).IsTrue(); + } + + [Test] + public async Task Confirm_Case_Empty_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader(""); + In = reader; + var res = Console.Confirm($"Enter yes:"); + await Assert.That(stringWriter.ToString()).Contains("Enter yes"); + await Assert.That(res).IsTrue(); + } + + [Test] + public async Task Confirm_Case_No_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("no"); + In = reader; + var res = Console.Confirm($"Enter no:"); + await Assert.That(stringWriter.ToString()).Contains("Enter no"); + await Assert.That(res).IsFalse(); + } + + [Test] + public async Task Confirm_CustomTrueValues_WithInterpolatedPrompt() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("ok"); + In = reader; + + var res = Console.Confirm(["ok", "okay"], false, $"Proceed?"); + + await Assert.That(stringWriter.ToStringAndFlush()).IsEqualTo("Proceed?"); + await Assert.That(res).IsTrue(); + } +} diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs b/PrettyConsole.UnitTests/AdvancedOutputsTests.cs similarity index 70% rename from PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs rename to PrettyConsole.UnitTests/AdvancedOutputsTests.cs index a3a2fa7..ec2ef1f 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs +++ b/PrettyConsole.UnitTests/AdvancedOutputsTests.cs @@ -1,8 +1,8 @@ -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class AdvancedOutputsTests { - [Fact] - public void Overwrite_ExecutesActionAndWritesOutput() { + [Test] + public async Task Overwrite_ExecutesActionAndWritesOutput() { Error = Utilities.GetWriter(out var writer); bool executed = false; int cursorLine = 0; @@ -16,12 +16,12 @@ public void Overwrite_ExecutesActionAndWritesOutput() { RenderingExtensions.ConfigureCursorAccessors(null, null); } - Assert.True(executed); - Assert.Contains("Progress", writer.ToString()); + await Assert.That(executed).IsTrue(); + await Assert.That(writer.ToString()).Contains("Progress"); } - [Fact] - public void Overwrite_WithState_ExecutesActionAndWritesOutput() { + [Test] + public async Task Overwrite_WithState_ExecutesActionAndWritesOutput() { Error = Utilities.GetWriter(out var writer); bool executed = false; int cursorLine = 0; @@ -35,21 +35,21 @@ public void Overwrite_WithState_ExecutesActionAndWritesOutput() { RenderingExtensions.ConfigureCursorAccessors(null, null); } - Assert.True(executed); - Assert.Contains("Done", writer.ToString()); + await Assert.That(executed).IsTrue(); + await Assert.That(writer.ToString()).Contains("Done"); } - [Fact] + [Test] public async Task TypeWrite_Regular() { Out = Utilities.GetWriter(out var stringWriter); await Console.TypeWrite("Hello world!", Green / Black, 10); - Assert.Contains("Hello world!", stringWriter.ToString()); + await Assert.That(stringWriter.ToString()).Contains("Hello world!"); } - [Fact] + [Test] public async Task TypeWriteLine_Regular() { Out = Utilities.GetWriter(out var stringWriter); await Console.TypeWriteLine("Hello world!", Green / ConsoleColor.Default, 10); - Assert.Contains("Hello world!" + Environment.NewLine, stringWriter.ToString()); + await Assert.That(stringWriter.ToString()).Contains("Hello world!" + Environment.NewLine); } -} \ No newline at end of file +} diff --git a/PrettyConsole.UnitTests/AssemblyInfo.cs b/PrettyConsole.UnitTests/AssemblyInfo.cs new file mode 100644 index 0000000..d82e8c0 --- /dev/null +++ b/PrettyConsole.UnitTests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using TUnit.Core; + +[assembly: NotInParallel] diff --git a/PrettyConsole.UnitTests/ConsoleColorTests.cs b/PrettyConsole.UnitTests/ConsoleColorTests.cs new file mode 100644 index 0000000..f971336 --- /dev/null +++ b/PrettyConsole.UnitTests/ConsoleColorTests.cs @@ -0,0 +1,36 @@ +namespace PrettyConsole.UnitTests; + +public class ConsoleColorTests { + [Test] + public async Task ConsoleColor_DivideOperator() { + var (fg, bg) = Red / Blue; + await Assert.That(fg).IsEqualTo(Red); + await Assert.That(bg).IsEqualTo(Blue); + } + + [Test] + public async Task ConsoleColor_DivideTupleOperator() { + var (fg, bg) = Red / (Blue / Green); + await Assert.That(fg).IsEqualTo(Red); + await Assert.That(bg).IsEqualTo(Green); + } + + [Test] + public async Task ConsoleColor_DefaultColors() { + var (fg, bg) = ConsoleColor.Default; + await Assert.That(fg).IsEqualTo(ConsoleColor.DefaultForeground); + await Assert.That(bg).IsEqualTo(ConsoleColor.DefaultBackground); + } + + [Test] + public async Task AnsiColors_DefaultForeground_UsesResetSequence() { + var sequence = AnsiColors.Foreground((ConsoleColor)(-1)); + await Assert.That(sequence).IsEqualTo("\e[39m"); + } + + [Test] + public async Task AnsiColors_DefaultBackground_UsesResetSequence() { + var sequence = AnsiColors.Background((ConsoleColor)(-1)); + await Assert.That(sequence).IsEqualTo("\e[49m"); + } +} diff --git a/PrettyConsole.UnitTests/GlobalUsings.cs b/PrettyConsole.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..1d25279 --- /dev/null +++ b/PrettyConsole.UnitTests/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using TUnit.Core; +global using TUnit.Assertions; +global using TUnit.Assertions.Extensions; + +global using static System.ConsoleColor; +global using static PrettyConsole.ConsoleContext; diff --git a/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs b/PrettyConsole.UnitTests/InputRequestExtensionsTests.cs similarity index 66% rename from PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs rename to PrettyConsole.UnitTests/InputRequestExtensionsTests.cs index 8f820db..07432c4 100644 --- a/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs +++ b/PrettyConsole.UnitTests/InputRequestExtensionsTests.cs @@ -1,8 +1,8 @@ -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class InputRequestExtensionsTests { - [Fact] - public void RequestAnyInput_WritesPrompt_AndInvokesReadKey() { + [Test] + public async Task RequestAnyInput_WritesPrompt_AndInvokesReadKey() { Out = Utilities.GetWriter(out var writer); bool invoked = false; @@ -14,10 +14,10 @@ public void RequestAnyInput_WritesPrompt_AndInvokesReadKey() { Console.RequestAnyInput($"Press something:"); - Assert.Contains("Press something:", writer.ToString()); - Assert.True(invoked); + await Assert.That(writer.ToString()).Contains("Press something:"); + await Assert.That(invoked).IsTrue(); } finally { InputRequestExtensions.ConfigureReadKey(null); } } -} \ No newline at end of file +} diff --git a/PrettyConsole.UnitTests/MarkupTests.cs b/PrettyConsole.UnitTests/MarkupTests.cs new file mode 100644 index 0000000..3373999 --- /dev/null +++ b/PrettyConsole.UnitTests/MarkupTests.cs @@ -0,0 +1,9 @@ +namespace PrettyConsole.UnitTests; + +public class MarkupTests { + [Test] + public async Task Markup_Enabled_MatchesConsoleRedirectionState() { + await Assert.That(Markup.Underline.Equals("\e[4m", StringComparison.Ordinal)).IsTrue(); + await Assert.That(Markup.Reset.Equals("\e[0m", StringComparison.Ordinal)).IsTrue(); + } +} diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.UnitTests/MenusTests.cs similarity index 58% rename from PrettyConsole.Tests.Unit/MenusTests.cs rename to PrettyConsole.UnitTests/MenusTests.cs index e796dea..aea1fa9 100644 --- a/PrettyConsole.Tests.Unit/MenusTests.cs +++ b/PrettyConsole.UnitTests/MenusTests.cs @@ -1,8 +1,8 @@ -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class MenusTests { - [Fact] - public void Selection_ReturnsSelectedChoice_WhenInputValid() { + [Test] + public async Task Selection_ReturnsSelectedChoice_WhenInputValid() { Out = Utilities.GetWriter(out var writer); In = Utilities.GetReader("2"); @@ -12,7 +12,7 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { var output = writer.ToStringAndFlush(); - Assert.Equal( + await Assert.That(Normalize(output)).IsEqualTo( """ Choose a fruit: 1) Apple @@ -20,15 +20,14 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { 3) Cherry Enter your choice: - """.Replace("\r\n", "\n"), - Normalize(output)); - Assert.Equal("Banana", result); + """.Replace("\r\n", "\n")); + await Assert.That(result).IsEqualTo("Banana"); static string Normalize(string value) => value.Replace("\r\n", "\n"); } - [Fact] - public void Selection_InvalidNumber_ReturnsEmptyString() { + [Test] + public async Task Selection_InvalidNumber_ReturnsEmptyString() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("5"); @@ -36,11 +35,11 @@ public void Selection_InvalidNumber_ReturnsEmptyString() { var result = Console.Selection(choices, $"Pick a number: "); - Assert.Equal(string.Empty, result); + await Assert.That(result).IsEqualTo(string.Empty); } - [Fact] - public void Selection_NonNumericInput_ReturnsEmptyString() { + [Test] + public async Task Selection_NonNumericInput_ReturnsEmptyString() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("abc"); @@ -48,11 +47,11 @@ public void Selection_NonNumericInput_ReturnsEmptyString() { var result = Console.Selection(choices, $"Pick a number: "); - Assert.Equal(string.Empty, result); + await Assert.That(result).IsEqualTo(string.Empty); } - [Fact] - public void MultiSelection_ReturnsSelectedChoices_InOrder() { + [Test] + public async Task MultiSelection_ReturnsSelectedChoices_InOrder() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("3 1"); @@ -60,11 +59,13 @@ public void MultiSelection_ReturnsSelectedChoices_InOrder() { var result = Console.MultiSelection(choices, $"Plants: "); - Assert.Equal(["Earth", "Mercury"], result); + await Assert.That(result.Length).IsEqualTo(2); + await Assert.That(result[0]).IsEqualTo("Earth"); + await Assert.That(result[1]).IsEqualTo("Mercury"); } - [Fact] - public void MultiSelection_InvalidEntry_ReturnsEmptyArray() { + [Test] + public async Task MultiSelection_InvalidEntry_ReturnsEmptyArray() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("2 x"); @@ -72,11 +73,11 @@ public void MultiSelection_InvalidEntry_ReturnsEmptyArray() { var result = Console.MultiSelection(choices, $"Letters: "); - Assert.Empty(result); + await Assert.That(result.Length).IsEqualTo(0); } - [Fact] - public void MultiSelection_EmptyInput_ReturnsEmptyArray() { + [Test] + public async Task MultiSelection_EmptyInput_ReturnsEmptyArray() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader(string.Empty); @@ -84,11 +85,11 @@ public void MultiSelection_EmptyInput_ReturnsEmptyArray() { var result = Console.MultiSelection(choices, $"Letters: "); - Assert.Empty(result); + await Assert.That(result.Length).IsEqualTo(0); } - [Fact] - public void TreeMenu_ValidSelection_ReturnsTuple() { + [Test] + public async Task TreeMenu_ValidSelection_ReturnsTuple() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("2 1"); @@ -99,12 +100,12 @@ public void TreeMenu_ValidSelection_ReturnsTuple() { var (option, subOption) = Console.TreeMenu(menu, $"Menu: "); - Assert.Equal("Edit", option); - Assert.Equal("Undo", subOption); + await Assert.That(option).IsEqualTo("Edit"); + await Assert.That(subOption).IsEqualTo("Undo"); } - [Fact] - public void TreeMenu_MissingSelectionParts_ThrowsArgumentException() { + [Test] + public async Task TreeMenu_MissingSelectionParts_ThrowsArgumentException() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("1"); @@ -112,11 +113,12 @@ public void TreeMenu_MissingSelectionParts_ThrowsArgumentException() { ["Files"] = new List { "Open" } }; - Assert.Throws(() => Console.TreeMenu(menu, $"Menu: ")); + await Assert.That(() => Console.TreeMenu(menu, $"Menu: ")) + .Throws(); } - [Fact] - public void TreeMenu_InvalidIndexes_ThrowsArgumentException() { + [Test] + public async Task TreeMenu_InvalidIndexes_ThrowsArgumentException() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("3 1"); @@ -125,11 +127,12 @@ public void TreeMenu_InvalidIndexes_ThrowsArgumentException() { ["Edit"] = new List { "Undo" } }; - Assert.Throws(() => Console.TreeMenu(menu, $"Menu: ")); + await Assert.That(() => Console.TreeMenu(menu, $"Menu: ")) + .Throws(); } - [Fact] - public void Table_WritesHeaderAndRows() { + [Test] + public async Task Table_WritesHeaderAndRows() { Out = Utilities.GetWriter(out var writer); var headers = new List { "Name", "Age" }; @@ -139,19 +142,20 @@ public void Table_WritesHeaderAndRows() { Console.Table(headers, [column1, column2]); var output = writer.ToString(); - Assert.Contains("Name", output); - Assert.Contains("Age", output); - Assert.Contains("Alice", output); - Assert.Contains("Bob", output); + await Assert.That(output).Contains("Name"); + await Assert.That(output).Contains("Age"); + await Assert.That(output).Contains("Alice"); + await Assert.That(output).Contains("Bob"); } - [Fact] - public void Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() { + [Test] + public async Task Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() { Out = Utilities.GetWriter(out _); var headers = new List { "Name", "Age" }; var column1 = new List { "Alice", "Bob" }; - Assert.Throws(() => Console.Table(headers, [column1])); + await Assert.That(() => Console.Table(headers, [column1])) + .Throws(); } -} \ No newline at end of file +} diff --git a/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj b/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj new file mode 100644 index 0000000..81ac9b2 --- /dev/null +++ b/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj @@ -0,0 +1,18 @@ + + + + enable + enable + Exe + net10.0 + + + + + + + + + + + \ No newline at end of file diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs new file mode 100644 index 0000000..cdfa51c --- /dev/null +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -0,0 +1,16 @@ +namespace PrettyConsole.UnitTests; + +public class PrettyConsoleExtensionsTests { + [Test] + public async Task WriteWhiteSpaces_WritesRequestedLength() { + var writer = new StringWriter(); + + writer.WriteWhiteSpaces(10); + await Assert.That(writer.ToString()).IsEqualTo(new string(' ', 10)); + + writer.GetStringBuilder().Clear(); + writer.WriteWhiteSpaces(300); + await Assert.That(writer.ToString().Length).IsEqualTo(300); + await Assert.That(writer.ToString().All(c => c == ' ')).IsTrue(); + } +} diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs similarity index 53% rename from PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs rename to PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs index 72507b7..509ee90 100644 --- a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class PrettyConsoleInterpolatedStringHandlerTests { private readonly StringWriter _writer; @@ -9,97 +9,97 @@ public PrettyConsoleInterpolatedStringHandlerTests() { Out = Utilities.GetWriter(out _writer); } - [Theory] - [InlineData(0, 0, 0, "0h 0m 0s")] - [InlineData(0, 1, 2, "0h 1m 2s")] - [InlineData(5, 59, 59, "5h 59m 59s")] - [InlineData(48, 30, 5, "48h 30m 5s")] - [InlineData(1234, 0, 1, "1234h 0m 1s")] - [InlineData(256204778, 48, 5, "256204778h 48m 5s")] // TimeSpan.MaxValue - public void AppendFormatted_TimeSpanDuration_WritesExpected(int hours, int minutes, int seconds, string expected) { + [Test] + [Arguments(0, 0, 0, "0h 0m 0s")] + [Arguments(0, 1, 2, "0h 1m 2s")] + [Arguments(5, 59, 59, "5h 59m 59s")] + [Arguments(48, 30, 5, "48h 30m 5s")] + [Arguments(1234, 0, 1, "1234h 0m 1s")] + [Arguments(256204778, 48, 5, "256204778h 48m 5s")] + public async Task AppendFormatted_TimeSpanDuration_WritesExpected(int hours, int minutes, int seconds, string expected) { var timeSpan = TimeSpan.FromHours(hours) .Add(TimeSpan.FromMinutes(minutes)) .Add(TimeSpan.FromSeconds(seconds)); Console.WriteInterpolated($"Elapsed {timeSpan:duration}"); - Assert.Equal($"Elapsed {expected}", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo($"Elapsed {expected}"); } - [Theory] - [InlineData(0d)] - [InlineData(512d)] - [InlineData(1024d)] - [InlineData(15360d)] - [InlineData(42_949_672_960d)] - [InlineData(1.1258999068426228e105)] // scales to just under 1e90 PB => uses default stack buffer - [InlineData(1.1258999068426251e105)] // scales to just over 1e90 PB => uses large stack buffer - [InlineData(double.MaxValue)] - public void AppendFormatted_DoubleBytes_WritesExpected(double value) { + [Test] + [Arguments(0d)] + [Arguments(512d)] + [Arguments(1024d)] + [Arguments(15360d)] + [Arguments(42_949_672_960d)] + [Arguments(1.1258999068426228e105)] + [Arguments(1.1258999068426251e105)] + [Arguments(double.MaxValue)] + public async Task AppendFormatted_DoubleBytes_WritesExpected(double value) { Console.WriteInterpolated($"Size {value:bytes}"); - Assert.Equal($"Size {FormatBytes(value)}", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo($"Size {FormatBytes(value)}"); } - [Fact] - public void CharsWritten_IgnoresAnsiColorAndMarkupSequences() { + [Test] + public async Task 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); + await Assert.That(written).IsEqualTo("Hi"); + await Assert.That(chars).IsEqualTo(2); } - [Theory] - [InlineData(5, " OK")] - [InlineData(-5, "OK ")] - public void Alignment_UsesVisibleLengthWhenMarkupPresent(int alignment, string expected) { + [Test] + [Arguments(5, " OK")] + [Arguments(-5, "OK ")] + public async Task 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); + await Assert.That(written).IsEqualTo(expected); + await Assert.That(chars).IsEqualTo(expected.Length); } - [Fact] - public void WriteLineInterpolated_ReturnsCharsWithoutNewline() { + [Test] + public async Task WriteLineInterpolated_ReturnsCharsWithoutNewline() { int chars = Console.WriteLineInterpolated($"Hi"); var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); - Assert.Equal("Hi" + Environment.NewLine, written); - Assert.Equal(2, chars); + await Assert.That(written).IsEqualTo("Hi" + Environment.NewLine); + await Assert.That(chars).IsEqualTo(2); } - [Fact] - public void WriteInterpolated_MixedPrimitivesAndFormats_ReturnsVisibleCount() { + [Test] + public async Task 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); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo(expected); + await Assert.That(chars).IsEqualTo(expected.Length); } - [Fact] - public void WriteInterpolated_ColorTuple_DoesNotAffectVisibleCount() { + [Test] + public async Task 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); + await Assert.That(written).IsEqualTo("ERR done"); + await Assert.That(chars).IsEqualTo("ERR done".Length); } - [Theory] - [InlineData(6)] - [InlineData(-6)] - public void WriteInterpolated_ColorTupleAlignment_UsesWidthOnly(int alignment) { + [Test] + [Arguments(6)] + [Arguments(-6)] + public async Task WriteInterpolated_ColorTupleAlignment_UsesWidthOnly(int alignment) { int chars = alignment > 0 ? Console.WriteInterpolated($"{(ConsoleColor.Blue, ConsoleColor.White),6}") : Console.WriteInterpolated($"{(ConsoleColor.Blue, ConsoleColor.White),-6}"); @@ -107,12 +107,12 @@ public void WriteInterpolated_ColorTupleAlignment_UsesWidthOnly(int alignment) { var written = Utilities.StripAnsiSequences(_writer.ToStringAndFlush()); var expected = new string(' ', 6); - Assert.Equal(expected, written); - Assert.Equal(6, chars); + await Assert.That(written).IsEqualTo(expected); + await Assert.That(chars).IsEqualTo(6); } - [Fact] - public void WriteLineInterpolated_WithColorsAndPrimitives_CountExcludesNewline() { + [Test] + public async Task WriteLineInterpolated_WithColorsAndPrimitives_CountExcludesNewline() { var duration = TimeSpan.FromSeconds(42); var writer = new StringWriter(); Error = writer; @@ -122,8 +122,8 @@ public void WriteLineInterpolated_WithColorsAndPrimitives_CountExcludesNewline() 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); + await Assert.That(stripped).IsEqualTo(expected); + await Assert.That(chars).IsEqualTo("[0h 0m 42s] done".Length); } private static string FormatBytes(double value) { @@ -140,4 +140,4 @@ private static string FormatBytes(double value) { } private static ReadOnlySpan FileSizeSuffix => new[] { "B", "KB", "MB", "GB", "TB", "PB" }; -} \ No newline at end of file +} diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs similarity index 63% rename from PrettyConsole.Tests.Unit/ProgressBarTests.cs rename to PrettyConsole.UnitTests/ProgressBarTests.cs index 2cf4a08..6b5f6c7 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -1,21 +1,11 @@ -namespace PrettyConsole.Tests.Unit; +using TUnit.Core.Attributes; -public class ProgressBarTests { - private readonly bool _consoleAvailable; - - public ProgressBarTests() { - try { - _ = Console.BufferWidth; - _ = Console.CursorLeft; - _consoleAvailable = true; - } catch (IOException) { - _consoleAvailable = false; - } - } +namespace PrettyConsole.UnitTests; - [Fact] - public void ProgressBar_Update_WritesStatusAndPercentage() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); +[SkipWhenConsoleUnavailable] +public class ProgressBarTests { + [Test] + public async Task ProgressBar_Update_WritesStatusAndPercentage() { Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -32,14 +22,13 @@ public void ProgressBar_Update_WritesStatusAndPercentage() { } var output = errorWriter.ToString(); - Assert.Contains("Loading", output); - Assert.Contains("#", output); - Assert.Contains("50", output); + await Assert.That(output).Contains("Loading"); + await Assert.That(output).Contains("#"); + await Assert.That(output).Contains("50"); } - [Fact] - public void ProgressBar_Update_SamePercentage_RerendersOutput() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); + [Test] + public async Task ProgressBar_Update_SamePercentage_RerendersOutput() { Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -59,14 +48,13 @@ public void ProgressBar_Update_SamePercentage_RerendersOutput() { } var output = errorWriter.ToString(); - Assert.NotEqual(string.Empty, output); - Assert.Contains("Loading", output); - Assert.Contains("25", output); + await Assert.That(output).IsNotEqualTo(string.Empty); + await Assert.That(output).Contains("Loading"); + await Assert.That(output).Contains("25"); } - [Fact] - public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); + [Test] + public async Task ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { var originalError = Error; int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -80,17 +68,16 @@ public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { bar.Update(75, "Working", sameLine: false); var output = errorWriter.ToString(); - Assert.Contains("Working", output); - Assert.Contains(Environment.NewLine + "[", output); + await Assert.That(output).Contains("Working"); + await Assert.That(output).Contains(Environment.NewLine + "["); } finally { Error = originalError; RenderingExtensions.ConfigureCursorAccessors(null, null); } } - [Fact] - public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); + [Test] + public async Task ProgressBar_WriteProgressBar_WritesFormattedOutput() { var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); @@ -98,17 +85,16 @@ public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { ProgressBar.WriteProgressBar(OutputPipe.Out, 75, Cyan, '*'); var output = outWriter.ToString(); - Assert.Contains("[", output); - Assert.Contains("75%", output); - Assert.Contains("*", output); + await Assert.That(output).Contains("["); + await Assert.That(output).Contains("75%"); + await Assert.That(output).Contains("*"); } finally { Out = originalOut; } } - [Fact] - public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); + [Test] + public async Task ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); @@ -116,17 +102,16 @@ public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { ProgressBar.WriteProgressBar(OutputPipe.Out, 50, Cyan, '*', maxLineWidth: 24); var output = outWriter.ToString(); - Assert.Equal(24, output.Length); - Assert.Equal('[', output[0]); - Assert.Equal('%', output[^1]); + await Assert.That(output.Length).IsEqualTo(24); + await Assert.That(output[0]).IsEqualTo('['); + await Assert.That(output[^1]).IsEqualTo('%'); } finally { Out = originalOut; } } - [Fact] - public void ProgressBar_Update_RespectsMaxLineWidth() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); + [Test] + public async Task ProgressBar_Update_RespectsMaxLineWidth() { Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; const int expectedWidth = 32; @@ -144,17 +129,16 @@ public void ProgressBar_Update_RespectsMaxLineWidth() { var output = Utilities.StripAnsiSequences(errorWriter.ToString()); int percentIndex = output.LastIndexOf('%'); - Assert.True(percentIndex > 0, "Output contains a percentage symbol."); + await Assert.That(percentIndex > 0).IsTrue(); int bracketIndex = output.LastIndexOf('[', percentIndex); - Assert.True(bracketIndex >= 0, "Output contains a bracketed progress bar."); + await Assert.That(bracketIndex >= 0).IsTrue(); var segment = output[bracketIndex..(percentIndex + 1)]; - Assert.Equal(expectedWidth, segment.Length); + await Assert.That(segment.Length).IsEqualTo(expectedWidth); } - [Fact] + [Test] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { - Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -165,16 +149,36 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() UpdateRate = 5 }; - var cancellation = TestContext.Current.CancellationToken; + var cancellation = CancellationToken.None; var result = await bar.RunAsync(Task.Run(async () => { await Task.Delay(20, cancellation); return 42; }, cancellation), "Working", cancellation); - Assert.Equal(42, result); - Assert.NotEqual(string.Empty, errorWriter.ToString()); + await Assert.That(result).IsEqualTo(42); + await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty); } finally { RenderingExtensions.ConfigureCursorAccessors(null, null); } } -} \ No newline at end of file +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class SkipWhenConsoleUnavailableAttribute : SkipAttribute { + public SkipWhenConsoleUnavailableAttribute() : base("Console handle unavailable for this environment.") { + } + + public override Task ShouldSkip(TestRegisteredContext testContext) => Task.FromResult(!ConsoleAvailability.IsAvailable()); +} + +internal static class ConsoleAvailability { + public static bool IsAvailable() { + try { + _ = Console.BufferWidth; + _ = Console.CursorLeft; + return true; + } catch (IOException) { + return false; + } + } +} diff --git a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs b/PrettyConsole.UnitTests/ReadLineExtensionsTests.cs similarity index 52% rename from PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs rename to PrettyConsole.UnitTests/ReadLineExtensionsTests.cs index eaf47bc..f03893d 100755 --- a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs +++ b/PrettyConsole.UnitTests/ReadLineExtensionsTests.cs @@ -1,80 +1,80 @@ -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class ReadLineExtensionsTests { - [Fact] - public void ReadLine_InterpolatedPrompt_WritesPromptAndReadsValue() { + [Test] + public async Task ReadLine_InterpolatedPrompt_WritesPromptAndReadsValue() { Out = Utilities.GetWriter(out _); var reader = Utilities.GetReader("123"); In = reader; var result = Console.ReadLine($"Enter number: "); - Assert.Equal("123", result); + await Assert.That(result).IsEqualTo("123"); } - [Fact] - public void TryReadLine_InterpolatedWithDefault_ReturnsDefaultOnFailure() { + [Test] + public async Task TryReadLine_InterpolatedWithDefault_ReturnsDefaultOnFailure() { Out = Utilities.GetWriter(out _); var reader = Utilities.GetReader("not-a-number"); In = reader; var parsed = Console.TryReadLine(out int result, 42, $"Enter number: "); - Assert.False(parsed); - Assert.Equal(42, result); + await Assert.That(parsed).IsFalse(); + await Assert.That(result).IsEqualTo(42); } - [Fact] - public void TryReadLine_Enum_InterpolatedPrompt_IgnoreCase() { + [Test] + public async Task TryReadLine_Enum_InterpolatedPrompt_IgnoreCase() { Out = Utilities.GetWriter(out _); var reader = Utilities.GetReader("yElLoW"); In = reader; var parsed = Console.TryReadLine(out ConsoleColor color, true, $"Enter enum: "); - Assert.True(parsed); - Assert.Equal(Yellow, color); + await Assert.That(parsed).IsTrue(); + await Assert.That(color).IsEqualTo(Yellow); } - [Fact] - public void TryReadLine_Generic_ParsesValue() { + [Test] + public async Task TryReadLine_Generic_ParsesValue() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("42"); var parsed = Console.TryReadLine(out int value, $"Number: "); - Assert.True(parsed); - Assert.Equal(42, value); + await Assert.That(parsed).IsTrue(); + await Assert.That(value).IsEqualTo(42); } - [Fact] - public void TryReadLine_Enum_WithDefault_ReturnsDefaultWhenInvalid() { + [Test] + public async Task TryReadLine_Enum_WithDefault_ReturnsDefaultWhenInvalid() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("not-a-color"); var parsed = Console.TryReadLine(out ConsoleColor color, ignoreCase: true, ConsoleColor.Blue, $"Enum: "); - Assert.False(parsed); - Assert.Equal(ConsoleColor.Blue, color); + await Assert.That(parsed).IsFalse(); + await Assert.That(color).IsEqualTo(ConsoleColor.Blue); } - [Fact] - public void ReadLine_Generic_ReturnsParsedValue() { + [Test] + public async Task ReadLine_Generic_ReturnsParsedValue() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("3.14"); var value = Console.ReadLine($"Value: "); - Assert.Equal(3.14, value); + await Assert.That(value).IsEqualTo(3.14); } - [Fact] - public void ReadLine_GenericWithDefault_ReturnsDefaultWhenInvalid() { + [Test] + public async Task ReadLine_GenericWithDefault_ReturnsDefaultWhenInvalid() { Out = Utilities.GetWriter(out _); In = Utilities.GetReader("not-number"); var value = Console.ReadLine(5, $"Value: "); - Assert.Equal(5, value); + await Assert.That(value).IsEqualTo(5); } -} \ No newline at end of file +} diff --git a/PrettyConsole.Tests.Unit/Utilities.cs b/PrettyConsole.UnitTests/Utilities.cs similarity index 96% rename from PrettyConsole.Tests.Unit/Utilities.cs rename to PrettyConsole.UnitTests/Utilities.cs index 594ecd1..63abf38 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.UnitTests/Utilities.cs @@ -2,7 +2,7 @@ using System.Text; using System.Text.RegularExpressions; -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public static partial class Utilities { public static StringReader GetReader(string str) => new(str); diff --git a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs b/PrettyConsole.UnitTests/WriteExtensionsTests.cs similarity index 61% rename from PrettyConsole.Tests.Unit/WriteExtensionsTests.cs rename to PrettyConsole.UnitTests/WriteExtensionsTests.cs index c243945..0564b76 100755 --- a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteExtensionsTests.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class WriteExtensionsTests { private readonly StringWriter _writer; @@ -11,36 +11,36 @@ public WriteExtensionsTests() { Error = Utilities.GetWriter(out _errorWriter); } - [Fact] - public void Write_Interpolated_WritesFormattedContent_ToOutPipe() { + [Test] + public async Task Write_Interpolated_WritesFormattedContent_ToOutPipe() { var originalOut = Out; var writer = new StringWriter(); Out = writer; try { Console.WriteInterpolated(OutputPipe.Out, $"Hello {42}"); - Assert.Equal("Hello 42", writer.ToString()); + await Assert.That(writer.ToString()).IsEqualTo("Hello 42"); } finally { Out = originalOut; } } - [Fact] - public void Write_Interpolated_WritesFormattedContent_ToErrorPipe() { + [Test] + public async Task Write_Interpolated_WritesFormattedContent_ToErrorPipe() { var originalError = Error; var writer = new StringWriter(); Error = writer; try { Console.WriteInterpolated(OutputPipe.Error, $"Error {123}"); - Assert.Equal("Error 123", writer.ToString()); + await Assert.That(writer.ToString()).IsEqualTo("Error 123"); } finally { Error = originalError; } } - [Fact] - public void Write_Interpolated_IgnoresColorTokensInOutput() { + [Test] + public async Task Write_Interpolated_IgnoresColorTokensInOutput() { var originalOut = Out; var writer = new StringWriter(); Out = writer; @@ -51,47 +51,47 @@ public void Write_Interpolated_IgnoresColorTokensInOutput() { var normalized = Utilities.StripAnsiSequences(writer.ToString()); - Assert.Equal("Colors Green Red", normalized); + await Assert.That(normalized).IsEqualTo("Colors Green Red"); } finally { Out = originalOut; } } - [Fact] - public void Write_SpanFormattable_NoColors() { + [Test] + public async Task Write_SpanFormattable_NoColors() { Console.Write(3.14); - Assert.Equal("3.14", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("3.14"); } - [Fact] - public void Write_SpanFormattable_ForegroundColor() { + [Test] + public async Task Write_SpanFormattable_ForegroundColor() { Console.Write(3.14, OutputPipe.Out, White); - Assert.Equal("3.14", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("3.14"); } - [Fact] - public void Write_SpanFormattable_ForegroundAndBackgroundColor() { + [Test] + public async Task Write_SpanFormattable_ForegroundAndBackgroundColor() { Console.Write(3.14, OutputPipe.Out, White, Black); - Assert.Equal("3.14", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("3.14"); } - [Fact] - public void Write_SpanFormattable_VeryLongObjectFormat() { + [Test] + public async Task Write_SpanFormattable_VeryLongObjectFormat() { var obj = new LongFormatStud(); Console.Write(obj); - Assert.Equal(new string('X', LongFormatStud.Length), _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo(new string('X', LongFormatStud.Length)); } - [Fact] - public void Write_SpanFormattable_WithFormatAndProvider() { + [Test] + public async Task Write_SpanFormattable_WithFormatAndProvider() { Console.Write(12.345, OutputPipe.Out, White, Black, "F2", CultureInfo.InvariantCulture); - Assert.Equal("12.35", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("12.35"); } - [Fact] - public void Write_ReadOnlySpan_WithColors_WritesToSelectedPipe() { + [Test] + public async Task Write_ReadOnlySpan_WithColors_WritesToSelectedPipe() { Console.Write("Data".AsSpan(), OutputPipe.Error, ConsoleColor.Green, ConsoleColor.Black); - Assert.Equal("Data", _errorWriter.ToStringAndFlush()); + await Assert.That(_errorWriter.ToStringAndFlush()).IsEqualTo("Data"); } private readonly ref struct LongFormatStud : ISpanFormattable { @@ -112,4 +112,4 @@ public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan return true; } } -} \ No newline at end of file +} diff --git a/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs similarity index 62% rename from PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs rename to PrettyConsole.UnitTests/WriteLineExtensionsTests.cs index 4ad4478..6c2dd9c 100755 --- a/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs @@ -1,4 +1,4 @@ -namespace PrettyConsole.Tests.Unit; +namespace PrettyConsole.UnitTests; public class WriteLineExtensionsTests { private readonly StringWriter _writer; @@ -9,23 +9,23 @@ public WriteLineExtensionsTests() { Error = Utilities.GetWriter(out _errorWriter); } - [Fact] - public void WriteLine_Interpolated_AppendsNewLine() { + [Test] + public async Task WriteLine_Interpolated_AppendsNewLine() { var originalOut = Out; var writer = new StringWriter(); Out = writer; try { Console.WriteLineInterpolated(OutputPipe.Out, $"Line {7}"); - Assert.Equal($"Line 7{writer.NewLine}", writer.ToString()); + await Assert.That(writer.ToString()).IsEqualTo($"Line 7{writer.NewLine}"); } finally { Out = originalOut; } } - [Fact] - public void WriteLine_ReadOnlySpan_WithColors_AppendsNewLine() { + [Test] + public async Task WriteLine_ReadOnlySpan_WithColors_AppendsNewLine() { Console.WriteLine("SpanLine".AsSpan(), OutputPipe.Out, ConsoleColor.Yellow, ConsoleColor.Black); - Assert.Equal($"SpanLine{_writer.NewLine}", _writer.ToStringAndFlush()); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo($"SpanLine{_writer.NewLine}"); } -} \ No newline at end of file +} diff --git a/PrettyConsole.slnx b/PrettyConsole.slnx index 91518fd..b83d298 100644 --- a/PrettyConsole.slnx +++ b/PrettyConsole.slnx @@ -1,6 +1,6 @@ - + diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 430e972..2dfcebf 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -66,5 +66,8 @@ <_Parameter1>PrettyConsole.Tests.Unit + + <_Parameter1>PrettyConsole.UnitTests + diff --git a/global.json b/global.json new file mode 100644 index 0000000..3140116 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} From dfcb0b2ffba78bfcd9b5a2a042be6ea2b85467c9 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 14:25:07 +0200 Subject: [PATCH 05/23] Add coverage --- PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj b/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj index 81ac9b2..6249b39 100644 --- a/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj +++ b/PrettyConsole.UnitTests/PrettyConsole.UnitTests.csproj @@ -8,6 +8,7 @@ + From 601a1ba135454b1555ae8aabf30bc2b804747d14 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 14:41:03 +0200 Subject: [PATCH 06/23] Clean unnecessary overloads --- .../PrettyConsoleInterpolatedStringHandler.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 5cb7998..cfcbce8 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -145,16 +145,6 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c AppendFormattedBackground(colors.Background); } - /// - /// Sets the foreground and background colors of the console - /// - /// - /// - public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors, int alignment) { - AppendFormatted(colors); - AppendSpan(ReadOnlySpan.Empty, alignment); - } - /// /// Append timeSpan with optional formatting. /// @@ -236,17 +226,10 @@ public void AppendFormatted(double num, int alignment, string? format = null) { } } - /// - /// Appends a value type that implements without boxing. - /// - 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 void AppendFormatted(T value, int alignment) where T : ISpanFormattable { + public void AppendFormatted(T value, int alignment = 0) where T : ISpanFormattable { AppendSpanFormattable(value, alignment, format: null); } From 1e0cf8824c7778ec5b3e07473b9fc728a04b5b9a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 14:41:13 +0200 Subject: [PATCH 07/23] Removed unrealistic tests --- ...PrettyConsoleInterpolatedStringHandlerTests.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs index 509ee90..db82317 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -96,21 +96,6 @@ public async Task WriteInterpolated_ColorTuple_DoesNotAffectVisibleCount() { await Assert.That(chars).IsEqualTo("ERR done".Length); } - [Test] - [Arguments(6)] - [Arguments(-6)] - public async Task 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); - - await Assert.That(written).IsEqualTo(expected); - await Assert.That(chars).IsEqualTo(6); - } - [Test] public async Task WriteLineInterpolated_WithColorsAndPrimitives_CountExcludesNewline() { var duration = TimeSpan.FromSeconds(42); From aaa1a8c30fc55ef99b98fbba6a5ffae59bc882f0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 14:49:24 +0200 Subject: [PATCH 08/23] Cover all constants in markup --- PrettyConsole.UnitTests/MarkupTests.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/PrettyConsole.UnitTests/MarkupTests.cs b/PrettyConsole.UnitTests/MarkupTests.cs index 3373999..60e6073 100644 --- a/PrettyConsole.UnitTests/MarkupTests.cs +++ b/PrettyConsole.UnitTests/MarkupTests.cs @@ -2,8 +2,16 @@ namespace PrettyConsole.UnitTests; public class MarkupTests { [Test] - public async Task Markup_Enabled_MatchesConsoleRedirectionState() { - await Assert.That(Markup.Underline.Equals("\e[4m", StringComparison.Ordinal)).IsTrue(); - await Assert.That(Markup.Reset.Equals("\e[0m", StringComparison.Ordinal)).IsTrue(); + [Arguments(Markup.Reset, "\e[0m")] + [Arguments(Markup.Underline, "\e[4m")] + [Arguments(Markup.ResetUnderline, "\e[24m")] + [Arguments(Markup.Bold, "\e[1m")] + [Arguments(Markup.ResetBold, "\e[22m")] + [Arguments(Markup.Italic, "\e[3m")] + [Arguments(Markup.ResetItalic, "\e[23m")] + [Arguments(Markup.Strikethrough, "\e[9m")] + [Arguments(Markup.ResetStrikethrough, "\e[29m")] + public async Task Markup_Constants(string actual, string expected) { + await Assert.That(actual).IsEqualTo(expected).IgnoringCase(); } } From 615dca7ad7e53b980705e52fc70c040136d5f2a4 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 14:53:40 +0200 Subject: [PATCH 09/23] Added tests for the constants in AnsiColors --- PrettyConsole.UnitTests/ConsoleColorTests.cs | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/PrettyConsole.UnitTests/ConsoleColorTests.cs b/PrettyConsole.UnitTests/ConsoleColorTests.cs index f971336..e47898b 100644 --- a/PrettyConsole.UnitTests/ConsoleColorTests.cs +++ b/PrettyConsole.UnitTests/ConsoleColorTests.cs @@ -33,4 +33,48 @@ public async Task AnsiColors_DefaultBackground_UsesResetSequence() { var sequence = AnsiColors.Background((ConsoleColor)(-1)); await Assert.That(sequence).IsEqualTo("\e[49m"); } + + [Test] + [Arguments(Black, "\e[30m")] + [Arguments(DarkBlue, "\e[34m")] + [Arguments(DarkGreen, "\e[32m")] + [Arguments(DarkCyan, "\e[36m")] + [Arguments(DarkRed, "\e[31m")] + [Arguments(DarkMagenta, "\e[35m")] + [Arguments(DarkYellow, "\e[33m")] + [Arguments(Gray, "\e[37m")] + [Arguments(DarkGray, "\e[90m")] + [Arguments(Blue, "\e[94m")] + [Arguments(Green, "\e[92m")] + [Arguments(Cyan, "\e[96m")] + [Arguments(Red, "\e[91m")] + [Arguments(Magenta, "\e[95m")] + [Arguments(Yellow, "\e[93m")] + [Arguments(White, "\e[97m")] + public async Task AnsiColors_ForegroundSequences(ConsoleColor color, string expectedSequence) { + var sequence = AnsiColors.Foreground(color); + await Assert.That(sequence).IsEqualTo(expectedSequence); + } + + [Test] + [Arguments(Black, "\e[40m")] + [Arguments(DarkBlue, "\e[44m")] + [Arguments(DarkGreen, "\e[42m")] + [Arguments(DarkCyan, "\e[46m")] + [Arguments(DarkRed, "\e[41m")] + [Arguments(DarkMagenta, "\e[45m")] + [Arguments(DarkYellow, "\e[43m")] + [Arguments(Gray, "\e[47m")] + [Arguments(DarkGray, "\e[100m")] + [Arguments(Blue, "\e[104m")] + [Arguments(Green, "\e[102m")] + [Arguments(Cyan, "\e[106m")] + [Arguments(Red, "\e[101m")] + [Arguments(Magenta, "\e[105m")] + [Arguments(Yellow, "\e[103m")] + [Arguments(White, "\e[107m")] + public async Task AnsiColors_BackgroundSequences(ConsoleColor color, string expectedSequence) { + var sequence = AnsiColors.Background(color); + await Assert.That(sequence).IsEqualTo(expectedSequence); + } } From 88a1ab47913ece349db1ac6ba8147f51816094ef Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 15:11:05 +0200 Subject: [PATCH 10/23] GetWriter > GetPipeTarget --- PrettyConsole/ConsoleContext.cs | 10 +++++----- .../PrettyConsoleInterpolatedStringHandler.cs | 2 +- PrettyConsole/ProgressBar.cs | 4 ++-- PrettyConsole/RenderingExtensions.cs | 6 +++--- PrettyConsole/WriteExtensions.cs | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/PrettyConsole/ConsoleContext.cs b/PrettyConsole/ConsoleContext.cs index b8b8873..cc7e224 100755 --- a/PrettyConsole/ConsoleContext.cs +++ b/PrettyConsole/ConsoleContext.cs @@ -29,7 +29,7 @@ public static class ConsoleContext { /// Gets the appropriate based on /// /// - internal static TextWriter GetWriter(OutputPipe pipe) + internal static TextWriter GetPipeTarget(OutputPipe pipe) => pipe switch { OutputPipe.Error => Error, _ => Out @@ -39,10 +39,10 @@ internal static TextWriter GetWriter(OutputPipe pipe) /// Gets the appropriate based on /// /// - internal static (TextWriter Writer, bool IsRedirected) GetWriterAndRedirection(OutputPipe pipe) { + internal static (TextWriter Writer, bool IsRedirected) GetPipeTargetAndState(OutputPipe pipe) { return pipe switch { - OutputPipe.Out => (Out, Console.IsOutputRedirected), - OutputPipe.Error => (Error, Console.IsErrorRedirected), + OutputPipe.Out => (Out, !ReferenceEquals(Out, Console.Out) || Console.IsOutputRedirected), + OutputPipe.Error => (Error, !ReferenceEquals(Error, Console.Error) || Console.IsErrorRedirected), _ => throw new InvalidOperationException("A pipe that isn't Out or Error is not supported."), }; } @@ -75,4 +75,4 @@ public void WriteWhiteSpaces(int length) { } private static readonly string WhiteSpaces = new(' ', 256); -} \ No newline at end of file +} diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index cfcbce8..94158de 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -64,7 +64,7 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo _buffer = BufferPool.Rent(_capacity); _currentForeground = ConsoleColor.DefaultForeground; _currentBackground = ConsoleColor.DefaultBackground; - (_writer, _isRedirected) = ConsoleContext.GetWriterAndRedirection(pipe); + (_writer, _isRedirected) = ConsoleContext.GetPipeTargetAndState(pipe); _provider = provider; shouldAppend = true; } diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 8980f22..8ff3579 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -85,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); - ConsoleContext.GetWriter(OutputPipe.Error).WriteWhiteSpaces(1); + ConsoleContext.GetPipeTarget(OutputPipe.Error).WriteWhiteSpaces(1); WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); } } else { @@ -136,7 +136,7 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo int barLength = Math.Max(0, constrainedWidth - decorationWidth); - var writer = ConsoleContext.GetWriter(pipe); + var writer = ConsoleContext.GetPipeTarget(pipe); Console.Write('[', pipe); if (barLength > 0) { diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index 3a832b7..3fd3e3b 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -27,7 +27,7 @@ internal static void ConfigureCursorAccessors(Func? cursorTopAccessor, Acti /// /// public static void WriteWhiteSpaces(int length, OutputPipe pipe = OutputPipe.Out) { - ConsoleContext.GetWriter(pipe).WriteWhiteSpaces(length); + ConsoleContext.GetPipeTarget(pipe).WriteWhiteSpaces(length); } /// @@ -39,7 +39,7 @@ public static void WriteWhiteSpaces(int length, OutputPipe pipe = OutputPipe.Out /// Useful for clearing output of overriding functions, like the ProgressBar /// public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { - var textWriter = ConsoleContext.GetWriter(pipe); + var textWriter = ConsoleContext.GetPipeTarget(pipe); var lineLength = ConsoleContext.GetWidthOrDefault(); var currentLine = GetCurrentLine(); GoToLine(currentLine); @@ -53,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) { - ConsoleContext.GetWriter(pipe).WriteLine(); + ConsoleContext.GetPipeTarget(pipe).WriteLine(); } /// diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 0ccab5e..63c88f0 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -135,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); - ConsoleContext.GetWriter(pipe).Write(span); + ConsoleContext.GetPipeTarget(pipe).Write(span); Console.ResetColor(); } } From bd28cff7d599bd465eee3a659ed01846cb06faca Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 15:30:14 +0200 Subject: [PATCH 11/23] Increase test coverage 1 --- PrettyConsole.UnitTests/ConsoleColorTests.cs | 11 +++ .../PrettyConsoleExtensionsTests.cs | 44 ++++++++++ ...tyConsoleInterpolatedStringHandlerTests.cs | 84 +++++++++++++++++++ PrettyConsole.UnitTests/ProgressBarTests.cs | 57 +++++++++++++ .../WriteExtensionsTests.cs | 8 ++ .../WriteLineExtensionsTests.cs | 36 ++++++++ PrettyConsole/AnsiColors.cs | 4 +- 7 files changed, 242 insertions(+), 2 deletions(-) diff --git a/PrettyConsole.UnitTests/ConsoleColorTests.cs b/PrettyConsole.UnitTests/ConsoleColorTests.cs index e47898b..5e04a60 100644 --- a/PrettyConsole.UnitTests/ConsoleColorTests.cs +++ b/PrettyConsole.UnitTests/ConsoleColorTests.cs @@ -77,4 +77,15 @@ public async Task AnsiColors_BackgroundSequences(ConsoleColor color, string expe var sequence = AnsiColors.Background(color); await Assert.That(sequence).IsEqualTo(expectedSequence); } + + [Test] + public async Task AnsiColors_InternalBuilders_MatchPublicAccessors() { + foreach (var color in Enum.GetValues()) { + var fgBuilt = AnsiColors.BuildForegroundSequence(color); + var bgBuilt = AnsiColors.BuildBackgroundSequence(color); + + await Assert.That(AnsiColors.Foreground(color)).IsEqualTo(fgBuilt); + await Assert.That(AnsiColors.Background(color)).IsEqualTo(bgBuilt); + } + } } diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs index cdfa51c..8f0a4a1 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -13,4 +13,48 @@ public async Task WriteWhiteSpaces_WritesRequestedLength() { await Assert.That(writer.ToString().Length).IsEqualTo(300); await Assert.That(writer.ToString().All(c => c == ' ')).IsTrue(); } + + [Test] + public async Task Console_WriteWhiteSpaces_RoutesToCorrectPipe() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteWhiteSpaces(5); + + await Assert.That(writer.ToString()).IsEqualTo(" "); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task ConsoleContext_GetPipeTargetAndState_ReportsCustomOutAsRedirected() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + var (pipeWriter, redirected) = ConsoleContext.GetPipeTargetAndState(OutputPipe.Out); + + await Assert.That(pipeWriter).IsSameReferenceAs(writer); + await Assert.That(redirected).IsTrue(); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task ConsoleContext_GetPipeTargetAndState_TracksConsoleStreams() { + var originalOut = Out; + try { + Out = Console.Out; + + var (pipeWriter, redirected) = ConsoleContext.GetPipeTargetAndState(OutputPipe.Out); + + await Assert.That(pipeWriter).IsSameReferenceAs(Console.Out); + await Assert.That(redirected).IsEqualTo(Console.IsOutputRedirected); + } finally { + Out = originalOut; + } + } } diff --git a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs index db82317..464108d 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -9,6 +9,90 @@ public PrettyConsoleInterpolatedStringHandlerTests() { Out = Utilities.GetWriter(out _writer); } + [Test] + public async Task ReadOnlySpanAlignment_PadsCorrectly() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + ReadOnlySpan span = "Hi".AsSpan(); + Console.WriteInterpolated($"{span,5}"); + + await Assert.That(writer.ToString()).IsEqualTo(" Hi"); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task GenericFormatsAndAlignment_WorkForISpanFormattable() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + decimal amount = 12.34m; + Console.WriteInterpolated($"{amount:F1} {(decimal)5.5,6:F2}"); + + await Assert.That(writer.ToString()).IsEqualTo("12.3 5.50"); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task ColorTokens_CountWhenNotRedirected() { + var originalOut = Out; + try { + Out = Console.Out; + + int chars = Console.WriteInterpolated($"{ConsoleColor.Red}X{ConsoleColor.Default}"); + + await Assert.That(chars).IsEqualTo(1); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task GrowAndEnsureCapacity_HandleLargePayloads() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + var longText = new string('x', 5000); + + Console.WriteInterpolated($"{longText}"); + + await Assert.That(writer.ToString().Length).IsEqualTo(5000); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task ThrowsAfterFlush_WhenAppendingFurther() { + var handler = new PrettyConsoleInterpolatedStringHandler(0, 0, OutputPipe.Out, provider: null, out var shouldAppend); + await Assert.That(shouldAppend).IsTrue(); + + handler.AppendLiteral("ok"); + handler.Flush(); + + await Assert.That(() => handler.AppendLiteral("fail")).Throws(); + } + + [Test] + public async Task AlignmentWithPaddingRight_AddsSpaces() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteInterpolated($"{123,-5}"); + + await Assert.That(writer.ToString()).IsEqualTo("123 "); + } finally { + Out = originalOut; + } + } + [Test] [Arguments(0, 0, 0, "0h 0m 0s")] [Arguments(0, 1, 2, "0h 1m 2s")] diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index 6b5f6c7..55f7f7c 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -137,6 +137,39 @@ public async Task ProgressBar_Update_RespectsMaxLineWidth() { await Assert.That(segment.Length).IsEqualTo(expectedWidth); } + [Test] + public async Task ProgressBar_Update_Overloads_WriteOutput() { + var originalError = Error; + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new ProgressBar { ProgressColor = Cyan }; + + bar.Update(10); + bar.Update(20.0, "status"); + } finally { + Error = originalError; + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + + await Assert.That(errorWriter.ToString()).Contains("status"); + } + + [Test] + public async Task ProgressBar_WriteProgressBar_DoubleOverload_WritesOutput() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + ProgressBar.WriteProgressBar(OutputPipe.Out, 33.3, Blue, '*'); + + await Assert.That(writer.ToString()).Contains("33%"); + } finally { + Out = originalOut; + } + } + [Test] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { Error = Utilities.GetWriter(out var errorWriter); @@ -161,6 +194,30 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() RenderingExtensions.ConfigureCursorAccessors(null, null); } } + + [Test] + public async Task IndeterminateProgressBar_RunAsync_OverloadsAndForegroundSetter() { + var originalError = Error; + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new IndeterminateProgressBar { + DisplayElapsedTime = false, + UpdateRate = 5 + }; + bar.ForegroundColor = Cyan; + + var genericResult = await bar.RunAsync(Task.Run(async () => { await Task.Delay(10); return 7; })); + await bar.RunAsync(Task.Run(async () => await Task.Delay(10))); + + await Assert.That(genericResult).IsEqualTo(7); + await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty); + } finally { + Error = originalError; + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] diff --git a/PrettyConsole.UnitTests/WriteExtensionsTests.cs b/PrettyConsole.UnitTests/WriteExtensionsTests.cs index 0564b76..b9dca60 100755 --- a/PrettyConsole.UnitTests/WriteExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteExtensionsTests.cs @@ -94,6 +94,14 @@ public async Task Write_ReadOnlySpan_WithColors_WritesToSelectedPipe() { await Assert.That(_errorWriter.ToStringAndFlush()).IsEqualTo("Data"); } + [Test] + public async Task Write_ReadOnlySpan_DefaultAndForeground() { + Console.Write("abc".AsSpan(), OutputPipe.Out); + Console.Write("XYZ".AsSpan(), OutputPipe.Out, ConsoleColor.Green); + + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("abcXYZ"); + } + private readonly ref struct LongFormatStud : ISpanFormattable { public const int Length = 1024; diff --git a/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs index 6c2dd9c..1bc1eec 100755 --- a/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs @@ -1,5 +1,7 @@ namespace PrettyConsole.UnitTests; +using System.Globalization; + public class WriteLineExtensionsTests { private readonly StringWriter _writer; private readonly StringWriter _errorWriter; @@ -28,4 +30,38 @@ public async Task WriteLine_ReadOnlySpan_WithColors_AppendsNewLine() { Console.WriteLine("SpanLine".AsSpan(), OutputPipe.Out, ConsoleColor.Yellow, ConsoleColor.Black); await Assert.That(_writer.ToStringAndFlush()).IsEqualTo($"SpanLine{_writer.NewLine}"); } + + [Test] + public async Task WriteLine_ISpanFormattable_Overloads() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteLine(3.14, OutputPipe.Out); + Console.WriteLine(2.71, OutputPipe.Out, ConsoleColor.Yellow); + Console.WriteLine(1.23, OutputPipe.Out, ConsoleColor.Yellow, ConsoleColor.Black); + Console.WriteLine(9.99, OutputPipe.Out, ConsoleColor.White, ConsoleColor.Black, "F1", CultureInfo.InvariantCulture); + + var expected = $"3.14{writer.NewLine}2.71{writer.NewLine}1.23{writer.NewLine}10.0{writer.NewLine}"; + await Assert.That(writer.ToString()).IsEqualTo(expected); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task WriteLine_ReadOnlySpan_DefaultAndForeground() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteLine("span".AsSpan(), OutputPipe.Out); + Console.WriteLine("more".AsSpan(), OutputPipe.Out, ConsoleColor.Cyan); + + var expected = $"span{writer.NewLine}more{writer.NewLine}"; + await Assert.That(writer.ToString()).IsEqualTo(expected); + } finally { + Out = originalOut; + } + } } diff --git a/PrettyConsole/AnsiColors.cs b/PrettyConsole/AnsiColors.cs index d2f67b7..3a710bf 100644 --- a/PrettyConsole/AnsiColors.cs +++ b/PrettyConsole/AnsiColors.cs @@ -37,7 +37,7 @@ public static string Background(ConsoleColor color) { } - private static string BuildForegroundSequence(ConsoleColor color) { + internal static string BuildForegroundSequence(ConsoleColor color) { return color switch { ConsoleColor.Black => "\e[30m", ConsoleColor.DarkBlue => "\e[34m", @@ -59,7 +59,7 @@ private static string BuildForegroundSequence(ConsoleColor color) { }; } - private static string BuildBackgroundSequence(ConsoleColor color) { + internal static string BuildBackgroundSequence(ConsoleColor color) { return color switch { ConsoleColor.Black => "\e[40m", ConsoleColor.DarkBlue => "\e[44m", From 1ca76c58c0876022d874b8dde5431ad19c19bb00 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 15:59:14 +0200 Subject: [PATCH 12/23] Increase test coverage 2 --- PrettyConsole.UnitTests/ConsoleColorTests.cs | 4 + PrettyConsole.UnitTests/MenusTests.cs | 25 ++++++ .../PrettyConsoleExtensionsTests.cs | 33 ++++++++ ...tyConsoleInterpolatedStringHandlerTests.cs | 84 +++++++++++++++++++ PrettyConsole.UnitTests/ProgressBarTests.cs | 79 +++++++++++++++++ .../WriteExtensionsTests.cs | 7 ++ .../WriteLineExtensionsTests.cs | 10 +-- PrettyConsole/AnsiColors.cs | 4 +- PrettyConsole/ConsoleContext.cs | 3 +- 9 files changed, 241 insertions(+), 8 deletions(-) diff --git a/PrettyConsole.UnitTests/ConsoleColorTests.cs b/PrettyConsole.UnitTests/ConsoleColorTests.cs index 5e04a60..2623b13 100644 --- a/PrettyConsole.UnitTests/ConsoleColorTests.cs +++ b/PrettyConsole.UnitTests/ConsoleColorTests.cs @@ -87,5 +87,9 @@ public async Task AnsiColors_InternalBuilders_MatchPublicAccessors() { await Assert.That(AnsiColors.Foreground(color)).IsEqualTo(fgBuilt); await Assert.That(AnsiColors.Background(color)).IsEqualTo(bgBuilt); } + + ConsoleColor @default = (ConsoleColor)(-1); + await Assert.That(AnsiColors.Foreground(@default)).IsEqualTo(AnsiColors.ForegroundResetSequence); + await Assert.That(AnsiColors.Background(@default)).IsEqualTo(AnsiColors.BackgroundResetSequence); } } diff --git a/PrettyConsole.UnitTests/MenusTests.cs b/PrettyConsole.UnitTests/MenusTests.cs index aea1fa9..5767bd8 100644 --- a/PrettyConsole.UnitTests/MenusTests.cs +++ b/PrettyConsole.UnitTests/MenusTests.cs @@ -131,6 +131,19 @@ await Assert.That(() => Console.TreeMenu(menu, $"Menu: ")) .Throws(); } + [Test] + public async Task TreeMenu_NonNumericInput_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("a b"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open" } + }; + + await Assert.That(() => Console.TreeMenu(menu, $"Menu: ")) + .Throws(); + } + [Test] public async Task Table_WritesHeaderAndRows() { Out = Utilities.GetWriter(out var writer); @@ -158,4 +171,16 @@ public async Task Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() await Assert.That(() => Console.Table(headers, [column1])) .Throws(); } + + [Test] + public async Task MultiSelection_MixedValidAndInvalidIndices_ReturnsOnlyValid() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("1 5 2"); + + var choices = new List { "One", "Two", "Three" }; + + var result = Console.MultiSelection(choices, $"Numbers: "); + + await Assert.That(result.Length).IsEqualTo(0); + } } diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs index 8f0a4a1..d757406 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -57,4 +57,37 @@ public async Task ConsoleContext_GetPipeTargetAndState_TracksConsoleStreams() { Out = originalOut; } } + + [Test] + public async Task ConsoleContext_GetPipeTargetAndState_ForErrorPipe() { + var originalErr = Error; + try { + Error = Utilities.GetWriter(out var writer); + + var (pipeWriter, redirected) = ConsoleContext.GetPipeTargetAndState(OutputPipe.Error); + + await Assert.That(pipeWriter).IsSameReferenceAs(writer); + await Assert.That(redirected).IsTrue(); + } finally { + Error = originalErr; + } + } + + [Test] + public async Task ConsoleContext_GetWidthOrDefault_WhenRedirected() { + var originalOut = Console.Out; + Console.SetOut(new StringWriter()); + + int width = ConsoleContext.GetWidthOrDefault(77); + Console.SetOut(originalOut); + + await Assert.That(width).IsEqualTo(77); + } + + [Test] + public async Task RenderingExtensions_DefaultCursorAccessor_IsInvoked() { + // use default cursor accessors (no override) + int line = Console.GetCurrentLine(); + await Assert.That(line).IsGreaterThanOrEqualTo(0); + } } diff --git a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs index 464108d..08beb2f 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -195,6 +195,90 @@ public async Task WriteLineInterpolated_WithColorsAndPrimitives_CountExcludesNew await Assert.That(chars).IsEqualTo("[0h 0m 42s] done".Length); } + [Test] + public async Task AppendFormattedBackground_ChangesBackgroundWhenNotRedirected() { + var originalOut = Out; + try { + Out = Console.Out; + int chars = Console.WriteInterpolated($"{ConsoleColor.Red}{ConsoleColor.Black / ConsoleColor.White}X"); + await Assert.That(chars).IsEqualTo(1); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task AppendFormattedObject_WithAlignment_UsesObjectToString() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + var obj = new object(); + + Console.WriteInterpolated($"{obj,6}"); + + await Assert.That(writer.ToString()).IsEqualTo($"{obj,6}"); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task AppendFormattedTimeSpan_WithAlignmentAndDurationFormat() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + var ts = TimeSpan.FromSeconds(5); + + Console.WriteInterpolated($"{ts,10:duration}"); + + await Assert.That(writer.ToString()).IsEqualTo("0h 0m 5s"); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task AppendFormattedDouble_WithAlignmentAndFormat() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteInterpolated($"{12.345,8:F2}"); + + await Assert.That(writer.ToString()).IsEqualTo(" 12.35"); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task AppendSpanFormattable_WithAlignmentAndFormat() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteInterpolated($"{1234,6:D}"); + + await Assert.That(writer.ToString()).IsEqualTo(" 1234"); + } finally { + Out = originalOut; + } + } + + [Test] + public async Task AppendSpan_LeftAndRightAlignment() { + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var writer); + + Console.WriteInterpolated($"{ "Hi",4}{ "Bye",-5}"); + + await Assert.That(writer.ToString()).IsEqualTo(" HiBye "); + } finally { + Out = originalOut; + } + } + private static string FormatBytes(double value) { const double formatBytesKb = 1024d; var suffix = 0; diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index 55f7f7c..1879567 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -170,6 +170,38 @@ public async Task ProgressBar_WriteProgressBar_DoubleOverload_WritesOutput() { } } + [Test] + public async Task ProgressBar_Update_DoubleOverload_WritesPercentage() { + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new ProgressBar { ProgressColor = Green }; + bar.Update(12.5); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + + await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty); + } + + [Test] + public async Task ProgressBar_Update_StatusSpan_SameLineFalse() { + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new ProgressBar { ProgressColor = Green }; + ReadOnlySpan status = "span-status".AsSpan(); + + bar.Update(30, status, sameLine: false); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + + await Assert.That(errorWriter.ToString()).Contains("span-status"); + } + [Test] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { Error = Utilities.GetWriter(out var errorWriter); @@ -218,6 +250,53 @@ public async Task IndeterminateProgressBar_RunAsync_OverloadsAndForegroundSetter RenderingExtensions.ConfigureCursorAccessors(null, null); } } + + [Test] + public async Task IndeterminateProgressBar_RunAsync_Generic_TaskAlreadyCompleted() { + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new IndeterminateProgressBar { + DisplayElapsedTime = false, + UpdateRate = 5 + }; + + var completed = Task.FromResult(5); + var result = await bar.RunAsync(completed, "done"); + + await Assert.That(result).IsEqualTo(5); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } + + [Test] + public async Task IndeterminateProgressBar_RunAsync_CancelsQuickly() { + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + using var cts = new CancellationTokenSource(); + try { + var bar = new IndeterminateProgressBar { + DisplayElapsedTime = false, + UpdateRate = 5 + }; + + var task = Task.Run(async () => { + await Task.Delay(1000, cts.Token); + }, cts.Token); + + cts.CancelAfter(10); + await bar.RunAsync(task, "cancelled", cts.Token); + } catch (OperationCanceledException) { + // expected in this path + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + + await Assert.That(errorWriter.ToString()).Contains("cancelled"); + } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] diff --git a/PrettyConsole.UnitTests/WriteExtensionsTests.cs b/PrettyConsole.UnitTests/WriteExtensionsTests.cs index b9dca60..ec118aa 100755 --- a/PrettyConsole.UnitTests/WriteExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteExtensionsTests.cs @@ -88,6 +88,13 @@ public async Task Write_SpanFormattable_WithFormatAndProvider() { await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("12.35"); } + [Test] + public async Task Write_SpanFormattable_WithColorsAndFormatProvider_DirectOverload() { + var value = 42.195; + Console.Write(value, OutputPipe.Out, White, Black, "F1", CultureInfo.InvariantCulture); + await Assert.That(_writer.ToStringAndFlush()).IsEqualTo("42.2"); + } + [Test] public async Task Write_ReadOnlySpan_WithColors_WritesToSelectedPipe() { Console.Write("Data".AsSpan(), OutputPipe.Error, ConsoleColor.Green, ConsoleColor.Black); diff --git a/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs index 1bc1eec..913e516 100755 --- a/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs @@ -58,10 +58,10 @@ public async Task WriteLine_ReadOnlySpan_DefaultAndForeground() { Console.WriteLine("span".AsSpan(), OutputPipe.Out); Console.WriteLine("more".AsSpan(), OutputPipe.Out, ConsoleColor.Cyan); - var expected = $"span{writer.NewLine}more{writer.NewLine}"; - await Assert.That(writer.ToString()).IsEqualTo(expected); - } finally { - Out = originalOut; - } + var expected = $"span{writer.NewLine}more{writer.NewLine}"; + await Assert.That(writer.ToString()).IsEqualTo(expected); + } finally { + Out = originalOut; } } +} diff --git a/PrettyConsole/AnsiColors.cs b/PrettyConsole/AnsiColors.cs index 3a710bf..bccd86d 100644 --- a/PrettyConsole/AnsiColors.cs +++ b/PrettyConsole/AnsiColors.cs @@ -1,8 +1,8 @@ namespace PrettyConsole; internal static class AnsiColors { - private const string ForegroundResetSequence = "\e[39m"; - private const string BackgroundResetSequence = "\e[49m"; + internal const string ForegroundResetSequence = "\e[39m"; + internal const string BackgroundResetSequence = "\e[49m"; private static readonly string[] ForegroundCodes; private static readonly string[] BackgroundCodes; diff --git a/PrettyConsole/ConsoleContext.cs b/PrettyConsole/ConsoleContext.cs index cc7e224..a0a7596 100755 --- a/PrettyConsole/ConsoleContext.cs +++ b/PrettyConsole/ConsoleContext.cs @@ -52,7 +52,8 @@ internal static (TextWriter Writer, bool IsRedirected) GetPipeTargetAndState(Out /// /// internal static int GetWidthOrDefault(int defaultWidth = 120) { - if (Console.IsOutputRedirected) { + // If output is redirected or a custom writer is injected, fall back to the provided default. + if (Console.IsOutputRedirected || !ReferenceEquals(Out, Console.Out)) { return defaultWidth; } return Console.BufferWidth; From fa9f37f1a2461c42f9a2e18981d238b36d077183 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 16:08:19 +0200 Subject: [PATCH 13/23] - --- PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs index d757406..7fb995b 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -73,16 +73,20 @@ public async Task ConsoleContext_GetPipeTargetAndState_ForErrorPipe() { } } +#pragma warning disable TUnit0055 // Do not overwrite the Console writer [Test] public async Task ConsoleContext_GetWidthOrDefault_WhenRedirected() { var originalOut = Console.Out; + Console.SetOut(new StringWriter()); + int width = ConsoleContext.GetWidthOrDefault(77); Console.SetOut(originalOut); await Assert.That(width).IsEqualTo(77); } +#pragma warning restore TUnit0055 // Do not overwrite the Console writer [Test] public async Task RenderingExtensions_DefaultCursorAccessor_IsInvoked() { From f81f9a3330db09c183a491573f93d466bbaa2214 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 16:13:14 +0200 Subject: [PATCH 14/23] Removed old docs --- PrettyConsole/InputRequestExtensions.cs | 6 +++--- PrettyConsole/ReadLineExtensions.cs | 14 +++++++------- PrettyConsole/WriteExtensions.cs | 4 ++-- PrettyConsole/WriteLineExtensions.cs | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index f5ecd05..ff7f1d5 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -23,7 +23,7 @@ internal static void ConfigureReadKey(Func? readKey) { /// /// Used to wait for user input /// - /// Interpolated string handler that streams the content. + /// public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.Flush(); _ = s_readKey(); @@ -32,7 +32,7 @@ public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyCon /// /// Used to get user confirmation with the default values ["y", "yes"] or just pressing enter /// - /// Interpolated string handler that streams the content. + /// /// /// It does not display a question mark or any other prompt, only the message /// @@ -43,7 +43,7 @@ public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyCon /// /// a collection of values that indicate positive confirmation /// if simply pressing enter is considered positive or not - /// Interpolated string handler that streams the content. + /// /// /// It does not display a question mark or any other prompt, only the message /// diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs index 0b7c64f..b647546 100755 --- a/PrettyConsole/ReadLineExtensions.cs +++ b/PrettyConsole/ReadLineExtensions.cs @@ -12,7 +12,7 @@ public static class ReadLineExtensions { /// /// /// The result of the parsing - /// Interpolated string handler that streams the content. + /// /// True if the parsing was successful, false otherwise public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { handler.Flush(); @@ -26,7 +26,7 @@ public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgum /// /// The result of the parsing /// The default value to return if parsing fails - /// Interpolated string handler that streams the content. + /// /// 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); @@ -44,7 +44,7 @@ public static bool TryReadLine(out T result, T @default, [InterpolatedStringH /// /// The result of the parsing /// Whether to ignore case when parsing - /// Interpolated string handler that streams the content. + /// /// 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); @@ -57,7 +57,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, [Interp /// The result of the parsing /// Whether to ignore case when parsing /// The default value to return if parsing fails - /// Interpolated string handler that streams the content. + /// /// 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.Flush(); @@ -72,7 +72,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ /// /// Used to request user input. /// - /// Interpolated string handler that streams the content. + /// /// A string if the user entered any, empty string otherwise - never null. [OverloadResolutionPriority(3)] public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { @@ -84,7 +84,7 @@ public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleI /// Used to request user input, validates and converts common types. /// /// - /// Interpolated string handler that streams the content. + /// /// The result of the parsing [OverloadResolutionPriority(2)] public static T? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { @@ -97,7 +97,7 @@ public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleI /// /// /// The default value to return if parsing fails - /// Interpolated string handler that streams the content. + /// /// The result of the parsing [OverloadResolutionPriority(1)] public static T ReadLine(T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 63c88f0..45078d1 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -10,7 +10,7 @@ public static class WriteExtensions { /// /// Writes interpolated content using to . /// - /// Interpolated string handler that streams the content. + /// /// The number of characters written by the handler. public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.Flush(); @@ -21,7 +21,7 @@ public static int WriteInterpolated([InterpolatedStringHandlerArgument] PrettyCo /// Writes interpolated content using . /// /// Destination pipe. Defaults to . - /// Interpolated string handler that streams the content. + /// /// The number of characters written by the handler. public static int WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { handler.Flush(); diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index 102e0e6..5c5cadd 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -8,7 +8,7 @@ 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 int WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { @@ -21,7 +21,7 @@ public static int WriteLineInterpolated([InterpolatedStringHandlerArgument] Pret /// Writes interpolated content using . /// /// Destination pipe. Defaults to . - /// Interpolated string handler that streams the content. + /// /// The number of characters written by the handler. public static int WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { handler.AppendNewLine(); From ef4a55a29e2781af9fde37792afea20d7e153546 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 16:34:38 +0200 Subject: [PATCH 15/23] Add WhiteSpace --- .../PrettyConsoleInterpolatedStringHandler.cs | 14 +++++++- PrettyConsole/ProgressBar.cs | 35 ++++++------------- PrettyConsole/WhiteSpace.cs | 7 ++++ Versions.md | 1 + 4 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 PrettyConsole/WhiteSpace.cs diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 94158de..e955248 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -137,7 +137,7 @@ public void AppendFormattedBackground(ConsoleColor color) { } /// - /// Sets the foreground and background colors of the console + /// Sets the foreground and background colors of the console. /// /// public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors) { @@ -145,6 +145,18 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c AppendFormattedBackground(colors.Background); } + /// + /// 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; + } + /// /// Append timeSpan with optional formatting. /// diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 8ff3579..4ec3ffa 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -46,7 +46,7 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty, true); + public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); /// /// Updates the progress bar with the specified percentage. @@ -55,7 +55,7 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty, true); + public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty); /// /// Updates the progress bar with the specified percentage and header text. @@ -82,7 +82,7 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr lock (_lock) { var currentLine = Console.GetCurrentLine(); if (sameLine) { - Console.ClearNextLines(1, OutputPipe.Error); + Console.ClearNextLines(1); if (status.Length > 0) { Console.Write(status, OutputPipe.Error, ForegroundColor); ConsoleContext.GetPipeTarget(OutputPipe.Error).WriteWhiteSpaces(1); @@ -91,7 +91,7 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr } else { bool hasStatus = status.Length > 0; int lines = hasStatus ? 2 : 1; - Console.ClearNextLines(lines, OutputPipe.Error); + Console.ClearNextLines(lines); if (hasStatus) Console.WriteLine(status, OutputPipe.Error, ForegroundColor); WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); } @@ -136,26 +136,13 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo int barLength = Math.Max(0, constrainedWidth - decorationWidth); - var writer = ConsoleContext.GetPipeTarget(pipe); - Console.Write('[', pipe); + int filled = Math.Min((int)(barLength * p * 0.01), barLength); + Span s = filled > 0 + ? stackalloc char[filled] + : Span.Empty; + s.Fill(progressChar); + int remaining = barLength - filled; - if (barLength > 0) { - int filled = Math.Min((int)(barLength * p * 0.01), barLength); - - if (filled > 0) { - Console.SetColors(progressColor, Console.BackgroundColor); - Span s = stackalloc char[filled]; - s.Fill(progressChar); - writer.Write(s); - Console.ResetColor(); - } - - int remaining = barLength - filled; - if (remaining > 0) { - writer.WriteWhiteSpaces(remaining); - } - } - - Console.WriteInterpolated(pipe, $"] {p,3}%"); + Console.WriteInterpolated(pipe, $"[{progressColor}{s}{ConsoleColor.DefaultForeground}{new WhiteSpace(remaining)}] {p,3}%"); } } \ No newline at end of file diff --git a/PrettyConsole/WhiteSpace.cs b/PrettyConsole/WhiteSpace.cs new file mode 100644 index 0000000..0314681 --- /dev/null +++ b/PrettyConsole/WhiteSpace.cs @@ -0,0 +1,7 @@ +namespace PrettyConsole; + +/// +/// Declares a of whitespaces +/// +/// +public readonly record struct WhiteSpace(int Length); \ No newline at end of file diff --git a/Versions.md b/Versions.md index 74aac64..7c328ae 100755 --- a/Versions.md +++ b/Versions.md @@ -3,6 +3,7 @@ ## v5.2.0 - `PrettyConsoleInterpolatedStringHandler` was rewritten to buffer the contents instead of stream them, along with other optimizations, massively improving performance. +- `WhiteSpace` is a new struct that declares a section of whitespace with length, `PrettyConsoleInterpolatedStringHandler` was thought to recognize it is a special parameter and will render a region of whitespaces with that length to the buffer. ## v5.1.0 From 8c033b3c0dadec4a750b413be65abb097e7934cf Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 16:38:42 +0200 Subject: [PATCH 16/23] Simplify code --- PrettyConsole/IndeterminateProgressBar.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 6f03156..f219ec8 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -88,8 +88,6 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d //ignore } - Console.ResetColor(); - ConsoleColor originalColor = Console.ForegroundColor; long startTime = Stopwatch.GetTimestamp(); long updateRateAsTicks = TimeSpan.FromMilliseconds(UpdateRate).Ticks; @@ -104,12 +102,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); while (!task.IsCompleted && !token.IsCancellationRequested) { - try { - Console.ForegroundColor = ForegroundColor; - ConsoleContext.Error.Write(AnimationSequence[seqIndex]); - } finally { - Console.ForegroundColor = originalColor; - } + Console.WriteInterpolated(OutputPipe.Error, $"{ForegroundColor}{AnimationSequence[seqIndex]}{ConsoleColor.DefaultForeground}"); if (header.Length > 0) { Console.WriteInterpolated(OutputPipe.Error, $" {header}"); @@ -160,8 +153,6 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d seqIndex = 0; } } - - Console.ResetColor(); } private Task RunAsyncNonGeneric(Task task, string header, CancellationToken token) => RunAsync(task, header, token); From 7b3bea6c9d2a3fb315d9fc0fb56d2990221ca341 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 16:48:28 +0200 Subject: [PATCH 17/23] Simplify code 2 --- PrettyConsole/MenuExtensions.cs | 41 +++++++++++++------------------- PrettyConsole/WriteExtensions.cs | 19 ++++++++------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index fc62896..cb811bd 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -66,7 +66,7 @@ public static string[] MultiSelection(TList choices, [InterpolatedStringH return []; } - var entries = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var entries = input.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (entries.Length is 0) { return []; @@ -105,38 +105,29 @@ 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 = ConsoleContext.GetWidthOrDefault(); - var array = pool.Rent(width); - try { - var span = new Span(array); - //Enumerate options and sub-options - for (int i = 0; i < menuKeys.Length; i++) { - var mainEntry = menuKeys[i]; - var subChoices = menu[mainEntry]; + //Enumerate options and sub-options + for (int i = 0; i < menuKeys.Length; i++) { + var mainEntry = menuKeys[i]; + var subChoices = menu[mainEntry]; - span.TryWrite($" {i + 1}) {mainEntry}", out int written); - ConsoleContext.Out.Write(span.Slice(0, written)); + int written = Console.WriteInterpolated($" {i + 1}) {mainEntry}"); - var remainingLength = maxMainOption - written; - if (remainingLength > 0) { - ConsoleContext.Out.WriteWhiteSpaces(remainingLength); - } - - for (int j = 0; j < subChoices.Count; j++) { - if (j is not 0) { - ConsoleContext.Out.WriteWhiteSpaces(maxMainOption); - } + var remainingLength = maxMainOption - written; + if (remainingLength > 0) { + ConsoleContext.Out.WriteWhiteSpaces(remainingLength); + } - span.TryWrite($" {j + 1}) {subChoices[j]}", out written); - ConsoleContext.Out.WriteLine(span.Slice(0, written)); + for (int j = 0; j < subChoices.Count; j++) { + if (j is not 0) { + ConsoleContext.Out.WriteWhiteSpaces(maxMainOption); } - Console.NewLine(); + Console.WriteInterpolated($" {j + 1}) {subChoices[j]}"); } - } finally { - pool.Return(array); + + Console.NewLine(); } string input = Console.ReadLine(string.Empty, $"Enter your main choice and sub choice separated with space: "); diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 45078d1..532d8e2 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -91,20 +91,21 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, where T : ISpanFormattable, allows ref struct { int lowerBound = 4096; var pool = ArrayPool.Shared; + char[] array; while (true) { - var array = pool.Rent(lowerBound); - try { - var buffer = new Span(array); - if (item.TryFormat(array, out int charsWritten, format, formatProvider)) { - Write(buffer.Slice(0, charsWritten), pipe, foreground, background); - return; - } - } finally { - pool.Return(array); + array = pool.Rent(lowerBound); + + var buffer = new Span(array); + if (item.TryFormat(array, out int charsWritten, format, formatProvider)) { + Write(buffer.Slice(0, charsWritten), pipe, foreground, background); + break; } + lowerBound *= 2; } + + pool.Return(array); } /// From 760c1360c023dd66126019674ca35b5e8a1742b8 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 17:35:35 +0200 Subject: [PATCH 18/23] Added header factory to indeterminateprogressbar --- .../Features/IndeterminateProgressBarTest.cs | 2 +- PrettyConsole.UnitTests/ProgressBarTests.cs | 14 ++--- PrettyConsole/IndeterminateProgressBar.cs | 54 +++++++++++-------- Versions.md | 1 + 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs index 24e0352..2272a89 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), "running..."); + await prg.RunAsync(Task.Delay(5_000), _ => $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}...", CancellationToken.None); } } \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index 1879567..ba596f3 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -215,10 +215,10 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() }; var cancellation = CancellationToken.None; - var result = await bar.RunAsync(Task.Run(async () => { + int result = await bar.RunAsync(Task.Run(async () => { await Task.Delay(20, cancellation); return 42; - }, cancellation), "Working", cancellation); + }, cancellation), _ => $"Working", cancellation); await Assert.That(result).IsEqualTo(42); await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty); @@ -263,7 +263,7 @@ public async Task IndeterminateProgressBar_RunAsync_Generic_TaskAlreadyCompleted }; var completed = Task.FromResult(5); - var result = await bar.RunAsync(completed, "done"); + var result = await bar.RunAsync(completed, _ => $"done"); await Assert.That(result).IsEqualTo(5); } finally { @@ -280,15 +280,15 @@ public async Task IndeterminateProgressBar_RunAsync_CancelsQuickly() { try { var bar = new IndeterminateProgressBar { DisplayElapsedTime = false, - UpdateRate = 5 + UpdateRate = 100 }; var task = Task.Run(async () => { - await Task.Delay(1000, cts.Token); + await Task.Delay(10000, cts.Token); }, cts.Token); - cts.CancelAfter(10); - await bar.RunAsync(task, "cancelled", cts.Token); + cts.CancelAfter(200); + await bar.RunAsync(task, _ => $"cancelled", cts.Token); } catch (OperationCanceledException) { // expected in this path } finally { diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index f219ec8..a22b857 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -21,7 +21,7 @@ public class IndeterminateProgressBar { /// /// You can also choose from some defaults in /// - public ReadOnlyCollection AnimationSequence { get; set; } = Patterns.Twirl; + public ReadOnlyCollection AnimationSequence { get; init; } = Patterns.Twirl; /// /// Gets or sets the foreground color of the progress bar. @@ -31,13 +31,13 @@ public class IndeterminateProgressBar { /// /// Gets or sets a value indicating whether to display the elapsed time in the progress bar. /// - public bool DisplayElapsedTime { get; set; } = true; + public bool DisplayElapsedTime { get; init; } = true; /// /// Gets or sets the update rate (in ms) of the indeterminate progress bar. /// /// Default = 200 - public int UpdateRate { get; set; } = 200; + public int UpdateRate { get; init; } = 200; /// /// Runs the indeterminate progress bar while the specified task is running. @@ -46,18 +46,20 @@ public class IndeterminateProgressBar { /// /// The output of the running task public async Task RunAsync(Task task, CancellationToken token = default) { - return await RunAsync(task, string.Empty, token); + await RunAsyncNonGeneric(task, null, token); + + return task.IsCompleted ? task.Result : await task; } /// - /// Runs the indeterminate progress bar while the specified task is running. + /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. /// /// - /// The header which to display before the progress char + /// Factory invoked every frame to render a header with . /// - /// The output of the running task - public async Task RunAsync(Task task, string header, CancellationToken token = default) { - await RunAsyncNonGeneric(task, header, token); + /// The output of the running task. + public async Task RunAsync(Task task, Func? headerFactory, CancellationToken token = default) { + await RunAsyncNonGeneric(task, headerFactory, token); return task.IsCompleted ? task.Result : await task; } @@ -68,18 +70,24 @@ public async Task RunAsync(Task task, string header, CancellationToken /// /// /// - public async Task RunAsync(Task task, CancellationToken token = default) { - await RunAsync(task, string.Empty, token); - } + public Task RunAsync(Task task, CancellationToken token = default) => RunAsyncNonGeneric(task, null, token); /// - /// Runs the indeterminate progress bar while the specified task is running. + /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. /// /// - /// The header which to display before the progress char + /// Factory invoked every frame to render a header with . /// - /// - public async Task RunAsync(Task task, string header, CancellationToken token = default) { + public Task RunAsync(Task task, Func? headerFactory, CancellationToken token) => RunAsyncNonGeneric(task, headerFactory, token); + + + /// + /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. + /// + /// + /// Factory invoked every frame to render a header with . + /// + private async Task RunAsyncNonGeneric(Task task, Func? headerFactory, CancellationToken token) { try { if (task.Status is not TaskStatus.Running) { task.Start(); @@ -98,14 +106,16 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token); // Cancel the delay token as soon as the bound task completes - _ = task.ContinueWith(static (t, state) => ((CancellationTokenSource)state!).Cancel(), linkedCts, + _ = task.ContinueWith(static (_, state) => ((CancellationTokenSource)state!).Cancel(), linkedCts, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); while (!task.IsCompleted && !token.IsCancellationRequested) { Console.WriteInterpolated(OutputPipe.Error, $"{ForegroundColor}{AnimationSequence[seqIndex]}{ConsoleColor.DefaultForeground}"); - if (header.Length > 0) { - Console.WriteInterpolated(OutputPipe.Error, $" {header}"); + if (headerFactory is not null) { + ConsoleContext.Error.WriteWhiteSpaces(1); + var headerHandler = headerFactory(OutputPipe.Error); + headerHandler.Flush(); } if (DisplayElapsedTime) { @@ -141,7 +151,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } // Always clear once per frame - Console.ClearNextLines(1, OutputPipe.Error); + Console.ClearNextLines(1); if (token.IsCancellationRequested || task.IsCompleted) { break; @@ -155,8 +165,6 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } } - private Task RunAsyncNonGeneric(Task task, string header, CancellationToken token) => RunAsync(task, header, token); - /// /// Provides constant animation sequences that can be used for /// @@ -206,4 +214,4 @@ public static readonly ReadOnlyCollection PingPong "| • |", ]); } -} \ No newline at end of file +} diff --git a/Versions.md b/Versions.md index 7c328ae..aaafbdf 100755 --- a/Versions.md +++ b/Versions.md @@ -4,6 +4,7 @@ - `PrettyConsoleInterpolatedStringHandler` was rewritten to buffer the contents instead of stream them, along with other optimizations, massively improving performance. - `WhiteSpace` is a new struct that declares a section of whitespace with length, `PrettyConsoleInterpolatedStringHandler` was thought to recognize it is a special parameter and will render a region of whitespaces with that length to the buffer. +- `IndeterminateProgressBar` overloads that accepted a `header` now accept a `Func` instead, which can be used to pull in data from variables via a closure and enable a more dynamic rendering of statuses. `OutputPipe` here should be ignored using `_` and not used inside the lambda, it is invoked internally to render to the correct pipe. ## v5.1.0 From c6615278c002f993e9fd417bc222978a77f73098 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 17:39:50 +0200 Subject: [PATCH 19/23] Fix formatting issue with treeMenu --- PrettyConsole/MenuExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index cb811bd..43930a5 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -124,7 +124,7 @@ public static (string option, string subOption) TreeMenu(Dictionary Date: Sat, 29 Nov 2025 17:58:19 +0200 Subject: [PATCH 20/23] Add simple string header overloads --- .../Features/IndeterminateProgressBarTest.cs | 2 +- PrettyConsole.UnitTests/ProgressBarTests.cs | 8 ++--- PrettyConsole/IndeterminateProgressBar.cs | 34 ++++++++++++++++--- .../PrettyConsoleInterpolatedStringHandler.cs | 10 ++++-- Versions.md | 2 +- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs index 2272a89..6b21598 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), _ => $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}...", CancellationToken.None); + await prg.RunAsync(Task.Delay(5_000), () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}..."), CancellationToken.None); } } \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index ba596f3..98af0f3 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -1,5 +1,3 @@ -using TUnit.Core.Attributes; - namespace PrettyConsole.UnitTests; [SkipWhenConsoleUnavailable] @@ -218,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), _ => $"Working", cancellation); + }, cancellation), () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"Working"), cancellation); await Assert.That(result).IsEqualTo(42); await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty); @@ -263,7 +261,7 @@ public async Task IndeterminateProgressBar_RunAsync_Generic_TaskAlreadyCompleted }; var completed = Task.FromResult(5); - var result = await bar.RunAsync(completed, _ => $"done"); + var result = await bar.RunAsync(completed, () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"done")); await Assert.That(result).IsEqualTo(5); } finally { @@ -288,7 +286,7 @@ public async Task IndeterminateProgressBar_RunAsync_CancelsQuickly() { }, cts.Token); cts.CancelAfter(200); - await bar.RunAsync(task, _ => $"cancelled", cts.Token); + await bar.RunAsync(task, "cancelled", cts.Token); } catch (OperationCanceledException) { // expected in this path } finally { diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index a22b857..30fb137 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -51,6 +51,18 @@ public async Task RunAsync(Task task, CancellationToken token = default return task.IsCompleted ? task.Result : await task; } + /// + /// Runs the indeterminate progress bar while the specified task is running. + /// + /// + /// + /// + public async Task RunAsync(Task task, string header, CancellationToken token) { + await RunAsyncNonGeneric(task, () => WrapHeader(header), token); + + return task.IsCompleted ? task.Result : await task; + } + /// /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. /// @@ -58,7 +70,7 @@ public async Task RunAsync(Task task, CancellationToken token = default /// 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, Func? headerFactory, CancellationToken token = default) { await RunAsyncNonGeneric(task, headerFactory, token); return task.IsCompleted ? task.Result : await task; @@ -72,13 +84,23 @@ public async Task RunAsync(Task task, Func public Task RunAsync(Task task, CancellationToken token = default) => RunAsyncNonGeneric(task, null, token); + /// + /// Runs the indeterminate progress bar while the specified task is running. + /// + /// + /// + /// + public Task RunAsync(Task task, string header, CancellationToken token) { + return RunAsyncNonGeneric(task, () => WrapHeader(header), token); + } + /// /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. /// /// /// 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, Func? headerFactory, CancellationToken token) => RunAsyncNonGeneric(task, headerFactory, token); /// @@ -87,7 +109,7 @@ public async Task RunAsync(Task task, Func /// 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, Func? headerFactory, CancellationToken token) { try { if (task.Status is not TaskStatus.Running) { task.Start(); @@ -114,8 +136,7 @@ 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/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index e955248..b87995b 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -2,8 +2,6 @@ namespace PrettyConsole; -//TODO: Check if optional values for alignment can reduce overloads - /// /// Interpolated string handler that handles formatting /// @@ -69,6 +67,14 @@ 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. /// diff --git a/Versions.md b/Versions.md index aaafbdf..6c3cfef 100755 --- a/Versions.md +++ b/Versions.md @@ -4,7 +4,7 @@ - `PrettyConsoleInterpolatedStringHandler` was rewritten to buffer the contents instead of stream them, along with other optimizations, massively improving performance. - `WhiteSpace` is a new struct that declares a section of whitespace with length, `PrettyConsoleInterpolatedStringHandler` was thought to recognize it is a special parameter and will render a region of whitespaces with that length to the buffer. -- `IndeterminateProgressBar` overloads that accepted a `header` now accept a `Func` instead, which can be used to pull in data from variables via a closure and enable a more dynamic rendering of statuses. `OutputPipe` here should be ignored using `_` and not used inside the lambda, it is invoked internally to render to the correct pipe. +- Add overloads to `IndeterminateProgressBar` that accept a `Func` instead, which can be used to pull in data from variables via a closure and enable a more dynamic rendering of statuses. `PrettyConsoleInterpolatedStringHandler.Build` should be used to return it attached to the right `OutputPipe`. ## v5.1.0 From e41ec910137b3ede520fedf20e4b267481ebf055 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 18:22:35 +0200 Subject: [PATCH 21/23] Updated docs --- AGENTS.md | 6 ++-- ...ks.StyledOutputBenchmarks-report-github.md | 15 ++++---- PrettyConsole/PrettyConsole.csproj | 7 ++-- README.md | 36 ++++++++++++++++--- Versions.md | 6 ++-- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b51ba3e..ccafa40 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.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. +- 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. Commands you’ll use often @@ -48,7 +48,7 @@ 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` 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. + - `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. - 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 @@ -66,7 +66,7 @@ High-level architecture and key concepts - 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. + - `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). - `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 6ce8fce..43174e5 100644 --- a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -3,15 +3,16 @@ BenchmarkDotNet v0.15.6, macOS 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 - Job-NEXDCO : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a + PGO1 : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a -OutlierMode=RemoveAll IterationCount=30 IterationTime=100ms -LaunchCount=3 WarmupCount=5 +Job=PGO1 OutlierMode=RemoveAll EnvironmentVariables=DOTNET_TieredPGO=1 +IterationCount=30 IterationTime=100ms LaunchCount=3 +WarmupCount=5 ``` | Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | |--------------- |------------:|--------------:|-------:|----------:|--------------:| -| PrettyConsole | 95.02 ns | 49.73x faster | - | - | NA | -| SpectreConsole | 4,725.48 ns | baseline | 2.0902 | 17840 B | | -| SystemConsole | 68.67 ns | 68.81x faster | 0.0028 | 24 B | 743.333x less | +| 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 | diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 2dfcebf..3d4110d 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -43,10 +43,9 @@ - - 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. + - `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. diff --git a/README.md b/README.md index e065df1..ef122d0 100755 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ PrettyConsole is a high-performance, ultra-low-latency, allocation-free extensio - 🚀 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 +- 🧱 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`) - ⛓ Output routing through `OutputPipe.Out` and `OutputPipe.Error` so piping/redirects continue to work @@ -22,11 +23,11 @@ BenchmarkDotNet measures [styled output performance](Benchmarks/BenchmarkDotNet. | Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | |--------------- |------------:|--------------:|-------:|----------:|--------------:| -| PrettyConsole | 95.02 ns | 49.73x faster | - | - | NA | -| SpectreConsole | 4,725.48 ns | baseline | 2.0902 | 17840 B | | -| SystemConsole | 68.67 ns | 68.81x faster | 0.0028 | 24 B | 743.333x less | +| 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 is **the go-to choice for ultra-low-latency, allocation-free console rendering**, running ~50× faster than Spectre.Console while allocating nothing at all—even beating the BCL when you count real-world rendering costs. +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. ## Installation @@ -47,7 +48,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. `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—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. ```csharp Console.WriteInterpolated($"Hello {ConsoleColor.Green / ConsoleColor.DefaultBackground}world{ConsoleColor.Default}!"); @@ -56,6 +57,9 @@ Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.Yellow / ConsoleColo if (!Console.TryReadLine(out int choice, $"Pick option {ConsoleColor.Cyan / ConsoleColor.DefaultBackground}1-5{ConsoleColor.Default}: ")) { Console.WriteLineInterpolated($"{ConsoleColor.Red / ConsoleColor.DefaultBackground}Not a number.{ConsoleColor.Default}"); } + +// Zero-allocation padding directly from the handler +Console.WriteInterpolated($"Header{new WhiteSpace(6)}Value"); ``` `ConsoleColor.DefaultForeground`, `ConsoleColor.DefaultBackground`, and the `/` operator overload make it easy to compose foreground/background tuples inline (`ConsoleColor.Red / ConsoleColor.White`). @@ -95,6 +99,17 @@ All fields collapse to `string.Empty` when markup is disabled, so the same call You can combine both, e.g., `$"{elapsed,8:duration}"`, to keep progress/status displays tidy. +- **`WhiteSpace` struct for padding** — pass `new WhiteSpace(length)` inside an interpolated string to emit that many spaces straight from the handler without allocating intermediate strings. + +- **Custom escape sequences** — if you need your own ANSI code (extra markup/colors), keep it in an interpolated hole instead of hardcoding it into the literal so the handler can treat it like other escape spans: + +```csharp +var rose = "\u001b[38;5;213m"; // custom 256-color escape +Console.WriteInterpolated($"{rose}accent text{Markup.Reset}"); +``` + +Avoid embedding the escape directly in the literal (`"\u001b[38;5;213maccent text"`), which would be measured as visible width and could skew padding/alignment. + ### Basic outputs ```csharp @@ -224,6 +239,17 @@ ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', ma `ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. You can also set `ProgressBar.MaxLineWidth` on the instance to limit the rendered `[=====] 42%` line width before each update, mirroring the `maxLineWidth` option on `ProgressBar.WriteProgressBar`. The helper `ProgressBar.WriteProgressBar` keeps the cursor on the same line, which is ideal inside `Console.Overwrite`, and accepts an optional `maxLineWidth` so the entire `[=====] 42%` line can be constrained for left-column layouts. +#### 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`: + +```csharp +var spinner = new IndeterminateProgressBar(); +await spinner.RunAsync(workTask, () => PrettyConsoleInterpolatedStringHandler.Build(OutputPipe.Error, $"Syncing {DateTime.Now:T}")); +``` + +The factory runs each frame, letting you inject dynamic status text without allocations. + #### Multiple progress bars with tasks + channels ```csharp diff --git a/Versions.md b/Versions.md index 6c3cfef..a865833 100755 --- a/Versions.md +++ b/Versions.md @@ -2,9 +2,9 @@ ## v5.2.0 -- `PrettyConsoleInterpolatedStringHandler` was rewritten to buffer the contents instead of stream them, along with other optimizations, massively improving performance. -- `WhiteSpace` is a new struct that declares a section of whitespace with length, `PrettyConsoleInterpolatedStringHandler` was thought to recognize it is a special parameter and will render a region of whitespaces with that length to the buffer. -- Add overloads to `IndeterminateProgressBar` that accept a `Func` instead, which can be used to pull in data from variables via a closure and enable a more dynamic rendering of statuses. `PrettyConsoleInterpolatedStringHandler.Build` should be used to return it attached to the right `OutputPipe`. +- `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`, letting each frame build status text with captured locals. Use `PrettyConsoleInterpolatedStringHandler.Build` to bind the handler to the correct `OutputPipe` inside the factory. ## v5.1.0 From c2b1187bbb54b91533d204ded9282d766c5c74de Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 18:24:01 +0200 Subject: [PATCH 22/23] Formatting --- PrettyConsole.UnitTests/AdvancedInputsTests.cs | 2 +- PrettyConsole.UnitTests/AdvancedOutputsTests.cs | 2 +- PrettyConsole.UnitTests/AssemblyInfo.cs | 2 +- PrettyConsole.UnitTests/ConsoleColorTests.cs | 2 +- PrettyConsole.UnitTests/GlobalUsings.cs | 4 ++-- .../InputRequestExtensionsTests.cs | 2 +- PrettyConsole.UnitTests/MarkupTests.cs | 2 +- PrettyConsole.UnitTests/MenusTests.cs | 2 +- .../PrettyConsoleExtensionsTests.cs | 2 +- .../PrettyConsoleInterpolatedStringHandlerTests.cs | 4 ++-- PrettyConsole.UnitTests/ProgressBarTests.cs | 2 +- PrettyConsole.UnitTests/ReadLineExtensionsTests.cs | 2 +- PrettyConsole.UnitTests/WriteExtensionsTests.cs | 2 +- PrettyConsole.UnitTests/WriteLineExtensionsTests.cs | 12 ++++++------ PrettyConsole/ConsoleContext.cs | 2 +- PrettyConsole/IndeterminateProgressBar.cs | 2 +- .../PrettyConsoleInterpolatedStringHandler.cs | 2 +- 17 files changed, 24 insertions(+), 24 deletions(-) diff --git a/PrettyConsole.UnitTests/AdvancedInputsTests.cs b/PrettyConsole.UnitTests/AdvancedInputsTests.cs index 726d7a6..83400d8 100755 --- a/PrettyConsole.UnitTests/AdvancedInputsTests.cs +++ b/PrettyConsole.UnitTests/AdvancedInputsTests.cs @@ -52,4 +52,4 @@ public async Task Confirm_CustomTrueValues_WithInterpolatedPrompt() { await Assert.That(stringWriter.ToStringAndFlush()).IsEqualTo("Proceed?"); await Assert.That(res).IsTrue(); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/AdvancedOutputsTests.cs b/PrettyConsole.UnitTests/AdvancedOutputsTests.cs index ec2ef1f..41cfc02 100755 --- a/PrettyConsole.UnitTests/AdvancedOutputsTests.cs +++ b/PrettyConsole.UnitTests/AdvancedOutputsTests.cs @@ -52,4 +52,4 @@ public async Task TypeWriteLine_Regular() { await Console.TypeWriteLine("Hello world!", Green / ConsoleColor.Default, 10); await Assert.That(stringWriter.ToString()).Contains("Hello world!" + Environment.NewLine); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/AssemblyInfo.cs b/PrettyConsole.UnitTests/AssemblyInfo.cs index d82e8c0..30c3a8c 100644 --- a/PrettyConsole.UnitTests/AssemblyInfo.cs +++ b/PrettyConsole.UnitTests/AssemblyInfo.cs @@ -1,3 +1,3 @@ using TUnit.Core; -[assembly: NotInParallel] +[assembly: NotInParallel] \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ConsoleColorTests.cs b/PrettyConsole.UnitTests/ConsoleColorTests.cs index 2623b13..5d43ac9 100644 --- a/PrettyConsole.UnitTests/ConsoleColorTests.cs +++ b/PrettyConsole.UnitTests/ConsoleColorTests.cs @@ -92,4 +92,4 @@ public async Task AnsiColors_InternalBuilders_MatchPublicAccessors() { await Assert.That(AnsiColors.Foreground(@default)).IsEqualTo(AnsiColors.ForegroundResetSequence); await Assert.That(AnsiColors.Background(@default)).IsEqualTo(AnsiColors.BackgroundResetSequence); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/GlobalUsings.cs b/PrettyConsole.UnitTests/GlobalUsings.cs index 1d25279..7f05115 100644 --- a/PrettyConsole.UnitTests/GlobalUsings.cs +++ b/PrettyConsole.UnitTests/GlobalUsings.cs @@ -1,6 +1,6 @@ -global using TUnit.Core; global using TUnit.Assertions; global using TUnit.Assertions.Extensions; +global using TUnit.Core; global using static System.ConsoleColor; -global using static PrettyConsole.ConsoleContext; +global using static PrettyConsole.ConsoleContext; \ No newline at end of file diff --git a/PrettyConsole.UnitTests/InputRequestExtensionsTests.cs b/PrettyConsole.UnitTests/InputRequestExtensionsTests.cs index 07432c4..339d3ac 100644 --- a/PrettyConsole.UnitTests/InputRequestExtensionsTests.cs +++ b/PrettyConsole.UnitTests/InputRequestExtensionsTests.cs @@ -20,4 +20,4 @@ public async Task RequestAnyInput_WritesPrompt_AndInvokesReadKey() { InputRequestExtensions.ConfigureReadKey(null); } } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/MarkupTests.cs b/PrettyConsole.UnitTests/MarkupTests.cs index 60e6073..85ff101 100644 --- a/PrettyConsole.UnitTests/MarkupTests.cs +++ b/PrettyConsole.UnitTests/MarkupTests.cs @@ -14,4 +14,4 @@ public class MarkupTests { public async Task Markup_Constants(string actual, string expected) { await Assert.That(actual).IsEqualTo(expected).IgnoringCase(); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/MenusTests.cs b/PrettyConsole.UnitTests/MenusTests.cs index 5767bd8..303054c 100644 --- a/PrettyConsole.UnitTests/MenusTests.cs +++ b/PrettyConsole.UnitTests/MenusTests.cs @@ -183,4 +183,4 @@ public async Task MultiSelection_MixedValidAndInvalidIndices_ReturnsOnlyValid() await Assert.That(result.Length).IsEqualTo(0); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs index 7fb995b..c5ccaa6 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -94,4 +94,4 @@ public async Task RenderingExtensions_DefaultCursorAccessor_IsInvoked() { int line = Console.GetCurrentLine(); await Assert.That(line).IsGreaterThanOrEqualTo(0); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs index 08beb2f..c5c4b46 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -271,7 +271,7 @@ public async Task AppendSpan_LeftAndRightAlignment() { try { Out = Utilities.GetWriter(out var writer); - Console.WriteInterpolated($"{ "Hi",4}{ "Bye",-5}"); + Console.WriteInterpolated($"{"Hi",4}{"Bye",-5}"); await Assert.That(writer.ToString()).IsEqualTo(" HiBye "); } finally { @@ -293,4 +293,4 @@ private static string FormatBytes(double value) { } private static ReadOnlySpan FileSizeSuffix => new[] { "B", "KB", "MB", "GB", "TB", "PB" }; -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index 98af0f3..f2b965e 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -315,4 +315,4 @@ public static bool IsAvailable() { return false; } } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ReadLineExtensionsTests.cs b/PrettyConsole.UnitTests/ReadLineExtensionsTests.cs index f03893d..985b062 100755 --- a/PrettyConsole.UnitTests/ReadLineExtensionsTests.cs +++ b/PrettyConsole.UnitTests/ReadLineExtensionsTests.cs @@ -77,4 +77,4 @@ public async Task ReadLine_GenericWithDefault_ReturnsDefaultWhenInvalid() { await Assert.That(value).IsEqualTo(5); } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/WriteExtensionsTests.cs b/PrettyConsole.UnitTests/WriteExtensionsTests.cs index ec118aa..ebdb6dd 100755 --- a/PrettyConsole.UnitTests/WriteExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteExtensionsTests.cs @@ -127,4 +127,4 @@ public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan return true; } } -} +} \ No newline at end of file diff --git a/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs index 913e516..34d8d56 100755 --- a/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs +++ b/PrettyConsole.UnitTests/WriteLineExtensionsTests.cs @@ -58,10 +58,10 @@ public async Task WriteLine_ReadOnlySpan_DefaultAndForeground() { Console.WriteLine("span".AsSpan(), OutputPipe.Out); Console.WriteLine("more".AsSpan(), OutputPipe.Out, ConsoleColor.Cyan); - var expected = $"span{writer.NewLine}more{writer.NewLine}"; - await Assert.That(writer.ToString()).IsEqualTo(expected); - } finally { - Out = originalOut; + var expected = $"span{writer.NewLine}more{writer.NewLine}"; + await Assert.That(writer.ToString()).IsEqualTo(expected); + } finally { + Out = originalOut; + } } -} -} +} \ No newline at end of file diff --git a/PrettyConsole/ConsoleContext.cs b/PrettyConsole/ConsoleContext.cs index a0a7596..3415cc9 100755 --- a/PrettyConsole/ConsoleContext.cs +++ b/PrettyConsole/ConsoleContext.cs @@ -76,4 +76,4 @@ public void WriteWhiteSpaces(int length) { } private static readonly string WhiteSpaces = new(' ', 256); -} +} \ No newline at end of file diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 30fb137..916e34d 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -238,4 +238,4 @@ public static readonly ReadOnlyCollection PingPong "| • |", ]); } -} +} \ No newline at end of file diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index b87995b..8858f5d 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -445,4 +445,4 @@ public void Flush() { BufferPool.Return(_buffer, false); _flushed = true; } -} +} \ No newline at end of file From a45f0b75d7496d32629fec586ae8fa4c95d3473f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 29 Nov 2025 18:28:44 +0200 Subject: [PATCH 23/23] Fix CI test on windows when no handle is available --- PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs index c5ccaa6..1fe9a1a 100644 --- a/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs +++ b/PrettyConsole.UnitTests/PrettyConsoleExtensionsTests.cs @@ -89,6 +89,7 @@ public async Task ConsoleContext_GetWidthOrDefault_WhenRedirected() { #pragma warning restore TUnit0055 // Do not overwrite the Console writer [Test] + [SkipWhenConsoleUnavailable] public async Task RenderingExtensions_DefaultCursorAccessor_IsInvoked() { // use default cursor accessors (no override) int line = Console.GetCurrentLine();