From e51565b1fdea9452c35f842aee4c0dcfd1b9b55a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Wed, 12 Nov 2025 21:07:53 +0200 Subject: [PATCH 01/75] Update to net10 --- PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj | 2 +- PrettyConsole.Tests/PrettyConsole.Tests.csproj | 2 +- PrettyConsole/PrettyConsole.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj index 79e6075..4137967 100755 --- a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj +++ b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj @@ -4,7 +4,7 @@ enable enable Exe - net9.0 + net10.0 true true diff --git a/PrettyConsole.Tests/PrettyConsole.Tests.csproj b/PrettyConsole.Tests/PrettyConsole.Tests.csproj index 9ca127a..3624228 100755 --- a/PrettyConsole.Tests/PrettyConsole.Tests.csproj +++ b/PrettyConsole.Tests/PrettyConsole.Tests.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable PrettyConsole.Tests diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 254c423..2190b9f 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 latest true @@ -25,7 +25,7 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 4.1.0 + 5.0.0 enable MIT True From 9032fd77837e9d30946bb7763506e1acf5d81367 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Wed, 12 Nov 2025 21:08:01 +0200 Subject: [PATCH 02/75] Use extension members --- PrettyConsole/Extensions.cs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/PrettyConsole/Extensions.cs b/PrettyConsole/Extensions.cs index c92ddff..f035d67 100644 --- a/PrettyConsole/Extensions.cs +++ b/PrettyConsole/Extensions.cs @@ -1,20 +1,22 @@ namespace PrettyConsole; internal static class Extensions { - private static readonly string WhiteSpaces = new(' ', 256); - - /// - /// Writes whitespace to a up to length by chucks - /// - /// - /// - internal static void WriteWhiteSpaces(this TextWriter writer, int length) { - ReadOnlySpan whiteSpaces = WhiteSpaces; + extension(TextWriter @this) { + /// + /// Writes whitespace to this up to length by chucks + /// + /// + internal void WriteWhiteSpaces(int length) { + ReadOnlySpan whiteSpaces = WhiteSpaces; - while (length > 0) { - int cur_length = Math.Min(length, 256); - writer.Write(whiteSpaces.Slice(0, cur_length)); - length -= cur_length; + while (length > 0) { + int cur_length = Math.Min(length, 256); + @this.Write(whiteSpaces.Slice(0, cur_length)); + length -= cur_length; + } } } + + + private static readonly string WhiteSpaces = new(' ', 256); } \ No newline at end of file From 63e217cdf6bf079828be0cb6298cddae2cf797f4 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Wed, 12 Nov 2025 21:10:15 +0200 Subject: [PATCH 03/75] - --- PrettyConsole/Extensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/PrettyConsole/Extensions.cs b/PrettyConsole/Extensions.cs index f035d67..3085774 100644 --- a/PrettyConsole/Extensions.cs +++ b/PrettyConsole/Extensions.cs @@ -17,6 +17,5 @@ internal void WriteWhiteSpaces(int length) { } } - private static readonly string WhiteSpaces = new(' ', 256); } \ No newline at end of file From 8dc4907878385ab7c3541e55e310340135864372 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 09:29:24 +0200 Subject: [PATCH 04/75] Update workflow --- .github/workflows/UnitTests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UnitTests.yaml b/.github/workflows/UnitTests.yaml index 0ff99eb..9abb409 100755 --- a/.github/workflows/UnitTests.yaml +++ b/.github/workflows/UnitTests.yaml @@ -13,13 +13,13 @@ jobs: uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main with: platform: ${{ matrix.platform }} - dotnet-version: 9.0.x + dotnet-version: 10.0.x test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj unit-tests-debug: uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main with: platform: ubuntu-latest - dotnet-version: 9.0.x + dotnet-version: 10.0.x test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj use-debug: true \ No newline at end of file From 04b4dbf901cff3d156a0799cde74749a22e2d004 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 09:47:13 +0200 Subject: [PATCH 05/75] Remove ColoredOutput 1 --- PrettyConsole/AdvancedInputs.cs | 51 --------- PrettyConsole/AdvancedOutputs.cs | 35 ++---- PrettyConsole/Color.cs | 42 ------- PrettyConsole/ColoredOutput.cs | 46 -------- PrettyConsole/ColoredOutputExtensions.cs | 21 ---- PrettyConsole/Extensions.cs | 7 +- PrettyConsole/GlobalUsings.cs | 2 - PrettyConsole/Menus.cs | 6 +- PrettyConsole/PrettyConsole.csproj | 4 + .../PrettyConsoleInterpolatedStringHandler.cs | 28 ----- PrettyConsole/ReadLine.cs | 104 ------------------ PrettyConsole/Write.cs | 51 --------- PrettyConsole/WriteLine.cs | 30 ----- 13 files changed, 22 insertions(+), 405 deletions(-) delete mode 100755 PrettyConsole/ColoredOutput.cs delete mode 100755 PrettyConsole/ColoredOutputExtensions.cs diff --git a/PrettyConsole/AdvancedInputs.cs b/PrettyConsole/AdvancedInputs.cs index 92e288d..6019ecf 100755 --- a/PrettyConsole/AdvancedInputs.cs +++ b/PrettyConsole/AdvancedInputs.cs @@ -1,21 +1,6 @@ namespace PrettyConsole; public static partial class Console { - /// - /// Used to wait for user input - /// - public static void RequestAnyInput(string message = "Press any key to continue...") { - RequestAnyInput([new ColoredOutput(message)]); - } - - /// - /// Used to wait for user input - /// - public static void RequestAnyInput(ReadOnlySpan output) { - Write(output); - _ = baseConsole.ReadKey(); - } - /// /// Used to wait for user input /// @@ -30,17 +15,6 @@ public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyCon /// public static ReadOnlySpan DefaultConfirmValues => new[] { "y", "yes" }; - /// - /// Used to get user confirmation with the default values ["y", "yes"] or just pressing enter - /// - /// - /// - /// It does not display a question mark or any other prompt, only the message - /// - public static bool Confirm(ReadOnlySpan message) { - return Confirm(message, DefaultConfirmValues); - } - /// /// Used to get user confirmation with the default values ["y", "yes"] or just pressing enter /// @@ -52,31 +26,6 @@ public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInte return Confirm(DefaultConfirmValues, true, handler); } - /// - /// Used to get user confirmation - /// - /// - /// a collection of values that indicate positive confirmation - /// if simply pressing enter is considered positive or not - /// - /// It does not display a question mark or any other prompt, only the message - /// - public static bool Confirm(ReadOnlySpan message, ReadOnlySpan trueValues, bool emptyIsTrue = true) { - Write(message); - var input = In.ReadLine(); - if (input is null or { Length: 0 }) { - return emptyIsTrue; - } - - foreach (var value in trueValues) { - if (input.Equals(value, StringComparison.OrdinalIgnoreCase)) { - return true; - } - } - - return false; - } - /// /// Used to get user confirmation /// diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs index 770fb3a..4783155 100755 --- a/PrettyConsole/AdvancedOutputs.cs +++ b/PrettyConsole/AdvancedOutputs.cs @@ -1,21 +1,6 @@ namespace PrettyConsole; public static partial class Console { - /// - /// Clears the current line and overrides it with - /// - /// - /// The output pipe to use - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - public static void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { - var currentLine = GetCurrentLine(); - ClearNextLines(1, pipe); - Write(output, pipe); - GoToLine(currentLine); - } - /// /// Runs that should involve some form of outputting to the console. Set according to the outputs you use in and configure the appropriate /// @@ -53,28 +38,26 @@ public static void Overwrite(TState state, Action action, int li private const int TypeWriteDefaultDelay = 200; /// - /// Types out the character by character with a delay of milliseconds between each character. + /// Types out character by character with a delay of milliseconds between each character, styled using . /// /// + /// /// Delay in milliseconds between each character. - public static async Task TypeWrite(ColoredOutput output, int delay = TypeWriteDefaultDelay) { - SetColors(output.ForegroundColor, output.BackgroundColor); - for (int i = 0; i < output.Value.Length - 1; i++) { - Out.Write(output.Value[i]); + public static async Task TypeWrite(string output, (ConsoleColor foregroundColor, ConsoleColor backgroundColor) colorTuple, int delay = TypeWriteDefaultDelay) { + foreach (char c in output) { + Write(c, OutputPipe.Out, colorTuple.foregroundColor, colorTuple.backgroundColor); await Task.Delay(delay); } - - Out.Write(output.Value[output.Value.Length - 1]); - ResetColors(); } /// - /// Types out the character by character with a delay of milliseconds between each character. + /// Types out character by character with a delay of milliseconds between each character, styled using followed by a line terminator. /// /// + /// /// Delay in milliseconds between each character. - public static async Task TypeWriteLine(ColoredOutput output, int delay = TypeWriteDefaultDelay) { - await TypeWrite(output, delay); + public static async Task TypeWriteLine(string output, (ConsoleColor foregroundColor, ConsoleColor backgroundColor) colorTuple, int delay = TypeWriteDefaultDelay) { + await TypeWrite(output, colorTuple, delay); NewLine(); } } \ No newline at end of file diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index 2d53649..b0f7d7e 100755 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -30,48 +30,6 @@ public static (ConsoleColor fg, ConsoleColor bg) operator /(Color foreground, Co return (foreground, background); } - /// - /// Creates a object by combining a string value with a color. - /// - /// The string value to combine with the color. - /// The color to apply to the string value. - /// A object representing the combination of the string value and color. - public static ColoredOutput operator *(string value, Color color) { - return new(value, color, DefaultBackgroundColor); - } - - /// - /// Creates a object by combining a string value with a color. - /// - /// The string value to combine with the color. - /// The color to apply to the string value. - /// A object representing the combination of the string value and color. - public static ColoredOutput operator *(object? value, Color color) { - var s = value?.ToString() ?? string.Empty; - return new(s, color, DefaultBackgroundColor); - } - - /// - /// Creates a object by combining a string value with a color. - /// - /// The string value to combine with the color. - /// The color to apply to the string value. - /// A object representing the combination of the string value and color. - public static ColoredOutput operator /(string value, Color color) { - return new(value, DefaultForegroundColor, color); - } - - /// - /// Creates a object by combining a string value with a color. - /// - /// The string value to combine with the color. - /// The color to apply to the string value. - /// A object representing the combination of the string value and color. - public static ColoredOutput operator /(object? value, Color color) { - var s = value?.ToString() ?? string.Empty; - return new(s, DefaultForegroundColor, color); - } - /// /// Gets a object representing the color black. /// diff --git a/PrettyConsole/ColoredOutput.cs b/PrettyConsole/ColoredOutput.cs deleted file mode 100755 index 43d303d..0000000 --- a/PrettyConsole/ColoredOutput.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Runtime.Versioning; - -namespace PrettyConsole; - -/// -/// Represents a colored output with string value, foreground color, and background color. -/// -[UnsupportedOSPlatform("android")] -[UnsupportedOSPlatform("browser")] -[UnsupportedOSPlatform("ios")] -[UnsupportedOSPlatform("tvos")] -public readonly record struct ColoredOutput(string Value, ConsoleColor ForegroundColor, ConsoleColor BackgroundColor) { - /// - /// Creates a new instance of with default colors - /// - /// - public ColoredOutput(string value) : this(value, Color.DefaultForegroundColor, Color.DefaultBackgroundColor) { } - - /// - /// Creates a new instance of with default background color - /// - /// - /// - public ColoredOutput(string value, ConsoleColor foregroundColor) : this(value, foregroundColor, Color.DefaultBackgroundColor) { } - - /// - /// Implicitly converts a string to a with default colors. - /// - public static implicit operator ColoredOutput(string value) { - return new(value); - } - - /// - /// Implicitly converts a to a with default colors. - /// - public static implicit operator ColoredOutput(ReadOnlySpan buffer) { - return new(new string(buffer)); - } - - /// - /// Creates a new instance of with a different background color - /// - public static ColoredOutput operator /(ColoredOutput output, Color color) { - return output with { BackgroundColor = color }; - } -} \ No newline at end of file diff --git a/PrettyConsole/ColoredOutputExtensions.cs b/PrettyConsole/ColoredOutputExtensions.cs deleted file mode 100755 index 9214bcb..0000000 --- a/PrettyConsole/ColoredOutputExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace PrettyConsole; - -/// -/// Provides extension methods for . -/// -public static class ColoredOutputExtensions { - /// - /// Creates a new instance of . - /// - public static ColoredOutput InColor(this string value, ConsoleColor foregroundColor) { - return InColor(value, foregroundColor, Color.DefaultBackgroundColor); - } - - /// - /// Creates a new instance of . - /// - public static ColoredOutput InColor(this string value, ConsoleColor foregroundColor, - ConsoleColor backgroundColor) { - return new(value, foregroundColor, backgroundColor); - } -} \ No newline at end of file diff --git a/PrettyConsole/Extensions.cs b/PrettyConsole/Extensions.cs index 3085774..9e07840 100644 --- a/PrettyConsole/Extensions.cs +++ b/PrettyConsole/Extensions.cs @@ -1,12 +1,15 @@ namespace PrettyConsole; -internal static class Extensions { +/// +/// Provides a set of convenient extensions +/// +public static class PrettyConsoleExtensions { extension(TextWriter @this) { /// /// Writes whitespace to this up to length by chucks /// /// - internal void WriteWhiteSpaces(int length) { + public void WriteWhiteSpaces(int length) { ReadOnlySpan whiteSpaces = WhiteSpaces; while (length > 0) { diff --git a/PrettyConsole/GlobalUsings.cs b/PrettyConsole/GlobalUsings.cs index bbdf4b8..28e2df2 100755 --- a/PrettyConsole/GlobalUsings.cs +++ b/PrettyConsole/GlobalUsings.cs @@ -1,3 +1 @@ -global using System.Runtime.CompilerServices; - global using baseConsole = System.Console; \ No newline at end of file diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 2a3b404..049c329 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -3,6 +3,7 @@ namespace PrettyConsole; +/* Upgrade to interpolated public static partial class Console { /// /// Enumerates a list of strings and allows the user to select one by number @@ -132,7 +133,7 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan(TList headers, ReadOnlySpan columns) wher Out.WriteLine(rowSeparation); } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 2190b9f..e5808b6 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -31,6 +31,10 @@ True README.md + + + + diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 56fa30c..081201d 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -115,29 +115,6 @@ public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor back } } - /// - /// Writes a segment and applies its colors for the duration of the write. - /// - /// Segment to write. - /// Optional alignment as provided by the interpolation. - public readonly void AppendFormatted(ColoredOutput output, int alignment = 0) { - Console.WriteCore(output, _writer); - if (alignment != 0) { - AppendSpan(ReadOnlySpan.Empty, alignment); - } - } - - /// - /// Writes a buffer of items. - /// - /// Segments to write. - public readonly void AppendFormatted(ReadOnlySpan outputs) { - if (outputs.Length is 0) { - return; - } - Console.WriteCore(outputs, _writer); - } - /// /// Append timeSpan with or without elapsed time formatting (human readable) /// @@ -224,11 +201,6 @@ public readonly void AppendFormatted(object? value, int alignment = 0, string? f return; } - if (value is ColoredOutput coloredOutput) { - AppendFormatted(coloredOutput, alignment); - return; - } - if (value is string str) { AppendString(str, alignment); return; diff --git a/PrettyConsole/ReadLine.cs b/PrettyConsole/ReadLine.cs index 21ec3a2..24f9d84 100755 --- a/PrettyConsole/ReadLine.cs +++ b/PrettyConsole/ReadLine.cs @@ -3,19 +3,6 @@ namespace PrettyConsole; public static partial class Console { - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The message to display to the user - /// The result of the parsing - /// True if the parsing was successful, false otherwise - public static bool TryReadLine(ReadOnlySpan message, out T? result) where T : IParsable { - Write(message, OutputPipe.Out); - var input = In.ReadLine(); - return T.TryParse(input, CultureInfo.CurrentCulture, out result); - } - /// /// Used to request user input, validates and converts common types. /// @@ -29,24 +16,6 @@ public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgum return T.TryParse(input, CultureInfo.CurrentCulture, out result); } - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The message to display to the user - /// The default value to return if parsing fails - /// The result of the parsing - /// True if the parsing was successful, false otherwise - public static bool TryReadLine(ReadOnlySpan message, T @default, out T result) where T : IParsable { - var couldParse = TryReadLine(message, out T? innerResult); - if (couldParse) { - result = innerResult!; - return true; - } - result = @default; - return false; - } - /// /// Used to request user input, validates and converts common types. /// @@ -65,18 +34,6 @@ public static bool TryReadLine(out T result, T @default, [InterpolatedStringH return false; } - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The message to display to the user - /// Whether to ignore case when parsing - /// The result of the parsing - /// Whether the parsing was successful - public static bool TryReadLine(ReadOnlySpan message, bool ignoreCase, out TEnum result) where TEnum : struct, Enum { - return TryReadLine(message, ignoreCase, default, out result); - } - /// /// Used to request user input, validates and converts common types. /// @@ -89,25 +46,6 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, [Interp return TryReadLine(out result, ignoreCase, default, handler); } - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The message to display to the user - /// Whether to ignore case when parsing - /// The default value to return if parsing fails - /// The result of the parsing - /// Whether the parsing was successful - public static bool TryReadLine(ReadOnlySpan message, bool ignoreCase, TEnum @default, out TEnum result) where TEnum : struct, Enum { - Write(message, OutputPipe.Out); - var input = In.ReadLine(); - var res = Enum.TryParse(input, ignoreCase, out result); - if (!res) { - result = @default; - } - return res; - } - /// /// Used to request user input, validates and converts common types. /// @@ -127,25 +65,6 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ return res; } - /// - /// Used to request user input without any prepended message - /// - /// - /// You can use or it's overloads in conjunction with this to create more complex input requests. - /// - public static string? ReadLine() { - return In.ReadLine(); - } - - /// - /// Used to request user input - /// - /// The message to display to the user - public static string? ReadLine(ReadOnlySpan message) { - Write(message, OutputPipe.Out); - return ReadLine(); - } - /// /// Used to request user input /// @@ -155,17 +74,6 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ return ReadLine(); } - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The message to display to the user - /// The result of the parsing - public static T? ReadLine(ReadOnlySpan message) where T : IParsable { - _ = TryReadLine(message, out T? result); - return result; - } - /// /// Used to request user input, validates and converts common types. /// @@ -177,18 +85,6 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ return result; } - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The message to display to the user - /// The default value to return if parsing fails - /// The result of the parsing - public static T ReadLine(ReadOnlySpan message, T @default) where T : IParsable { - _ = TryReadLine(message, @default, out T result); - return result; - } - /// /// Used to request user input, validates and converts common types. /// diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index 160f504..a89da76 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -123,55 +123,4 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor GetWriter(pipe).Write(span); ResetColors(); } - - /// - /// Write a to the error console - /// - /// - /// The output pipe to use - /// - /// To end line, use - /// - public static void Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out) { - WriteCore(output, GetWriter(pipe)); - } - - /// - /// Write a to the error console - /// - /// - /// The writer to use - /// - /// To end line, use - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void WriteCore(ColoredOutput output, TextWriter writer) { - SetColors(output.ForegroundColor, output.BackgroundColor); - writer.Write(output.Value); - ResetColors(); - } - - /// - /// Write a number of to the console - /// - /// - /// The output pipe to use - public static void Write(ReadOnlySpan outputs, OutputPipe pipe = OutputPipe.Out) { - WriteCore(outputs, GetWriter(pipe)); - } - - /// - /// Write a number of to the console - /// - /// - /// The writer to use - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void WriteCore(ReadOnlySpan outputs, TextWriter writer) { - if (outputs.Length is 0) { - return; - } - foreach (var output in outputs) { - WriteCore(output, writer); - } - } } \ No newline at end of file diff --git a/PrettyConsole/WriteLine.cs b/PrettyConsole/WriteLine.cs index e32cd89..0ed7fa1 100755 --- a/PrettyConsole/WriteLine.cs +++ b/PrettyConsole/WriteLine.cs @@ -20,36 +20,6 @@ public static void WriteLine(OutputPipe pipe, [InterpolatedStringHandlerArgument NewLine(pipe); } - /// - /// Write a to the error console - /// - /// - /// The output pipe to use - /// - /// To not end line, use - /// - public static void WriteLine(ColoredOutput output, OutputPipe pipe = OutputPipe.Out) { - Write(output, pipe); - NewLine(pipe); - } - - /// - /// WriteLine a number of to the console - /// - /// - /// The output pipe to use - /// - /// In overloads of WriteLine with multiple parameters, only the last will end the line. - /// - public static void WriteLine(ReadOnlySpan outputs, OutputPipe pipe = OutputPipe.Out) { - if (outputs.Length is 0) { - return; - } - - Write(outputs, pipe); - NewLine(pipe); - } - /// /// WriteLine an item that implements without boxing directly to the output writer /// From ae0fcf3b1ad35592f0c401e5f81972692ac10ca4 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:09:59 +0200 Subject: [PATCH 06/75] Rework library to extend System.Console and ConsoleColor --- PrettyConsole.Tests.Unit/AdvancedInputs.cs | 50 +-- PrettyConsole.Tests.Unit/AdvancedOutputs.cs | 22 +- PrettyConsole.Tests.Unit/ColorTests.cs | 119 ------ PrettyConsole.Tests.Unit/ConsoleColorTests.cs | 24 ++ PrettyConsole.Tests.Unit/GlobalUsings.cs | 3 +- PrettyConsole.Tests.Unit/MenusTests.cs | 22 +- .../PrettyConsoleExtensionsTests.cs | 16 + PrettyConsole.Tests.Unit/ProgressBarTests.cs | 10 +- PrettyConsole.Tests.Unit/ReadLine.cs | 88 ----- .../ReadLineExtensionsTests.cs | 46 +++ PrettyConsole.Tests.Unit/Utilities.cs | 7 +- PrettyConsole.Tests.Unit/UtilityTests.cs | 34 -- .../{Write.cs => WriteExtensionsTests.cs} | 80 +--- PrettyConsole.Tests.Unit/WriteLine.cs | 49 --- .../WriteLineExtensionsTests.cs | 25 ++ .../Features/ColoredOutputTest.cs | 49 ++- .../Features/IndeterminateProgressBarTest.cs | 4 +- .../Features/MultiProgressBarTest.cs | 22 +- .../Features/MultiSelectionTest.cs | 6 +- .../Features/ProgressBarDefaultTest.cs | 8 +- .../Features/ProgressBarMultiLineTest.cs | 8 +- PrettyConsole.Tests/Features/SelectionTest.cs | 6 +- PrettyConsole.Tests/Features/TableTest.cs | 4 +- PrettyConsole.Tests/Features/TreeMenuTest.cs | 6 +- PrettyConsole.Tests/IPrettyConsoleTest.cs | 12 +- PrettyConsole.Tests/Program.cs | 10 +- PrettyConsole/AdvancedInputs.cs | 53 --- PrettyConsole/AdvancedOutputExtensions.cs | 68 ++++ PrettyConsole/AdvancedOutputs.cs | 63 ---- PrettyConsole/Color.cs | 112 ------ PrettyConsole/ColorDefaults.cs | 24 -- PrettyConsole/Console.cs | 12 - PrettyConsole/ConsoleColorExtensions.cs | 59 +++ PrettyConsole/ConsolePipes.cs | 14 +- PrettyConsole/GlobalUsings.cs | 1 - PrettyConsole/IndeterminateProgressBar.cs | 354 +++++++++--------- PrettyConsole/InputRequestExtensions.cs | 58 +++ PrettyConsole/MenuExtensions.cs | 219 +++++++++++ PrettyConsole/Menus.cs | 215 ----------- PrettyConsole/PrettyConsole.csproj | 2 +- ...tensions.cs => PrettyConsoleExtensions.cs} | 12 +- .../PrettyConsoleInterpolatedStringHandler.cs | 17 +- PrettyConsole/ProgressBar.cs | 270 +++++++------ PrettyConsole/ReadLine.cs | 99 ----- PrettyConsole/ReadLineExtensions.cs | 104 +++++ PrettyConsole/RenderingControls.cs | 65 ---- PrettyConsole/RenderingExtensions.cs | 58 +++ PrettyConsole/Write.cs | 126 ------- PrettyConsole/WriteExtensions.cs | 131 +++++++ PrettyConsole/WriteLine.cs | 115 ------ PrettyConsole/WriteLineExtensions.cs | 121 ++++++ 51 files changed, 1356 insertions(+), 1746 deletions(-) delete mode 100644 PrettyConsole.Tests.Unit/ColorTests.cs create mode 100644 PrettyConsole.Tests.Unit/ConsoleColorTests.cs create mode 100644 PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs delete mode 100755 PrettyConsole.Tests.Unit/ReadLine.cs create mode 100755 PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs delete mode 100644 PrettyConsole.Tests.Unit/UtilityTests.cs rename PrettyConsole.Tests.Unit/{Write.cs => WriteExtensionsTests.cs} (51%) delete mode 100755 PrettyConsole.Tests.Unit/WriteLine.cs create mode 100755 PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs delete mode 100755 PrettyConsole/AdvancedInputs.cs create mode 100755 PrettyConsole/AdvancedOutputExtensions.cs delete mode 100755 PrettyConsole/AdvancedOutputs.cs delete mode 100755 PrettyConsole/Color.cs delete mode 100755 PrettyConsole/ColorDefaults.cs delete mode 100755 PrettyConsole/Console.cs create mode 100644 PrettyConsole/ConsoleColorExtensions.cs delete mode 100755 PrettyConsole/GlobalUsings.cs create mode 100755 PrettyConsole/InputRequestExtensions.cs create mode 100755 PrettyConsole/MenuExtensions.cs delete mode 100755 PrettyConsole/Menus.cs rename PrettyConsole/{Extensions.cs => PrettyConsoleExtensions.cs} (63%) mode change 100644 => 100755 delete mode 100755 PrettyConsole/ReadLine.cs create mode 100755 PrettyConsole/ReadLineExtensions.cs delete mode 100755 PrettyConsole/RenderingControls.cs create mode 100755 PrettyConsole/RenderingExtensions.cs delete mode 100755 PrettyConsole/Write.cs create mode 100755 PrettyConsole/WriteExtensions.cs delete mode 100755 PrettyConsole/WriteLine.cs create mode 100755 PrettyConsole/WriteLineExtensions.cs diff --git a/PrettyConsole.Tests.Unit/AdvancedInputs.cs b/PrettyConsole.Tests.Unit/AdvancedInputs.cs index 7b5d6ac..f6537d4 100755 --- a/PrettyConsole.Tests.Unit/AdvancedInputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedInputs.cs @@ -1,52 +1,12 @@ namespace PrettyConsole.Tests.Unit; public class AdvancedInputs { - [Fact] - public void Confirm_Case_Y() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("y"); - In = reader; - var res = Confirm(["Enter y" * Color.White]); - Assert.Contains("Enter y", stringWriter.ToString()); - Assert.True(res); - } - - [Fact] - public void Confirm_Case_Yes() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("yes"); - In = reader; - var res = Confirm(["Enter yes" * Color.White]); - Assert.Contains("Enter yes", stringWriter.ToString()); - Assert.True(res); - } - - [Fact] - public void Confirm_Case_Empty() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader(""); - In = reader; - var res = Confirm(["Enter yes" * Color.White]); - Assert.Contains("Enter yes", stringWriter.ToString()); - Assert.True(res); - } - - [Fact] - public void Confirm_Case_No() { - Out = Utilities.GetWriter(out var stringWriter); - var reader = Utilities.GetReader("no"); - In = reader; - var res = Confirm(["Enter no" * Color.White]); - Assert.Contains("Enter no", stringWriter.ToString()); - Assert.False(res); - } - [Fact] public void Confirm_Case_Y_Interpolated() { Out = Utilities.GetWriter(out var stringWriter); var reader = Utilities.GetReader("y"); In = reader; - var res = Confirm($"Enter y:"); + var res = Console.Confirm($"Enter y:"); Assert.Contains("Enter y:", stringWriter.ToString()); Assert.True(res); } @@ -56,7 +16,7 @@ public void Confirm_Case_Yes_Interpolated() { Out = Utilities.GetWriter(out var stringWriter); var reader = Utilities.GetReader("yes"); In = reader; - var res = Confirm($"Enter yes:"); + var res = Console.Confirm($"Enter yes:"); Assert.Contains("Enter yes", stringWriter.ToString()); Assert.True(res); } @@ -66,7 +26,7 @@ public void Confirm_Case_Empty_Interpolated() { Out = Utilities.GetWriter(out var stringWriter); var reader = Utilities.GetReader(""); In = reader; - var res = Confirm($"Enter yes:"); + var res = Console.Confirm($"Enter yes:"); Assert.Contains("Enter yes", stringWriter.ToString()); Assert.True(res); } @@ -76,7 +36,7 @@ public void Confirm_Case_No_Interpolated() { Out = Utilities.GetWriter(out var stringWriter); var reader = Utilities.GetReader("no"); In = reader; - var res = Confirm($"Enter no:"); + var res = Console.Confirm($"Enter no:"); Assert.Contains("Enter no", stringWriter.ToString()); Assert.False(res); } @@ -87,7 +47,7 @@ public void Confirm_CustomTrueValues_WithInterpolatedPrompt() { var reader = Utilities.GetReader("ok"); In = reader; - var res = Confirm(["ok", "okay"], false, $"Proceed?"); + var res = Console.Confirm(["ok", "okay"], false, $"Proceed?"); Assert.Equal("Proceed?", stringWriter.ToStringAndFlush()); Assert.True(res); diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs index 4dac2ec..290a880 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs @@ -1,25 +1,15 @@ namespace PrettyConsole.Tests.Unit; public class AdvancedOutputs { - [Fact] - public void OverwriteCurrentLine_WritesOutputToPipe() { - Utilities.SkipIfNoInteractiveConsole(); - Error = Utilities.GetWriter(out var writer); - - OverwriteCurrentLine(["Updating" * Color.Green], OutputPipe.Error); - - Assert.Contains("Updating", writer.ToString()); - } - [Fact] public void Overwrite_ExecutesActionAndWritesOutput() { Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var writer); bool executed = false; - Overwrite(() => { + Console.Overwrite(() => { executed = true; - Write(OutputPipe.Error, $"Progress"); + Console.WriteInterpolated(OutputPipe.Error, $"Progress"); }, lines: 1, pipe: OutputPipe.Error); Assert.True(executed); @@ -32,9 +22,9 @@ public void Overwrite_WithState_ExecutesActionAndWritesOutput() { Error = Utilities.GetWriter(out var writer); bool executed = false; - Overwrite("Done", status => { + Console.Overwrite("Done", status => { executed = true; - Write(OutputPipe.Error, $"{status}"); + Console.WriteInterpolated(OutputPipe.Error, $"{status}"); }, lines: 1, pipe: OutputPipe.Error); Assert.True(executed); @@ -44,14 +34,14 @@ public void Overwrite_WithState_ExecutesActionAndWritesOutput() { [Fact] public async Task TypeWrite_Regular() { Out = Utilities.GetWriter(out var stringWriter); - await TypeWrite("Hello world!" * Color.Green, 10); + await Console.TypeWrite("Hello world!", Green / Black, 10); Assert.Contains("Hello world!", stringWriter.ToString()); } [Fact] public async Task TypeWriteLine_Regular() { Out = Utilities.GetWriter(out var stringWriter); - await TypeWriteLine("Hello world!" * Color.Green, 10); + await Console.TypeWriteLine("Hello world!", Green / ConsoleColor.Default, 10); Assert.Contains("Hello world!" + Environment.NewLine, stringWriter.ToString()); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ColorTests.cs b/PrettyConsole.Tests.Unit/ColorTests.cs deleted file mode 100644 index ed7d15b..0000000 --- a/PrettyConsole.Tests.Unit/ColorTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class ColorTests { - [Fact] - public void Color_StaticFields_CoverAllConsoleColor() { - HashSet colors = [ - Color.Black, - Color.Gray, - Color.DarkGray, - Color.White, - Color.Red, - Color.Blue, - Color.Green, - Color.Yellow, - Color.Cyan, - Color.Magenta, - Color.DarkRed, - Color.DarkBlue, - Color.DarkGreen, - Color.DarkYellow, - Color.DarkCyan, - Color.DarkMagenta, - ]; - - var stock = Enum.GetValues().ToHashSet(); - stock.SymmetricExceptWith(colors); - Assert.Empty(stock); // If stock isn't empty, Color doesn't cover all values - } - - [Fact] - public void Color_StaticField_EqualsConsoleColor() { - (ConsoleColor, Color)[] colors = [ - (ConsoleColor.Black, Color.Black), - (ConsoleColor.Gray, Color.Gray), - (ConsoleColor.DarkGray, Color.DarkGray), - (ConsoleColor.White, Color.White), - (ConsoleColor.Red, Color.Red), - (ConsoleColor.Blue, Color.Blue), - (ConsoleColor.Green, Color.Green), - (ConsoleColor.Yellow, Color.Yellow), - (ConsoleColor.Cyan, Color.Cyan), - (ConsoleColor.Magenta, Color.Magenta), - (ConsoleColor.DarkRed, Color.DarkRed), - (ConsoleColor.DarkBlue, Color.DarkBlue), - (ConsoleColor.DarkGreen, Color.DarkGreen), - (ConsoleColor.DarkYellow, Color.DarkYellow), - (ConsoleColor.DarkCyan, Color.DarkCyan), - (ConsoleColor.DarkMagenta, Color.DarkMagenta), - ]; - - foreach (var (consoleColor, color) in colors) { - Assert.Equal(consoleColor, color); - } - } - - [Fact] - public void Color_DivideColorOperator() { - var (fg, bg) = Color.Red / Color.Blue; - Assert.Equal(ConsoleColor.Red, fg); - Assert.Equal(ConsoleColor.Blue, bg); - } - - [Fact] - public void Color_AsteriskOperator() { - var coloredOutput = "Hello" * Color.Green; - Assert.Equal(ConsoleColor.Green, coloredOutput.ForegroundColor); - Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); - } - - [Fact] - public void Color_DivideOperator() { - var coloredOutput = "Hello" / Color.Red; - Assert.Equal(Color.DefaultForegroundColor, coloredOutput.ForegroundColor); - Assert.Equal(ConsoleColor.Red, coloredOutput.BackgroundColor); - } - - [Fact] - public void Color_ObjectOperator() { - var coloredOutput = 3 * Color.Green; - Assert.Equal("3", coloredOutput.Value); - } - - [Fact] - public void Color_DefaultColors() { - var (fg, bg) = Color.Default; - Assert.Equal(Color.DefaultForegroundColor, fg); - Assert.Equal(Color.DefaultBackgroundColor, bg); - } - - [Fact] - public void ColoredOutput_ForegroundCtor() { - var coloredOutput = new ColoredOutput("Hello", Color.Red); - Assert.Equal(ConsoleColor.Red, coloredOutput.ForegroundColor); - Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); - } - - [Fact] - public void ColoredOutput_StringOperator() { - ColoredOutput coloredOutput = "Hello"; - Assert.Equal("Hello", coloredOutput.Value); - Assert.Equal(Color.DefaultForegroundColor, coloredOutput.ForegroundColor); - Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); - } - - [Fact] - public void ColoredOutput_ReadOnlySpanOperator() { - ColoredOutput coloredOutput = "Hello".AsSpan(); - Assert.Equal("Hello", coloredOutput.Value); - Assert.Equal(Color.DefaultForegroundColor, coloredOutput.ForegroundColor); - Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); - } - - [Fact] - public void ColoredOutput_DivideOperator() { - var coloredOutput = "Hello" * Color.Red / Color.Blue; - Assert.Equal(ConsoleColor.Red, coloredOutput.ForegroundColor); - Assert.Equal(ConsoleColor.Blue, coloredOutput.BackgroundColor); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs new file mode 100644 index 0000000..057103d --- /dev/null +++ b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs @@ -0,0 +1,24 @@ +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); + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index fa85189..4f0c8ca 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -2,4 +2,5 @@ global using Xunit; -global using static PrettyConsole.Console; \ No newline at end of file +global using static PrettyConsole.PrettyConsoleExtensions; +global using static System.ConsoleColor; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.Tests.Unit/MenusTests.cs index ba08c40..771cbca 100644 --- a/PrettyConsole.Tests.Unit/MenusTests.cs +++ b/PrettyConsole.Tests.Unit/MenusTests.cs @@ -8,7 +8,7 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { var choices = new List { "Apple", "Banana", "Cherry" }; - var result = Selection(["Choose a fruit:"], choices); + var result = Console.Selection(choices, $"Choose a fruit:"); var output = writer.ToStringAndFlush(); @@ -34,7 +34,7 @@ public void Selection_InvalidNumber_ReturnsEmptyString() { var choices = new List { "One", "Two" }; - var result = Selection(["Pick a number:"], choices); + var result = Console.Selection(choices, $"Pick a number: "); Assert.Equal(string.Empty, result); } @@ -46,7 +46,7 @@ public void Selection_NonNumericInput_ReturnsEmptyString() { var choices = new List { "First", "Second" }; - var result = Selection(["Pick a number:"], choices); + var result = Console.Selection(choices, $"Pick a number: "); Assert.Equal(string.Empty, result); } @@ -58,7 +58,7 @@ public void MultiSelection_ReturnsSelectedChoices_InOrder() { var choices = new List { "Mercury", "Venus", "Earth" }; - var result = MultiSelection(["Planets:"], choices); + var result = Console.MultiSelection(choices, $"Plants: "); Assert.Equal(["Earth", "Mercury"], result); } @@ -70,7 +70,7 @@ public void MultiSelection_InvalidEntry_ReturnsEmptyArray() { var choices = new List { "Alpha", "Beta", "Gamma" }; - var result = MultiSelection(["Letters:"], choices); + var result = Console.MultiSelection(choices, $"Letters: "); Assert.Empty(result); } @@ -82,7 +82,7 @@ public void MultiSelection_EmptyInput_ReturnsEmptyArray() { var choices = new List { "Alpha", "Beta" }; - var result = MultiSelection(["Letters:"], choices); + var result = Console.MultiSelection(choices, $"Letters: "); Assert.Empty(result); } @@ -97,7 +97,7 @@ public void TreeMenu_ValidSelection_ReturnsTuple() { ["Edit"] = new List { "Undo", "Redo" } }; - var (option, subOption) = TreeMenu(["Menu:"], menu); + var (option, subOption) = Console.TreeMenu(menu, $"Menu: "); Assert.Equal("Edit", option); Assert.Equal("Undo", subOption); @@ -112,7 +112,7 @@ public void TreeMenu_MissingSelectionParts_ThrowsArgumentException() { ["Files"] = new List { "Open" } }; - Assert.Throws(() => TreeMenu(["Menu:"], menu)); + Assert.Throws(() => Console.TreeMenu(menu, $"Menu: ")); } [Fact] @@ -125,7 +125,7 @@ public void TreeMenu_InvalidIndexes_ThrowsArgumentException() { ["Edit"] = new List { "Undo" } }; - Assert.Throws(() => TreeMenu(["Menu:"], menu)); + Assert.Throws(() => Console.TreeMenu(menu, $"Menu: ")); } [Fact] @@ -136,7 +136,7 @@ public void Table_WritesHeaderAndRows() { var column1 = new List { "Alice", "Bob" }; var column2 = new List { "30", "25" }; - Table(headers, [column1, column2]); + Console.Table(headers, [column1, column2]); var output = writer.ToString(); Assert.Contains("Name", output); @@ -152,6 +152,6 @@ public void Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() { var headers = new List { "Name", "Age" }; var column1 = new List { "Alice", "Bob" }; - Assert.Throws(() => Table(headers, [column1])); + Assert.Throws(() => Console.Table(headers, [column1])); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs b/PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs new file mode 100644 index 0000000..82f263b --- /dev/null +++ b/PrettyConsole.Tests.Unit/PrettyConsoleExtensionsTests.cs @@ -0,0 +1,16 @@ +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/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 70fb571..dff23e4 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -8,8 +8,8 @@ public void ProgressBar_Update_WritesStatusAndPercentage() { var bar = new ProgressBar { ProgressChar = '#', - ForegroundColor = ConsoleColor.White, - ProgressColor = ConsoleColor.Green + ForegroundColor = White, + ProgressColor = Green }; bar.Update(50, "Loading"); @@ -27,8 +27,8 @@ public void ProgressBar_Update_SamePercentage_RerendersOutput() { var bar = new ProgressBar { ProgressChar = '#', - ForegroundColor = ConsoleColor.White, - ProgressColor = ConsoleColor.Green + ForegroundColor = White, + ProgressColor = Green }; bar.Update(25, "Loading"); @@ -72,7 +72,7 @@ public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { try { Out = Utilities.GetWriter(out var outWriter); - ProgressBar.WriteProgressBar(OutputPipe.Out, 75, ConsoleColor.Cyan, '*'); + ProgressBar.WriteProgressBar(OutputPipe.Out, 75, Cyan, '*'); var output = outWriter.ToString(); Assert.Contains("[", output); diff --git a/PrettyConsole.Tests.Unit/ReadLine.cs b/PrettyConsole.Tests.Unit/ReadLine.cs deleted file mode 100755 index 393752c..0000000 --- a/PrettyConsole.Tests.Unit/ReadLine.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class ReadLine { - [Fact] - public void ReadLine_String_NoOutput() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("Hello world!"); - In = reader; - Assert.Equal("Hello world!", ReadLine()); - } - - [Fact] - public void ReadLine_String_WithOutput() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("Hello world!"); - In = reader; - Assert.Equal("Hello world!", ReadLine(["Enter something:"])); - } - - [Fact] - public void ReadLine_Int() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("5"); - In = reader; - Assert.Equal(5, ReadLine(["Enter num:"])); - } - - [Fact] - public void ReadLine_Int_InvalidWithDefault() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("Hello"); - In = reader; - Assert.Equal(5, ReadLine(["Enter num:"], 5)); - } - - [Fact] - public void TryReadLine_Int_InvalidWithDefault() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("Hello"); - In = reader; - Assert.False(TryReadLine(["Enter num:"], 5, out int num)); - Assert.Equal(5, num); - } - - [Fact] - public void TryReadLine_Enum_IgnoreCase() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("bLack"); - In = reader; - Assert.True(TryReadLine(["Enter color:"], true, out ConsoleColor color)); - Assert.Equal(ConsoleColor.Black, color); - } - - [Fact] - public void ReadLine_InterpolatedPrompt_WritesPromptAndReadsValue() { - Out = Utilities.GetWriter(out _); - var reader = Utilities.GetReader("123"); - In = reader; - - var result = ReadLine($"Enter number: "); - - Assert.Equal("123", result); - } - - [Fact] - public void TryReadLine_InterpolatedWithDefault_ReturnsDefaultOnFailure() { - Out = Utilities.GetWriter(out _); - var reader = Utilities.GetReader("not-a-number"); - In = reader; - - var parsed = TryReadLine(out int result, 42, $"Enter number: "); - - Assert.False(parsed); - Assert.Equal(42, result); - } - - [Fact] - public void TryReadLine_Enum_InterpolatedPrompt_IgnoreCase() { - Out = Utilities.GetWriter(out _); - var reader = Utilities.GetReader("yElLoW"); - In = reader; - - var parsed = TryReadLine(out ConsoleColor color, true, $"Enter enum: "); - - Assert.True(parsed); - Assert.Equal(ConsoleColor.Yellow, color); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs new file mode 100755 index 0000000..cd94210 --- /dev/null +++ b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs @@ -0,0 +1,46 @@ +namespace PrettyConsole.Tests.Unit; + +public class ReadLineExtensionsTests { + [Fact] + public void ReadLine_String_NoOutput() { + Out = Utilities.GetWriter(out var _); + var reader = Utilities.GetReader("Hello world!"); + In = reader; + Assert.Equal("Hello world!", Console.ReadLine()); + } + + [Fact] + public void ReadLine_InterpolatedPrompt_WritesPromptAndReadsValue() { + Out = Utilities.GetWriter(out _); + var reader = Utilities.GetReader("123"); + In = reader; + + var result = Console.ReadLine($"Enter number: "); + + Assert.Equal("123", result); + } + + [Fact] + public void 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); + } + + [Fact] + public void 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); + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index 641116a..b6d0d07 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -1,9 +1,6 @@ -using System; using System.Globalization; using System.Text; -using Xunit; - namespace PrettyConsole.Tests.Unit; public static class Utilities { @@ -25,12 +22,12 @@ public static string ToStringAndFlush(this StringWriter writer) { public static void SkipIfNoInteractiveConsole() { const string reason = "Interactive console APIs are not available in this environment."; - if (System.Console.IsOutputRedirected) { + if (Console.IsOutputRedirected) { Assert.Skip(reason); } try { - _ = System.Console.CursorTop; + _ = Console.CursorTop; } catch (System.IO.IOException) { Assert.Skip(reason); } catch (PlatformNotSupportedException) { diff --git a/PrettyConsole.Tests.Unit/UtilityTests.cs b/PrettyConsole.Tests.Unit/UtilityTests.cs deleted file mode 100644 index fe6458d..0000000 --- a/PrettyConsole.Tests.Unit/UtilityTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class UtilityTests { - [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 == ' ')); - } - - [Fact] - public void InColor_SetsForegroundWithDefaultBackground() { - var colored = "hello".InColor(ConsoleColor.Cyan); - - Assert.Equal("hello", colored.Value); - Assert.Equal(ConsoleColor.Cyan, colored.ForegroundColor); - Assert.Equal(Color.DefaultBackgroundColor, colored.BackgroundColor); - } - - [Fact] - public void InColor_WithBackground_SetsBothColors() { - var colored = "world".InColor(ConsoleColor.Green, ConsoleColor.Black); - - Assert.Equal("world", colored.Value); - Assert.Equal(ConsoleColor.Green, colored.ForegroundColor); - Assert.Equal(ConsoleColor.Black, colored.BackgroundColor); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/Write.cs b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs similarity index 51% rename from PrettyConsole.Tests.Unit/Write.cs rename to PrettyConsole.Tests.Unit/WriteExtensionsTests.cs index 8282cb0..806723b 100755 --- a/PrettyConsole.Tests.Unit/Write.cs +++ b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs @@ -1,10 +1,10 @@ namespace PrettyConsole.Tests.Unit; -public class Write { +public class WriteExtensionsTests { private readonly StringWriter _writer; private readonly StringWriter _errorWriter; - public Write() { + public WriteExtensionsTests() { Out = Utilities.GetWriter(out _writer); Error = Utilities.GetWriter(out _errorWriter); } @@ -16,7 +16,7 @@ public void Write_Interpolated_WritesFormattedContent_ToOutPipe() { Out = writer; try { - Write(OutputPipe.Out, $"Hello {42}"); + Console.WriteInterpolated(OutputPipe.Out, $"Hello {42}"); Assert.Equal("Hello 42", writer.ToString()); } finally { Out = originalOut; @@ -30,7 +30,7 @@ public void Write_Interpolated_WritesFormattedContent_ToErrorPipe() { Error = writer; try { - Write(OutputPipe.Error, $"Error {123}"); + Console.WriteInterpolated(OutputPipe.Error, $"Error {123}"); Assert.Equal("Error 123", writer.ToString()); } finally { Error = originalError; @@ -44,8 +44,8 @@ public void Write_Interpolated_IgnoresColorTokensInOutput() { Out = writer; try { - Write(OutputPipe.Out, - $"Colors {Color.Black / Color.Green}Green{Color.Default} {Color.Red}Red{Color.Default}"); + Console.WriteInterpolated(OutputPipe.Out, + $"Colors {Black / Green}Green{ConsoleColor.Default} {Red}Red{ConsoleColor.Default}"); Assert.Equal("Colors Green Red", writer.ToString()); } finally { @@ -55,89 +55,29 @@ public void Write_Interpolated_IgnoresColorTokensInOutput() { [Fact] public void Write_SpanFormattable_NoColors() { - Write(3.14); + Console.Write(3.14); Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_ForegroundColor() { - Write(3.14, OutputPipe.Out, Color.White); + Console.Write(3.14, OutputPipe.Out, White); Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_ForegroundAndBackgroundColor() { - Write(3.14, OutputPipe.Out, Color.White, Color.Black); + Console.Write(3.14, OutputPipe.Out, White, Black); Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_VeryLongObjectFormat() { var obj = new LongFormatStud(); - Write(obj); + Console.Write(obj); Assert.Equal(new string('X', LongFormatStud.Length), _writer.ToStringAndFlush()); } - [Fact] - public void Write_ColoredOutput_Single() { - Write("Hello world!" * Color.Green); - Assert.Equal("Hello world!", _writer.ToStringAndFlush()); - } - - [Fact] - public void Write_ColoredOutput_Multiple() { - Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"]); - Assert.Equal("Hello David!", _writer.ToStringAndFlush()); - } - - [Fact] - public void WriteError_ColoredOutput_Single() { - Write("Hello world!" * Color.Yellow, OutputPipe.Error); - Assert.Equal("Hello world!", _errorWriter.ToStringAndFlush()); - } - - [Fact] - public void WriteError_ColoredOutput_Single2() { - Write(["Hello world!" * Color.Green], OutputPipe.Error); - Assert.Equal("Hello world!", _errorWriter.ToStringAndFlush()); - } - - [Fact] - public void WriteError_ColoredOutput_Multiple() { - Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); - Assert.Equal("Hello David!", _errorWriter.ToStringAndFlush()); - } - - [Fact] - public void Write_Interpolated_RightAlignmentPadsWithSpaces() { - Write($"Value {42,5}"); - Assert.Equal("Value 42", _writer.ToStringAndFlush()); - } - - [Fact] - public void Write_Interpolated_LeftAlignmentPadsWithSpaces() { - Write($"Value {42,-5}"); - Assert.Equal("Value 42 ", _writer.ToStringAndFlush()); - } - - [Fact] - public void Write_Interpolated_TimeSpanHumanReadableFormat() { - Write($"Elapsed {TimeSpan.FromSeconds(75):hr}"); - Assert.Equal("Elapsed 01:15m", _writer.ToStringAndFlush()); - } - - [Fact] - public void Write_Interpolated_ColoredOutputSpan_WritesValues() { - ReadOnlySpan outputs = [ - "Hi " * Color.Green, - "There" * Color.Yellow - ]; - - Write($"Span {outputs}"); - - Assert.Equal("Span Hi There", _writer.ToStringAndFlush()); - } - private readonly ref struct LongFormatStud : ISpanFormattable { public const int Length = 1024; diff --git a/PrettyConsole.Tests.Unit/WriteLine.cs b/PrettyConsole.Tests.Unit/WriteLine.cs deleted file mode 100755 index 7ddb670..0000000 --- a/PrettyConsole.Tests.Unit/WriteLine.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class WriteLine { - private readonly StringWriter _writer; - private readonly StringWriter _errorWriter; - - public WriteLine() { - Out = Utilities.GetWriter(out _writer); - Error = Utilities.GetWriter(out _errorWriter); - } - - [Fact] - public void WriteLine_Interpolated_AppendsNewLine() { - var originalOut = Out; - var writer = new StringWriter(); - Out = writer; - - try { - WriteLine(OutputPipe.Out, $"Line {7}"); - Assert.Equal($"Line 7{writer.NewLine}", writer.ToString()); - } finally { - Out = originalOut; - } - } - - [Fact] - public void WriteLine_ColoredOutput_Single() { - WriteLine("Hello world!" * Color.Green); - Assert.Equal("Hello world!".WithNewLine(), _writer.ToStringAndFlush()); - } - - [Fact] - public void WriteLine_ColoredOutput_Multiple() { - WriteLine(["Hello " * Color.Green, "David" * Color.Yellow, "!"]); - Assert.Equal("Hello David!".WithNewLine(), _writer.ToStringAndFlush()); - } - - [Fact] - public void WriteLineError_ColoredOutput_Single() { - WriteLine("Hello world!" * Color.Green, OutputPipe.Error); - Assert.Equal("Hello world!".WithNewLine(), _errorWriter.ToStringAndFlush()); - } - - [Fact] - public void WriteLineError_ColoredOutput_Multiple() { - WriteLine(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); - Assert.Equal("Hello David!".WithNewLine(), _errorWriter.ToStringAndFlush()); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs b/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs new file mode 100755 index 0000000..9d8f79d --- /dev/null +++ b/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs @@ -0,0 +1,25 @@ +namespace PrettyConsole.Tests.Unit; + +public class WriteLineExtensionsTests { + private readonly StringWriter _writer; + private readonly StringWriter _errorWriter; + + public WriteLineExtensionsTests() { + Out = Utilities.GetWriter(out _writer); + Error = Utilities.GetWriter(out _errorWriter); + } + + [Fact] + public void 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()); + } finally { + Out = originalOut; + } + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/ColoredOutputTest.cs b/PrettyConsole.Tests/Features/ColoredOutputTest.cs index 7ddcb62..50e8485 100755 --- a/PrettyConsole.Tests/Features/ColoredOutputTest.cs +++ b/PrettyConsole.Tests/Features/ColoredOutputTest.cs @@ -1,4 +1,4 @@ -using static PrettyConsole.Console; +using static System.ConsoleColor; namespace PrettyConsole.Tests.Features; @@ -6,32 +6,27 @@ public sealed class ColoredOutputTest : IPrettyConsoleTest { public string FeatureName => "ColoredOutput"; public ValueTask Implementation() { - WriteLine(["foreground = Red, background = White\t", "Test" * Color.Red / Color.White]); - WriteLine(["foreground = Green, background = Black\t", "Test" * Color.Green / Color.Black]); - WriteLine(["foreground = Blue, background = Yellow\t", "Test" * Color.Blue / Color.Yellow]); - WriteLine(["foreground = Yellow, background = Blue\t", "Test" * Color.Yellow / Color.Blue]); - WriteLine(["foreground = White, background = Red\t", "Test" * Color.White / Color.Red]); - WriteLine(["foreground = Black, background = Green\t", "Test" * Color.Black / Color.Green]); - WriteLine(["foreground = Cyan, background = Magenta\t", "Test" * Color.Cyan / Color.Magenta]); - WriteLine(["foreground = Magenta, background = Cyan\t", "Test" * Color.Magenta / Color.Cyan]); - WriteLine(["foreground = Gray, background = DarkGray\t", "Test" * Color.Gray / Color.DarkGray]); - WriteLine(["foreground = DarkGray, background = Gray\t", "Test" * Color.DarkGray / Color.Gray]); - WriteLine(["foreground = DarkRed, background = DarkGreen\t", "Test" * Color.DarkRed / Color.DarkGreen]); - WriteLine(["foreground = DarkGreen, background = DarkRed\t", "Test" * Color.DarkGreen / Color.DarkRed]); - WriteLine(["foreground = DarkBlue, background = DarkYellow\t", "Test" * Color.DarkBlue / Color.DarkYellow]); - WriteLine(["foreground = DarkYellow, background = DarkBlue\t", "Test" * Color.DarkYellow / Color.DarkBlue]); - WriteLine(["foreground = DarkMagenta, background = DarkCyan\t", "Test" * Color.DarkMagenta / Color.DarkCyan]); - WriteLine(["foreground = DarkCyan, background = DarkMagenta\t", "Test" * Color.DarkCyan / Color.DarkMagenta]); - WriteLine(["foreground = Black, background = White\t", "Test" * Color.Black / Color.White]); - WriteLine(["foreground = White, background = Black\t", "Test" * Color.White / Color.Black]); - WriteLine(["foreground = Red, background = Green\t", "Test" * Color.Red / Color.Green]); - WriteLine(["foreground = Green, background = Red\t", "Test" * Color.Green / Color.Red]); - WriteLine(["foreground = Blue, background = Yellow\t", "Test" * Color.Blue / Color.Yellow]); - WriteLine(["foreground = Yellow, background = Blue\t", "Test" * Color.Yellow / Color.Blue]); - WriteLine(["foreground = White, background = Red\t", "Test" * Color.White / Color.Red]); - WriteLine(["foreground = Black, background = Green\t", "Test" * Color.Black / Color.Green]); - WriteLine(["foreground = Cyan, background = Magenta\t", "Test" * Color.Cyan / Color.Magenta]); - WriteLine(["foreground = Magenta, background = Cyan\t", "Test" * Color.Magenta / Color.Cyan]); + Console.WriteLineInterpolated($"foreground = Red, background = White\t{Red / White}Test"); + Console.WriteLineInterpolated($"foreground = Green, background = Black\t{Green / Black}Test"); + Console.WriteLineInterpolated($"foreground = Yellow, background = Blue\t{Yellow / Blue}Test"); + Console.WriteLineInterpolated($"foreground = Cyan, background = Magenta\t{Cyan / Magenta}Test"); + Console.WriteLineInterpolated($"foreground = Magenta, background = Cyan\t{Magenta / Cyan}Test"); + Console.WriteLineInterpolated($"foreground = Gray, background = DarkGray\t{Gray / DarkGray}Test"); + Console.WriteLineInterpolated($"foreground = DarkGray, background = Gray\t{DarkGray / Gray}Test"); + Console.WriteLineInterpolated($"foreground = DarkRed, background = DarkGreen\t{DarkRed / DarkGreen}Test"); + Console.WriteLineInterpolated($"foreground = DarkGreen, background = DarkRed\t{DarkGreen / DarkRed}Test"); + Console.WriteLineInterpolated($"foreground = DarkBlue, background = DarkYellow\t{DarkBlue / DarkYellow}Test"); + Console.WriteLineInterpolated($"foreground = DarkYellow, background = DarkBlue\t{DarkYellow / DarkBlue}Test"); + Console.WriteLineInterpolated($"foreground = DarkMagenta, background = DarkCyan\t{DarkMagenta / DarkCyan}Test"); + Console.WriteLineInterpolated($"foreground = DarkCyan, background = DarkMagenta\t{DarkCyan / DarkMagenta}Test"); + Console.WriteLineInterpolated($"foreground = Black, background = White\t{Black / White}Test"); + Console.WriteLineInterpolated($"foreground = White, background = Black\t{White / Black}Test"); + Console.WriteLineInterpolated($"foreground = Red, background = Green\t{Red / Green}Test"); + Console.WriteLineInterpolated($"foreground = Green, background = Red\t{Green / Red}Test"); + Console.WriteLineInterpolated($"foreground = Blue, background = Yellow\t{Blue / Yellow}Test"); + Console.WriteLineInterpolated($"foreground = Yellow, background = Blue\t{Yellow / Blue}Test"); + Console.WriteLineInterpolated($"foreground = White, background = Red\t{White / Red}Test"); + Console.WriteLineInterpolated($"foreground = Black, background = Green\t{Black / Green}Test"); return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs index 6017716..24e0352 100755 --- a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; public sealed class IndeterminateProgressBarTest : IPrettyConsoleTest { @@ -8,7 +6,7 @@ public sealed class IndeterminateProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new IndeterminateProgressBar { AnimationSequence = IndeterminateProgressBar.Patterns.Braille, - ForegroundColor = Color.Magenta, + ForegroundColor = ConsoleColor.Magenta, // UpdateRate = 120, DisplayElapsedTime = true }; diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs index 9fbc019..6bc5011 100755 --- a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; public sealed class MultiProgressBarTest : IPrettyConsoleTest { @@ -7,22 +5,22 @@ public sealed class MultiProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { const int count = 333; - var currentLine = GetCurrentLine(); + var currentLine = Console.GetCurrentLine(); for (int i = 1; i <= count; i++) { double percentage = 100 * (double)i / count; - Overwrite((int)percentage, p => { - Write(OutputPipe.Error, $"Task {1}: "); - ProgressBar.WriteProgressBar(OutputPipe.Error, p, Color.Magenta); - NewLine(OutputPipe.Error); - Write(OutputPipe.Error, $"Task {2}: "); - ProgressBar.WriteProgressBar(OutputPipe.Error, p, Color.Magenta); - NewLine(OutputPipe.Error); + Console.Overwrite((int)percentage, p => { + Console.WriteInterpolated(OutputPipe.Error, $"Task {1}: "); + ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta); + Console.NewLine(OutputPipe.Error); + Console.WriteInterpolated(OutputPipe.Error, $"Task {2}: "); + ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta); + Console.NewLine(OutputPipe.Error); }, 2); await Task.Delay(15); } - ClearNextLines(2, OutputPipe.Error); - WriteLine(OutputPipe.Error, $"Done"); + Console.ClearNextLines(2, OutputPipe.Error); + Console.WriteLineInterpolated(OutputPipe.Error, $"Done"); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/MultiSelectionTest.cs b/PrettyConsole.Tests/Features/MultiSelectionTest.cs index e72b467..90696b5 100755 --- a/PrettyConsole.Tests/Features/MultiSelectionTest.cs +++ b/PrettyConsole.Tests/Features/MultiSelectionTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; public sealed class MultiSelectionTest : IPrettyConsoleTest { @@ -12,8 +10,8 @@ public ValueTask Implementation() { "Option 3" ]; - var selected = MultiSelection(["Select an option:"], options); - WriteLine($"Selected: [{string.Join(", ", selected)}]"); + var selected = Console.MultiSelection(options, $"Select an option: "); + Console.WriteLineInterpolated($"Selected: [{string.Join(", ", selected)}]"); return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs b/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs index 34fca71..066a771 100755 --- a/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; /// @@ -10,7 +8,7 @@ public sealed class ProgressBarDefaultTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new ProgressBar { - ProgressColor = Color.Magenta, + ProgressColor = ConsoleColor.Magenta, }; const int count = 333; for (int i = 1; i <= count; i++) { @@ -18,7 +16,7 @@ public async ValueTask Implementation() { prg.Update(percentage, "TESTING"); await Task.Delay(15); } - ClearNextLines(1, OutputPipe.Error); - WriteLine(OutputPipe.Error, $"Done"); + Console.ClearNextLines(1, OutputPipe.Error); + Console.WriteLineInterpolated(OutputPipe.Error, $"Done"); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs b/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs index 25a0e6b..6b341da 100755 --- a/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; /// @@ -10,7 +8,7 @@ public sealed class ProgressBarMultiLineTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new ProgressBar { - ProgressColor = Color.Magenta, + ProgressColor = ConsoleColor.Magenta, }; const int count = 333; for (int i = 1; i <= count; i++) { @@ -18,7 +16,7 @@ public async ValueTask Implementation() { prg.Update(percentage, "TESTING", false); await Task.Delay(15); } - ClearNextLines(2, OutputPipe.Error); - WriteLine(OutputPipe.Error, $"Done"); + Console.ClearNextLines(2, OutputPipe.Error); + Console.WriteLineInterpolated(OutputPipe.Error, $"Done"); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/SelectionTest.cs b/PrettyConsole.Tests/Features/SelectionTest.cs index 96e4446..03d93d7 100755 --- a/PrettyConsole.Tests/Features/SelectionTest.cs +++ b/PrettyConsole.Tests/Features/SelectionTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; public sealed class SelectionTest : IPrettyConsoleTest { @@ -12,8 +10,8 @@ public ValueTask Implementation() { "Option 3" ]; - var selected = Selection(["Select an option"], options); - WriteLine($"Selected: {selected}"); + var selected = Console.Selection(options, $"Select an option: "); + Console.WriteLineInterpolated($"Selected: {selected}"); return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/TableTest.cs b/PrettyConsole.Tests/Features/TableTest.cs index 344edd4..01d3bed 100755 --- a/PrettyConsole.Tests/Features/TableTest.cs +++ b/PrettyConsole.Tests/Features/TableTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; public sealed class TableTest : IPrettyConsoleTest { @@ -9,7 +7,7 @@ public ValueTask Implementation() { var attributes = Enum.GetNames(); var lowered = attributes.Select(x => x.ToLower()).ToArray(); - Table(["attributes", "lowered"], [attributes, lowered]); + Console.Table(["attributes", "lowered"], [attributes, lowered]); return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/TreeMenuTest.cs b/PrettyConsole.Tests/Features/TreeMenuTest.cs index 7592eb0..a557376 100755 --- a/PrettyConsole.Tests/Features/TreeMenuTest.cs +++ b/PrettyConsole.Tests/Features/TreeMenuTest.cs @@ -1,5 +1,3 @@ -using static PrettyConsole.Console; - namespace PrettyConsole.Tests.Features; public sealed class TreeMenuTest : IPrettyConsoleTest { @@ -12,8 +10,8 @@ public ValueTask Implementation() { { "Option 3", ["Option 3.1", "Option 3.2", "Option 3.3"] } }; - var (main, sub) = TreeMenu(["Select an option"], options); - WriteLine($"Selected: ({main}, {sub})"); + var (main, sub) = Console.TreeMenu(options, $"Select an option: "); + Console.WriteLineInterpolated($"Selected: ({main}, {sub})"); return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/PrettyConsole.Tests/IPrettyConsoleTest.cs b/PrettyConsole.Tests/IPrettyConsoleTest.cs index fbc0914..d1369f8 100755 --- a/PrettyConsole.Tests/IPrettyConsoleTest.cs +++ b/PrettyConsole.Tests/IPrettyConsoleTest.cs @@ -1,4 +1,4 @@ -using static PrettyConsole.Console; +using static System.ConsoleColor; namespace PrettyConsole.Tests; @@ -8,11 +8,11 @@ public interface IPrettyConsoleTest { ValueTask Implementation(); public async ValueTask Render() { - WriteLine(["Test: ", FeatureName * Color.Black / Color.White]); - NewLine(); + Console.WriteLineInterpolated($"Test: {Black / White}{FeatureName}"); + Console.NewLine(); await Implementation(); - NewLine(); - RequestAnyInput(["Press any key to continue to next feature..." * Color.Green]); - NewLine(); + Console.NewLine(); + Console.RequestAnyInput($"{Green}Press any key to continue to next feature..."); + Console.NewLine(); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 68ac523..28b5d53 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -2,8 +2,6 @@ using PrettyConsole.Tests; using PrettyConsole.Tests.Features; -using static PrettyConsole.Console; - // var assembly = Assembly.GetExecutingAssembly(); // var tests = assembly.GetTypes() @@ -25,7 +23,7 @@ foreach (var test in tests) { await test.Render(); - NewLine(); + Console.NewLine(); } #pragma warning disable CS8321 // Local function is declared but never used @@ -34,8 +32,8 @@ static void Measure(string label, Action action) { long before = GC.GetAllocatedBytesForCurrentThread(); action(); long after = GC.GetAllocatedBytesForCurrentThread(); - NewLine(); - WriteLine($"{label} - allocated {after - before} bytes"); - NewLine(); + Console.NewLine(); + Console.WriteLineInterpolated($"{label} - allocated {after - before} bytes"); + Console.NewLine(); } #pragma warning restore CS8321 // Local function is declared but never used \ No newline at end of file diff --git a/PrettyConsole/AdvancedInputs.cs b/PrettyConsole/AdvancedInputs.cs deleted file mode 100755 index 6019ecf..0000000 --- a/PrettyConsole/AdvancedInputs.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace PrettyConsole; - -public static partial class Console { - /// - /// Used to wait for user input - /// - /// Interpolated string handler that streams the content. - public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - _ = baseConsole.ReadKey(); - } - - /// - /// Used to get user confirmation with the default values ["y", "yes"] - /// - public static ReadOnlySpan DefaultConfirmValues => new[] { "y", "yes" }; - - /// - /// 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 - /// - public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - return Confirm(DefaultConfirmValues, true, handler); - } - - /// - /// Used to get user confirmation - /// - /// 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 - /// - public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = true, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - var input = In.ReadLine(); - if (input is null or { Length: 0 }) { - return emptyIsTrue; - } - - foreach (var value in trueValues) { - if (input.Equals(value, StringComparison.OrdinalIgnoreCase)) { - return true; - } - } - - return false; - } -} \ No newline at end of file diff --git a/PrettyConsole/AdvancedOutputExtensions.cs b/PrettyConsole/AdvancedOutputExtensions.cs new file mode 100755 index 0000000..c2e486f --- /dev/null +++ b/PrettyConsole/AdvancedOutputExtensions.cs @@ -0,0 +1,68 @@ +namespace PrettyConsole; + +/// +/// Provides methods extending with advanced output extensions. +/// +public static class AdvancedOutputExtensions { + private const int TypeWriteDefaultDelay = 200; + + extension(Console) { + /// + /// Runs that should involve some form of outputting to the console. Set according to the outputs you use in and configure the appropriate + /// + /// The output action. + /// The amount of lines to clear. + /// The output pipe to use. + /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines + /// + public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { + var currentLine = Console.GetCurrentLine(); + Console.ClearNextLines(lines, pipe); + action(); + Console.GoToLine(currentLine); + } + + /// + /// Runs that should involve some form of outputting to the console and use to prevent closure allocation. Set according to the outputs you use in and configure the appropriate + /// + /// + /// The parameters that needs to use. + /// The output action. + /// The amount of lines to clear. + /// The output pipe to use. + /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines + /// + public static void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) where TState : allows ref struct { + var currentLine = Console.GetCurrentLine(); + Console.ClearNextLines(lines, pipe); + action(state); + Console.GoToLine(currentLine); + } + + /// + /// Types out character by character with a delay of milliseconds between each character, styled using . + /// + /// + /// + /// Delay in milliseconds between each character. + public static async Task TypeWrite(string output, (ConsoleColor foregroundColor, ConsoleColor backgroundColor) colorTuple, int delay = TypeWriteDefaultDelay) { + foreach (char c in output) { + Console.Write(c, OutputPipe.Out, colorTuple.foregroundColor, colorTuple.backgroundColor); + await Task.Delay(delay); + } + } + + /// + /// Types out character by character with a delay of milliseconds between each character, styled using followed by a line terminator. + /// + /// + /// + /// Delay in milliseconds between each character. + public static async Task TypeWriteLine(string output, (ConsoleColor foregroundColor, ConsoleColor backgroundColor) colorTuple, int delay = TypeWriteDefaultDelay) { + await TypeWrite(output, colorTuple, delay); + Console.NewLine(); + } + } +} \ No newline at end of file diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs deleted file mode 100755 index 4783155..0000000 --- a/PrettyConsole/AdvancedOutputs.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace PrettyConsole; - -public static partial class Console { - /// - /// Runs that should involve some form of outputting to the console. Set according to the outputs you use in and configure the appropriate - /// - /// The output action. - /// The amount of lines to clear. - /// The output pipe to use. - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { - var currentLine = GetCurrentLine(); - ClearNextLines(lines, pipe); - action(); - GoToLine(currentLine); - } - - /// - /// Runs that should involve some form of outputting to the console and use to prevent closure allocation. Set according to the outputs you use in and configure the appropriate - /// - /// - /// The parameters that needs to use. - /// The output action. - /// The amount of lines to clear. - /// The output pipe to use. - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - public static void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) where TState : allows ref struct { - var currentLine = GetCurrentLine(); - ClearNextLines(lines, pipe); - action(state); - GoToLine(currentLine); - } - - private const int TypeWriteDefaultDelay = 200; - - /// - /// Types out character by character with a delay of milliseconds between each character, styled using . - /// - /// - /// - /// Delay in milliseconds between each character. - public static async Task TypeWrite(string output, (ConsoleColor foregroundColor, ConsoleColor backgroundColor) colorTuple, int delay = TypeWriteDefaultDelay) { - foreach (char c in output) { - Write(c, OutputPipe.Out, colorTuple.foregroundColor, colorTuple.backgroundColor); - await Task.Delay(delay); - } - } - - /// - /// Types out character by character with a delay of milliseconds between each character, styled using followed by a line terminator. - /// - /// - /// - /// Delay in milliseconds between each character. - public static async Task TypeWriteLine(string output, (ConsoleColor foregroundColor, ConsoleColor backgroundColor) colorTuple, int delay = TypeWriteDefaultDelay) { - await TypeWrite(output, colorTuple, delay); - NewLine(); - } -} \ No newline at end of file diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs deleted file mode 100755 index b0f7d7e..0000000 --- a/PrettyConsole/Color.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ReSharper disable InconsistentNaming -using System.Runtime.Versioning; - -namespace PrettyConsole; - -/// -/// Represents a color used for console output. -/// -[UnsupportedOSPlatform("android")] -[UnsupportedOSPlatform("browser")] -[UnsupportedOSPlatform("ios")] -[UnsupportedOSPlatform("tvos")] -public readonly partial record struct Color(ConsoleColor ConsoleColor) { - /// - /// Implicitly converts a to a . - /// - /// The to convert. - /// The value associated with the specified . - public static implicit operator ConsoleColor(Color color) { - return color.ConsoleColor; - } - - /// - /// Creates a tuple for foreground and background color - /// - /// - /// - /// - public static (ConsoleColor fg, ConsoleColor bg) operator /(Color foreground, Color background) { - return (foreground, background); - } - - /// - /// Gets a object representing the color black. - /// - public static readonly Color Black = new(ConsoleColor.Black); - - /// - /// Gets a object representing the color dark blue. - /// - public static readonly Color DarkBlue = new(ConsoleColor.DarkBlue); - - /// - /// Gets a object representing the color dark green. - /// - public static readonly Color DarkGreen = new(ConsoleColor.DarkGreen); - - /// - /// Gets a object representing the color dark cyan. - /// - public static readonly Color DarkCyan = new(ConsoleColor.DarkCyan); - - /// - /// Gets a object representing the color dark red. - /// - public static readonly Color DarkRed = new(ConsoleColor.DarkRed); - - /// - /// Gets a object representing the color dark magenta. - /// - public static readonly Color DarkMagenta = new(ConsoleColor.DarkMagenta); - - /// - /// Gets a object representing the color dark yellow. - /// - public static readonly Color DarkYellow = new(ConsoleColor.DarkYellow); - - /// - /// Gets a object representing the color gray. - /// - public static readonly Color Gray = new(ConsoleColor.Gray); - - /// - /// Gets a object representing the color dark gray. - /// - public static readonly Color DarkGray = new(ConsoleColor.DarkGray); - - /// - /// Gets a object representing the color blue. - /// - public static readonly Color Blue = new(ConsoleColor.Blue); - - /// - /// Gets a object representing the color green. - /// - public static readonly Color Green = new(ConsoleColor.Green); - - /// - /// Gets a object representing the color cyan. - /// - public static readonly Color Cyan = new(ConsoleColor.Cyan); - - /// - /// Gets a object representing the color red. - /// - public static readonly Color Red = new(ConsoleColor.Red); - - /// - /// Gets a object representing the color magenta. - /// - public static readonly Color Magenta = new(ConsoleColor.Magenta); - - /// - /// Gets a object representing the color yellow. - /// - public static readonly Color Yellow = new(ConsoleColor.Yellow); - - /// - /// Gets a object representing the color white. - /// - public static readonly Color White = new(ConsoleColor.White); -} \ No newline at end of file diff --git a/PrettyConsole/ColorDefaults.cs b/PrettyConsole/ColorDefaults.cs deleted file mode 100755 index 93133f8..0000000 --- a/PrettyConsole/ColorDefaults.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace PrettyConsole; - -public readonly partial record struct Color { - /// - /// Represents the default color for the shell (changes based on platform) - /// - public static readonly ConsoleColor DefaultForegroundColor; - - /// - /// Represents the default color for the shell (changes based on platform) - /// - public static readonly ConsoleColor DefaultBackgroundColor; - - /// - /// Returns a tuple of the default foreground and background colors - /// - public static (ConsoleColor fg, ConsoleColor bg) Default => (DefaultForegroundColor, DefaultBackgroundColor); - - static Color() { - baseConsole.ResetColor(); - DefaultForegroundColor = baseConsole.ForegroundColor; - DefaultBackgroundColor = baseConsole.BackgroundColor; - } -} \ No newline at end of file diff --git a/PrettyConsole/Console.cs b/PrettyConsole/Console.cs deleted file mode 100755 index 799d075..0000000 --- a/PrettyConsole/Console.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.Versioning; - -namespace PrettyConsole; - -/// -/// The static class the provides the abstraction over System.Console -/// -[UnsupportedOSPlatform("android")] -[UnsupportedOSPlatform("browser")] -[UnsupportedOSPlatform("ios")] -[UnsupportedOSPlatform("tvos")] -public static partial class Console; \ No newline at end of file diff --git a/PrettyConsole/ConsoleColorExtensions.cs b/PrettyConsole/ConsoleColorExtensions.cs new file mode 100644 index 0000000..9bfb5ae --- /dev/null +++ b/PrettyConsole/ConsoleColorExtensions.cs @@ -0,0 +1,59 @@ +namespace PrettyConsole; + +/// +/// Provides methods extending ; +/// +public static class ConsoleColorExtensions { + /// + /// Returns the default foreground color for the shell. + /// + public static readonly ConsoleColor DefaultForegroundColor; + + /// + /// Returns the default background color for the shell. + /// + public static readonly ConsoleColor DefaultBackgroundColor; + + static ConsoleColorExtensions() { + Console.ResetColor(); + DefaultForegroundColor = Console.ForegroundColor; + DefaultBackgroundColor = Console.BackgroundColor; + } + + extension(ConsoleColor color) { + /// + /// Returns the default foreground color for the shell. + /// + public static ConsoleColor DefaultForeground => DefaultForegroundColor; + + /// + /// Returns the default background color for the shell. + /// + public static ConsoleColor DefaultBackground => DefaultBackgroundColor; + + /// + /// Returns a tuple of (, ) + /// + public static (ConsoleColor, ConsoleColor) Default => (DefaultForegroundColor, DefaultBackgroundColor); + + /// + /// Returns a tuple of (, ) + /// + /// + /// + /// + public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, ConsoleColor background) { + return (foreground, background); + } + + /// + /// Returns a tuple of (, ) + /// + /// + /// + /// + public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, (ConsoleColor tupleForeground, ConsoleColor tupleBackground) colorTuple) { + return (foreground, colorTuple.tupleBackground); + } + } +} \ No newline at end of file diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index 4f64541..ce2eada 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -1,20 +1,20 @@ namespace PrettyConsole; -public static partial class Console { +public static partial class PrettyConsoleExtensions { /// /// The standard input stream. /// - public static TextWriter Out { get; internal set; } = baseConsole.Out; + public static TextWriter Out { get; internal set; } = Console.Out; /// /// The error output stream. /// - public static TextWriter Error { get; internal set; } = baseConsole.Error; + public static TextWriter Error { get; internal set; } = Console.Error; /// /// The standard input stream. /// - public static TextReader In { get; internal set; } = baseConsole.In; + public static TextReader In { get; internal set; } = Console.In; /// /// Gets the appropriate based on @@ -29,15 +29,15 @@ internal static TextWriter GetWriter(OutputPipe pipe) }; /// - /// Returns the current console buffer width or if + /// Returns the current console buffer width or if /// /// /// [MethodImpl(MethodImplOptions.AggressiveOptimization)] internal static int GetWidthOrDefault(int defaultWidth = 120) { - if (baseConsole.IsOutputRedirected) { + if (Console.IsOutputRedirected) { return defaultWidth; } - return baseConsole.BufferWidth; + return Console.BufferWidth; } } \ No newline at end of file diff --git a/PrettyConsole/GlobalUsings.cs b/PrettyConsole/GlobalUsings.cs deleted file mode 100755 index 28e2df2..0000000 --- a/PrettyConsole/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using baseConsole = System.Console; \ No newline at end of file diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index ff6894a..4ed3601 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -3,217 +3,216 @@ namespace PrettyConsole; -public static partial class Console { +/// +/// Represents an indeterminate progress bar that visually indicates the progress of a time-consuming task. +/// +/// +/// +/// After the time-consuming task is completed, the progress bar is removed from the console. and the next output will take its place. +/// +/// +/// The cancellation token parameter on the RunAsync methods is to cancel the progress bar (not necessarily the task) and end it any time. +/// +/// +public class IndeterminateProgressBar { /// - /// Represents an indeterminate progress bar that visually indicates the progress of a time-consuming task. + /// Contains the characters that will be iterated through while running /// /// - /// - /// After the time-consuming task is completed, the progress bar is removed from the console. and the next output will take its place. - /// - /// - /// The cancellation token parameter on the RunAsync methods is to cancel the progress bar (not necessarily the task) and end it any time. - /// + /// You can also choose from some defaults in /// - public class IndeterminateProgressBar { - /// - /// Contains the characters that will be iterated through while running - /// - /// - /// You can also choose from some defaults in - /// - public ReadOnlyCollection AnimationSequence { get; set; } = Patterns.Twirl; + public ReadOnlyCollection AnimationSequence { get; set; } = Patterns.Twirl; - // A length of whitespace padding to the end - private const int PaddingLength = 10; + // A length of whitespace padding to the end + private const int PaddingLength = 10; - /// - /// Gets or sets the foreground color of the progress bar. - /// - public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; + /// + /// Gets or sets the foreground color of the progress bar. + /// + public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.DefaultForeground; - /// - /// Gets or sets a value indicating whether to display the elapsed time in the progress bar. - /// - public bool DisplayElapsedTime { get; set; } = true; + /// + /// Gets or sets a value indicating whether to display the elapsed time in the progress bar. + /// + public bool DisplayElapsedTime { get; set; } = true; - /// - /// Gets or sets the update rate (in ms) of the indeterminate progress bar. - /// - /// Default = 200 - public int UpdateRate { get; set; } = 200; + /// + /// Gets or sets the update rate (in ms) of the indeterminate progress bar. + /// + /// Default = 200 + public int UpdateRate { get; set; } = 200; - /// - /// Runs the indeterminate progress bar while the specified task is running. - /// - /// - /// - /// The output of the running task - public async Task RunAsync(Task task, CancellationToken token = default) { - return await RunAsync(task, string.Empty, token); - } + /// + /// Runs the indeterminate progress bar while the specified task is running. + /// + /// + /// + /// The output of the running task + public async Task RunAsync(Task task, CancellationToken token = default) { + return await RunAsync(task, string.Empty, token); + } - /// - /// Runs the indeterminate progress bar while the specified task is running. - /// - /// - /// The header which to display before the progress char - /// - /// The output of the running task - public async Task RunAsync(Task task, string header, CancellationToken token = default) { - await RunAsyncNonGeneric(task, header, token); - - return task.IsCompleted ? task.Result : await task; - } + /// + /// Runs the indeterminate progress bar while the specified task is running. + /// + /// + /// The header which to display before the progress char + /// + /// The output of the running task + public async Task RunAsync(Task task, string header, CancellationToken token = default) { + await RunAsyncNonGeneric(task, header, token); + + return task.IsCompleted ? task.Result : await task; + } - /// - /// Runs the indeterminate progress bar while the specified task is running. - /// - /// - /// - /// - public async Task RunAsync(Task task, CancellationToken token = default) { - await RunAsync(task, string.Empty, token); - } + /// + /// Runs the indeterminate progress bar while the specified task is running. + /// + /// + /// + /// + public async Task RunAsync(Task task, CancellationToken token = default) { + await RunAsync(task, string.Empty, token); + } - /// - /// Runs the indeterminate progress bar while the specified task is running. - /// - /// - /// The header which to display before the progress char - /// - /// - public async Task RunAsync(Task task, string header, CancellationToken token = default) { - try { - if (task.Status is not TaskStatus.Running) { - task.Start(); - } - } catch { - //ignore + /// + /// Runs the indeterminate progress bar while the specified task is running. + /// + /// + /// The header which to display before the progress char + /// + /// + public async Task RunAsync(Task task, string header, CancellationToken token = default) { + try { + if (task.Status is not TaskStatus.Running) { + task.Start(); } + } catch { + //ignore + } - ResetColors(); - ConsoleColor originalColor = baseConsole.ForegroundColor; - long startTime = Stopwatch.GetTimestamp(); - long updateRateAsTicks = TimeSpan.FromMilliseconds(UpdateRate).Ticks; + Console.ResetColor(); + ConsoleColor originalColor = Console.ForegroundColor; + long startTime = Stopwatch.GetTimestamp(); + long updateRateAsTicks = TimeSpan.FromMilliseconds(UpdateRate).Ticks; - // Maintain a stable cadence that accounts for render time - long nextTick = startTime; - int seqIndex = 0; + // Maintain a stable cadence that accounts for render time + long nextTick = startTime; + int seqIndex = 0; - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token); + 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, - CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + // Cancel the delay token as soon as the bound task completes + _ = task.ContinueWith(static (t, state) => ((CancellationTokenSource)state!).Cancel(), linkedCts, + CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - while (!task.IsCompleted && !token.IsCancellationRequested) { - try { - baseConsole.ForegroundColor = ForegroundColor; - Error.Write(AnimationSequence[seqIndex]); - } finally { - baseConsole.ForegroundColor = originalColor; - } + while (!task.IsCompleted && !token.IsCancellationRequested) { + try { + Console.ForegroundColor = ForegroundColor; + PrettyConsoleExtensions.Error.Write(AnimationSequence[seqIndex]); + } finally { + Console.ForegroundColor = originalColor; + } - if (header.Length > 0) { - Error.Write(' '); - Error.Write(header); - } + if (header.Length > 0) { + PrettyConsoleExtensions.Error.WriteWhiteSpaces(1); + PrettyConsoleExtensions.Error.Write(header.AsSpan()); + } - if (DisplayElapsedTime) { - var elapsed = Stopwatch.GetElapsedTime(startTime); - Write(OutputPipe.Error, $" [Elapsed: {elapsed:hr}]"); - } + if (DisplayElapsedTime) { + var elapsed = Stopwatch.GetElapsedTime(startTime); + Console.WriteInterpolated(OutputPipe.Error, $" [Elapsed: {elapsed:hr}]"); + } - Error.WriteWhiteSpaces(PaddingLength); + PrettyConsoleExtensions.Error.WriteWhiteSpaces(PaddingLength); - // Compute sleep to maintain UpdateRate between frame starts - var now = Stopwatch.GetTimestamp(); - nextTick += updateRateAsTicks; - var remaining = nextTick - now; + // Compute sleep to maintain UpdateRate between frame starts + var now = Stopwatch.GetTimestamp(); + nextTick += updateRateAsTicks; + var remaining = nextTick - now; - if (remaining <= 0) { - // If we are late by >= one period, snap schedule to now to avoid burst catch-up - if (-remaining >= updateRateAsTicks) { - nextTick = now; + if (remaining <= 0) { + // If we are late by >= one period, snap schedule to now to avoid burst catch-up + if (-remaining >= updateRateAsTicks) { + nextTick = now; + } + } else { + try { + // Coarse delay for most of the remainder + var remainingMs = (int)TimeSpan.FromTicks(remaining).TotalMilliseconds; + if (remainingMs > 1) { + await Task.Delay(remainingMs - 1, linkedCts.Token).ConfigureAwait(false); } - } else { - try { - // Coarse delay for most of the remainder - var remainingMs = (int)TimeSpan.FromTicks(remaining).TotalMilliseconds; - if (remainingMs > 1) { - await Task.Delay(remainingMs - 1, linkedCts.Token).ConfigureAwait(false); - } - // Fine spin for the last ~1ms to improve smoothness - var sw = new SpinWait(); - while (!linkedCts.IsCancellationRequested && Stopwatch.GetTimestamp() < nextTick) { - sw.SpinOnce(); - } - } catch (OperationCanceledException) { - // Either external cancellation or task completed + // Fine spin for the last ~1ms to improve smoothness + var sw = new SpinWait(); + while (!linkedCts.IsCancellationRequested && Stopwatch.GetTimestamp() < nextTick) { + sw.SpinOnce(); } + } catch (OperationCanceledException) { + // Either external cancellation or task completed } + } - // Always clear once per frame - ClearNextLines(1, OutputPipe.Error); - - if (token.IsCancellationRequested || task.IsCompleted) { - break; - } + // Always clear once per frame + Console.ClearNextLines(1, OutputPipe.Error); - // Advance animation sequence index without allocations - seqIndex++; - if (seqIndex == AnimationSequence.Count) { - seqIndex = 0; - } + if (token.IsCancellationRequested || task.IsCompleted) { + break; } - ResetColors(); + // Advance animation sequence index without allocations + seqIndex++; + if (seqIndex == AnimationSequence.Count) { + seqIndex = 0; + } } - [MethodImpl(MethodImplOptions.NoInlining)] - private Task RunAsyncNonGeneric(Task task, string header, CancellationToken token) => RunAsync(task, header, token); + Console.ResetColor(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private Task RunAsyncNonGeneric(Task task, string header, CancellationToken token) => RunAsync(task, header, token); + + /// + /// Provides constant animation sequences that can be used for + /// + public static class Patterns { + /// + /// A twirl animation sequence + /// + public static readonly ReadOnlyCollection Twirl + = new(["|", "/", "-", "\\"]); + + /// + /// A braille animation sequence + /// + public static readonly ReadOnlyCollection Braille + = new(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); + + /// + /// A running person animation sequence + /// + public static readonly ReadOnlyCollection RunningPerson + = new(["🧎‍➡️", "🧍", "🚶‍➡️", "🏃‍➡️", " "]); + + /// + /// A sad smiley animation sequence ("what's taking so long??") + /// + public static readonly ReadOnlyCollection SadSmiley + = new(["😞", "😣", "😖", "😫", "😩", " "]); /// - /// Provides constant animation sequences that can be used for + /// A loading-bar animation sequence /// - public static class Patterns { - /// - /// A twirl animation sequence - /// - public static readonly ReadOnlyCollection Twirl - = new(["|", "/", "-", "\\"]); - - /// - /// A braille animation sequence - /// - public static readonly ReadOnlyCollection Braille - = new(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); - - /// - /// A running person animation sequence - /// - public static readonly ReadOnlyCollection RunningPerson - = new(["🧎‍➡️", "🧍", "🚶‍➡️", "🏃‍➡️", " "]); - - /// - /// A sad smiley animation sequence ("what's taking so long??") - /// - public static readonly ReadOnlyCollection SadSmiley - = new(["😞", "😣", "😖", "😫", "😩", " "]); - - /// - /// A loading-bar animation sequence - /// - public static readonly ReadOnlyCollection LoadingBar - = new(["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]"]); - - /// - /// An ASCII ping-pong animation sequence - /// - public static readonly ReadOnlyCollection PingPong - = new([ - "|• |", + public static readonly ReadOnlyCollection LoadingBar + = new(["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]"]); + + /// + /// An ASCII ping-pong animation sequence + /// + public static readonly ReadOnlyCollection PingPong + = new([ + "|• |", "| • |", "| • |", "| • |", @@ -221,7 +220,6 @@ public static readonly ReadOnlyCollection PingPong "| • |", "| • |", "| • |", - ]); - } + ]); } } \ No newline at end of file diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs new file mode 100755 index 0000000..06be2fe --- /dev/null +++ b/PrettyConsole/InputRequestExtensions.cs @@ -0,0 +1,58 @@ +namespace PrettyConsole; + +/// +/// Provides methods extending with input request extensions. +/// +public static class InputRequestExtensions { + /// + /// Used to get user confirmation with the default values ["y", "yes"] + /// + public static ReadOnlySpan DefaultConfirmValues => new[] { "y", "yes" }; + + extension(Console) { + /// + /// Used to wait for user input + /// + /// Interpolated string handler that streams the content. + public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + _ = Console.ReadKey(); + } + + /// + /// 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 + /// + public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + return Confirm(DefaultConfirmValues, true, handler); + } + + /// + /// Used to get user confirmation + /// + /// 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 + /// + public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = true, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + var input = PrettyConsoleExtensions.In.ReadLine(); + if (input is null or { Length: 0 }) { + return emptyIsTrue; + } + + foreach (var value in trueValues) { + if (input.Equals(value, StringComparison.OrdinalIgnoreCase)) { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs new file mode 100755 index 0000000..7766c13 --- /dev/null +++ b/PrettyConsole/MenuExtensions.cs @@ -0,0 +1,219 @@ +using System.Buffers; +using System.Runtime.InteropServices; + +namespace PrettyConsole; + +/// +/// Provides methods extending with menu rendering controls. +/// +public static class MenuExtensions { + extension(Console) { + /// + /// Enumerates a list of strings and allows the user to select one by number + /// + /// Any collection of strings + /// title + /// The selected string, or empty if the choice was invalid. + /// + /// This validates the input for you. + /// + public static string Selection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) + where TList : IList { + Console.ResetColor(); + Console.NewLine(); + + for (int i = 0; i < choices.Count; i++) { + Console.WriteLineInterpolated($" {i + 1}) {choices[i]}"); + } + + Console.NewLine(); + + if (!Console.TryReadLine(out int selected, $"Enter your choice: ")) { + return string.Empty; + } + + selected--; + + if ((uint)selected >= (uint)choices.Count) { + return string.Empty; + } + + return choices[selected]; + } + + /// + /// Enumerates a list of strings and allows the user to select multiple strings by any order, and uses the default index color (White) + /// + /// Any collection of strings + /// title + /// An array containing any selected choices by order of selection, or empty array if any choice is invalid + /// + /// This validates the input for you. + /// + public static string[] MultiSelection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) + where TList : IList { + Console.ResetColor(); + Console.NewLine(); + + for (int i = 0; i < choices.Count; i++) { + Console.WriteLineInterpolated($" {i + 1}) {choices[i]}"); + } + + Console.NewLine(); + + string input = Console.ReadLine(string.Empty, $"Enter your choices separated with spaces: "); + + if (input.Length == 0) { + return []; + } + + var entries = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + if (entries.Length is 0) { + return []; + } + + var arr = new string[entries.Length]; + for (int i = 0; i < arr.Length; i++) { + var entry = entries[i]; + if (!int.TryParse(entry, out var selected) || selected < 1 || selected > choices.Count) { + return []; + } + + selected--; + arr[i] = choices[selected]; + } + + return arr; + } + + /// + /// Enumerates a menu containing main option as well as sub options and allows the user to select both. + /// + /// This function is great where more options or categories are required than can provide. + /// + /// + /// A nested dictionary containing menu titles + /// title + /// The selected main option and selected sub option + /// + /// This validates the input for you. + /// + public static (string option, string subOption) TreeMenu(Dictionary menu, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { + Console.ResetColor(); + Console.NewLine(); + + var menuKeys = menu.Keys.ToArray(); + var maxMainOption = menuKeys.Max(static x => x.Length) + 10; // Used to make sub-tree prefix spaces uniform + + using var bufferOwner = BufferPool.Shared.Rent(out var buffer); + var width = PrettyConsoleExtensions.GetWidthOrDefault(); + buffer.EnsureCapacity(width); + CollectionsMarshal.SetCount(buffer, width); + var span = CollectionsMarshal.AsSpan(buffer); + + //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); + PrettyConsoleExtensions.Out.Write(span.Slice(0, written)); + + var remainingLength = maxMainOption - written; + if (remainingLength > 0) { + PrettyConsoleExtensions.Out.WriteWhiteSpaces(remainingLength); + } + + for (int j = 0; j < subChoices.Count; j++) { + if (j is not 0) { + PrettyConsoleExtensions.Out.WriteWhiteSpaces(maxMainOption); + } + + span.TryWrite($" {j + 1}) {subChoices[j]}", out written); + PrettyConsoleExtensions.Out.WriteLine(span.Slice(0, written)); + } + + Console.NewLine(); + } + + string input = Console.ReadLine(string.Empty, $"Enter your main choice and sub choice separated with space: "); + + var selected = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (selected.Length is not 2) { + throw new ArgumentException("Invalid input, must have 2 selections"); + } + + // Validate + if (!int.TryParse(selected[0], out var mainNum) || mainNum < 1 || mainNum > menuKeys.Length) { + throw new ArgumentException(nameof(mainNum)); + } + + var mainChoice = menuKeys[mainNum - 1]; + + if (!int.TryParse(selected[1], out var subNum) || subNum < 1 || subNum > menu[mainChoice].Count) { + throw new ArgumentException(nameof(subNum)); + } + + var subChoice = menu[mainChoice][subNum - 1]; + + return (mainChoice, subChoice); + } + + /// + /// Draws a table + /// + /// + /// + /// + public static void Table(TList headers, ReadOnlySpan columns) where TList : IList { + if (headers.Count != columns.Length) { + throw new ArgumentException("Headers and columns must be of the same length"); + } + + const char rowSeparator = '-'; + const string columnSeparator = " | "; + + Span lengths = stackalloc int[columns.Length]; + for (int i = 0; i < lengths.Length; i++) { + lengths[i] = columns[i].Max(y => y.Length); + } + + var height = int.MinValue; + foreach (var column in columns) { + if (column.Count > height) { + height = column.Count; + } + } + + + var columnsLength = columns.Length; + List buffer = new(columnsLength); + + for (int i = 0; i < columnsLength; i++) { + buffer.Add(headers[i].PadRight(lengths[i])); + } + + var header = string.Join(columnSeparator, buffer); + buffer.Clear(); + + Span rowSeparation = stackalloc char[header.Length]; + rowSeparation.Fill(rowSeparator); + + PrettyConsoleExtensions.Out.WriteLine(header); + PrettyConsoleExtensions.Out.WriteLine(rowSeparation); + for (int row = 0; row < height; row++) { + for (int i = 0; i < columnsLength; i++) { + buffer.Add(columns[i][row].PadRight(lengths[i])); + } + + var line = string.Join(columnSeparator, buffer); + buffer.Clear(); + PrettyConsoleExtensions.Out.WriteLine(line); + } + + PrettyConsoleExtensions.Out.WriteLine(rowSeparation); + } + } +} \ No newline at end of file diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs deleted file mode 100755 index 049c329..0000000 --- a/PrettyConsole/Menus.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System.Buffers; -using System.Runtime.InteropServices; - -namespace PrettyConsole; - -/* Upgrade to interpolated -public static partial class Console { - /// - /// Enumerates a list of strings and allows the user to select one by number - /// - /// - /// Any collection of strings - /// The selected string, or empty if the choice was invalid. - /// - /// This validates the input for you. - /// - public static string Selection(ReadOnlySpan title, TList choices) - where TList : IList { - WriteLine(title); - - for (int i = 0; i < choices.Count; i++) { - WriteLine($" {i + 1}) {choices[i]}"); - } - - NewLine(); - - if (!TryReadLine(["Enter your choice: "], out int selected)) { - return string.Empty; - } - - selected--; - - if ((uint)selected >= (uint)choices.Count) { - return string.Empty; - } - - return choices[selected]; - } - - /// - /// Enumerates a list of strings and allows the user to select multiple strings by any order, and uses the default index color (White) - /// - /// Optional, null or whitespace will not be displayed - /// Any collection of strings - /// An array containing any selected choices by order of selection, or empty array if any choice is invalid - /// - /// This validates the input for you. - /// - public static string[] MultiSelection(ReadOnlySpan title, TList choices) - where TList : IList { - WriteLine(title); - - for (int i = 0; i < choices.Count; i++) { - WriteLine($" {i + 1}) {choices[i]}"); - } - - NewLine(); - - var input = ReadLine(["Enter your choices separated with spaces: "]); - - if (string.IsNullOrWhiteSpace(input)) { - return []; - } - - var entries = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); - - if (entries.Length is 0) { - return []; - } - - var arr = new string[entries.Length]; - for (int i = 0; i < arr.Length; i++) { - var entry = entries[i]; - if (!int.TryParse(entry, out var selected) || selected < 1 || selected > choices.Count) { - return []; - } - - selected--; - arr[i] = choices[selected]; - } - - return arr; - } - - /// - /// Enumerates a menu containing main option as well as sub options and allows the user to select both. - /// - /// This function is great where more options or categories are required than can provide. - /// - /// - /// Optional, null or whitespace will not be displayed - /// A nested dictionary containing menu titles - /// The selected main option and selected sub option - /// - /// This validates the input for you. - /// - public static (string option, string subOption) TreeMenu(ReadOnlySpan title, - Dictionary menu) where TList : IList { - WriteLine(title); - NewLine(); - - var menuKeys = menu.Keys.ToArray(); - var maxMainOption = menuKeys.Max(static x => x.Length) + 10; // Used to make sub-tree prefix spaces uniform - - using var bufferOwner = BufferPool.Shared.Rent(out var buffer); - var width = GetWidthOrDefault(); - buffer.EnsureCapacity(width); - CollectionsMarshal.SetCount(buffer, width); - var span = CollectionsMarshal.AsSpan(buffer); - - //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); - Out.Write(span.Slice(0, written)); - - var remainingLength = maxMainOption - written; - if (remainingLength > 0) { - Out.WriteWhiteSpaces(remainingLength); - } - - for (int j = 0; j < subChoices.Count; j++) { - if (j is not 0) { - Out.WriteWhiteSpaces(maxMainOption); - } - - span.TryWrite($" {j + 1}) {subChoices[j]}", out written); - Out.WriteLine(span.Slice(0, written)); - } - - NewLine(); - } - - string input = ReadLine(string.Empty, $"Enter your main choice and sub choice separated with space: "); - - var selected = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - if (selected.Length is not 2) { - throw new ArgumentException("Invalid input, must have 2 selections"); - } - - // Validate - if (!int.TryParse(selected[0], out var mainNum) || mainNum < 1 || mainNum > menuKeys.Length) { - throw new ArgumentException(nameof(mainNum)); - } - - var mainChoice = menuKeys[mainNum - 1]; - - if (!int.TryParse(selected[1], out var subNum) || subNum < 1 || subNum > menu[mainChoice].Count) { - throw new ArgumentException(nameof(subNum)); - } - - var subChoice = menu[mainChoice][subNum - 1]; - - return (mainChoice, subChoice); - } - - /// - /// Draws a table - /// - /// - /// - /// - public static void Table(TList headers, ReadOnlySpan columns) where TList : IList { - if (headers.Count != columns.Length) { - throw new ArgumentException("Headers and columns must be of the same length"); - } - - const char rowSeparator = '-'; - const string columnSeparator = " | "; - - Span lengths = stackalloc int[columns.Length]; - for (int i = 0; i < lengths.Length; i++) { - lengths[i] = columns[i].Max(y => y.Length); - } - - var height = int.MinValue; - foreach (var column in columns) { - if (column.Count > height) { - height = column.Count; - } - } - - - var columnsLength = columns.Length; - List buffer = new(columnsLength); - - for (int i = 0; i < columnsLength; i++) { - buffer.Add(headers[i].PadRight(lengths[i])); - } - - var header = string.Join(columnSeparator, buffer); - buffer.Clear(); - - Span rowSeparation = stackalloc char[header.Length]; - rowSeparation.Fill(rowSeparator); - - Out.WriteLine(header); - Out.WriteLine(rowSeparation); - for (int row = 0; row < height; row++) { - for (int i = 0; i < columnsLength; i++) { - buffer.Add(columns[i][row].PadRight(lengths[i])); - } - - var line = string.Join(columnSeparator, buffer); - buffer.Clear(); - Out.WriteLine(line); - } - - Out.WriteLine(rowSeparation); - } -} -*/ \ No newline at end of file diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index e5808b6..0eba229 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -31,7 +31,7 @@ True README.md - + diff --git a/PrettyConsole/Extensions.cs b/PrettyConsole/PrettyConsoleExtensions.cs old mode 100644 new mode 100755 similarity index 63% rename from PrettyConsole/Extensions.cs rename to PrettyConsole/PrettyConsoleExtensions.cs index 9e07840..631b27a --- a/PrettyConsole/Extensions.cs +++ b/PrettyConsole/PrettyConsoleExtensions.cs @@ -1,10 +1,16 @@ +using System.Runtime.Versioning; + namespace PrettyConsole; /// -/// Provides a set of convenient extensions +/// The static class the provides the abstraction over and other extensions. /// -public static class PrettyConsoleExtensions { - extension(TextWriter @this) { +[UnsupportedOSPlatform("android")] +[UnsupportedOSPlatform("browser")] +[UnsupportedOSPlatform("ios")] +[UnsupportedOSPlatform("tvos")] +public static partial class PrettyConsoleExtensions { + extension(TextWriter @this) { /// /// Writes whitespace to this up to length by chucks /// diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 081201d..12900bf 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -1,3 +1,4 @@ +using static System.Console; using System.Runtime.InteropServices; namespace PrettyConsole; @@ -41,7 +42,7 @@ 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) { - _writer = Console.GetWriter(pipe); + _writer = PrettyConsoleExtensions.GetWriter(pipe); _provider = provider; shouldAppend = true; } @@ -93,14 +94,7 @@ public readonly void AppendFormatted(char value, int alignment = 0) { /// Sets the console foreground color to . /// public readonly void AppendFormatted(ConsoleColor color) { - Console.SetColors(color, baseConsole.BackgroundColor); - } - - /// - /// Sets the console foreground color to . - /// - public readonly void AppendFormatted(Color color) { - Console.SetColors(color, baseConsole.BackgroundColor); + Console.SetColors(color, BackgroundColor); } /// @@ -196,11 +190,6 @@ public readonly void AppendFormatted(object? value, int alignment = 0, string? f return; } - if (value is Color color) { - AppendFormatted(color); - return; - } - if (value is string str) { AppendString(str, alignment); return; diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 7d1fbce..466539b 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,153 +1,151 @@ namespace PrettyConsole; -public static partial class Console { +/// +/// Represents a progress bar that can be displayed in the console. +/// +/// +/// +/// The progress bar update isn't tied to unit of time, it's up to the user to update it as needed. By managing the when the Update method is called, the user can have a more precise control over the progress bar. More calls, means more frequent rendering but at the cost of performance (very frequent updates may cause the terminal to lose sync with the method and will produce visual bugs, such as items not rendering in the right place). +/// +/// +/// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. +/// +/// +public class ProgressBar { /// - /// Represents a progress bar that can be displayed in the console. + /// The default characters used for the progress bar filled portion. /// + public const char DefaultProgressChar = '■'; + + /// + /// Gets or sets the character used to represent the progress. + /// + public char ProgressChar { get; set; } = DefaultProgressChar; + + /// + /// Gets or sets the foreground color of the status (if rendered). + /// + public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.DefaultForeground; + + /// + /// Gets or sets the color of the progress portion of the bar. + /// + public ConsoleColor ProgressColor { get; set; } = ConsoleColor.DefaultForeground; + + private readonly Lock _lock = new(); + + /// + /// Updates the progress bar with the specified percentage. + /// + /// The percentage value (0-100) representing the progress. /// - /// - /// The progress bar update isn't tied to unit of time, it's up to the user to update it as needed. By managing the when the Update method is called, the user can have a more precise control over the progress bar. More calls, means more frequent rendering but at the cost of performance (very frequent updates may cause the terminal to lose sync with the method and will produce visual bugs, such as items not rendering in the right place) - /// - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public class ProgressBar { - /// - /// The default characters used for the progress bar filled portion. - /// - public const char DefaultProgressChar = '■'; - - /// - /// Gets or sets the character used to represent the progress. - /// - public char ProgressChar { get; set; } = DefaultProgressChar; - - /// - /// Gets or sets the foreground color of the status (if rendered). - /// - public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; - - /// - /// Gets or sets the color of the progress portion of the bar. - /// - public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; - - private readonly Lock _lock = new(); - - /// - /// Updates the progress bar with the specified percentage. - /// - /// The percentage value (0-100) representing the progress. - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty, true); - - /// - /// Updates the progress bar with the specified percentage. - /// - /// The percentage value (0-100) representing the progress. - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty, true); - - /// - /// Updates the progress bar with the specified percentage and header text. - /// - /// The percentage value (0-100) representing the progress. - /// The status text to be displayed after the progress bar. - /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) - => Update((int)percentage, status, sameLine); - - /// - /// Updates the progress bar with the specified percentage and header text. - /// - /// The percentage value (0-100) representing the progress. - /// The status text to be displayed after the progress bar. - /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. - /// - /// Please remember to clear the used lines after the last call to this method, you can use - /// - public void Update(int percentage, ReadOnlySpan status, bool sameLine = true) { - lock (_lock) { - var currentLine = GetCurrentLine(); - if (sameLine) { - ClearNextLines(1, OutputPipe.Error); - if (status.Length > 0) { - Write(status, OutputPipe.Error, ForegroundColor); - Write(' '); - WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); - } - } else { - bool hasStatus = status.Length > 0; - int lines = hasStatus ? 2 : 1; - ClearNextLines(lines, OutputPipe.Error); - if (hasStatus) WriteLine(status, OutputPipe.Error, ForegroundColor); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty, true); + + /// + /// Updates the progress bar with the specified percentage. + /// + /// The percentage value (0-100) representing the progress. + /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty, true); + + /// + /// Updates the progress bar with the specified percentage and header text. + /// + /// The percentage value (0-100) representing the progress. + /// The status text to be displayed after the progress bar. + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. + /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) + => Update((int)percentage, status, sameLine); + + /// + /// Updates the progress bar with the specified percentage and header text. + /// + /// The percentage value (0-100) representing the progress. + /// The status text to be displayed after the progress bar. + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. + /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. + /// + public void Update(int percentage, ReadOnlySpan status, bool sameLine = true) { + lock (_lock) { + var currentLine = Console.GetCurrentLine(); + if (sameLine) { + Console.ClearNextLines(1, OutputPipe.Error); + if (status.Length > 0) { + Console.Write(status, OutputPipe.Error, ForegroundColor); + Console.Write(' ', OutputPipe.Error); WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } - GoToLine(currentLine); + } else { + bool hasStatus = status.Length > 0; + int lines = hasStatus ? 2 : 1; + Console.ClearNextLines(lines, OutputPipe.Error); + if (hasStatus) Console.WriteLine(status, OutputPipe.Error, ForegroundColor); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } + Console.GoToLine(currentLine); } + } - /// - /// Writes a single progress bar segment without tracking state. - /// - /// The output pipe to write to. - /// The percentage value (0-100) representing the progress. - /// The color used for the filled segment of the bar. - /// The character used to render the filled portion of the bar. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar); - - /// - /// Writes a single progress bar segment without tracking state. - /// - /// The output pipe to write to. - /// The percentage value (0-100) representing the progress. - /// The color used for the filled segment of the bar. - /// The character used to render the filled portion of the bar. - [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] - public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { - ResetColors(); - - int p = Math.Clamp(percentage, 0, 100); - int bufferWidth = GetWidthOrDefault() - baseConsole.CursorLeft; - - const int bracketsAndSpacing = 3; // '[' + ']' + ' ' - const int percentageWidth = 3; // numeric portion width - const int percentSymbolLength = 1; // '%' character - int barLength = Math.Max(0, bufferWidth - (bracketsAndSpacing + percentageWidth + percentSymbolLength)); - - var writer = GetWriter(pipe); - writer.Write('['); - - if (barLength > 0) { - int filled = Math.Min((int)(barLength * p * 0.01), barLength); - - if (filled > 0) { - SetColors(progressColor, baseConsole.BackgroundColor); - Span s = stackalloc char[filled]; - s.Fill(progressChar); - writer.Write(s); - ResetColors(); - } + /// + /// Writes a single progress bar segment without tracking state. + /// + /// The output pipe to write to. + /// The percentage value (0-100) representing the progress. + /// The color used for the filled segment of the bar. + /// The character used to render the filled portion of the bar. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar); - int remaining = barLength - filled; - if (remaining > 0) { - writer.WriteWhiteSpaces(remaining); - } + /// + /// Writes a single progress bar segment without tracking state. + /// + /// The output pipe to write to. + /// The percentage value (0-100) representing the progress. + /// The color used for the filled segment of the bar. + /// The character used to render the filled portion of the bar. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] + public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { + Console.ResetColor(); + + int p = Math.Clamp(percentage, 0, 100); + int bufferWidth = PrettyConsoleExtensions.GetWidthOrDefault() - Console.CursorLeft; + + const int bracketsAndSpacing = 3; // '[' + ']' + ' ' + const int percentageWidth = 3; // numeric portion width + const int percentSymbolLength = 1; // '%' character + int barLength = Math.Max(0, bufferWidth - (bracketsAndSpacing + percentageWidth + percentSymbolLength)); + + var writer = PrettyConsoleExtensions.GetWriter(pipe); + Console.Write('[', pipe); + + 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(); } - Write(pipe, $"] {p,3}%"); + int remaining = barLength - filled; + if (remaining > 0) { + writer.WriteWhiteSpaces(remaining); + } } + + Console.WriteInterpolated(pipe, $"] {p,3}%"); } } \ No newline at end of file diff --git a/PrettyConsole/ReadLine.cs b/PrettyConsole/ReadLine.cs deleted file mode 100755 index 24f9d84..0000000 --- a/PrettyConsole/ReadLine.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Globalization; - -namespace PrettyConsole; - -public static partial class Console { - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// 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 { - ResetColors(); - var input = In.ReadLine(); - return T.TryParse(input, CultureInfo.CurrentCulture, out result); - } - - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// 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); - if (couldParse) { - result = innerResult!; - return true; - } - result = @default; - return false; - } - - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// 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); - } - - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// 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 { - ResetColors(); - var input = In.ReadLine(); - var res = Enum.TryParse(input, ignoreCase, out result); - if (!res) { - result = @default; - } - return res; - } - - /// - /// Used to request user input - /// - /// Interpolated string handler that streams the content. - public static string? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - return ReadLine(); - } - - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// Interpolated string handler that streams the content. - /// The result of the parsing - public static T? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { - _ = TryReadLine(out T? result, handler); - return result; - } - - /// - /// Used to request user input, validates and converts common types. - /// - /// - /// The default value to return if parsing fails - /// Interpolated string handler that streams the content. - /// The result of the parsing - public static T ReadLine(T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { - _ = TryReadLine(out T result, @default, handler); - return result; - } -} \ No newline at end of file diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs new file mode 100755 index 0000000..81b9d09 --- /dev/null +++ b/PrettyConsole/ReadLineExtensions.cs @@ -0,0 +1,104 @@ +using System.Globalization; + +namespace PrettyConsole; + +/// +/// Provides methods extending the overloads of . +/// +public static class ReadLineExtensions { + extension(Console) { + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// 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 { + Console.ResetColor(); + var input = PrettyConsoleExtensions.In.ReadLine(); + return T.TryParse(input, CultureInfo.CurrentCulture, out result); + } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// 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); + if (couldParse) { + result = innerResult!; + return true; + } + result = @default; + return false; + } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// 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); + } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// 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 { + Console.ResetColor(); + var input = PrettyConsoleExtensions.In.ReadLine(); + var res = Enum.TryParse(input, ignoreCase, out result); + if (!res) { + result = @default; + } + return res; + } + + /// + /// Used to request user input + /// + /// Interpolated string handler that streams the content. + public static string? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + return ReadLine(); + } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// Interpolated string handler that streams the content. + /// The result of the parsing + public static T? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + _ = TryReadLine(out T? result, handler); + return result; + } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// The default value to return if parsing fails + /// Interpolated string handler that streams the content. + /// The result of the parsing + public static T ReadLine(T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + _ = TryReadLine(out T result, @default, handler); + return result; + } + } +} \ No newline at end of file diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs deleted file mode 100755 index da27206..0000000 --- a/PrettyConsole/RenderingControls.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace PrettyConsole; - -public static partial class Console { - /// - /// Clears the next (regular output) - /// - /// Amount of lines to clear - /// The output pipe to use - /// - /// Useful for clearing output of overriding functions, like the ProgressBar - /// - public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { - var textWriter = GetWriter(pipe); - var lineLength = GetWidthOrDefault(); - var currentLine = GetCurrentLine(); - GoToLine(currentLine); - for (int i = 0; i < lines; i++) { - textWriter.WriteWhiteSpaces(lineLength); - } - GoToLine(currentLine); - } - - /// - /// Used to clear all previous outputs to the console - /// - public static void Clear() => baseConsole.Clear(); - - /// - /// 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) { - GetWriter(pipe).WriteLine(); - } - - /// - /// Sets the colors of the console output - /// - public static void SetColors(ConsoleColor foreground, ConsoleColor background) { - baseConsole.ForegroundColor = foreground; - baseConsole.BackgroundColor = background; - } - - /// - /// Resets the colors of the console output - /// - public static void ResetColors() { - baseConsole.ResetColor(); - } - - /// - /// Gets the current line number - /// - /// - public static int GetCurrentLine() { - return baseConsole.CursorTop; - } - - /// - /// Moves the cursor to the specified line - /// - /// - public static void GoToLine(int line) { - baseConsole.SetCursorPosition(0, line); - } -} \ No newline at end of file diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs new file mode 100755 index 0000000..7f05644 --- /dev/null +++ b/PrettyConsole/RenderingExtensions.cs @@ -0,0 +1,58 @@ +namespace PrettyConsole; + +/// +/// Provides methods extending with more rendering methods. +/// +public static partial class RenderingExtensions { + extension(Console) { + /// + /// Clears the next (regular output) + /// + /// Amount of lines to clear + /// The output pipe to use + /// + /// Useful for clearing output of overriding functions, like the ProgressBar + /// + public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { + var textWriter = PrettyConsoleExtensions.GetWriter(pipe); + var lineLength = PrettyConsoleExtensions.GetWidthOrDefault(); + var currentLine = GetCurrentLine(); + GoToLine(currentLine); + for (int i = 0; i < lines; i++) { + textWriter.WriteWhiteSpaces(lineLength); + } + GoToLine(currentLine); + } + + /// + /// Used to end current line or write an empty one, depends whether the current line has any text + /// + public static void NewLine(OutputPipe pipe = OutputPipe.Out) { + PrettyConsoleExtensions.GetWriter(pipe).WriteLine(); + } + + /// + /// Sets the colors of the console output + /// + public static void SetColors(ConsoleColor foreground, ConsoleColor background) { + Console.ForegroundColor = foreground; + Console.BackgroundColor = background; + } + + /// + /// Gets the current line number + /// + /// + public static int GetCurrentLine() { + return Console.CursorTop; + } + + /// + /// Moves the cursor to the specified line + /// + /// + public static void GoToLine(int line) { + Console.SetCursorPosition(0, line); + } + } +} \ No newline at end of file diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs deleted file mode 100755 index a89da76..0000000 --- a/PrettyConsole/Write.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Runtime.InteropServices; - -namespace PrettyConsole; - -public static partial class Console { - /// - /// Writes interpolated content using to . - /// - /// Interpolated string handler that streams the content. - public static void Write([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - } - - /// - /// Writes interpolated content using . - /// - /// Destination pipe. Defaults to . - /// Interpolated string handler that streams the content. - public static void Write(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - } - - /// - /// Writes an item that implements without boxing directly to the output writer - /// - /// - /// The output pipe to use - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void Write(T item, OutputPipe pipe = OutputPipe.Out) - where T : ISpanFormattable, allows ref struct { - Write(item, pipe, Color.DefaultForegroundColor, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); - } - - /// - /// Writes an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) - where T : ISpanFormattable, allows ref struct { - Write(item, pipe, foreground, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); - } - - /// - /// Writes an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// background color - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) - where T : ISpanFormattable, allows ref struct { - Write(item, pipe, foreground, background, ReadOnlySpan.Empty, null); - } - - /// - /// Writes an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// background color - /// item format - /// format provider - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, - ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) - where T : ISpanFormattable, allows ref struct { - using var listOwner = BufferPool.Shared.Rent(out var lst); - int upperBound = BufferPool.ListStartingSize; - while (true) { - lst.EnsureCapacity(upperBound); - CollectionsMarshal.SetCount(lst, upperBound); - var span = CollectionsMarshal.AsSpan(lst); - if (item.TryFormat(span, out int charsWritten, format, formatProvider)) { - Write(span.Slice(0, charsWritten), pipe, foreground, background); - break; - } else { - upperBound *= 2; - } - } - } - - /// - /// Writes a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground) { - Write(span, pipe, foreground, Color.DefaultBackgroundColor); - } - - /// - /// Writes a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// background color - public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { - SetColors(foreground, background); - GetWriter(pipe).Write(span); - ResetColors(); - } -} \ No newline at end of file diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs new file mode 100755 index 0000000..e9ff74c --- /dev/null +++ b/PrettyConsole/WriteExtensions.cs @@ -0,0 +1,131 @@ +using System.Runtime.InteropServices; + +namespace PrettyConsole; + +/// +/// Provides methods extending the overloads of . +/// +public static class WriteExtensions { + extension(Console) { + /// + /// Writes interpolated content using to . + /// + /// Interpolated string handler that streams the content. + public static void WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + } + + /// + /// Writes interpolated content using . + /// + /// Destination pipe. Defaults to . + /// Interpolated string handler that streams the content. + public static void WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + } + + /// + /// Writes an item that implements without boxing directly to the output writer + /// + /// + /// The output pipe to use + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void Write(T item, OutputPipe pipe = OutputPipe.Out) + where T : ISpanFormattable, allows ref struct { + Write(item, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground, ReadOnlySpan.Empty, null); + } + + /// + /// Writes an item that implements without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) + where T : ISpanFormattable, allows ref struct { + Write(item, pipe, foreground, ConsoleColor.DefaultBackground, ReadOnlySpan.Empty, null); + } + + /// + /// Writes an item that implements without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// background color + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) + where T : ISpanFormattable, allows ref struct { + Write(item, pipe, foreground, background, ReadOnlySpan.Empty, null); + } + + /// + /// Writes an item that implements without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// background color + /// item format + /// format provider + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, + ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) + where T : ISpanFormattable, allows ref struct { + using var listOwner = BufferPool.Shared.Rent(out var lst); + int upperBound = BufferPool.ListStartingSize; + while (true) { + lst.EnsureCapacity(upperBound); + CollectionsMarshal.SetCount(lst, upperBound); + var span = CollectionsMarshal.AsSpan(lst); + if (item.TryFormat(span, out int charsWritten, format, formatProvider)) { + Write(span.Slice(0, charsWritten), pipe, foreground, background); + break; + } else { + upperBound *= 2; + } + } + } + + /// + /// Writes a without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground) { + Write(span, pipe, foreground, ConsoleColor.DefaultBackground); + } + + /// + /// Writes a without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// background color + public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { + Console.SetColors(foreground, background); + PrettyConsoleExtensions.GetWriter(pipe).Write(span); + Console.ResetColor(); + } + } +} \ No newline at end of file diff --git a/PrettyConsole/WriteLine.cs b/PrettyConsole/WriteLine.cs deleted file mode 100755 index 0ed7fa1..0000000 --- a/PrettyConsole/WriteLine.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace PrettyConsole; - -public static partial class Console { - /// - /// Writes interpolated content using to . - /// - /// Interpolated string handler that streams the content. - public static void WriteLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - NewLine(OutputPipe.Out); - } - - /// - /// Writes interpolated content using . - /// - /// Destination pipe. Defaults to . - /// Interpolated string handler that streams the content. - public static void WriteLine(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - ResetColors(); - NewLine(pipe); - } - - /// - /// WriteLine an item that implements without boxing directly to the output writer - /// - /// - /// The output pipe to use - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) - where T : ISpanFormattable, allows ref struct { - WriteLine(item, pipe, Color.DefaultForegroundColor, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); - } - - /// - /// WriteLine an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground) - where T : ISpanFormattable, allows ref struct { - WriteLine(item, pipe, foreground, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); - } - - /// - /// WriteLine an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// background color - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, - ConsoleColor background) where T : ISpanFormattable, allows ref struct { - WriteLine(item, pipe, foreground, background, ReadOnlySpan.Empty, null); - } - - /// - /// WriteLine an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// background color - /// item format - /// format provider - /// - /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. - /// - public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, - ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) - where T : ISpanFormattable, allows ref struct { - Write(item, pipe, foreground, background, format, formatProvider); - NewLine(pipe); - } - - /// - /// WriteLine a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground) { - Write(span, pipe, foreground, Color.DefaultBackgroundColor); - NewLine(pipe); - } - - /// - /// WriteLine a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput - /// - /// - /// The output pipe to use - /// foreground color - /// background color - public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { - Write(span, pipe, foreground, background); - NewLine(pipe); - } -} \ No newline at end of file diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs new file mode 100755 index 0000000..fd8e5c2 --- /dev/null +++ b/PrettyConsole/WriteLineExtensions.cs @@ -0,0 +1,121 @@ +namespace PrettyConsole; + +/// +/// Provides methods extending the overloads of . +/// +public static class WriteLineExtensions { + extension(Console) { + /// + /// Writes interpolated content using to . + /// + /// Interpolated string handler that streams the content. + [OverloadResolutionPriority(int.MaxValue)] + public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + Console.NewLine(OutputPipe.Out); + } + + /// + /// Writes interpolated content using . + /// + /// Destination pipe. Defaults to . + /// Interpolated string handler that streams the content. + public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + Console.ResetColor(); + Console.NewLine(pipe); + } + + /// + /// WriteLine an item that implements without boxing directly to the output writer + /// + /// + /// The output pipe to use + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) + where T : ISpanFormattable, allows ref struct { + WriteLine(item, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground, ReadOnlySpan.Empty, null); + } + + /// + /// WriteLine an item that implements without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground) + where T : ISpanFormattable, allows ref struct { + WriteLine(item, pipe, foreground, ConsoleColor.DefaultBackground, ReadOnlySpan.Empty, null); + } + + /// + /// WriteLine an item that implements without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// background color + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, + ConsoleColor background) where T : ISpanFormattable, allows ref struct { + WriteLine(item, pipe, foreground, background, ReadOnlySpan.Empty, null); + } + + /// + /// WriteLine an item that implements without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// background color + /// item format + /// format provider + /// + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// + public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, + ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) + where T : ISpanFormattable, allows ref struct { + Console.Write(item, pipe, foreground, background, format, formatProvider); + Console.NewLine(pipe); + } + + /// + /// WriteLine a without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground) { + Console.Write(span, pipe, foreground, ConsoleColor.DefaultBackground); + Console.NewLine(pipe); + } + + /// + /// WriteLine a without boxing directly to the output writer, + /// in the same color convention as ColoredOutput + /// + /// + /// The output pipe to use + /// foreground color + /// background color + public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { + Console.Write(span, pipe, foreground, background); + Console.NewLine(pipe); + } + } +} \ No newline at end of file From 6b28423586b0ab7dbad3353c14cedf3a2d0ae0fe Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:18:50 +0200 Subject: [PATCH 07/75] Removed tests that tried to mock test external method --- PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs index cd94210..d983f65 100755 --- a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs @@ -1,14 +1,6 @@ namespace PrettyConsole.Tests.Unit; public class ReadLineExtensionsTests { - [Fact] - public void ReadLine_String_NoOutput() { - Out = Utilities.GetWriter(out var _); - var reader = Utilities.GetReader("Hello world!"); - In = reader; - Assert.Equal("Hello world!", Console.ReadLine()); - } - [Fact] public void ReadLine_InterpolatedPrompt_WritesPromptAndReadsValue() { Out = Utilities.GetWriter(out _); From d3978912e5f49a1edf9040f50453eb52bac5c42c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:25:59 +0200 Subject: [PATCH 08/75] Use internal apis --- PrettyConsole/ProgressBar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 466539b..cbbad07 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -83,7 +83,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); - Console.Write(' ', OutputPipe.Error); + PrettyConsoleExtensions.GetWriter(OutputPipe.Error).WriteWhiteSpaces(1); WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } } else { @@ -127,7 +127,7 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo int barLength = Math.Max(0, bufferWidth - (bracketsAndSpacing + percentageWidth + percentSymbolLength)); var writer = PrettyConsoleExtensions.GetWriter(pipe); - Console.Write('[', pipe); + Console.Write('[', pipe); if (barLength > 0) { int filled = Math.Min((int)(barLength * p * 0.01), barLength); From c77daabdf9387e462029952df3e7a4ead987a660 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:26:05 +0200 Subject: [PATCH 09/75] Fix old docs --- PrettyConsole/WriteExtensions.cs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index e9ff74c..4dee12a 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -25,7 +25,7 @@ public static void WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandler } /// - /// Writes an item that implements without boxing directly to the output writer + /// Writes an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -39,8 +39,7 @@ public static void Write(T item, OutputPipe pipe = OutputPipe.Out) } /// - /// Writes an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// Writes an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -55,8 +54,7 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) } /// - /// Writes an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// Writes an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -72,8 +70,7 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, Co } /// - /// Writes an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// Writes an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -104,8 +101,16 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, } /// - /// Writes a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// Writes a without boxing directly to the selected . + /// + /// + /// The output pipe to use + public static void Write(ReadOnlySpan span, OutputPipe pipe) { + Write(span, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground); + } + + /// + /// Writes a without boxing directly to the selected . /// /// /// The output pipe to use @@ -115,8 +120,7 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor } /// - /// Writes a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// Writes a without boxing directly to the selected . /// /// /// The output pipe to use From 71d7fce33821db7be801f208ba6d5a439fe03c0b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:28:14 +0200 Subject: [PATCH 10/75] Fix old docs 2 --- PrettyConsole/WriteLineExtensions.cs | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index fd8e5c2..d9d330f 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -26,7 +26,7 @@ public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHan } /// - /// WriteLine an item that implements without boxing directly to the output writer + /// WriteLine an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -40,8 +40,7 @@ public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) } /// - /// WriteLine an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// WriteLine an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -56,8 +55,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground } /// - /// WriteLine an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// WriteLine an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -73,8 +71,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground } /// - /// WriteLine an item that implements without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// WriteLine an item that implements without boxing directly to the selected . /// /// /// The output pipe to use @@ -94,20 +91,26 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground } /// - /// WriteLine a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// WriteLine a without boxing directly to the selected . + /// + /// + /// The output pipe to use + public static void WriteLine(ReadOnlySpan span, OutputPipe pipe) { + Console.WriteLine(span, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground); + } + + /// + /// WriteLine a without boxing directly to the selected . /// /// /// The output pipe to use /// foreground color public static void WriteLine(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground) { - Console.Write(span, pipe, foreground, ConsoleColor.DefaultBackground); - Console.NewLine(pipe); + Console.WriteLine(span, pipe, foreground, ConsoleColor.DefaultBackground); } /// - /// WriteLine a without boxing directly to the output writer, - /// in the same color convention as ColoredOutput + /// WriteLine a without boxing directly to the selected . /// /// /// The output pipe to use From 972252c75952b5b5a454553fb0efb3fc8601f288 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:30:44 +0200 Subject: [PATCH 11/75] fix docs 3 --- PrettyConsole/RenderingExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index 7f05644..88633c9 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -6,7 +6,7 @@ namespace PrettyConsole; public static partial class RenderingExtensions { extension(Console) { /// - /// Clears the next (regular output) + /// Clears the next . /// /// Amount of lines to clear /// The output pipe to use @@ -25,14 +25,14 @@ 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 + /// Used to end current line or write an empty one, depends whether the current line has any text. /// public static void NewLine(OutputPipe pipe = OutputPipe.Out) { PrettyConsoleExtensions.GetWriter(pipe).WriteLine(); } /// - /// Sets the colors of the console output + /// Sets the colors of the console output. /// public static void SetColors(ConsoleColor foreground, ConsoleColor background) { Console.ForegroundColor = foreground; @@ -40,7 +40,7 @@ public static void SetColors(ConsoleColor foreground, ConsoleColor background) { } /// - /// Gets the current line number + /// Gets the current line number. /// /// public static int GetCurrentLine() { @@ -48,7 +48,7 @@ public static int GetCurrentLine() { } /// - /// Moves the cursor to the specified line + /// Moves the cursor to the start of the specified line. /// /// public static void GoToLine(int line) { From a5d0cc301330c35dabc5f9bc2865a02c5c3c5c2f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:35:02 +0200 Subject: [PATCH 12/75] Removed newlines --- PrettyConsole.Tests/Features/MultiProgressBarTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs index 6bc5011..31fe0d1 100755 --- a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -12,10 +12,8 @@ public async ValueTask Implementation() { Console.Overwrite((int)percentage, p => { Console.WriteInterpolated(OutputPipe.Error, $"Task {1}: "); ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta); - Console.NewLine(OutputPipe.Error); Console.WriteInterpolated(OutputPipe.Error, $"Task {2}: "); ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta); - Console.NewLine(OutputPipe.Error); }, 2); await Task.Delay(15); From 2d29e3ac670a827cd9dc9aa2c0cac4929186f06b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:52:15 +0200 Subject: [PATCH 13/75] Added SpanFormattable for object --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 12900bf..dc3a3e2 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -195,6 +195,11 @@ public readonly void AppendFormatted(object? value, int alignment = 0, string? f return; } + if (value is ISpanFormattable spanFormattable) { + AppendSpanFormattable(spanFormattable, alignment, null); + return; + } + if (value is IFormattable formattable) { AppendString(formattable.ToString(format, _provider), alignment); return; From ae962bc578c96d59f9bd55eca1e71dbab20a17ea Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 17:52:21 +0200 Subject: [PATCH 14/75] Updated versions --- Versions.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Versions.md b/Versions.md index b7187c5..12a63d6 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,32 @@ # Versions +## v5.0.0 - .NET 10+ + +This version contains a lot of breaking changes, but they were necessary to trim legacy and sub-optimal things from the library to ensure it remains the best performing library for stylized console outputs. + +### Removed + +- `ColoredOutput` is completely gone, all APIs that used it - are gone as well. +- `Color` struct is gone too, extension members allowed me to rework it's uses to the built-in `System.ConsoleColor`. + +### Changed + +- All of the public facing APIs can now be invoked directly from `System.Console` + - `Write` and `Write(ReadOnlySpan, OutputPipe)` remain by the same names. + - `Write` and `WriteLine` that use `PrettyConsoleInterpolatedStringHandler` were renamed to `WriteInterpolated` and `WriteLineInterpolated` respectively - This is slightly more verbose but required to bypass overload resolution which prefers the build-in `Write` and `WriteLine` methods of `System.Console`. + - `Selection`, `Menu`, `Table`, `ReadLine`, `TryReadLine` were all added to `System.Console`. +- `ConsoleColor` now has static properties to allow the same semantics as `Color` used to. + - `ConsoleColor.DefaultForeground` and `ConsoleColor.DefaultBackground` bind to the defaults of the shell and can be used to work with the defaults or reset colors. + - `ConsoleColor.Default` returns a tuple of `(DefaultForeground, DefaultBackground)` + - `ConsoleColor / ConsoleColor` also returns a tuple where the first the foreground and second a background. + - `ConsoleColor / (ConsoleColor, ConsoleColor)` returns a tuple of the foreground, and the background from second tuple. + - Those can be used with the string handler `$"{ConsoleColor.Red}Hello{ConsoleColor.Default}"` to write colored zero allocation outputs. +- `ProgressBar` and `IndeterminateProgressBar` are no longer nested classes of `Console` (since it doesn't exist anymore) - this can affect your using statements. + +### Added + +- `TextWriter` which is the object backing `Console.Out` and `Console.Error` now has a static extension `WriteWhiteSpaces(int)`, which can be used to write paddings and whatever else without any allocations. It was previously an internal method but I chose to expose it for all of you. + ## v4.1.0 ### Added From 0982714e2c0e94c80ccf02e7c5fec7ac3f140d1e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 18:07:39 +0200 Subject: [PATCH 15/75] Update agents.md --- AGENTS.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 51caff3..a244b9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,12 @@ Repository: PrettyConsole Summary -- PrettyConsole is a high-performance, allocation-conscious wrapper over System.Console that provides structured colored output, input helpers, rendering controls, menus, and progress bars. It targets net9.0, is trimming/AOT ready, and ships SourceLink metadata for debugging. +- PrettyConsole is a high-performance, allocation-conscious extension layer over System.Console (implemented via C# extension members) that provides structured colored output, input helpers, rendering controls, menus, and progress bars. It targets net10.0, is trimming/AOT ready, and ships SourceLink metadata for debugging. - Solution layout: - PrettyConsole/ — main library - PrettyConsole.Tests/ — interactive/demo runner (manually selects visual feature demos) - PrettyConsole.Tests.Unit/ — xUnit v3 unit tests using Microsoft Testing Platform +- v5.0.0 (November 2025) removed the legacy `ColoredOutput`/`Color` types; color composition now flows through `ConsoleColor` helpers and tuples exposed by the library. Commands you’ll use often @@ -43,28 +44,30 @@ Repo-specific agent rules and conventions High-level architecture and key concepts - Console facade - - `PrettyConsole.Console` is a static, partial wrapper over `System.Console`. It exposes the live `In`, `Out`, and `Error` streams, and adds helpers like `NewLine`, `Clear`, `ClearNextLines`, `GetCurrentLine`, `GoToLine`, `SetColors`, and `ResetColors` for structured rendering. + - Extension members declared via `extension(Console)` attach directly to `System.Console`, so APIs such as `Console.WriteInterpolated`, `Console.TryReadLine`, `Console.Overwrite`, etc. light up once `using PrettyConsole;` (optionally with `using static System.Console;`) is in scope. `PrettyConsoleExtensions` still exposes the live `In`, `Out`, and `Error` streams plus helpers like `GetWidthOrDefault`. - Output routing - - `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `Console.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly. + - `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `PrettyConsoleExtensions.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly. - Interpolated string handler - - `PrettyConsoleInterpolatedStringHandler` enables zero-allocation `$"..."` calls for `Write`, `WriteLine`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors automatically reset after each invocation, and handlers respect the selected pipe and optional `IFormatProvider`. + - `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. - Coloring model - - `ColoredOutput` and the `Color` record provide terse composition via `"Text" * Color.Red / Color.Blue` and implicit conversions. Default foreground/background values are stored on `Color` so spans render without string allocations. + - `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. - Write APIs - - `Write`/`WriteLine` cover interpolated strings, `ColoredOutput` spans, raw `ReadOnlySpan`, and generic `ISpanFormattable` values (including `ref struct`s) with optional foreground/background colors and format providers. Internally they rely on `BufferPool` to avoid allocation spikes. + - `WriteInterpolated`/`WriteLineInterpolated` host the interpolated-string handler; `Write`/`WriteLine` overloads target `ISpanFormattable` values (including `ref struct`s) and raw `ReadOnlySpan` spans with optional foreground/background overrides. Implementations rent buffers via `BufferPool` to avoid allocation spikes and always reset colors. +- TextWriter helpers + - `PrettyConsoleExtensions` surfaces a `TextWriter.WriteWhiteSpaces(int)` extension (available via `PrettyConsoleExtensions.Out/Error`) for allocation-free padding. Use it instead of creating temporary `string`s when building menus, tables, or progress output. - Inputs - `ReadLine`/`TryReadLine` support `IParsable` types, optional defaults, enum parsing with `ignoreCase`, and interpolated prompts. `Confirm` exposes `DefaultConfirmValues`, overloads for custom truthy tokens, and interpolated prompts; `RequestAnyInput` blocks on `ReadKey` with colored prompts if desired. - Rendering controls - `ClearNextLines`, `GoToLine`, and `GetCurrentLine` coordinate bounded screen regions; `Clear` wipes the buffer when safe. These helpers underpin progress rendering and overwrite scenarios. - Advanced outputs - - `OverwriteCurrentLine`, `Overwrite`, and `Overwrite` run user actions while clearing a configurable number of lines, enabling reactive text dashboards without leaving artifacts. `TypeWrite`/`TypeWriteLine` animate character-by-character output with adjustable delays. + - `OverwriteCurrentLine`, `Overwrite`, and `Overwrite` run user actions while clearing a configurable number of lines. Set the `lines` argument to however many rows you emit during the action (e.g., the multi-progress sample uses `lines: 2`) and call `Console.ClearNextLines` once after the last overwrite to remove residual UI. `TypeWrite`/`TypeWriteLine` animate character-by-character output with adjustable delays. - Menus and tables - `Selection` returns a single choice or empty string on invalid input; `MultiSelection` parses space-separated indices into string arrays; `TreeMenu` renders two-level hierarchies and validates input (throwing `ArgumentException` when selections are invalid); `Table` renders headers + columns with width calculations. - Progress bars - `IndeterminateProgressBar` binds to running `Task` instances, optionally starts tasks, supports cancellable `RunAsync` overloads, exposes `AnimationSequence`, `Patterns`, `ForegroundColor`, `DisplayElapsedTime`, and `UpdateRate`. Frames render on the error pipe and auto-clear. - - `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. + - `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 net9.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project. + - `PrettyConsole.csproj` targets net10.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project. Testing structure and workflows @@ -76,7 +79,8 @@ Testing structure and workflows Notes and gotchas -- The library aims to minimize allocations; prefer span-based overloads (ReadOnlySpan, ReadOnlySpan) for best performance when contributing. +- The library aims to minimize allocations; prefer span-based overloads (`ReadOnlySpan`, `ISpanFormattable`) plus the inline `ConsoleColor` tuples instead of recreating strings or structs. - When authoring new features, pick the appropriate OutputPipe to keep CLI piping behavior intact. - On macOS terminals, ANSI is supported; Windows legacy terminals are handled via ANSI-compatible rendering in the library. -- `ProgressBar.Update` re-renders on every call (even when the percentage is unchanged) and accepts `sameLine` to place the status above the bar; the static `ProgressBar.WriteProgressBar` renders one-off bars for multi-progress scenarios. +- `ProgressBar.Update` re-renders on every call (even when the percentage is unchanged) and accepts `sameLine` to place the status above the bar; the static `ProgressBar.WriteProgressBar` renders one-off bars without writing a trailing newline, so rely on `Console.Overwrite`/`lines` to stack multiple bars cleanly. +- After the final `Overwrite`/`Overwrite` call in a rendering loop, call `Console.ClearNextLines(totalLines, pipe)` once more to clear the region and prevent ghost text. From 37b6e8d8772782c0b86f3a3858ac9b76e08d7748 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 18:40:07 +0200 Subject: [PATCH 16/75] Update readme --- README.md | 333 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 171 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 3aa1fda..a9df94a 100755 --- a/README.md +++ b/README.md @@ -1,245 +1,254 @@ # PrettyConsole -An abstraction over `System.Console` that adds new input and output methods, colors and advanced outputs like progress bars and menus. And everything is ansi supported so it works on legacy systems and terminals. +PrettyConsole is a high-performance, allocation-conscious extension layer over `System.Console`. The library uses C# extension members (`extension(Console)`) so every API lights up directly on `System.Console` once `using PrettyConsole;` (and optionally `using static System.Console;`) is in scope. It targets **.NET 10.0**, is trimming/AOT ready, preserves SourceLink metadata, and keeps the familiar console experience while adding structured rendering, menus, progress bars, and advanced input helpers. ## Features -* 🚀 High performance, low allocations and span-first APIs -* 🪶 Very lightweight (no external dependencies) -* ✨ Zero-allocation interpolated string handler for inline colors and formatting -* 💾 Supports legacy ANSI terminals (like Windows 7) -* 🔥 Complete NativeAOT compatibility -* Supports all major platforms (Windows, Linux, Mac) -* ⛓ Uses original output pipes, so that your CLI's can be piped properly +* 🚀 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 +* 🧰 Rich input helpers (`TryReadLine`, `Confirm`, `RequestAnyInput`) with `IParsable` and enum support +* ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `TextWriter.WriteWhiteSpaces`) +* ⛓ Output routing through `OutputPipe.Out` and `OutputPipe.Error` so piping/redirects continue to work -## Installation [![NUGET DOWNLOADS](https://img.shields.io/nuget/dt/PrettyConsole?label=Downloads)](https://www.nuget.org/packages/PrettyConsole/) +## Installation -> dotnet add package PrettyConsole +```bash +dotnet add package PrettyConsole +``` ## Usage -Everything starts off with the using statements, I recommend using the `Console` statically +### Bring PrettyConsole APIs into scope ```csharp -using static PrettyConsole.Console; // Access to all Console methods -using PrettyConsole; // Access to the Color struct and OutputPipe enum +using PrettyConsole; // Extension members + OutputPipe +using static System.Console; // Optional for terser call sites ``` -### Interpolated Strings +This setup lets you call `Console.WriteInterpolated`, `Console.Overwrite`, `Console.TryReadLine`, etc. The original `System.Console` APIs remain available—call `System.Console.ReadKey()` or `System.Console.SetCursorPosition()` directly whenever you need something the extensions do not provide. + +### Interpolated strings & inline colors -`PrettyConsoleInterpolatedStringHandler` lets you stream interpolated text directly to the selected pipe without allocating intermediate strings, while still using the familiar `$"..."` syntax. +`PrettyConsoleInterpolatedStringHandler` streams interpolated content directly to the selected pipe without allocating. Colors auto-reset at the end of each call. ```csharp -Write($"Hello {Color.Green}world{Color.Default}!"); -Write(OutputPipe.Error, $"{Color.Yellow}Warning:{Color.Default} {message}"); +Console.WriteInterpolated($"Hello {ConsoleColor.Green / ConsoleColor.DefaultBackground}world{ConsoleColor.Default}!"); +Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.Yellow / ConsoleColor.DefaultBackground}warning{ConsoleColor.Default}: {message}"); -if (!TryReadLine(out int choice, $"Pick option {Color.Cyan}1-5{Color.Default}: ")) { - WriteLine($"{Color.Red}Not a number.{Color.Default}"); +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}"); } ``` -Colors reset automatically at the end of each call. Use `Color.Default` (or explicit background tuples) when you need to restore colors mid-string. +`ConsoleColor.DefaultForeground`, `ConsoleColor.DefaultBackground`, and the `/` operator overload make it easy to compose foreground/background tuples inline (`ConsoleColor.Red / ConsoleColor.White`). -When interpolating `TimeSpan` values you can also apply the special `:hr` format specifier to get compact, human-readable output (`ms`, `ss`, `mm`, `hh`, or `dd` depending on the magnitude): +#### Formatting & alignment helpers -```csharp -var elapsed = stopwatch.Elapsed; -WriteLine($"Completed in {elapsed:hr}"); -``` +- **`TimeSpan :hr` format** — the interpolated string handler understands the custom `:hr` specifier. It renders the span using the most appropriate unit (e.g., `950ms`, `12s`, `03m`, `02h`, `1d`) without allocating temporaries: -### ColoredOutput + ```csharp + var elapsed = stopwatch.Elapsed; + Console.WriteInterpolated($"Completed in {elapsed:hr}"); + ``` -PrettyConsole uses an equation inspired syntax to colorize text. The syntax is as follows: +- **Alignment** — standard alignment syntax works the same way it does with regular interpolated strings, but the handler writes directly into the console buffer. This keeps columnar output zero-allocation friendly: -```csharp -WriteLine("Test" * Color.Red / Color.Blue); -``` + ```csharp + Console.WriteInterpolated($"|{"Label",-10}|{value,10:0.00}|"); + ``` -i.e `TEXT * FOREGROUND / BACKGROUND` +You can combine both, e.g., `$"{elapsed,8:hr}"`, to keep progress/status displays tidy. -Any the 2 colors can be played with just like a real equation, omit the foreground and the default will be used, -same goes for the background. +### Basic outputs -### Basic Outputs +```csharp +// Interpolated text +Console.WriteInterpolated($"Processed {items} items in {elapsed:hr}"); +Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Magenta}debug{ConsoleColor.Default}"); -The most basic method for outputting is `Write`, which has multiple overloads. All equivalents exist for `WriteLine`: +// Span + color overloads (no boxing) +ReadOnlySpan header = "Title"; +Console.Write(header, OutputPipe.Out, ConsoleColor.White, ConsoleColor.DarkBlue); +Console.NewLine(); // writes newline to the default output pipe -```csharp -// Usage + overload highlights: -Write($"Interpolated {Color.Blue}string{Color.Default}"); -Write(OutputPipe.Error, $"..."); -Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out); -Write(ReadOnlySpan outputs, OutputPipe pipe = OutputPipe.Out); -Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground); -Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background); -Write(T value, OutputPipe pipe = OutputPipe.Out) where T : ISpanFormattable; -Write(T value, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, - ReadOnlySpan format, IFormatProvider? provider); +// ISpanFormattable (works with ref structs) +Console.Write(percentage, OutputPipe.Out, ConsoleColor.Cyan, ConsoleColor.DefaultBackground, format: "F2", formatProvider: null); ``` -Overload for `WriteLine` are available with the same parameters +Behind the scenes these overloads rent buffers via `BufferPool` and route output to the correct pipe through `PrettyConsoleExtensions.GetWriter`. -### Basic Inputs - -These are the methods for reading user input: +### Basic inputs ```csharp -// Examples: -string? ReadLine(); // ReadLine -string? ReadLine(ReadOnlySpan); -string? ReadLine($"Prompt {Color.Green}text{Color.Default}: "); -T? ReadLine(ReadOnlySpan); // T : IParsable -T? ReadLine($"Prompt {Color.Cyan}text{Color.Default}: "); -T ReadLine(ReadOnlySpan, T @default); // @default will be returned if parsing fails -T ReadLine(T @default, $"Prompt {Color.Cyan}text{Color.Default}: "); -bool TryReadLine(ReadOnlySpan, out T?); // T : IParsable -bool TryReadLine(out T?, $"Prompt {Color.Cyan}text{Color.Default}: "); -bool TryReadLine(ReadOnlySpan, T @default, out T); // @default will be returned if parsing fails -bool TryReadLine(out T, T @default, $"Prompt {Color.Cyan}text{Color.Default}: "); -bool TryReadLine(ReadOnlySpan, bool ignoreCase, out TEnum?); // TEnum : struct, Enum -bool TryReadLine(out TEnum, bool ignoreCase, $"Prompt {Color.Cyan}text{Color.Default}: "); // TEnum : struct, Enum -bool TryReadLine(ReadOnlySpan, bool ignoreCase, TEnum @default, out TEnum); // @default will be returned if parsing fails -bool TryReadLine(out TEnum, bool ignoreCase, TEnum @default, $"Prompt {Color.Cyan}text{Color.Default}: "); -``` +if (!Console.TryReadLine(out int port, $"Port ({ConsoleColor.Green}5000{ConsoleColor.Default}): ")) { + port = 5000; +} + +// `TryReadLine` and `TryReadLine` with defaults +if (!Console.TryReadLine(out DayOfWeek day, ignoreCase: true, $"Day? ")) { + day = DayOfWeek.Monday; +} -I always recommend using `TryReadLine` instead of `ReadLine` as you need to maintain less null checks and the result, -especially with `@default` is much more concise. +var apiKey = Console.ReadLine($"Enter API key ({ConsoleColor.DarkGray}optional{ConsoleColor.Default}): "); +``` -### Advanced Inputs +All input helpers work with `IParsable` and enums, respect the active culture, and honor `OutputPipe` when prompts are colored. -These are some special methods for inputs: +### Advanced inputs ```csharp -// These will wait for the user to press any key -void RequestAnyInput(string message = "Press any key to continue..."); -void RequestAnyInput(ReadOnlySpan output); -RequestAnyInput($"Press {Color.Yellow}any key{Color.Default} to continue..."); -// These request confirmation by special input from user -bool Confirm(ReadOnlySpan message); // uses the default values ["y", "yes"] -// the default values can also be used by you at Console.DefaultConfirmValues -bool Confirm(ReadOnlySpan message, ReadOnlySpan trueValues, bool emptyIsTrue = true); -bool Confirm($"Deploy to production? ({Color.Green}y{Color.Default}/{Color.Red}n{Color.Default}) "); -bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue, $"Overwrite existing files? "); -``` +Console.RequestAnyInput($"Press {ConsoleColor.Yellow}any key{ConsoleColor.Default} to continue…"); + +if (!Console.Confirm($"Deploy to production? ({ConsoleColor.Green}y{ConsoleColor.Default}/{ConsoleColor.Red}n{ConsoleColor.Default}) ")) { + return; +} -### Rendering Controls +var customTruths = new[] { "sure", "do it" }; +bool overwrite = Console.Confirm(customTruths, emptyIsTrue: false, $"Overwrite existing files? "); +``` -To aid in rendering and building your own complex outputs, there are many methods that simplify some processes. +### Rendering helpers ```csharp -ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error); // clears the next lines -NewLine(OutputPipe pipe = OutputPipe.Out); // outputs a new line -SetColors(ConsoleColor foreground, ConsoleColor background); // sets the colors of the console output -ResetColors(); // resets the colors of the console output -int GetCurrentLine(); // returns the current line number -GoToLine(int line); // moves the cursor to the specified line +Console.ClearNextLines(3, OutputPipe.Error); +int line = Console.GetCurrentLine(); +// … draw something … +Console.GoToLine(line); +Console.SetColors(ConsoleColor.White, ConsoleColor.DarkBlue); +Console.ResetColors(); ``` -Combining `ClearNextLines` with `GoToLine` will enable you to efficiently use the same space in the console for continuous output, such as progress outputting, for some cases there are also built-in methods for this, more on that later. - -### Advanced Outputs +`PrettyConsoleExtensions.Out`/`Error` expose the live writers. Each writer now has `WriteWhiteSpaces(int)` for zero-allocation padding: ```csharp -// This method will essentially write a line, clear it, go back to same position -// This allows a form of text-only progress bar -void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error); -void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error); -void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) - where TState : allows ref struct; -// This methods will write a character at a time, with a delay between each character -async Task TypeWrite(ColoredOutput output, int delay = TypeWriteDefaultDelay); -async Task TypeWriteLine(ColoredOutput output, int delay = TypeWriteDefaultDelay); +PrettyConsoleExtensions.Error.WriteWhiteSpaces(8); // pad status blocks ``` -### Menus +### Advanced outputs ```csharp -// This prints an index view of the list, allows the user to select by index -// returns the actual choice that corresponds to the index -string Selection(ReadOnlySpan title, TList choices) where TList : IList {} -// Same as selection but allows the user to select multiple indexes -// Separated by spaces, and returns an array of the actual choices that correspond to the indexes -string[] MultiSelection(ReadOnlySpan title, TList choices) where TList : IList {} -// This prints a tree menu of 2 levels, allows the user to select the index -// Of the first and second level and returns the corresponding choices -(string option, string subOption) TreeMenu(ReadOnlySpan title, - Dictionary menu) where TList : IList {} -// This prints a table with headers, and columns for each list -void Table(TList headers, ReadOnlySpan columns) where TList : IList {} +Console.Overwrite(() => { + Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Cyan}Working…{ConsoleColor.Default}"); + Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.DarkGray}Elapsed:{ConsoleColor.Default} {stopwatch.Elapsed:hr}"); +}, lines: 2); + +// Prevent closure allocations with state + generic overload +Console.Overwrite((left, right), tuple => { + Console.WriteInterpolated($"{tuple.left} ←→ {tuple.right}"); +}, lines: 1); + +await Console.TypeWrite("Booting systems…", (ConsoleColor.Green, ConsoleColor.Black)); +await Console.TypeWriteLine("Ready.", ConsoleColor.Default); ``` -### Progress Bars - -There are two types of progress bars here, they both are implemented using a class to maintain states. +Always call `Console.ClearNextLines(totalLines, pipe)` once after the last `Overwrite` to erase the region when you are done. -#### IndeterminateProgressBar +### Menus and tables ```csharp -var prg = new IndeterminateProgressBar(); // this setups the internal states -// Then you need to provide either a Task or Task, the progress bar binds to it and runs until the task completes -await prg.RunAsync(task, "Running...", cancellationToken); // There are also overloads without header -// if the task is not started before being passed to the progress bar, it will be started automatically -// It is even better this way to synchronize the runtime of the progress bar with the task -prg.AnimationSequence = IndeterminateProgressBar.Patterns.CarriageReturn; // customize the animation +var choice = Console.Selection("Pick an environment:", new[] { "Dev", "QA", "Prod" }); +var multi = Console.MultiSelection("Services to restart:", new[] { "API", "Worker", "Scheduler" }); +var (area, action) = Console.TreeMenu("Actions", new Dictionary> { + ["Users"] = new[] { "List", "Create", "Disable" }, + ["Jobs"] = new[] { "Queue", "Retry" } +}); + +Console.Table( + headers: new[] { "Name", "Status" }, + columns: new[] { + new[] { "API", "Worker" }, + new[] { "Running", "Stopped" } + } +); ``` -#### ProgressBar +Menus validate user input (throwing `ArgumentException` on invalid selections) and use the padding helpers internally to keep columns aligned. -`ProgressBar` is a more powerful version, but requires a percentage of progress. +### Progress bars ```csharp -// ProgressBar implements IDisposable -var prg = new ProgressBar(); -// then on each time the progress percentage is actually changed, you call Update -Update(percentage, ReadOnlySpan status); -// There are also overloads without header, and percentage can be either int or double (0-100). -// Update re-renders on every call, even if the percentage hasn't changed, so you can refresh the status text. -// Also, you can change some of the visual properties of the progress bar after initialization -// by using the properties of the ProgressBar class -prg.ProgressChar = '■'; // Character to fill the progress bar -prg.ForegroundColor = Color.Red; // Color of the empty part -prg.ProgressColor = Color.Blue; // The color of the filled part -// Pass sameLine: false to render the status on a separate line above the bar. -prg.Update(percentage, "Downloading", sameLine: false); - -// Need a static, one-off render? Use the helper: -ProgressBar.WriteProgressBar(OutputPipe.Error, percentage, Color.Green, '*'); +using var progress = new ProgressBar { + ProgressChar = '■', + ForegroundColor = ConsoleColor.DarkGray, + ProgressColor = ConsoleColor.Green, +}; + +for (int i = 0; i <= 100; i += 5) { + progress.Update(i, $"Downloading chunk {i / 5}"); + await Task.Delay(50); +} + +// Need separate status + bar lines? sameLine: false +progress.Update(42.5, "Syncing", sameLine: false); + +// One-off render without state +ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*'); ``` -##### Multiple Progress Bars with `Overwrite` +`ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. The helper `ProgressBar.WriteProgressBar` keeps the cursor on the same line, which is ideal inside `Console.Overwrite`. -You can combine the static helper with `Overwrite` to redraw several progress bars inside the same console window—perfect for tracking multiple downloads or tasks: +#### Multiple progress bars with tasks + channels ```csharp -var downloads = new[] { "Video.mp4", "Archive.zip" }; -var progress = new double[downloads.Length]; +using System.Linq; +using System.Threading.Channels; -Overwrite(progress, state => { - for (int i = 0; i < downloads.Length; i++) { - Write(OutputPipe.Error, $"Task {i + 1} ({downloads[i]}): "); - ProgressBar.WriteProgressBar(OutputPipe.Error, state[i], Color.Cyan); - NewLine(OutputPipe.Error); +var downloads = new[] { "Video.mp4", "Archive.zip", "Assets.pak" }; +var progress = new double[downloads.Length]; +var updates = Channel.CreateUnbounded<(int index, double percent)>(); + +// Producers push progress updates +var producers = downloads + .Select((name, index) => Task.Run(async () => { + for (int p = 0; p <= 100; p += Random.Shared.Next(5, 15)) { + await updates.Writer.WriteAsync((index, p)); + await Task.Delay(Random.Shared.Next(40, 120)); + } + })) + .ToArray(); + +// Consumer renders stacked bars each time an update arrives +var consumer = Task.Run(async () => { + await foreach (var (index, percent) in updates.Reader.ReadAllAsync()) { + progress[index] = percent; + + Console.Overwrite(progress, state => { + for (int i = 0; i < state.Length; i++) { + Console.WriteInterpolated(OutputPipe.Error, $"Task {i + 1} ({downloads[i]}): "); + ProgressBar.WriteProgressBar(OutputPipe.Error, state[i], ConsoleColor.Cyan); + } + }, lines: downloads.Length, pipe: OutputPipe.Error); } -}, lines: downloads.Length, pipe: OutputPipe.Error); +}); + +await Task.WhenAll(producers); +updates.Writer.Complete(); +await consumer; + +Console.ClearNextLines(downloads.Length, OutputPipe.Error); // ensure no artifacts remain ``` -Update the `progress` array elsewhere and call `Overwrite` again to refresh the stacked bars without leaving artifacts. +Each producer reports progress over the channel, the consumer loops with `ReadAllAsync`, and `Console.Overwrite` redraws the stacked bars on every update. After the consumer completes, clear the region once to remove the progress UI. -### Pipes +### Pipes & writers -`Console` wraps over `System.Console` and uses its `In`, `Out`, and `Error` streams. Since the names of the classes are identical, combining them in usage is somewhat painful as the compiler doesn't know which overloads to use. To aid in most cases, -`Console` exposes those streams as static properties, and you can use them directly. +PrettyConsole keeps the original console streams accessible: -In rare cases, you will need something that there is in `System.Console` but not in `Console`, such as `ReadKey`, or `SetCursorPosition`, some events or otherwise, then you can simply call `System.Console`, this added verbosity is a worthy trade-off. +```csharp +TextWriter @out = PrettyConsoleExtensions.Out; +TextWriter @err = PrettyConsoleExtensions.Error; +TextReader @in = PrettyConsoleExtensions.In; +``` -## Contributing +Use these when you need direct writer access (custom buffering, `WriteWhiteSpaces`, etc.). In cases where you must call raw `System.Console` APIs (e.g., `Console.ReadKey(true)`), do so explicitly—PrettyConsole never hides the built-in console. -This project uses an MIT license, if you want to contribute, you can do so by forking the repository and creating a pull request. +## Contributing -If you have feature requests or bug reports, please create an issue. +Contributions are welcome! Fork the repo, create a branch, and open a pull request. Bug reports and feature requests are tracked through GitHub issues. ## Contact -For bug reports, feature requests or offers of support/sponsorship contact +For bug reports, feature requests, or sponsorship inquiries reach out at . > This project is proudly made in Israel 🇮🇱 for the benefit of mankind. From 472d75fbaa9d9655d4293948da1ca7ec6a46ac81 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 18:55:49 +0200 Subject: [PATCH 17/75] Added size limiting to progress bar --- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 21 +++++++++++++++- .../MultiProgressBarLeftAlignedTest.cs | 24 +++++++++++++++++++ PrettyConsole.Tests/Program.cs | 1 + PrettyConsole/ProgressBar.cs | 20 ++++++++++++---- README.md | 4 ++-- 5 files changed, 62 insertions(+), 8 deletions(-) create mode 100755 PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index dff23e4..3356e1e 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -83,6 +83,25 @@ public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { } } + [Fact] + public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { + Utilities.SkipIfNoInteractiveConsole(); + + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var outWriter); + + 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]); + } finally { + Out = originalOut; + } + } + [Fact] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { Utilities.SkipIfNoInteractiveConsole(); @@ -103,4 +122,4 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() Assert.Equal(42, result); Assert.NotEqual(string.Empty, errorWriter.ToString()); } -} \ No newline at end of file +} diff --git a/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs new file mode 100755 index 0000000..053aa78 --- /dev/null +++ b/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs @@ -0,0 +1,24 @@ +namespace PrettyConsole.Tests.Features; + +public sealed class MultiProgressBarLeftAlignedTest : IPrettyConsoleTest { + public string FeatureName => "MultiProgressBar"; + + public async ValueTask Implementation() { + const int count = 333; + var currentLine = Console.GetCurrentLine(); + for (int i = 1; i <= count; i++) { + double percentage = 100 * (double)i / count; + + Console.Overwrite((int)percentage, p => { + ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta, maxLineWidth: 50); + Console.WriteLineInterpolated(OutputPipe.Error, $" - Task {1}"); + ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta, maxLineWidth: 50); + Console.WriteInterpolated(OutputPipe.Error, $" - Task {2}"); + }, 2); + + await Task.Delay(15); + } + Console.ClearNextLines(2, OutputPipe.Error); + Console.WriteLineInterpolated(OutputPipe.Error, $"Done"); + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 28b5d53..0e47621 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -19,6 +19,7 @@ new ProgressBarDefaultTest(), new ProgressBarMultiLineTest(), new MultiProgressBarTest(), + new MultiProgressBarLeftAlignedTest(), }; foreach (var test in tests) { diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index cbbad07..32dbf73 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -104,8 +104,10 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr /// The percentage value (0-100) representing the progress. /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. + /// Optional total line length (including brackets and percentage). When provided, the rendered output will not exceed this width unless the decorations already require more characters. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar); + public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) + => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar, maxLineWidth); /// /// Writes a single progress bar segment without tracking state. @@ -114,17 +116,25 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr /// The percentage value (0-100) representing the progress. /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. + /// Optional total line length (including brackets and percentage). When provided, the rendered output will not exceed this width unless the decorations already require more characters. [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] - public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { + public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) { Console.ResetColor(); int p = Math.Clamp(percentage, 0, 100); - int bufferWidth = PrettyConsoleExtensions.GetWidthOrDefault() - Console.CursorLeft; + int bufferWidth = Math.Max(0, PrettyConsoleExtensions.GetWidthOrDefault() - Console.CursorLeft); const int bracketsAndSpacing = 3; // '[' + ']' + ' ' const int percentageWidth = 3; // numeric portion width const int percentSymbolLength = 1; // '%' character - int barLength = Math.Max(0, bufferWidth - (bracketsAndSpacing + percentageWidth + percentSymbolLength)); + const int decorationWidth = bracketsAndSpacing + percentageWidth + percentSymbolLength; + + int constrainedWidth = bufferWidth; + if (maxLineWidth.HasValue && maxLineWidth.Value > 0) { + constrainedWidth = Math.Min(bufferWidth, Math.Max(maxLineWidth.Value, decorationWidth)); + } + + int barLength = Math.Max(0, constrainedWidth - decorationWidth); var writer = PrettyConsoleExtensions.GetWriter(pipe); Console.Write('[', pipe); @@ -148,4 +158,4 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo Console.WriteInterpolated(pipe, $"] {p,3}%"); } -} \ No newline at end of file +} diff --git a/README.md b/README.md index a9df94a..e8c8b49 100755 --- a/README.md +++ b/README.md @@ -183,10 +183,10 @@ for (int i = 0; i <= 100; i += 5) { progress.Update(42.5, "Syncing", sameLine: false); // One-off render without state -ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*'); +ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', maxLineWidth: 32); ``` -`ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. The helper `ProgressBar.WriteProgressBar` keeps the cursor on the same line, which is ideal inside `Console.Overwrite`. +`ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. 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. #### Multiple progress bars with tasks + channels From 8d84562a3d168e995109249a045c5cb2b395bf32 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 20:01:16 +0200 Subject: [PATCH 18/75] Change "hr" format and add alignment overload --- .../PrettyConsoleInterpolatedStringHandler.cs | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index dc3a3e2..575609a 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -114,35 +114,25 @@ public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor back /// /// /// - public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) { + public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) + => AppendFormatted(timeSpan, alignment: 0, format); + + /// + /// Append timeSpan with optional alignment support. + /// + /// + /// + /// + public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? format = null) { if (format != "hr") { - AppendSpanFormattable(timeSpan, 0, format); + AppendSpanFormattable(timeSpan, alignment, format); return; } - if (timeSpan.TotalSeconds < 1) { - AppendSpanFormattable(timeSpan.Milliseconds, 0, null); - AppendSpan("ms", 0); - } else if (timeSpan.TotalSeconds < 60) { - AppendSpanFormattable(timeSpan.Seconds, 0, "00"); - AppendFormatted(':'); - AppendSpanFormattable(timeSpan.Milliseconds, 0, "00"); - AppendFormatted('s'); - } else if (timeSpan.TotalSeconds < 3600) { - AppendSpanFormattable(timeSpan.Minutes, 0, "00"); - AppendFormatted(':'); - AppendSpanFormattable(timeSpan.Seconds, 0, "00"); - AppendFormatted('m'); - } else if (timeSpan.TotalSeconds < 86400) { - AppendSpanFormattable(timeSpan.Hours, 0, "00"); - AppendFormatted(':'); - AppendSpanFormattable(timeSpan.Minutes, 0, "00"); - AppendSpan("hr", 0); - } else { - AppendSpanFormattable(timeSpan.Days, 0, "00"); - AppendFormatted(':'); - AppendSpanFormattable(timeSpan.Hours, 0, "00"); - AppendSpan("d", 0); - } + AppendSpanFormattable((int)timeSpan.TotalHours, alignment, null); + AppendSpanFormattable(':', alignment, null); + AppendSpanFormattable(timeSpan.Minutes, alignment, null); + AppendSpanFormattable(':', alignment, null); + AppendSpanFormattable(timeSpan.Seconds, alignment, null); } /// @@ -268,4 +258,4 @@ private readonly void WritePadding(int count) { _writer.WriteWhiteSpaces(count); } } -#pragma warning restore CA1822 // Mark members as static \ No newline at end of file +#pragma warning restore CA1822 // Mark members as static From 48995c1e115dcf9df96b0efc09686b667bfbe972 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 20:07:23 +0200 Subject: [PATCH 19/75] fix hr alignment --- .../PrettyConsoleInterpolatedStringHandler.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 575609a..973d030 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -128,11 +128,21 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f AppendSpanFormattable(timeSpan, alignment, format); return; } - AppendSpanFormattable((int)timeSpan.TotalHours, alignment, null); - AppendSpanFormattable(':', alignment, null); - AppendSpanFormattable(timeSpan.Minutes, alignment, null); - AppendSpanFormattable(':', alignment, null); - AppendSpanFormattable(timeSpan.Seconds, alignment, null); + + using var owner = BufferPool.Shared.Rent(out var buffer); + int upperBound = BufferPool.ListStartingSize; + + while (true) { + buffer.EnsureCapacity(upperBound); + CollectionsMarshal.SetCount(buffer, upperBound); + var span = CollectionsMarshal.AsSpan(buffer); + if (span.TryWrite($"{(int)timeSpan.TotalHours}:{timeSpan.Minutes}:{timeSpan.Seconds}", out int written)) { + AppendSpan(span.Slice(0, written), alignment); + break; + } + + upperBound *= 2; + } } /// From 64f8cb1034256f3451236f7ca9241e53abbe0f6d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 20:08:54 +0200 Subject: [PATCH 20/75] - --- PrettyConsole.Tests/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 0e47621..7cce6aa 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -37,4 +37,4 @@ static void Measure(string label, Action action) { Console.WriteLineInterpolated($"{label} - allocated {after - before} bytes"); Console.NewLine(); } -#pragma warning restore CS8321 // Local function is declared but never used \ No newline at end of file +#pragma warning restore CS8321 // Local function is declared but never used From 66259f605d5f70c56c2e68d5a6274c01500775be Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 20:12:35 +0200 Subject: [PATCH 21/75] Constant output length for timeSpan hr --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 973d030..102bcb1 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -136,7 +136,7 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f buffer.EnsureCapacity(upperBound); CollectionsMarshal.SetCount(buffer, upperBound); var span = CollectionsMarshal.AsSpan(buffer); - if (span.TryWrite($"{(int)timeSpan.TotalHours}:{timeSpan.Minutes}:{timeSpan.Seconds}", out int written)) { + if (span.TryWrite($"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}", out int written)) { AppendSpan(span.Slice(0, written), alignment); break; } From 3b3b87e925da89175780f10243078444333157a0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 20:17:33 +0200 Subject: [PATCH 22/75] Update hr description in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8c8b49..42cef2d 100755 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ if (!Console.TryReadLine(out int choice, $"Pick option {ConsoleColor.Cyan / Cons #### Formatting & alignment helpers -- **`TimeSpan :hr` format** — the interpolated string handler understands the custom `:hr` specifier. It renders the span using the most appropriate unit (e.g., `950ms`, `12s`, `03m`, `02h`, `1d`) without allocating temporaries: +- **`TimeSpan :hr` format** — the interpolated string handler understands the custom `:hr` specifier. It renders elapsed time as `totalHours:minutes:seconds` (e.g., `00:05:32`, `27:12:03`, `123:00:00`) without allocating, and the hour component keeps growing past 24 so long-running tasks stay accurate: ```csharp var elapsed = stopwatch.Elapsed; From aa061d9d75e3d9dc73de90f7358e17ca62f70692 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 13 Nov 2025 20:22:52 +0200 Subject: [PATCH 23/75] Simplify code and remove end padding --- PrettyConsole/IndeterminateProgressBar.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 4ed3601..61e3845 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -23,9 +23,6 @@ public class IndeterminateProgressBar { /// public ReadOnlyCollection AnimationSequence { get; set; } = Patterns.Twirl; - // A length of whitespace padding to the end - private const int PaddingLength = 10; - /// /// Gets or sets the foreground color of the progress bar. /// @@ -115,8 +112,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } if (header.Length > 0) { - PrettyConsoleExtensions.Error.WriteWhiteSpaces(1); - PrettyConsoleExtensions.Error.Write(header.AsSpan()); + Console.WriteInterpolated(OutputPipe.Error, $" {header}"); } if (DisplayElapsedTime) { @@ -124,8 +120,6 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d Console.WriteInterpolated(OutputPipe.Error, $" [Elapsed: {elapsed:hr}]"); } - PrettyConsoleExtensions.Error.WriteWhiteSpaces(PaddingLength); - // Compute sleep to maintain UpdateRate between frame starts var now = Stopwatch.GetTimestamp(); nextTick += updateRateAsTicks; From 6938025da49d224ff9a2ceac5bc3e99b1087d1ce Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 08:07:39 +0200 Subject: [PATCH 24/75] Forward format for boxed arguments --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 102bcb1..0122bf8 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -196,7 +196,7 @@ public readonly void AppendFormatted(object? value, int alignment = 0, string? f } if (value is ISpanFormattable spanFormattable) { - AppendSpanFormattable(spanFormattable, alignment, null); + AppendSpanFormattable(spanFormattable, alignment, format); return; } From 47569dd5fca5b5edb2dee4a0c43c7a8b9a87e032 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 08:07:48 +0200 Subject: [PATCH 25/75] Formatting --- PrettyConsole.Tests/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 7cce6aa..55a56e8 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -28,7 +28,6 @@ } #pragma warning disable CS8321 // Local function is declared but never used - static void Measure(string label, Action action) { long before = GC.GetAllocatedBytesForCurrentThread(); action(); From 825ad55b63649014b3d9060c93892361dc5afd0d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 08:34:03 +0200 Subject: [PATCH 26/75] Added benchmarks --- Benchmarks/Benchmarks.csproj | 18 +++++++++ Benchmarks/Config.cs | 60 +++++++++++++++++++++++++++++ Benchmarks/Program.cs | 6 +++ Benchmarks/StyledOutputBenchmark.cs | 48 +++++++++++++++++++++++ PrettyConsole.slnx | 1 + 5 files changed, 133 insertions(+) create mode 100644 Benchmarks/Benchmarks.csproj create mode 100644 Benchmarks/Config.cs create mode 100644 Benchmarks/Program.cs create mode 100644 Benchmarks/StyledOutputBenchmark.cs diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj new file mode 100644 index 0000000..9eb2182 --- /dev/null +++ b/Benchmarks/Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs new file mode 100644 index 0000000..c9cb1a1 --- /dev/null +++ b/Benchmarks/Config.cs @@ -0,0 +1,60 @@ +using System.Collections.Immutable; + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Order; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +using Perfolizer.Mathematics.OutlierDetection; + +namespace Benchmarks; + +public class Config : ManualConfig { + public Config() { + SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); + AddDiagnoser(MemoryDiagnoser.Default); + AddJob(Job.MediumRun.WithOutlierMode(OutlierMode.RemoveAll)); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(RankColumn.Arabic); + HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); + WithOrderer(new GroupByTypeOrderer()); + WithOptions(ConfigOptions.JoinSummary); + WithOptions(ConfigOptions.StopOnFirstError); + WithOptions(ConfigOptions.DisableLogFile); + AddLogger(ConsoleLogger.Default); + } +} + +internal sealed class GroupByTypeOrderer : IOrderer { + // Keep execution order as-declared (you can customize if you want) + public IEnumerable GetExecutionOrder( + ImmutableArray benchmarksCase, + IEnumerable? order = null) + => benchmarksCase; + + // Sort rows in the summary: first by Type, then Method, then Params + public IEnumerable GetSummaryOrder( + ImmutableArray cases, Summary summary) => + cases.OrderBy(c => c.Descriptor.Type.FullName) + .ThenBy(c => c.Parameters.DisplayInfo); + + // We don’t use highlight groups + public string? GetHighlightGroupKey(BenchmarkCase benchmarkCase) => null; + + // Tell BDN how to “group” rows in a joined summary (the section separator) + public string GetLogicalGroupKey( + ImmutableArray all, BenchmarkCase benchmarkCase) + => benchmarkCase.Descriptor.Type.FullName!; + + // Order the groups themselves (by class name) + public IEnumerable> GetLogicalGroupOrder( + IEnumerable> logicalGroups, + IEnumerable? order = null) + => logicalGroups.OrderBy(g => g.Key); + + public bool SeparateLogicalGroups => true; +} \ No newline at end of file diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs new file mode 100644 index 0000000..5dcbf51 --- /dev/null +++ b/Benchmarks/Program.cs @@ -0,0 +1,6 @@ +using BenchmarkDotNet.Running; + +using Benchmarks; + +var customConfig = new Config(); +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, customConfig); \ No newline at end of file diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs new file mode 100644 index 0000000..7b31fb7 --- /dev/null +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -0,0 +1,48 @@ +using System.Runtime.CompilerServices; + +using BenchmarkDotNet.Attributes; + +using Spectre.Console; + +using PrettyConsole; +using static System.ConsoleColor; + +namespace Benchmarks; + +/// +/// Runs a benchmark printing the following output: +/// Hello {Green}John{ResetColor}, status = {Cyan}{Percentage}{Reset}%, Elapsed = {Yellow}{Elapsed:c}{Reset} +/// +public class StyledOutputBenchmarks { + private static readonly TimeSpan Elapsed = new(1, 25, 31); + private const double Percentage = 57.91; + + [Benchmark(Baseline = true)] + [MethodImpl(MethodImplOptions.NoInlining)] + public void SystemConsole() { + Console.Write("Hello "); + Console.ForegroundColor = Green; + Console.Write("John"); + Console.ResetColor(); + Console.Write(", status = "); + Console.ForegroundColor = Cyan; + Console.Write(Percentage); + Console.ResetColor(); + Console.Write("%, elapsed = "); + Console.ForegroundColor = Yellow; + Console.WriteLine("{0:c}", Elapsed); + Console.ResetColor(); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void SpectreConsole() { + AnsiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void PrettyConsole() { + Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.Default}, status = {Cyan}{Percentage}{ConsoleColor.Default}%, elapsed = {Yellow}{Elapsed:hr}"); + } +} \ No newline at end of file diff --git a/PrettyConsole.slnx b/PrettyConsole.slnx index 61c5abd..91518fd 100644 --- a/PrettyConsole.slnx +++ b/PrettyConsole.slnx @@ -1,4 +1,5 @@ + From 7071703d90e70a228039b791f195b632f8af036d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 09:21:54 +0200 Subject: [PATCH 27/75] Added benchmark results to git --- .gitignore | 2 +- ...ks.StyledOutputBenchmarks-report-github.md | 17 ++++++++++ Benchmarks/Config.cs | 20 +++++++----- Benchmarks/StyledOutputBenchmark.cs | 31 ++++++++++--------- 4 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md diff --git a/.gitignore b/.gitignore index 62d8b6d..58be9a1 100755 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,7 @@ nunit-*.xml dlldata.c # Benchmark Results -BenchmarkDotNet.Artifacts/ +# BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json diff --git a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md new file mode 100644 index 0000000..d9ccd1e --- /dev/null +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -0,0 +1,17 @@ +``` + +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 + MediumRun : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a + +Job=MediumRun OutlierMode=RemoveAll IterationCount=50 +IterationTime=100ms LaunchCount=1 WarmupCount=10 + +``` +| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | +|--------------- |---------:|------:|-------:|----------:|------------:| +| PrettyConsole | 7.835 μs | 1.00 | - | - | NA | +| SpectreConsole | 7.392 μs | 0.94 | 2.0400 | 17840 B | NA | +| SystemConsole | 7.400 μs | 0.95 | - | 56 B | NA | diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index c9cb1a1..f1de942 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -3,29 +3,35 @@ using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Order; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using Perfolizer.Horology; + using Perfolizer.Mathematics.OutlierDetection; namespace Benchmarks; public class Config : ManualConfig { public Config() { - SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); AddDiagnoser(MemoryDiagnoser.Default); - AddJob(Job.MediumRun.WithOutlierMode(OutlierMode.RemoveAll)); + AddJob(Job.MediumRun + .WithOutlierMode(OutlierMode.RemoveAll) + .WithLaunchCount(1) + .WithWarmupCount(10) + .WithIterationCount(50) + .WithIterationTime(TimeInterval.FromMilliseconds(100))); AddColumnProvider(DefaultColumnProviders.Instance); - AddColumn(RankColumn.Arabic); - HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); + // AddColumn(RankColumn.Arabic); + HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); WithOrderer(new GroupByTypeOrderer()); WithOptions(ConfigOptions.JoinSummary); WithOptions(ConfigOptions.StopOnFirstError); WithOptions(ConfigOptions.DisableLogFile); - AddLogger(ConsoleLogger.Default); + AddExporter(MarkdownExporter.GitHub); } } @@ -57,4 +63,4 @@ public IEnumerable> GetLogicalGroupOrder( => logicalGroups.OrderBy(g => g.Key); public bool SeparateLogicalGroups => true; -} \ No newline at end of file +} diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index 7b31fb7..cfcaeeb 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -18,8 +18,22 @@ public class StyledOutputBenchmarks { private const double Percentage = 57.91; [Benchmark(Baseline = true)] - [MethodImpl(MethodImplOptions.NoInlining)] - public void SystemConsole() { + // [MethodImpl(MethodImplOptions.NoInlining)] + public int PrettyConsole() { + Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.DefaultForeground}, status = {Cyan}{Percentage}{ConsoleColor.DefaultForeground}%, elapsed = {Yellow}{Elapsed:c}"); + return int.MaxValue; + } + + [Benchmark] + // [MethodImpl(MethodImplOptions.NoInlining)] + public int SpectreConsole() { + AnsiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); + return int.MaxValue; + } + + [Benchmark] + // [MethodImpl(MethodImplOptions.NoInlining)] + public int SystemConsole() { Console.Write("Hello "); Console.ForegroundColor = Green; Console.Write("John"); @@ -32,17 +46,6 @@ public void SystemConsole() { Console.ForegroundColor = Yellow; Console.WriteLine("{0:c}", Elapsed); Console.ResetColor(); - } - - [Benchmark] - [MethodImpl(MethodImplOptions.NoInlining)] - public void SpectreConsole() { - AnsiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); - } - - [Benchmark] - [MethodImpl(MethodImplOptions.NoInlining)] - public void PrettyConsole() { - Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.Default}, status = {Cyan}{Percentage}{ConsoleColor.Default}%, elapsed = {Yellow}{Elapsed:hr}"); + return int.MaxValue; } } \ No newline at end of file From 5a080b327adf4d0067577f712b5497209e48a6f0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 14:21:43 +0200 Subject: [PATCH 28/75] Added initial implementation of markup --- PrettyConsole/Markup.cs | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 PrettyConsole/Markup.cs diff --git a/PrettyConsole/Markup.cs b/PrettyConsole/Markup.cs new file mode 100644 index 0000000..654f60b --- /dev/null +++ b/PrettyConsole/Markup.cs @@ -0,0 +1,47 @@ +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; + + /// + /// Enables underlined text. + /// + public static readonly string Underline = string.Empty; + + /// + /// Disables underlined text. + /// + public static readonly string ResetUnderline = string.Empty; + + /// + /// Enables bold text. + /// + public static readonly string Bold = string.Empty; + + /// + /// Disables bold text. + /// + public static readonly string ResetBold = string.Empty; + + static Markup() { + Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; + if (Enabled) { + Reset = "\u001b[0m"; + Underline = "\u001b[4m"; + ResetUnderline = "\u001b[24m"; + Bold = "\u001b[1m"; + ResetBold = "\u001b[22m"; + } + } +} From 26b9484444de60b72070f0625e0c17363e8b85a1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 14:28:08 +0200 Subject: [PATCH 29/75] Added more options --- PrettyConsole/Markup.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/PrettyConsole/Markup.cs b/PrettyConsole/Markup.cs index 654f60b..f248e08 100644 --- a/PrettyConsole/Markup.cs +++ b/PrettyConsole/Markup.cs @@ -34,6 +34,26 @@ public static class Markup { /// public static readonly string ResetBold = string.Empty; + /// + /// Enables italic text. + /// + public static readonly string Italic = string.Empty; + + /// + /// Disables italic text. + /// + public static readonly string ResetItalic = string.Empty; + + /// + /// Enables strikethrough text. + /// + public static readonly string Strikethrough = string.Empty; + + /// + /// Disables strikethrough text. + /// + public static readonly string ResetStrikethrough = string.Empty; + static Markup() { Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; if (Enabled) { @@ -42,6 +62,10 @@ static Markup() { ResetUnderline = "\u001b[24m"; Bold = "\u001b[1m"; ResetBold = "\u001b[22m"; + Italic = "\u001b[3m"; + ResetItalic = "\u001b[23m"; + Strikethrough = "\u001b[9m"; + ResetStrikethrough = "\u001b[29m"; } } } From fc4481d04806e81c90f29c0f608038146115ec48 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 15:05:20 +0200 Subject: [PATCH 30/75] Added full support for markup feature --- AGENTS.md | 2 ++ README.md | 10 ++++++++++ Versions.md | 1 + 3 files changed, 13 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index a244b9e..0c526d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,8 @@ High-level architecture and key concepts - `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. - Coloring model - `ConsoleColor` now exposes `DefaultForeground`, `DefaultBackground`, and `Default` tuple properties plus `/` operator overloads so you can inline foreground/background tuples (`$"{ConsoleColor.Red / ConsoleColor.White}Error"`). These tuples play nicely with the interpolated string handler and keep color resets allocation-free. +- Markup decorations + - The `Markup` static class exposes ANSI sequences for underline, bold, italic, and strikethrough. Fields expand to escape codes only when output/error aren’t redirected; otherwise they collapse to empty strings so callers can safely interpolate them without extra checks. - Write APIs - `WriteInterpolated`/`WriteLineInterpolated` host the interpolated-string handler; `Write`/`WriteLine` overloads target `ISpanFormattable` values (including `ref struct`s) and raw `ReadOnlySpan` spans with optional foreground/background overrides. Implementations rent buffers via `BufferPool` to avoid allocation spikes and always reset colors. - TextWriter helpers diff --git a/README.md b/README.md index 42cef2d..c7facc3 100755 --- a/README.md +++ b/README.md @@ -43,6 +43,16 @@ if (!Console.TryReadLine(out int choice, $"Pick option {ConsoleColor.Cyan / Cons `ConsoleColor.DefaultForeground`, `ConsoleColor.DefaultBackground`, and the `/` operator overload make it easy to compose foreground/background tuples inline (`ConsoleColor.Red / ConsoleColor.White`). +#### Inline decorations via `Markup` + +When ANSI escape sequences are safe to emit (`Console.IsOutputRedirected`/`IsErrorRedirected` are both `false`), the `Markup` helper exposes ready-to-use toggles for underline, bold, italic, and strikethrough: + +```csharp +Console.WriteLineInterpolated($"{Markup.Bold}Build{Markup.ResetBold} {Markup.Underline}completed{Markup.ResetUnderline} in {elapsed:hr}"); +``` + +All fields collapse to `string.Empty` when markup is disabled, so the same call sites continue to work when output is redirected or the terminal ignores decorations. Use `Markup.Reset` if you want to reset every decoration at once. + #### Formatting & alignment helpers - **`TimeSpan :hr` format** — the interpolated string handler understands the custom `:hr` specifier. It renders elapsed time as `totalHours:minutes:seconds` (e.g., `00:05:32`, `27:12:03`, `123:00:00`) without allocating, and the hour component keeps growing past 24 so long-running tasks stay accurate: diff --git a/Versions.md b/Versions.md index 12a63d6..48d4b9d 100755 --- a/Versions.md +++ b/Versions.md @@ -26,6 +26,7 @@ This version contains a lot of breaking changes, but they were necessary to trim ### Added - `TextWriter` which is the object backing `Console.Out` and `Console.Error` now has a static extension `WriteWhiteSpaces(int)`, which can be used to write paddings and whatever else without any allocations. It was previously an internal method but I chose to expose it for all of you. +- `Markup` static class provides ANSI escape-sequence toggles (underline, bold, italic, strikethrough) that automatically collapse to empty strings when output/error are redirected, so callers can opt into inline decorations without additional checks. ## v4.1.0 From 7d5f50105e0c87e927288216fe7a6f90ade15faa Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 16:41:18 +0200 Subject: [PATCH 31/75] - --- PrettyConsole/PrettyConsole.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 0eba229..59c3709 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -2,6 +2,7 @@ net10.0 latest + true true true From 522982a163b703984f2a3c9a854d37cb0e837d5b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 16:42:12 +0200 Subject: [PATCH 32/75] Use ArrayPool to remove BufferPool overhead --- .../PrettyConsoleInterpolatedStringHandler.cs | 132 +++++++++--------- 1 file changed, 64 insertions(+), 68 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 0122bf8..2a0b7ae 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -1,5 +1,6 @@ +using System.Buffers; + using static System.Console; -using System.Runtime.InteropServices; namespace PrettyConsole; @@ -51,9 +52,7 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo /// Appends a literal segment supplied by the compiler. /// public readonly void AppendLiteral(string value) { - if (!string.IsNullOrEmpty(value)) { - _writer.Write(value); - } + _writer.Write(value); } /// @@ -81,11 +80,6 @@ public readonly void AppendFormatted(scoped ReadOnlySpan value, int alignm /// Character to write. /// Optional alignment as provided by the interpolation. public readonly void AppendFormatted(char value, int alignment = 0) { - if (alignment == 0) { - _writer.Write(value); - return; - } - Span buffer = [value]; AppendSpan(buffer, alignment); } @@ -129,19 +123,25 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f return; } - using var owner = BufferPool.Shared.Rent(out var buffer); - int upperBound = BufferPool.ListStartingSize; + Span buffer = stackalloc char[128]; + if (buffer.TryWrite($"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}", out int written)) { + AppendSpan(buffer.Slice(0, written), alignment); + return; + } + + int lowerBound = 4096; + var pool = ArrayPool.Shared; while (true) { - buffer.EnsureCapacity(upperBound); - CollectionsMarshal.SetCount(buffer, upperBound); - var span = CollectionsMarshal.AsSpan(buffer); - if (span.TryWrite($"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}", out int written)) { - AppendSpan(span.Slice(0, written), alignment); + var array = pool.Rent(lowerBound); + buffer = new Span(array); + if (buffer.TryWrite($"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}", out written)) { + AppendSpan(buffer.Slice(0, written), alignment); + pool.Return(array); break; } - - upperBound *= 2; + pool.Return(array); + lowerBound *= 2; } } @@ -180,64 +180,69 @@ public readonly void AppendFormatted(T value, int alignment, string? format) /// Optional alignment as provided by the interpolation. /// Optional format specifier. public readonly void AppendFormatted(object? value, int alignment = 0, string? format = null) { - if (value is null) { - AppendSpan(ReadOnlySpan.Empty, alignment); - return; + switch (value) { + case null: { + AppendSpan(ReadOnlySpan.Empty, alignment); + break; + } + case ConsoleColor consoleColor: { + AppendFormatted(consoleColor); + break; + } + case ISpanFormattable spanFormattable: { + AppendSpanFormattable(spanFormattable, alignment, format); + break; + } + case IFormattable formattable: { + AppendString(formattable.ToString(format, _provider), alignment); + break; + } + case string str: { + AppendString(str, alignment); + break; + } + default: { + AppendString(value.ToString(), alignment); + break; + } } - - if (value is ConsoleColor consoleColor) { - AppendFormatted(consoleColor); - return; - } - - if (value is string str) { - AppendString(str, alignment); - return; - } - - if (value is ISpanFormattable spanFormattable) { - AppendSpanFormattable(spanFormattable, alignment, format); - return; - } - - if (value is IFormattable formattable) { - AppendString(formattable.ToString(format, _provider), alignment); - return; - } - - AppendString(value.ToString(), alignment); } private readonly void AppendSpanFormattable(T value, int alignment, string? format) where T : ISpanFormattable { - using var owner = BufferPool.Shared.Rent(out var buffer); - int upperBound = BufferPool.ListStartingSize; - var formatSpan = format is null ? ReadOnlySpan.Empty : format.AsSpan(); + 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) { - buffer.EnsureCapacity(upperBound); - CollectionsMarshal.SetCount(buffer, upperBound); - var span = CollectionsMarshal.AsSpan(buffer); - if (value.TryFormat(span, out int charsWritten, formatSpan, _provider)) { - AppendSpan(span.Slice(0, charsWritten), alignment); + var array = pool.Rent(lowerBound); + buffer = new Span(array); + if (value.TryFormat(buffer, out charsWritten, formatSpan, _provider)) { + AppendSpan(buffer.Slice(0, charsWritten), alignment); + pool.Return(array); break; } - - upperBound *= 2; + pool.Return(array); + lowerBound *= 2; } } private readonly void AppendString(string? value, int alignment) { - if (string.IsNullOrEmpty(value)) { - AppendSpan(ReadOnlySpan.Empty, alignment); - return; - } - + // AppendSpan handles null and empty spans AppendSpan(value.AsSpan(), alignment); } private readonly void AppendSpan(scoped ReadOnlySpan span, int alignment) { - if (alignment != 0) { + if (span.IsEmpty) return; + else if (alignment == 0) { + _writer.Write(span); + } else { bool leftAlign = alignment < 0; int width = Math.Abs(alignment); int padding = width - span.Length; @@ -252,19 +257,10 @@ private readonly void AppendSpan(scoped ReadOnlySpan span, int alignment) if (padding > 0 && leftAlign) { WritePadding(padding); } - return; - } - - if (!span.IsEmpty) { - _writer.Write(span); } } private readonly void WritePadding(int count) { - if (count <= 0) { - return; - } - _writer.WriteWhiteSpaces(count); } } From d6509e78c75409f23b676690b8b2a8dcd5b30c1b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 16:52:27 +0200 Subject: [PATCH 33/75] Allow empty span alignment --- .../PrettyConsoleInterpolatedStringHandler.cs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 2a0b7ae..3f59d90 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -239,24 +239,26 @@ private readonly void AppendString(string? value, int alignment) { } private readonly void AppendSpan(scoped ReadOnlySpan span, int alignment) { - if (span.IsEmpty) return; - else if (alignment == 0) { - _writer.Write(span); - } else { - bool leftAlign = alignment < 0; - int width = Math.Abs(alignment); - int padding = width - span.Length; - if (padding > 0 && !leftAlign) { - WritePadding(padding); - } - + if (alignment == 0) { if (!span.IsEmpty) { _writer.Write(span); } + return; + } - if (padding > 0 && leftAlign) { - WritePadding(padding); - } + bool leftAlign = alignment < 0; + int width = Math.Abs(alignment); + int padding = width - span.Length; + if (padding > 0 && !leftAlign) { + WritePadding(padding); + } + + if (!span.IsEmpty) { + _writer.Write(span); + } + + if (padding > 0 && leftAlign) { + WritePadding(padding); } } From 7d3ccd356051d8ab957bd81c98daf34de410bdfb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 17:25:46 +0200 Subject: [PATCH 34/75] Use arraypool --- PrettyConsole.Tests.Unit/BufferPoolTests.cs | 46 ------- PrettyConsole/BufferPool.cs | 112 ------------------ PrettyConsole/MenuExtensions.cs | 48 ++++---- .../PrettyConsoleInterpolatedStringHandler.cs | 26 ++-- PrettyConsole/WriteExtensions.cs | 25 ++-- 5 files changed, 56 insertions(+), 201 deletions(-) delete mode 100644 PrettyConsole.Tests.Unit/BufferPoolTests.cs delete mode 100644 PrettyConsole/BufferPool.cs diff --git a/PrettyConsole.Tests.Unit/BufferPoolTests.cs b/PrettyConsole.Tests.Unit/BufferPoolTests.cs deleted file mode 100644 index 48034cf..0000000 --- a/PrettyConsole.Tests.Unit/BufferPoolTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class BufferPoolTests { - [Fact] - public void Rent_ReturnsFastItemAfterReturn() { - using var pool = CreatePool(); - - var owner = pool.Rent(out var firstBuffer); - firstBuffer.Add('x'); - owner.Dispose(); - - using var secondOwner = pool.Rent(out var reusedBuffer); - - Assert.Same(firstBuffer, reusedBuffer); - Assert.Empty(reusedBuffer); - } - - [Fact] - public void Return_DropsOversizedLists() { - using var pool = CreatePool(); - - List oversized; - using (var owner = pool.Rent(out var buffer)) { - oversized = buffer; - buffer.AddRange(new string('x', BufferPool.ListMaxSize + 1)); - } - - using var nextOwner = pool.Rent(out var nextBuffer); - - Assert.NotSame(oversized, nextBuffer); - Assert.Equal(BufferPool.ListStartingSize, nextBuffer.Capacity); - } - - [Fact] - public void Value_ThrowsAfterDispose() { - using var pool = CreatePool(); - - var owner = pool.Rent(out _); - owner.Dispose(); - - Assert.Throws(() => _ = owner.Value); - } - - private static BufferPool CreatePool() - => (BufferPool)Activator.CreateInstance(typeof(BufferPool), nonPublic: true)!; -} \ No newline at end of file diff --git a/PrettyConsole/BufferPool.cs b/PrettyConsole/BufferPool.cs deleted file mode 100644 index e5c954a..0000000 --- a/PrettyConsole/BufferPool.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Diagnostics; -using System.Threading.Channels; - -namespace PrettyConsole; - -internal sealed class BufferPool : IDisposable { - private bool _disposed; - private readonly Channel> _channel; - private readonly ThreadLocal?> _fastItem; - - internal const int ListStartingSize = 256; - internal const int ListMaxSize = 4096; - - public static readonly BufferPool Shared = new(); - - private BufferPool() { - _channel = Channel.CreateBounded>(new BoundedChannelOptions(Environment.ProcessorCount * 2) { - SingleWriter = false, - SingleReader = false, - FullMode = BoundedChannelFullMode.DropWrite - }); - _fastItem = new(() => null, trackAllValues: false); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public PooledObjectOwner Rent(out List value) { - ObjectDisposedException.ThrowIf(_disposed, this); - var item = _fastItem.Value; - if (item is not null) { - _fastItem.Value = null; - value = item; - return new(this, value); - } - if (_channel.Reader.TryRead(out item)) { - value = item; - return new(this, value); - } - value = new List(ListStartingSize); - return new(this, value); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void Return(List item) { - ObjectDisposedException.ThrowIf(_disposed, this); - if (!AcceptAndClear(item)) { - return; - } - if (_fastItem.Value is null) { - _fastItem.Value = item; - return; - } - _channel.Writer.TryWrite(item); - } - - /// - /// Checks if should be accepted back to the pool, and clears it if it should. - /// - /// - /// - private static bool AcceptAndClear(List item) { - if (item.Count > ListMaxSize) { - return false; - } - item.Clear(); - return true; - } - - public void Dispose() { - if (_disposed) { - return; - } - - while (_channel.Reader.TryRead(out var it)) { - (it as IDisposable)?.Dispose(); - } - _fastItem?.Dispose(); - - _disposed = true; - GC.SuppressFinalize(this); - } - - [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal struct PooledObjectOwner : IDisposable { - private BufferPool? _pool; - private List? _value; - public readonly List Value - => _value ?? throw new InvalidOperationException("The buffer was already returned to the pool."); - - internal PooledObjectOwner(BufferPool pool, List value) { - _pool = pool; - _value = value; - } - - public void Dispose() { - var pool = _pool; - var value = _value; - if (pool is null || value is null) return; - - _pool = null; - _value = null; - - pool.Return(value); - } - - private readonly string GetDebuggerDisplay() { - const string type = "List"; - return _pool is not null - ? $"Owner<{type}>" - : $"Owner<{type}> [returned]"; - } - } -} \ No newline at end of file diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index 7766c13..821203d 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Runtime.InteropServices; namespace PrettyConsole; @@ -106,36 +105,39 @@ public static (string option, string subOption) TreeMenu(Dictionary x.Length) + 10; // Used to make sub-tree prefix spaces uniform - using var bufferOwner = BufferPool.Shared.Rent(out var buffer); + var pool = ArrayPool.Shared; var width = PrettyConsoleExtensions.GetWidthOrDefault(); - buffer.EnsureCapacity(width); - CollectionsMarshal.SetCount(buffer, width); - var span = CollectionsMarshal.AsSpan(buffer); + 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); - PrettyConsoleExtensions.Out.Write(span.Slice(0, written)); + span.TryWrite($" {i + 1}) {mainEntry}", out int written); + PrettyConsoleExtensions.Out.Write(span.Slice(0, written)); - var remainingLength = maxMainOption - written; - if (remainingLength > 0) { - PrettyConsoleExtensions.Out.WriteWhiteSpaces(remainingLength); - } + var remainingLength = maxMainOption - written; + if (remainingLength > 0) { + PrettyConsoleExtensions.Out.WriteWhiteSpaces(remainingLength); + } + + for (int j = 0; j < subChoices.Count; j++) { + if (j is not 0) { + PrettyConsoleExtensions.Out.WriteWhiteSpaces(maxMainOption); + } - for (int j = 0; j < subChoices.Count; j++) { - if (j is not 0) { - PrettyConsoleExtensions.Out.WriteWhiteSpaces(maxMainOption); + span.TryWrite($" {j + 1}) {subChoices[j]}", out written); + PrettyConsoleExtensions.Out.WriteLine(span.Slice(0, written)); } - span.TryWrite($" {j + 1}) {subChoices[j]}", out written); - PrettyConsoleExtensions.Out.WriteLine(span.Slice(0, written)); + Console.NewLine(); } - - Console.NewLine(); - } + } finally { + pool.Return(array); + } string input = Console.ReadLine(string.Empty, $"Enter your main choice and sub choice separated with space: "); diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 3f59d90..88a73ac 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -91,16 +91,22 @@ public readonly void AppendFormatted(ConsoleColor color) { Console.SetColors(color, BackgroundColor); } + /// + /// Sets the foreground and background colors of the console + /// + /// + public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors) { + Console.SetColors(colors.foreground, colors.background); + } + /// /// Sets the foreground and background colors of the console /// /// /// - public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors, int alignment = 0) { + public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors, int alignment) { Console.SetColors(colors.foreground, colors.background); - if (alignment != 0) { - AppendSpan(ReadOnlySpan.Empty, alignment); - } + AppendSpan(ReadOnlySpan.Empty, alignment); } /// @@ -222,13 +228,15 @@ private readonly void AppendSpanFormattable(T value, int alignment, string? f while (true) { var array = pool.Rent(lowerBound); - buffer = new Span(array); - if (value.TryFormat(buffer, out charsWritten, formatSpan, _provider)) { - AppendSpan(buffer.Slice(0, charsWritten), alignment); + try { + buffer = new Span(array); + if (value.TryFormat(array, out charsWritten, formatSpan, _provider)) { + AppendSpan(buffer.Slice(0, charsWritten), alignment); + return; + } + } finally { pool.Return(array); - break; } - pool.Return(array); lowerBound *= 2; } } diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index 4dee12a..e6e7622 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Buffers; namespace PrettyConsole; @@ -85,18 +85,21 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, Co public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) where T : ISpanFormattable, allows ref struct { - using var listOwner = BufferPool.Shared.Rent(out var lst); - int upperBound = BufferPool.ListStartingSize; + int lowerBound = 4096; + var pool = ArrayPool.Shared; + while (true) { - lst.EnsureCapacity(upperBound); - CollectionsMarshal.SetCount(lst, upperBound); - var span = CollectionsMarshal.AsSpan(lst); - if (item.TryFormat(span, out int charsWritten, format, formatProvider)) { - Write(span.Slice(0, charsWritten), pipe, foreground, background); - break; - } else { - upperBound *= 2; + 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); } + lowerBound *= 2; } } From 6641c2315ed3f85ab67ff970cac0487a22d9bd8f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 17:36:28 +0200 Subject: [PATCH 35/75] Faster color management --- PrettyConsole/ConsoleColorExtensions.cs | 2 ++ .../PrettyConsoleInterpolatedStringHandler.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/PrettyConsole/ConsoleColorExtensions.cs b/PrettyConsole/ConsoleColorExtensions.cs index 9bfb5ae..9bdc7c2 100644 --- a/PrettyConsole/ConsoleColorExtensions.cs +++ b/PrettyConsole/ConsoleColorExtensions.cs @@ -42,6 +42,7 @@ static ConsoleColorExtensions() { /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, ConsoleColor background) { return (foreground, background); } @@ -52,6 +53,7 @@ public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, C /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, (ConsoleColor tupleForeground, ConsoleColor tupleBackground) colorTuple) { return (foreground, colorTuple.tupleBackground); } diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 88a73ac..fd620a5 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -1,7 +1,5 @@ using System.Buffers; -using static System.Console; - namespace PrettyConsole; #pragma warning disable CA1822 // Mark members as static @@ -88,15 +86,16 @@ public readonly void AppendFormatted(char value, int alignment = 0) { /// Sets the console foreground color to . /// public readonly void AppendFormatted(ConsoleColor color) { - Console.SetColors(color, BackgroundColor); + Console.ForegroundColor = color; } /// /// Sets the foreground and background colors of the console /// /// - public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors) { - Console.SetColors(colors.foreground, colors.background); + public readonly void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors) { + Console.ForegroundColor = colors.Foreground; + Console.BackgroundColor = colors.Background; } /// @@ -104,8 +103,9 @@ public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor back /// /// /// - public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors, int alignment) { - Console.SetColors(colors.foreground, colors.background); + public readonly void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors, int alignment) { + Console.ForegroundColor = colors.Foreground; + Console.BackgroundColor = colors.Background; AppendSpan(ReadOnlySpan.Empty, alignment); } From e42d04623cd9664e795cd9929b4b8685287f560e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 18:34:57 +0200 Subject: [PATCH 36/75] Make Handler smarter and ANSI aware --- PrettyConsole/InputRequestExtensions.cs | 4 +- PrettyConsole/MenuExtensions.cs | 6 +- .../PrettyConsoleInterpolatedStringHandler.cs | 58 ++++++++++++++----- PrettyConsole/ReadLineExtensions.cs | 6 +- PrettyConsole/WriteExtensions.cs | 4 +- PrettyConsole/WriteLineExtensions.cs | 4 +- 6 files changed, 54 insertions(+), 28 deletions(-) diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index 06be2fe..e8d09c6 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -15,7 +15,7 @@ public static class InputRequestExtensions { /// /// Interpolated string handler that streams the content. public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - Console.ResetColor(); + handler.ResetColors(); _ = Console.ReadKey(); } @@ -40,7 +40,7 @@ public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInte /// 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) { - Console.ResetColor(); + handler.ResetColors(); var input = PrettyConsoleExtensions.In.ReadLine(); if (input is null or { Length: 0 }) { return emptyIsTrue; diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index 821203d..ec12307 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -18,7 +18,7 @@ public static class MenuExtensions { /// public static string Selection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { - Console.ResetColor(); + handler.ResetColors(); Console.NewLine(); for (int i = 0; i < choices.Count; i++) { @@ -51,7 +51,7 @@ public static string Selection(TList choices, [InterpolatedStringHandlerA /// public static string[] MultiSelection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { - Console.ResetColor(); + handler.ResetColors(); Console.NewLine(); for (int i = 0; i < choices.Count; i++) { @@ -99,7 +99,7 @@ public static string[] MultiSelection(TList choices, [InterpolatedStringH /// This validates the input for you. /// public static (string option, string subOption) TreeMenu(Dictionary menu, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { - Console.ResetColor(); + handler.ResetColors(); Console.NewLine(); var menuKeys = menu.Keys.ToArray(); diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index fd620a5..67120ce 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -2,14 +2,27 @@ namespace PrettyConsole; -#pragma warning disable CA1822 // Mark members as static /// /// Interpolated string handler that streams segments directly to an while allowing inline color changes. /// [InterpolatedStringHandler] -public readonly ref struct PrettyConsoleInterpolatedStringHandler { +public ref struct PrettyConsoleInterpolatedStringHandler { private readonly TextWriter _writer; private readonly IFormatProvider? _provider; + private static readonly Action ChangeFg; + private static readonly Action ChangeBg; + private ConsoleColor _currentForeground; + private ConsoleColor _currentBackground; + + static PrettyConsoleInterpolatedStringHandler() { + if (Color.Enabled) { + ChangeFg = static (writer, color) => writer.Write(Color.Foreground(color)); + ChangeBg = static (writer, color) => writer.Write(Color.Background(color)); + } else { + ChangeFg = static (_, color) => Console.ForegroundColor = color; + ChangeBg = static (_, color) => Console.BackgroundColor = color; + } + } /// /// Creates a new handler that writes to . @@ -41,6 +54,8 @@ 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) { + _currentForeground = ConsoleColor.DefaultForeground; + _currentBackground = ConsoleColor.DefaultBackground; _writer = PrettyConsoleExtensions.GetWriter(pipe); _provider = provider; shouldAppend = true; @@ -85,17 +100,26 @@ public readonly void AppendFormatted(char value, int alignment = 0) { /// /// Sets the console foreground color to . /// - public readonly void AppendFormatted(ConsoleColor color) { - Console.ForegroundColor = color; + public void AppendFormatted(ConsoleColor color) { + if (_currentForeground != color) { + _currentForeground = color; + ChangeFg(_writer, _currentForeground); + } } /// /// Sets the foreground and background colors of the console /// /// - public readonly void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors) { - Console.ForegroundColor = colors.Foreground; - Console.BackgroundColor = colors.Background; + 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; + ChangeFg(_writer, _currentBackground); + } } /// @@ -103,9 +127,8 @@ public readonly void AppendFormatted((ConsoleColor Foreground, ConsoleColor Back /// /// /// - public readonly void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors, int alignment) { - Console.ForegroundColor = colors.Foreground; - Console.BackgroundColor = colors.Background; + public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) colors, int alignment) { + AppendFormatted(colors); AppendSpan(ReadOnlySpan.Empty, alignment); } @@ -191,10 +214,6 @@ public readonly void AppendFormatted(object? value, int alignment = 0, string? f AppendSpan(ReadOnlySpan.Empty, alignment); break; } - case ConsoleColor consoleColor: { - AppendFormatted(consoleColor); - break; - } case ISpanFormattable spanFormattable: { AppendSpanFormattable(spanFormattable, alignment, format); break; @@ -273,5 +292,12 @@ private readonly void AppendSpan(scoped ReadOnlySpan span, int alignment) private readonly void WritePadding(int count) { _writer.WriteWhiteSpaces(count); } -} -#pragma warning restore CA1822 // Mark members as static + + /// + /// Resets the console colors if they changed. + /// + public readonly void ResetColors() { + if (_currentForeground != ConsoleColor.DefaultForeground) ChangeFg(_writer, ConsoleColor.DefaultForeground); + if (_currentBackground != ConsoleColor.DefaultBackground) ChangeFg(_writer, ConsoleColor.DefaultBackground); + } +} \ No newline at end of file diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs index 81b9d09..6b78176 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 { - Console.ResetColor(); + handler.ResetColors(); var input = PrettyConsoleExtensions.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 { - Console.ResetColor(); + handler.ResetColors(); var input = PrettyConsoleExtensions.In.ReadLine(); var res = Enum.TryParse(input, ignoreCase, out result); if (!res) { @@ -74,7 +74,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ /// /// Interpolated string handler that streams the content. public static string? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - Console.ResetColor(); + handler.ResetColors(); return ReadLine(); } diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index e6e7622..ca8115f 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -12,7 +12,7 @@ public static class WriteExtensions { /// /// Interpolated string handler that streams the content. public static void WriteInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - Console.ResetColor(); + handler.ResetColors(); } /// @@ -21,7 +21,7 @@ public static void WriteInterpolated([InterpolatedStringHandlerArgument] PrettyC /// Destination pipe. Defaults to . /// Interpolated string handler that streams the content. public static void WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - Console.ResetColor(); + handler.ResetColors(); } /// diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index d9d330f..eebc07c 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -11,7 +11,7 @@ public static class WriteLineExtensions { /// Interpolated string handler that streams the content. [OverloadResolutionPriority(int.MaxValue)] public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - Console.ResetColor(); + handler.ResetColors(); Console.NewLine(OutputPipe.Out); } @@ -21,7 +21,7 @@ public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] Pre /// Destination pipe. Defaults to . /// Interpolated string handler that streams the content. public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - Console.ResetColor(); + handler.ResetColors(); Console.NewLine(pipe); } From 5e65ab2999082b4168e5f00a2441ece248ec9bbb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 18:35:14 +0200 Subject: [PATCH 37/75] Clean benchmarks --- ...chmarks.StyledOutputBenchmarks-report-github.md | 14 +++++++------- Benchmarks/Config.cs | 2 +- Benchmarks/StyledOutputBenchmark.cs | 5 ----- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md index d9ccd1e..c881501 100644 --- a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -3,15 +3,15 @@ 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 - MediumRun : .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 + Job-WZRQBO : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a -Job=MediumRun OutlierMode=RemoveAll IterationCount=50 -IterationTime=100ms LaunchCount=1 WarmupCount=10 +OutlierMode=RemoveAll IterationCount=50 IterationTime=100ms +LaunchCount=1 WarmupCount=10 ``` | Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | |--------------- |---------:|------:|-------:|----------:|------------:| -| PrettyConsole | 7.835 μs | 1.00 | - | - | NA | -| SpectreConsole | 7.392 μs | 0.94 | 2.0400 | 17840 B | NA | -| SystemConsole | 7.400 μs | 0.95 | - | 56 B | NA | +| PrettyConsole | 7.575 μs | 1.00 | - | - | NA | +| SpectreConsole | 7.525 μs | 1.00 | 2.1217 | 17840 B | NA | +| SystemConsole | 7.165 μs | 0.95 | - | 56 B | NA | diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index f1de942..e21cb43 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -18,7 +18,7 @@ namespace Benchmarks; public class Config : ManualConfig { public Config() { AddDiagnoser(MemoryDiagnoser.Default); - AddJob(Job.MediumRun + AddJob(Job.Default .WithOutlierMode(OutlierMode.RemoveAll) .WithLaunchCount(1) .WithWarmupCount(10) diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index cfcaeeb..bfb9be6 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -1,5 +1,3 @@ -using System.Runtime.CompilerServices; - using BenchmarkDotNet.Attributes; using Spectre.Console; @@ -18,21 +16,18 @@ public class StyledOutputBenchmarks { private const double Percentage = 57.91; [Benchmark(Baseline = true)] - // [MethodImpl(MethodImplOptions.NoInlining)] public int PrettyConsole() { Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.DefaultForeground}, status = {Cyan}{Percentage}{ConsoleColor.DefaultForeground}%, elapsed = {Yellow}{Elapsed:c}"); return int.MaxValue; } [Benchmark] - // [MethodImpl(MethodImplOptions.NoInlining)] public int SpectreConsole() { AnsiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); return int.MaxValue; } [Benchmark] - // [MethodImpl(MethodImplOptions.NoInlining)] public int SystemConsole() { Console.Write("Hello "); Console.ForegroundColor = Green; From f0c612b1b3fcb53ff483844dabe8a9014cc5426f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 18:40:44 +0200 Subject: [PATCH 38/75] Fix invalid delegate call --- .../PrettyConsoleInterpolatedStringHandler.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 67120ce..a362a31 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -118,7 +118,7 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c } if (_currentBackground != colors.Background) { _currentBackground = colors.Background; - ChangeFg(_writer, _currentBackground); + ChangeBg(_writer, _currentBackground); } } @@ -296,8 +296,14 @@ private readonly void WritePadding(int count) { /// /// Resets the console colors if they changed. /// - public readonly void ResetColors() { - if (_currentForeground != ConsoleColor.DefaultForeground) ChangeFg(_writer, ConsoleColor.DefaultForeground); - if (_currentBackground != ConsoleColor.DefaultBackground) ChangeFg(_writer, ConsoleColor.DefaultBackground); + public void ResetColors() { + if (_currentForeground != ConsoleColor.DefaultForeground) { + _currentForeground = ConsoleColor.DefaultForeground; + ChangeFg(_writer, _currentForeground); + } + if (_currentBackground != ConsoleColor.DefaultBackground) { + _currentBackground = ConsoleColor.DefaultBackground; + ChangeBg(_writer, _currentBackground); + } } } \ No newline at end of file From e7a74051fb8085ec6318bd6759301dd11f2cea4d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 18:47:16 +0200 Subject: [PATCH 39/75] Added ansi colors internal class --- PrettyConsole/Color.cs | 83 +++++++++++++++++++ .../PrettyConsoleInterpolatedStringHandler.cs | 6 +- 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 PrettyConsole/Color.cs diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs new file mode 100644 index 0000000..7599e02 --- /dev/null +++ b/PrettyConsole/Color.cs @@ -0,0 +1,83 @@ +namespace PrettyConsole; + +internal static class AnsiColors { + private static readonly string[] ForegroundCodes = new string[16]; + private static readonly string[] BackgroundCodes = new string[16]; + + /// + /// Gets a value indicating whether ANSI color sequences are emitted. + /// + public static readonly bool Enabled; + + static AnsiColors() { + Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; + for (int i = 0; i < ForegroundCodes.Length; i++) { + ForegroundCodes[i] = string.Empty; + BackgroundCodes[i] = string.Empty; + } + + if (!Enabled) { + return; + } + + foreach (var color in Enum.GetValues()) { + int index = (int)color; + ForegroundCodes[index] = BuildForegroundSequence(color); + BackgroundCodes[index] = BuildBackgroundSequence(color); + } + } + + /// + /// Gets the ANSI sequence for the specified foreground color or an empty string when disabled. + /// + public static string Foreground(ConsoleColor color) => ForegroundCodes[(int)color]; + + /// + /// Gets the ANSI sequence for the specified background color or an empty string when disabled. + /// + public static string Background(ConsoleColor color) => BackgroundCodes[(int)color]; + + private static string BuildForegroundSequence(ConsoleColor color) { + return color switch { + ConsoleColor.Black => "\u001b[30m", + ConsoleColor.DarkBlue => "\u001b[34m", + ConsoleColor.DarkGreen => "\u001b[32m", + ConsoleColor.DarkCyan => "\u001b[36m", + ConsoleColor.DarkRed => "\u001b[31m", + ConsoleColor.DarkMagenta => "\u001b[35m", + ConsoleColor.DarkYellow => "\u001b[33m", + ConsoleColor.Gray => "\u001b[37m", + ConsoleColor.DarkGray => "\u001b[90m", + ConsoleColor.Blue => "\u001b[94m", + ConsoleColor.Green => "\u001b[92m", + ConsoleColor.Cyan => "\u001b[96m", + ConsoleColor.Red => "\u001b[91m", + ConsoleColor.Magenta => "\u001b[95m", + ConsoleColor.Yellow => "\u001b[93m", + ConsoleColor.White => "\u001b[97m", + _ => "\u001b[39m" + }; + } + + private static string BuildBackgroundSequence(ConsoleColor color) { + return color switch { + ConsoleColor.Black => "\u001b[40m", + ConsoleColor.DarkBlue => "\u001b[44m", + ConsoleColor.DarkGreen => "\u001b[42m", + ConsoleColor.DarkCyan => "\u001b[46m", + ConsoleColor.DarkRed => "\u001b[41m", + ConsoleColor.DarkMagenta => "\u001b[45m", + ConsoleColor.DarkYellow => "\u001b[43m", + ConsoleColor.Gray => "\u001b[47m", + ConsoleColor.DarkGray => "\u001b[100m", + ConsoleColor.Blue => "\u001b[104m", + ConsoleColor.Green => "\u001b[102m", + ConsoleColor.Cyan => "\u001b[106m", + ConsoleColor.Red => "\u001b[101m", + ConsoleColor.Magenta => "\u001b[105m", + ConsoleColor.Yellow => "\u001b[103m", + ConsoleColor.White => "\u001b[107m", + _ => "\u001b[49m" + }; + } +} diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index a362a31..f6f5cd0 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -15,9 +15,9 @@ public ref struct PrettyConsoleInterpolatedStringHandler { private ConsoleColor _currentBackground; static PrettyConsoleInterpolatedStringHandler() { - if (Color.Enabled) { - ChangeFg = static (writer, color) => writer.Write(Color.Foreground(color)); - ChangeBg = static (writer, color) => writer.Write(Color.Background(color)); + 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; From 96ed58998789da0ac9c11067414fcb3ebddcb02b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 19:45:17 +0200 Subject: [PATCH 40/75] Fix index out of range when ConsoleColor is -1 --- PrettyConsole/Color.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index 7599e02..fd0ce5c 100644 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -1,6 +1,9 @@ namespace PrettyConsole; internal static class AnsiColors { + private const string ForegroundResetSequence = "\u001b[39m"; + private const string BackgroundResetSequence = "\u001b[49m"; + private static readonly string[] ForegroundCodes = new string[16]; private static readonly string[] BackgroundCodes = new string[16]; @@ -30,12 +33,22 @@ static AnsiColors() { /// /// Gets the ANSI sequence for the specified foreground color or an empty string when disabled. /// - public static string Foreground(ConsoleColor color) => ForegroundCodes[(int)color]; + public static string Foreground(ConsoleColor color) { + int index = (int)color; + if (index == -1) return ForegroundResetSequence; + return ForegroundCodes[index]; + } + /// /// Gets the ANSI sequence for the specified background color or an empty string when disabled. /// - public static string Background(ConsoleColor color) => BackgroundCodes[(int)color]; + public static string Background(ConsoleColor color) { + int index = (int)color; + if (index == -1) return BackgroundResetSequence; + return BackgroundCodes[index]; + } + private static string BuildForegroundSequence(ConsoleColor color) { return color switch { @@ -55,7 +68,7 @@ private static string BuildForegroundSequence(ConsoleColor color) { ConsoleColor.Magenta => "\u001b[95m", ConsoleColor.Yellow => "\u001b[93m", ConsoleColor.White => "\u001b[97m", - _ => "\u001b[39m" + _ => ForegroundResetSequence }; } @@ -77,7 +90,7 @@ private static string BuildBackgroundSequence(ConsoleColor color) { ConsoleColor.Magenta => "\u001b[105m", ConsoleColor.Yellow => "\u001b[103m", ConsoleColor.White => "\u001b[107m", - _ => "\u001b[49m" + _ => BackgroundResetSequence }; } } From 95931d607d4b3dcac850e8849cb692fe63b931f9 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 19:48:02 +0200 Subject: [PATCH 41/75] Skip work when disabled --- PrettyConsole/Color.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index fd0ce5c..0b7dd23 100644 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -4,8 +4,8 @@ internal static class AnsiColors { private const string ForegroundResetSequence = "\u001b[39m"; private const string BackgroundResetSequence = "\u001b[49m"; - private static readonly string[] ForegroundCodes = new string[16]; - private static readonly string[] BackgroundCodes = new string[16]; + private static readonly string[] ForegroundCodes = default!; + private static readonly string[] BackgroundCodes = default!; /// /// Gets a value indicating whether ANSI color sequences are emitted. @@ -14,15 +14,10 @@ internal static class AnsiColors { static AnsiColors() { Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; - for (int i = 0; i < ForegroundCodes.Length; i++) { - ForegroundCodes[i] = string.Empty; - BackgroundCodes[i] = string.Empty; - } - - if (!Enabled) { - return; - } + if (!Enabled) return; + ForegroundCodes = new string[16]; + BackgroundCodes = new string[16]; foreach (var color in Enum.GetValues()) { int index = (int)color; ForegroundCodes[index] = BuildForegroundSequence(color); From 560efb4cfa97b3574100e16d81b443f7aefc9db8 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 20:16:56 +0200 Subject: [PATCH 42/75] Fix no default overload binding to string and creating invalid handler --- PrettyConsole/ReadLineExtensions.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/ReadLineExtensions.cs b/PrettyConsole/ReadLineExtensions.cs index 6b78176..44c83a0 100755 --- a/PrettyConsole/ReadLineExtensions.cs +++ b/PrettyConsole/ReadLineExtensions.cs @@ -70,12 +70,14 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ } /// - /// Used to request user input + /// Used to request user input. /// /// Interpolated string handler that streams the content. - public static string? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { - handler.ResetColors(); - return ReadLine(); + /// A string if the user entered any, empty string otherwise - never null. + [OverloadResolutionPriority(3)] + public static string ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + _ = TryReadLine(out string result, string.Empty, handler); + return result; } /// @@ -84,6 +86,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ /// /// 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 { _ = TryReadLine(out T? result, handler); return result; @@ -96,6 +99,7 @@ public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @ /// 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 { _ = TryReadLine(out T result, @default, handler); return result; From ecc63312e3da57d68b68d58bdd99e0668479d45c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 20:17:20 +0200 Subject: [PATCH 43/75] Properly test sequences --- PrettyConsole.Tests.Unit/ConsoleColorTests.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs index 057103d..c4f7b65 100644 --- a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs +++ b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs @@ -21,4 +21,20 @@ public void ConsoleColor_DefaultColors() { Assert.Equal(ConsoleColor.DefaultForeground, fg); Assert.Equal(ConsoleColor.DefaultBackground, bg); } -} \ No newline at end of file + + [Fact] + public void AnsiColors_DefaultForeground_UsesResetSequence() { + if (AnsiColors.Enabled) { + var sequence = AnsiColors.Foreground((ConsoleColor)(-1)); + Assert.Equal("\u001b[39m", sequence); + } + } + + [Fact] + public void AnsiColors_DefaultBackground_UsesResetSequence() { + if (AnsiColors.Enabled) { + var sequence = AnsiColors.Background((ConsoleColor)(-1)); + Assert.Equal("\u001b[49m", sequence); + } + } +} From 21163f454f5f060d07c8f9b55876776ce65b3def Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 20:30:49 +0200 Subject: [PATCH 44/75] Fixed inaccurate test --- PrettyConsole.Tests.Unit/Utilities.cs | 18 +++++++++++++++--- .../WriteExtensionsTests.cs | 8 +++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/PrettyConsole.Tests.Unit/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index b6d0d07..310e6fe 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -1,9 +1,10 @@ using System.Globalization; using System.Text; +using System.Text.RegularExpressions; namespace PrettyConsole.Tests.Unit; -public static class Utilities { +public static partial class Utilities { public static StringReader GetReader(string str) => new(str); public static TextWriter GetWriter(out StringWriter writer) { @@ -19,6 +20,14 @@ public static string ToStringAndFlush(this StringWriter writer) { public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); + public static string StripAnsiSequences(string value) { + if (string.IsNullOrEmpty(value)) { + return string.Empty; + } + + return AnsiSequenceRegex().Replace(value, string.Empty); + } + public static void SkipIfNoInteractiveConsole() { const string reason = "Interactive console APIs are not available in this environment."; @@ -28,10 +37,13 @@ public static void SkipIfNoInteractiveConsole() { try { _ = Console.CursorTop; - } catch (System.IO.IOException) { + } catch (IOException) { Assert.Skip(reason); } catch (PlatformNotSupportedException) { Assert.Skip(reason); } } -} \ No newline at end of file + + [GeneratedRegex("\\u001b\\[[0-9;]*m", RegexOptions.Compiled)] + private static partial Regex AnsiSequenceRegex(); +} diff --git a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs index 806723b..25c205c 100755 --- a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs @@ -45,9 +45,11 @@ public void Write_Interpolated_IgnoresColorTokensInOutput() { try { Console.WriteInterpolated(OutputPipe.Out, - $"Colors {Black / Green}Green{ConsoleColor.Default} {Red}Red{ConsoleColor.Default}"); + $"Colors {Black / Green}Green{ConsoleColor.Default} {Red}Red"); - Assert.Equal("Colors Green Red", writer.ToString()); + var normalized = Utilities.StripAnsiSequences(writer.ToString()); + + Assert.Equal("Colors Green Red", normalized); } finally { Out = originalOut; } @@ -96,4 +98,4 @@ public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan return true; } } -} \ No newline at end of file +} From 6f152717d801f1bbc1f64cf45ac3ba6b924408d1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 21:01:25 +0200 Subject: [PATCH 45/75] Added Markup tests --- PrettyConsole.Tests.Unit/MarkupTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/MarkupTests.cs diff --git a/PrettyConsole.Tests.Unit/MarkupTests.cs b/PrettyConsole.Tests.Unit/MarkupTests.cs new file mode 100644 index 0000000..d33e06e --- /dev/null +++ b/PrettyConsole.Tests.Unit/MarkupTests.cs @@ -0,0 +1,16 @@ +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("\u001b[4m", Markup.Underline); + Assert.Equal("\u001b[0m", Markup.Reset); + } else { + Assert.Equal(string.Empty, Markup.Underline); + Assert.Equal(string.Empty, Markup.Reset); + } + } +} From 23747537cae4cda1272c744eb98d81ca6043aef6 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 21:02:38 +0200 Subject: [PATCH 46/75] Increase coverage --- .../ReadLineExtensionsTests.cs | 44 ++++++++++++++++++- .../WriteExtensionsTests.cs | 14 ++++++ .../WriteLineExtensionsTests.cs | 8 +++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs index d983f65..ee76c5b 100755 --- a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs @@ -35,4 +35,46 @@ public void TryReadLine_Enum_InterpolatedPrompt_IgnoreCase() { Assert.True(parsed); Assert.Equal(Yellow, color); } -} \ No newline at end of file + + [Fact] + public void 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); + } + + [Fact] + public void 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); + } + + [Fact] + public void ReadLine_Generic_ReturnsParsedValue() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("3.14"); + + var value = Console.ReadLine($"Value: "); + + Assert.Equal(3.14, value); + } + + [Fact] + public void ReadLine_GenericWithDefault_ReturnsDefaultWhenInvalid() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("not-number"); + + var value = Console.ReadLine(5, $"Value: "); + + Assert.Equal(5, value); + } +} diff --git a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs index 25c205c..9fa8cd9 100755 --- a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace PrettyConsole.Tests.Unit; public class WriteExtensionsTests { @@ -80,6 +82,18 @@ public void Write_SpanFormattable_VeryLongObjectFormat() { Assert.Equal(new string('X', LongFormatStud.Length), _writer.ToStringAndFlush()); } + [Fact] + public void Write_SpanFormattable_WithFormatAndProvider() { + Console.Write(12.345, OutputPipe.Out, White, Black, "F2", CultureInfo.InvariantCulture); + Assert.Equal("12.35", _writer.ToStringAndFlush()); + } + + [Fact] + public void Write_ReadOnlySpan_WithColors_WritesToSelectedPipe() { + Console.Write("Data".AsSpan(), OutputPipe.Error, ConsoleColor.Green, ConsoleColor.Black); + Assert.Equal("Data", _errorWriter.ToStringAndFlush()); + } + private readonly ref struct LongFormatStud : ISpanFormattable { public const int Length = 1024; diff --git a/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs b/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs index 9d8f79d..ed86340 100755 --- a/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs @@ -22,4 +22,10 @@ public void WriteLine_Interpolated_AppendsNewLine() { Out = originalOut; } } -} \ No newline at end of file + + [Fact] + public void WriteLine_ReadOnlySpan_WithColors_AppendsNewLine() { + Console.WriteLine("SpanLine".AsSpan(), OutputPipe.Out, ConsoleColor.Yellow, ConsoleColor.Black); + Assert.Equal($"SpanLine{_writer.NewLine}", _writer.ToStringAndFlush()); + } +} From 2c16e208c7b32ecd7411c879b471d89cec03b2c4 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 21:05:25 +0200 Subject: [PATCH 47/75] Extended mocking capabilities --- .../InputRequestExtensionsTests.cs | 23 +++++++++++++++++++ PrettyConsole/InputRequestExtensions.cs | 14 +++++++++-- PrettyConsole/RenderingExtensions.cs | 22 +++++++++++++++--- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs diff --git a/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs b/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs new file mode 100644 index 0000000..a0cd14a --- /dev/null +++ b/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs @@ -0,0 +1,23 @@ +namespace PrettyConsole.Tests.Unit; + +public class InputRequestExtensionsTests { + [Fact] + public void RequestAnyInput_WritesPrompt_AndInvokesReadKey() { + Out = Utilities.GetWriter(out var writer); + bool invoked = false; + + try { + InputRequestExtensions.ConfigureReadKey(() => { + invoked = true; + return new ConsoleKeyInfo('x', ConsoleKey.X, false, false, false); + }); + + Console.RequestAnyInput($"Press something:"); + + Assert.Contains("Press something:", writer.ToString()); + Assert.True(invoked); + } finally { + InputRequestExtensions.ConfigureReadKey(null); + } + } +} diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index e8d09c6..8046f31 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -4,6 +4,16 @@ namespace PrettyConsole; /// Provides methods extending with input request extensions. /// public static class InputRequestExtensions { + private static Func s_readKey = Console.ReadKey; + + /// + /// Allows tests to override how Console.ReadKey is performed. + /// + /// Delegate that returns a . + internal static void ConfigureReadKey(Func? readKey) { + s_readKey = readKey ?? Console.ReadKey; + } + /// /// Used to get user confirmation with the default values ["y", "yes"] /// @@ -16,7 +26,7 @@ public static class InputRequestExtensions { /// Interpolated string handler that streams the content. public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); - _ = Console.ReadKey(); + _ = s_readKey(); } /// @@ -55,4 +65,4 @@ public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = t return false; } } -} \ No newline at end of file +} diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index 88633c9..ef4cc4c 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -4,6 +4,22 @@ namespace PrettyConsole; /// Provides methods extending with more rendering methods. /// public static partial class RenderingExtensions { + private static readonly Func DefaultCursorTopAccessor = static () => Console.CursorTop; + private static readonly Action DefaultSetCursorPosition = Console.SetCursorPosition; + + private static Func s_cursorTopAccessor = DefaultCursorTopAccessor; + private static Action s_setCursorPosition = DefaultSetCursorPosition; + + /// + /// Allows tests to override how cursor information is retrieved. + /// + /// Delegate that returns the current cursor row. + /// Delegate that positions the cursor. + internal static void ConfigureCursorAccessors(Func? cursorTopAccessor, Action? setCursorPosition) { + s_cursorTopAccessor = cursorTopAccessor ?? DefaultCursorTopAccessor; + s_setCursorPosition = setCursorPosition ?? DefaultSetCursorPosition; + } + extension(Console) { /// /// Clears the next . @@ -44,7 +60,7 @@ public static void SetColors(ConsoleColor foreground, ConsoleColor background) { /// /// public static int GetCurrentLine() { - return Console.CursorTop; + return s_cursorTopAccessor(); } /// @@ -52,7 +68,7 @@ public static int GetCurrentLine() { /// /// public static void GoToLine(int line) { - Console.SetCursorPosition(0, line); + s_setCursorPosition(0, line); } } -} \ No newline at end of file +} From 04899f2ebf7171439db679ba9bf6232a056f6634 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 14 Nov 2025 21:19:54 +0200 Subject: [PATCH 48/75] Increase test coverage --- ...vancedInputs.cs => AdvancedInputsTests.cs} | 2 +- ...ncedOutputs.cs => AdvancedOutputsTests.cs} | 36 +++++--- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 85 ++++++++++--------- 3 files changed, 70 insertions(+), 53 deletions(-) rename PrettyConsole.Tests.Unit/{AdvancedInputs.cs => AdvancedInputsTests.cs} (98%) rename PrettyConsole.Tests.Unit/{AdvancedOutputs.cs => AdvancedOutputsTests.cs} (53%) diff --git a/PrettyConsole.Tests.Unit/AdvancedInputs.cs b/PrettyConsole.Tests.Unit/AdvancedInputsTests.cs similarity index 98% rename from PrettyConsole.Tests.Unit/AdvancedInputs.cs rename to PrettyConsole.Tests.Unit/AdvancedInputsTests.cs index f6537d4..f9e84bc 100755 --- a/PrettyConsole.Tests.Unit/AdvancedInputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedInputsTests.cs @@ -1,6 +1,6 @@ namespace PrettyConsole.Tests.Unit; -public class AdvancedInputs { +public class AdvancedInputsTests { [Fact] public void Confirm_Case_Y_Interpolated() { Out = Utilities.GetWriter(out var stringWriter); diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs b/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs similarity index 53% rename from PrettyConsole.Tests.Unit/AdvancedOutputs.cs rename to PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs index 290a880..6940a3b 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs @@ -1,16 +1,20 @@ namespace PrettyConsole.Tests.Unit; -public class AdvancedOutputs { +public class AdvancedOutputsTests { [Fact] public void Overwrite_ExecutesActionAndWritesOutput() { - Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var writer); bool executed = false; - - Console.Overwrite(() => { - executed = true; - Console.WriteInterpolated(OutputPipe.Error, $"Progress"); - }, lines: 1, pipe: OutputPipe.Error); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + Console.Overwrite(() => { + executed = true; + Console.WriteInterpolated(OutputPipe.Error, $"Progress"); + }, lines: 1, pipe: OutputPipe.Error); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } Assert.True(executed); Assert.Contains("Progress", writer.ToString()); @@ -18,14 +22,18 @@ public void Overwrite_ExecutesActionAndWritesOutput() { [Fact] public void Overwrite_WithState_ExecutesActionAndWritesOutput() { - Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var writer); bool executed = false; - - Console.Overwrite("Done", status => { - executed = true; - Console.WriteInterpolated(OutputPipe.Error, $"{status}"); - }, lines: 1, pipe: OutputPipe.Error); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + Console.Overwrite("Done", status => { + executed = true; + Console.WriteInterpolated(OutputPipe.Error, $"{status}"); + }, lines: 1, pipe: OutputPipe.Error); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } Assert.True(executed); Assert.Contains("Done", writer.ToString()); @@ -44,4 +52,4 @@ public async Task TypeWriteLine_Regular() { await Console.TypeWriteLine("Hello world!", Green / ConsoleColor.Default, 10); Assert.Contains("Hello world!" + Environment.NewLine, stringWriter.ToString()); } -} \ No newline at end of file +} diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 3356e1e..4b22761 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -3,16 +3,20 @@ namespace PrettyConsole.Tests.Unit; public class ProgressBarTests { [Fact] public void ProgressBar_Update_WritesStatusAndPercentage() { - Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new ProgressBar { + ProgressChar = '#', + ForegroundColor = White, + ProgressColor = Green + }; - var bar = new ProgressBar { - ProgressChar = '#', - ForegroundColor = White, - ProgressColor = Green - }; - - bar.Update(50, "Loading"); + bar.Update(50, "Loading"); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } var output = errorWriter.ToString(); Assert.Contains("Loading", output); @@ -22,19 +26,23 @@ public void ProgressBar_Update_WritesStatusAndPercentage() { [Fact] public void ProgressBar_Update_SamePercentage_RerendersOutput() { - Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new ProgressBar { + ProgressChar = '#', + ForegroundColor = White, + ProgressColor = Green + }; - var bar = new ProgressBar { - ProgressChar = '#', - ForegroundColor = White, - ProgressColor = Green - }; - - bar.Update(25, "Loading"); - errorWriter.ToStringAndFlush(); + bar.Update(25, "Loading"); + errorWriter.ToStringAndFlush(); - bar.Update(25, "Loading"); + bar.Update(25, "Loading"); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } var output = errorWriter.ToString(); Assert.NotEqual(string.Empty, output); @@ -44,9 +52,9 @@ public void ProgressBar_Update_SamePercentage_RerendersOutput() { [Fact] public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { - Utilities.SkipIfNoInteractiveConsole(); - var originalError = Error; + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); try { Error = Utilities.GetWriter(out var errorWriter); @@ -61,13 +69,12 @@ public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { Assert.Contains(Environment.NewLine + "[", output); } finally { Error = originalError; + RenderingExtensions.ConfigureCursorAccessors(null, null); } } [Fact] public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { - Utilities.SkipIfNoInteractiveConsole(); - var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); @@ -85,8 +92,6 @@ public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { [Fact] public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { - Utilities.SkipIfNoInteractiveConsole(); - var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); @@ -104,22 +109,26 @@ public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { [Fact] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { - Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new IndeterminateProgressBar { + AnimationSequence = new(["|", "/"]), + DisplayElapsedTime = false, + UpdateRate = 5 + }; - var bar = new IndeterminateProgressBar { - AnimationSequence = new(["|", "/"]), - DisplayElapsedTime = false, - UpdateRate = 5 - }; - - var cancellation = TestContext.Current.CancellationToken; - var result = await bar.RunAsync(Task.Run(async () => { - await Task.Delay(20, cancellation); - return 42; - }, cancellation), "Working", cancellation); + var cancellation = TestContext.Current.CancellationToken; + 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()); + Assert.Equal(42, result); + Assert.NotEqual(string.Empty, errorWriter.ToString()); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } } } From 3b25bc131438ea92e36ba2727a5e72a9f1f899ef Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 11:11:01 +0200 Subject: [PATCH 49/75] Made handler struct to widen use-cases --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index f6f5cd0..8a6cd0e 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -6,7 +6,7 @@ namespace PrettyConsole; /// Interpolated string handler that streams segments directly to an while allowing inline color changes. /// [InterpolatedStringHandler] -public ref struct PrettyConsoleInterpolatedStringHandler { +public struct PrettyConsoleInterpolatedStringHandler { private readonly TextWriter _writer; private readonly IFormatProvider? _provider; private static readonly Action ChangeFg; From 6704206309e21e0d435bfd9c132fce6b68c7b575 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 11:11:38 +0200 Subject: [PATCH 50/75] Mock TextWriter to remove IO influence on metrics --- Benchmarks/StyledOutputBenchmark.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index bfb9be6..fd3c8fa 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -15,7 +15,27 @@ public class StyledOutputBenchmarks { private static readonly TimeSpan Elapsed = new(1, 25, 31); private const double Percentage = 57.91; - [Benchmark(Baseline = true)] + private TextWriter _outputWriter = default!; + private IAnsiConsole _ansiConsole = default!; + + [GlobalSetup] + public void GlobalSetup() { + _outputWriter = Console.Out; + PrettyConsoleExtensions.Out = TextWriter.Null; + _ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings { + Out = new AnsiConsoleOutput(TextWriter.Null) + }); + Console.SetOut(TextWriter.Null); + } + + [GlobalCleanup] + public void GlobalCleanup() { + PrettyConsoleExtensions.Out = _outputWriter; + _ansiConsole = AnsiConsole.Console; + Console.SetOut(_outputWriter); + } + + [Benchmark] public int PrettyConsole() { Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.DefaultForeground}, status = {Cyan}{Percentage}{ConsoleColor.DefaultForeground}%, elapsed = {Yellow}{Elapsed:c}"); return int.MaxValue; @@ -23,7 +43,7 @@ public int PrettyConsole() { [Benchmark] public int SpectreConsole() { - AnsiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); + _ansiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); return int.MaxValue; } From 818f48b469b069166e33e9ff5c073bdcfcee4953 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 11:11:59 +0200 Subject: [PATCH 51/75] Add visibility to benchmarks to allow mocking --- PrettyConsole/PrettyConsole.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 59c3709..aef724d 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -76,6 +76,9 @@ <_Parameter1>PrettyConsole.Tests.Unit + + <_Parameter1>Benchmarks + From 5927cbe75acce004f45de0df3fd5d22ea950e806 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 11:12:25 +0200 Subject: [PATCH 52/75] Update config for more stability --- Benchmarks/Config.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index e21cb43..501d188 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -17,15 +17,15 @@ namespace Benchmarks; public class Config : ManualConfig { public Config() { + SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); AddDiagnoser(MemoryDiagnoser.Default); AddJob(Job.Default - .WithOutlierMode(OutlierMode.RemoveAll) - .WithLaunchCount(1) - .WithWarmupCount(10) - .WithIterationCount(50) - .WithIterationTime(TimeInterval.FromMilliseconds(100))); + .WithOutlierMode(OutlierMode.DontRemove) + .WithLaunchCount(3) + .WithWarmupCount(5) + .WithIterationCount(20) + .WithIterationTime(TimeInterval.FromMilliseconds(500))); AddColumnProvider(DefaultColumnProviders.Instance); - // AddColumn(RankColumn.Arabic); HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); WithOrderer(new GroupByTypeOrderer()); WithOptions(ConfigOptions.JoinSummary); From 4dbb0ecdc7ac97174458097bd73bb950f7e5d751 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 11:34:09 +0200 Subject: [PATCH 53/75] Expose NewLine from handler --- PrettyConsole/MenuExtensions.cs | 6 +++--- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 5 +++++ PrettyConsole/WriteLineExtensions.cs | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index ec12307..d3721ec 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -19,7 +19,7 @@ public static class MenuExtensions { public static string Selection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { handler.ResetColors(); - Console.NewLine(); + handler.AppendNewLine(); for (int i = 0; i < choices.Count; i++) { Console.WriteLineInterpolated($" {i + 1}) {choices[i]}"); @@ -52,7 +52,7 @@ public static string Selection(TList choices, [InterpolatedStringHandlerA public static string[] MultiSelection(TList choices, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { handler.ResetColors(); - Console.NewLine(); + handler.AppendNewLine(); for (int i = 0; i < choices.Count; i++) { Console.WriteLineInterpolated($" {i + 1}) {choices[i]}"); @@ -100,7 +100,7 @@ public static string[] MultiSelection(TList choices, [InterpolatedStringH /// public static (string option, string subOption) TreeMenu(Dictionary menu, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TList : IList { handler.ResetColors(); - Console.NewLine(); + handler.AppendNewLine(); 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/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 8a6cd0e..bc08ece 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -306,4 +306,9 @@ public void ResetColors() { ChangeBg(_writer, _currentBackground); } } + + /// + /// Writes a new line to the used internally. + /// + public readonly void AppendNewLine() => _writer.WriteLine(); } \ No newline at end of file diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index eebc07c..97bd2ff 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -12,7 +12,7 @@ public static class WriteLineExtensions { [OverloadResolutionPriority(int.MaxValue)] public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); - Console.NewLine(OutputPipe.Out); + handler.AppendNewLine(); } /// @@ -22,7 +22,7 @@ public static void WriteLineInterpolated([InterpolatedStringHandlerArgument] Pre /// Interpolated string handler that streams the content. public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { handler.ResetColors(); - Console.NewLine(pipe); + handler.AppendNewLine(); } /// From 08b269c1e064ff8273ab5ab71d81ee20b9684a0e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 12:20:24 +0200 Subject: [PATCH 54/75] Updated the benchmark to use spectre as baseline --- ...marks.StyledOutputBenchmarks-report-github.md | 16 ++++++++-------- Benchmarks/StyledOutputBenchmark.cs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md index c881501..a860cdd 100644 --- a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -4,14 +4,14 @@ 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-WZRQBO : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a + Job-GIBNDH : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a -OutlierMode=RemoveAll IterationCount=50 IterationTime=100ms -LaunchCount=1 WarmupCount=10 +OutlierMode=DontRemove IterationCount=20 IterationTime=500ms +LaunchCount=3 WarmupCount=5 ``` -| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | -|--------------- |---------:|------:|-------:|----------:|------------:| -| PrettyConsole | 7.575 μs | 1.00 | - | - | NA | -| SpectreConsole | 7.525 μs | 1.00 | 2.1217 | 17840 B | NA | -| SystemConsole | 7.165 μs | 0.95 | - | 56 B | NA | +| Method | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +|--------------- |------------:|--------------:|-------:|-------:|----------:|--------------:| +| PrettyConsole | 95.06 ns | 49.95x faster | - | - | - | NA | +| SpectreConsole | 4,747.67 ns | baseline | 2.1255 | 0.0191 | 17840 B | | +| SystemConsole | 67.90 ns | 69.92x faster | 0.0028 | - | 24 B | 743.333x less | diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index fd3c8fa..ffff0b5 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -41,7 +41,7 @@ public int PrettyConsole() { return int.MaxValue; } - [Benchmark] + [Benchmark(Baseline = true)] public int SpectreConsole() { _ansiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); return int.MaxValue; From fae1767df4c612b31e7c3135180bde229132c6cc Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 12:36:55 +0200 Subject: [PATCH 55/75] Renamed hr format to duration, and added bytes for double --- .../PrettyConsoleInterpolatedStringHandler.cs | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index bc08ece..d918d87 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -133,7 +133,7 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c } /// - /// Append timeSpan with or without elapsed time formatting (human readable) + /// Append timeSpan with optional formatting. /// /// /// @@ -141,19 +141,19 @@ public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) => AppendFormatted(timeSpan, alignment: 0, format); /// - /// Append timeSpan with optional alignment support. + /// Append timeSpan with optional alignment support and formatting. /// /// /// /// public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? format = null) { - if (format != "hr") { + if (format != "duration") { AppendSpanFormattable(timeSpan, alignment, format); return; } Span buffer = stackalloc char[128]; - if (buffer.TryWrite($"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}", out int written)) { + if (buffer.TryWrite($"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m {timeSpan.Seconds}s", out int written)) { AppendSpan(buffer.Slice(0, written), alignment); return; } @@ -164,7 +164,7 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f while (true) { var array = pool.Rent(lowerBound); buffer = new Span(array); - if (buffer.TryWrite($"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}", out written)) { + if (buffer.TryWrite($"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m {timeSpan.Seconds}s", out written)) { AppendSpan(buffer.Slice(0, written), alignment); pool.Return(array); break; @@ -174,6 +174,43 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f } } + private static readonly string[] FileSizeSuffix = ["B", "KB", "MB", "GB", "TB", "PB"]; + + /// + /// Append double with optional formatting. + /// + /// + /// + public readonly void AppendFormatted(double num, string? format = null) + => AppendFormatted(num, alignment: 0, format); + + /// + /// Append double with optional alignment and formatting. + /// + /// + /// + /// + public readonly void AppendFormatted(double num, int alignment, string? format = null) { + if (format != "bytes") { + AppendSpanFormattable(num, alignment, format); + return; + } + + const double formatBytesKb = 1024d; + const double formatBytesDivisor = 1 / formatBytesKb; + var suffix = 0; + while (suffix < FileSizeSuffix.Length - 1 && num >= formatBytesKb) { + num *= formatBytesDivisor; + suffix++; + } + var unit = FileSizeSuffix[suffix]; + + Span buffer = stackalloc char[128]; + if (buffer.TryWrite($"{num:#,##0.##} {unit}", out int written)) { + AppendSpan(buffer.Slice(0, written), alignment); + } + } + /// /// Appends a value type that implements without boxing. /// From 44927d1b931911072df533a74a3a6a216158a8c9 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 12:43:51 +0200 Subject: [PATCH 56/75] Update callsite with new format name --- PrettyConsole/IndeterminateProgressBar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 61e3845..433ad2c 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -117,7 +117,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d if (DisplayElapsedTime) { var elapsed = Stopwatch.GetElapsedTime(startTime); - Console.WriteInterpolated(OutputPipe.Error, $" [Elapsed: {elapsed:hr}]"); + Console.WriteInterpolated(OutputPipe.Error, $" [Elapsed: {elapsed:duration}]"); } // Compute sleep to maintain UpdateRate between frame starts @@ -216,4 +216,4 @@ public static readonly ReadOnlyCollection PingPong "| • |", ]); } -} \ No newline at end of file +} From 199b4a93190520d315e5f6db41d63a2a16194a53 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 12:47:29 +0200 Subject: [PATCH 57/75] Update output of duration in readme examples --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c7facc3..e84ba34 100755 --- a/README.md +++ b/README.md @@ -48,18 +48,18 @@ if (!Console.TryReadLine(out int choice, $"Pick option {ConsoleColor.Cyan / Cons When ANSI escape sequences are safe to emit (`Console.IsOutputRedirected`/`IsErrorRedirected` are both `false`), the `Markup` helper exposes ready-to-use toggles for underline, bold, italic, and strikethrough: ```csharp -Console.WriteLineInterpolated($"{Markup.Bold}Build{Markup.ResetBold} {Markup.Underline}completed{Markup.ResetUnderline} in {elapsed:hr}"); +Console.WriteLineInterpolated($"{Markup.Bold}Build{Markup.ResetBold} {Markup.Underline}completed{Markup.ResetUnderline} in {elapsed:duration}"); // e.g. "completed in 2h 3m 17s" ``` All fields collapse to `string.Empty` when markup is disabled, so the same call sites continue to work when output is redirected or the terminal ignores decorations. Use `Markup.Reset` if you want to reset every decoration at once. #### Formatting & alignment helpers -- **`TimeSpan :hr` format** — the interpolated string handler understands the custom `:hr` specifier. It renders elapsed time as `totalHours:minutes:seconds` (e.g., `00:05:32`, `27:12:03`, `123:00:00`) without allocating, and the hour component keeps growing past 24 so long-running tasks stay accurate: +- **`TimeSpan :duration` format** — the interpolated string handler understands the custom `:duration` specifier. It emits integer `hours`/`minutes`/`seconds` tokens (e.g., `5h 32m 12s`, `27h 12m 3s`, `123h 0m 0s`) without allocations, and the hour component keeps growing past 24 so long-running tasks stay accurate. Minutes/seconds are not zero-padded so the output stays compact: ```csharp var elapsed = stopwatch.Elapsed; - Console.WriteInterpolated($"Completed in {elapsed:hr}"); + Console.WriteInterpolated($"Completed in {elapsed:duration}"); // Completed in 12h 5m 33s ``` - **Alignment** — standard alignment syntax works the same way it does with regular interpolated strings, but the handler writes directly into the console buffer. This keeps columnar output zero-allocation friendly: @@ -68,13 +68,13 @@ All fields collapse to `string.Empty` when markup is disabled, so the same call Console.WriteInterpolated($"|{"Label",-10}|{value,10:0.00}|"); ``` -You can combine both, e.g., `$"{elapsed,8:hr}"`, to keep progress/status displays tidy. +You can combine both, e.g., `$"{elapsed,8:duration}"`, to keep progress/status displays tidy. ### Basic outputs ```csharp // Interpolated text -Console.WriteInterpolated($"Processed {items} items in {elapsed:hr}"); +Console.WriteInterpolated($"Processed {items} items in {elapsed:duration}"); // Processed 42 items in 3h 44m 9s Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Magenta}debug{ConsoleColor.Default}"); // Span + color overloads (no boxing) @@ -140,7 +140,7 @@ PrettyConsoleExtensions.Error.WriteWhiteSpaces(8); // pad status blocks ```csharp Console.Overwrite(() => { Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Cyan}Working…{ConsoleColor.Default}"); - Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.DarkGray}Elapsed:{ConsoleColor.Default} {stopwatch.Elapsed:hr}"); + Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.DarkGray}Elapsed:{ConsoleColor.Default} {stopwatch.Elapsed:duration}"); // Elapsed: 0h 1m 12s }, lines: 2); // Prevent closure allocations with state + generic overload From 0c3170c7daf5057c7b254e72182b9d4bf9cca92f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 12:53:26 +0200 Subject: [PATCH 58/75] Added bytes formatting docs --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e84ba34..d3fba49 100755 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ PrettyConsole is a high-performance, allocation-conscious extension layer over ` ## Features -* 🚀 Zero-allocation interpolated string handler (`PrettyConsoleInterpolatedStringHandler`) for inline colors and formatting -* 🎨 Inline color composition with `ConsoleColor` tuples and helpers (`DefaultForeground`, `DefaultBackground`, `Default`) -* 🔁 Advanced rendering primitives (`Overwrite`, `ClearNextLines`, `GoToLine`, progress bars) that respect console pipes -* 🧰 Rich input helpers (`TryReadLine`, `Confirm`, `RequestAnyInput`) with `IParsable` and enum support -* ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `TextWriter.WriteWhiteSpaces`) -* ⛓ Output routing through `OutputPipe.Out` and `OutputPipe.Error` so piping/redirects continue to work +- 🚀 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 +- 🧰 Rich input helpers (`TryReadLine`, `Confirm`, `RequestAnyInput`) with `IParsable` and enum support +- ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `TextWriter.WriteWhiteSpaces`) +- ⛓ Output routing through `OutputPipe.Out` and `OutputPipe.Error` so piping/redirects continue to work ## Installation @@ -62,6 +62,14 @@ All fields collapse to `string.Empty` when markup is disabled, so the same call Console.WriteInterpolated($"Completed in {elapsed:duration}"); // Completed in 12h 5m 33s ``` +- **`double :bytes` format** — pass any `double` (cast integral sizes if needed) with the `:bytes` specifier to render human-friendly binary size units. Values scale by powers of 1024 through `B`, `KB`, `MB`, `GB`, `TB`, `PB`, and use the `#,##0.##` format so thousands separators and up to two decimal digits follow the current culture: + + ```csharp + var transferred = 12_884_901d; + Console.WriteInterpolated($"Uploaded {transferred:bytes}"); // Uploaded 12.3 MB + Console.WriteInterpolated($"Remaining {remaining,8:bytes}"); // right-aligned units stay tidy + ``` + - **Alignment** — standard alignment syntax works the same way it does with regular interpolated strings, but the handler writes directly into the console buffer. This keeps columnar output zero-allocation friendly: ```csharp From 31b647cdf81e0228e730319509410ff485a2f319 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:09:12 +0200 Subject: [PATCH 59/75] formatting --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index d918d87..b7e772f 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -348,4 +348,4 @@ public void ResetColors() { /// Writes a new line to the used internally. /// public readonly void AppendNewLine() => _writer.WriteLine(); -} \ No newline at end of file +} From 06b7788bb12f017056223b1b89fe70ba5b21e79c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:25:57 +0200 Subject: [PATCH 60/75] Address larger than default file sizes by using larger buffer --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index b7e772f..71892e7 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -205,7 +205,11 @@ public readonly void AppendFormatted(double num, int alignment, string? format = } var unit = FileSizeSuffix[suffix]; - Span buffer = stackalloc char[128]; + 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); } From 77cc1c17d0c7f3f657cb67cefb8435687a8bb346 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:35:36 +0200 Subject: [PATCH 61/75] Tweak handler custom formatting for more accurate buffer management --- .../PrettyConsoleInterpolatedStringHandler.cs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 71892e7..2648350 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -152,25 +152,9 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f return; } - Span buffer = stackalloc char[128]; + 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); - return; - } - - int lowerBound = 4096; - var pool = ArrayPool.Shared; - - while (true) { - var array = pool.Rent(lowerBound); - buffer = new Span(array); - if (buffer.TryWrite($"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m {timeSpan.Seconds}s", out written)) { - AppendSpan(buffer.Slice(0, written), alignment); - pool.Return(array); - break; - } - pool.Return(array); - lowerBound *= 2; } } From 93a72425f8b6a2d351f6ed0f2709d7bb445d4bc2 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:35:53 +0200 Subject: [PATCH 62/75] Added test to cover handler custom formats --- ...tyConsoleInterpolatedStringHandlerTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs new file mode 100644 index 0000000..8b49651 --- /dev/null +++ b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -0,0 +1,58 @@ +using System.Globalization; + +namespace PrettyConsole.Tests.Unit; + +public class PrettyConsoleInterpolatedStringHandlerTests { + private readonly StringWriter _writer; + + 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) { + var timeSpan = TimeSpan.FromHours(hours) + .Add(TimeSpan.FromMinutes(minutes)) + .Add(TimeSpan.FromSeconds(seconds)); + + Console.WriteInterpolated($"Elapsed {timeSpan:duration}"); + + Assert.Equal($"Elapsed {expected}", _writer.ToStringAndFlush()); + } + + [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) { + Console.WriteInterpolated($"Size {value:bytes}"); + + Assert.Equal($"Size {FormatBytes(value)}", _writer.ToStringAndFlush()); + } + + private static string FormatBytes(double value) { + const double formatBytesKb = 1024d; + var suffix = 0; + var num = value; + + while (suffix < FileSizeSuffix.Length - 1 && num >= formatBytesKb) { + num /= formatBytesKb; + suffix++; + } + + return string.Format(CultureInfo.CurrentCulture, "{0:#,##0.##} {1}", num, FileSizeSuffix[suffix]); + } + + private static readonly string[] FileSizeSuffix = ["B", "KB", "MB", "GB", "TB", "PB"]; +} From 0411cdd795b7d1c412ff3d8bfbae22a3a15352eb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:37:02 +0200 Subject: [PATCH 63/75] Formatting --- Benchmarks/Config.cs | 6 +- Benchmarks/StyledOutputBenchmark.cs | 93 +++++++-------- .../AdvancedOutputsTests.cs | 2 +- PrettyConsole.Tests.Unit/ConsoleColorTests.cs | 2 +- PrettyConsole.Tests.Unit/GlobalUsings.cs | 2 +- .../InputRequestExtensionsTests.cs | 6 +- PrettyConsole.Tests.Unit/MarkupTests.cs | 2 +- ...tyConsoleInterpolatedStringHandlerTests.cs | 2 +- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 2 +- .../ReadLineExtensionsTests.cs | 2 +- PrettyConsole.Tests.Unit/Utilities.cs | 2 +- .../WriteExtensionsTests.cs | 2 +- .../WriteLineExtensionsTests.cs | 2 +- PrettyConsole.Tests/Program.cs | 2 +- PrettyConsole/Color.cs | 2 +- PrettyConsole/ConsoleColorExtensions.cs | 108 +++++++++--------- PrettyConsole/IndeterminateProgressBar.cs | 2 +- PrettyConsole/InputRequestExtensions.cs | 2 +- PrettyConsole/Markup.cs | 2 +- PrettyConsole/MenuExtensions.cs | 2 +- PrettyConsole/PrettyConsoleExtensions.cs | 2 +- .../PrettyConsoleInterpolatedStringHandler.cs | 14 +-- PrettyConsole/ProgressBar.cs | 2 +- PrettyConsole/RenderingExtensions.cs | 2 +- 24 files changed, 133 insertions(+), 132 deletions(-) diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index 501d188..3581fcd 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -26,12 +26,12 @@ public Config() { .WithIterationCount(20) .WithIterationTime(TimeInterval.FromMilliseconds(500))); AddColumnProvider(DefaultColumnProviders.Instance); - HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); + HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); WithOrderer(new GroupByTypeOrderer()); WithOptions(ConfigOptions.JoinSummary); WithOptions(ConfigOptions.StopOnFirstError); WithOptions(ConfigOptions.DisableLogFile); - AddExporter(MarkdownExporter.GitHub); + AddExporter(MarkdownExporter.GitHub); } } @@ -63,4 +63,4 @@ public IEnumerable> GetLogicalGroupOrder( => logicalGroups.OrderBy(g => g.Key); public bool SeparateLogicalGroups => true; -} +} \ No newline at end of file diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index ffff0b5..ce7c54d 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -1,8 +1,9 @@ using BenchmarkDotNet.Attributes; +using PrettyConsole; + using Spectre.Console; -using PrettyConsole; using static System.ConsoleColor; namespace Benchmarks; @@ -12,55 +13,55 @@ namespace Benchmarks; /// Hello {Green}John{ResetColor}, status = {Cyan}{Percentage}{Reset}%, Elapsed = {Yellow}{Elapsed:c}{Reset} /// public class StyledOutputBenchmarks { - private static readonly TimeSpan Elapsed = new(1, 25, 31); - private const double Percentage = 57.91; + private static readonly TimeSpan Elapsed = new(1, 25, 31); + private const double Percentage = 57.91; - private TextWriter _outputWriter = default!; - private IAnsiConsole _ansiConsole = default!; + private TextWriter _outputWriter = default!; + private IAnsiConsole _ansiConsole = default!; - [GlobalSetup] - public void GlobalSetup() { - _outputWriter = Console.Out; - PrettyConsoleExtensions.Out = TextWriter.Null; - _ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings { - Out = new AnsiConsoleOutput(TextWriter.Null) - }); - Console.SetOut(TextWriter.Null); - } + [GlobalSetup] + public void GlobalSetup() { + _outputWriter = Console.Out; + PrettyConsoleExtensions.Out = TextWriter.Null; + _ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings { + Out = new AnsiConsoleOutput(TextWriter.Null) + }); + Console.SetOut(TextWriter.Null); + } - [GlobalCleanup] - public void GlobalCleanup() { - PrettyConsoleExtensions.Out = _outputWriter; - _ansiConsole = AnsiConsole.Console; - Console.SetOut(_outputWriter); - } + [GlobalCleanup] + public void GlobalCleanup() { + PrettyConsoleExtensions.Out = _outputWriter; + _ansiConsole = AnsiConsole.Console; + Console.SetOut(_outputWriter); + } - [Benchmark] - public int PrettyConsole() { - Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.DefaultForeground}, status = {Cyan}{Percentage}{ConsoleColor.DefaultForeground}%, elapsed = {Yellow}{Elapsed:c}"); - return int.MaxValue; - } + [Benchmark] + public int PrettyConsole() { + Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.DefaultForeground}, status = {Cyan}{Percentage}{ConsoleColor.DefaultForeground}%, elapsed = {Yellow}{Elapsed:c}"); + return int.MaxValue; + } - [Benchmark(Baseline = true)] - public int SpectreConsole() { - _ansiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); - return int.MaxValue; - } + [Benchmark(Baseline = true)] + public int SpectreConsole() { + _ansiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]"); + return int.MaxValue; + } - [Benchmark] - public int SystemConsole() { - Console.Write("Hello "); - Console.ForegroundColor = Green; - Console.Write("John"); - Console.ResetColor(); - Console.Write(", status = "); - Console.ForegroundColor = Cyan; - Console.Write(Percentage); - Console.ResetColor(); - Console.Write("%, elapsed = "); - Console.ForegroundColor = Yellow; - Console.WriteLine("{0:c}", Elapsed); - Console.ResetColor(); - return int.MaxValue; - } + [Benchmark] + public int SystemConsole() { + Console.Write("Hello "); + Console.ForegroundColor = Green; + Console.Write("John"); + Console.ResetColor(); + Console.Write(", status = "); + Console.ForegroundColor = Cyan; + Console.Write(Percentage); + Console.ResetColor(); + Console.Write("%, elapsed = "); + Console.ForegroundColor = Yellow; + Console.WriteLine("{0:c}", Elapsed); + Console.ResetColor(); + return int.MaxValue; + } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs b/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs index 6940a3b..a3a2fa7 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputsTests.cs @@ -52,4 +52,4 @@ public async Task TypeWriteLine_Regular() { await Console.TypeWriteLine("Hello world!", Green / ConsoleColor.Default, 10); Assert.Contains("Hello world!" + Environment.NewLine, stringWriter.ToString()); } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs index c4f7b65..6cf4256 100644 --- a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs +++ b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs @@ -37,4 +37,4 @@ public void AnsiColors_DefaultBackground_UsesResetSequence() { Assert.Equal("\u001b[49m", sequence); } } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index 4f0c8ca..645853a 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -2,5 +2,5 @@ global using Xunit; +global using static System.ConsoleColor; global using static PrettyConsole.PrettyConsoleExtensions; -global using static System.ConsoleColor; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs b/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs index a0cd14a..8f820db 100644 --- a/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/InputRequestExtensionsTests.cs @@ -17,7 +17,7 @@ public void RequestAnyInput_WritesPrompt_AndInvokesReadKey() { Assert.Contains("Press something:", writer.ToString()); Assert.True(invoked); } finally { - InputRequestExtensions.ConfigureReadKey(null); - } + InputRequestExtensions.ConfigureReadKey(null); + } } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/MarkupTests.cs b/PrettyConsole.Tests.Unit/MarkupTests.cs index d33e06e..e1f9c7c 100644 --- a/PrettyConsole.Tests.Unit/MarkupTests.cs +++ b/PrettyConsole.Tests.Unit/MarkupTests.cs @@ -13,4 +13,4 @@ public void Markup_Enabled_MatchesConsoleRedirectionState() { Assert.Equal(string.Empty, Markup.Reset); } } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs index 8b49651..60571b5 100644 --- a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -55,4 +55,4 @@ private static string FormatBytes(double value) { } private static readonly string[] FileSizeSuffix = ["B", "KB", "MB", "GB", "TB", "PB"]; -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 4b22761..63a8f72 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -131,4 +131,4 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() RenderingExtensions.ConfigureCursorAccessors(null, null); } } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs index ee76c5b..eaf47bc 100755 --- a/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/ReadLineExtensionsTests.cs @@ -77,4 +77,4 @@ public void ReadLine_GenericWithDefault_ReturnsDefaultWhenInvalid() { Assert.Equal(5, value); } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index 310e6fe..175c472 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -46,4 +46,4 @@ public static void SkipIfNoInteractiveConsole() { [GeneratedRegex("\\u001b\\[[0-9;]*m", RegexOptions.Compiled)] private static partial Regex AnsiSequenceRegex(); -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs index 9fa8cd9..c243945 100755 --- a/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/WriteExtensionsTests.cs @@ -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.Tests.Unit/WriteLineExtensionsTests.cs index ed86340..4ad4478 100755 --- a/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs +++ b/PrettyConsole.Tests.Unit/WriteLineExtensionsTests.cs @@ -28,4 +28,4 @@ public void WriteLine_ReadOnlySpan_WithColors_AppendsNewLine() { Console.WriteLine("SpanLine".AsSpan(), OutputPipe.Out, ConsoleColor.Yellow, ConsoleColor.Black); Assert.Equal($"SpanLine{_writer.NewLine}", _writer.ToStringAndFlush()); } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 55a56e8..d729b3d 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -36,4 +36,4 @@ static void Measure(string label, Action action) { Console.WriteLineInterpolated($"{label} - allocated {after - before} bytes"); Console.NewLine(); } -#pragma warning restore CS8321 // Local function is declared but never used +#pragma warning restore CS8321 // Local function is declared but never used \ No newline at end of file diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index 0b7dd23..2f144ea 100644 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -88,4 +88,4 @@ private static string BuildBackgroundSequence(ConsoleColor color) { _ => BackgroundResetSequence }; } -} +} \ No newline at end of file diff --git a/PrettyConsole/ConsoleColorExtensions.cs b/PrettyConsole/ConsoleColorExtensions.cs index 9bdc7c2..b350e60 100644 --- a/PrettyConsole/ConsoleColorExtensions.cs +++ b/PrettyConsole/ConsoleColorExtensions.cs @@ -4,58 +4,58 @@ namespace PrettyConsole; /// Provides methods extending ; /// public static class ConsoleColorExtensions { - /// - /// Returns the default foreground color for the shell. - /// - public static readonly ConsoleColor DefaultForegroundColor; - - /// - /// Returns the default background color for the shell. - /// - public static readonly ConsoleColor DefaultBackgroundColor; - - static ConsoleColorExtensions() { - Console.ResetColor(); - DefaultForegroundColor = Console.ForegroundColor; - DefaultBackgroundColor = Console.BackgroundColor; - } - - extension(ConsoleColor color) { - /// - /// Returns the default foreground color for the shell. - /// - public static ConsoleColor DefaultForeground => DefaultForegroundColor; - - /// - /// Returns the default background color for the shell. - /// - public static ConsoleColor DefaultBackground => DefaultBackgroundColor; - - /// - /// Returns a tuple of (, ) - /// - public static (ConsoleColor, ConsoleColor) Default => (DefaultForegroundColor, DefaultBackgroundColor); - - /// - /// Returns a tuple of (, ) - /// - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, ConsoleColor background) { - return (foreground, background); - } - - /// - /// Returns a tuple of (, ) - /// - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, (ConsoleColor tupleForeground, ConsoleColor tupleBackground) colorTuple) { - return (foreground, colorTuple.tupleBackground); - } - } + /// + /// Returns the default foreground color for the shell. + /// + public static readonly ConsoleColor DefaultForegroundColor; + + /// + /// Returns the default background color for the shell. + /// + public static readonly ConsoleColor DefaultBackgroundColor; + + static ConsoleColorExtensions() { + Console.ResetColor(); + DefaultForegroundColor = Console.ForegroundColor; + DefaultBackgroundColor = Console.BackgroundColor; + } + + extension(ConsoleColor color) { + /// + /// Returns the default foreground color for the shell. + /// + public static ConsoleColor DefaultForeground => DefaultForegroundColor; + + /// + /// Returns the default background color for the shell. + /// + public static ConsoleColor DefaultBackground => DefaultBackgroundColor; + + /// + /// Returns a tuple of (, ) + /// + public static (ConsoleColor, ConsoleColor) Default => (DefaultForegroundColor, DefaultBackgroundColor); + + /// + /// Returns a tuple of (, ) + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, ConsoleColor background) { + return (foreground, background); + } + + /// + /// Returns a tuple of (, ) + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (ConsoleColor, ConsoleColor) operator /(ConsoleColor foreground, (ConsoleColor tupleForeground, ConsoleColor tupleBackground) colorTuple) { + return (foreground, colorTuple.tupleBackground); + } + } } \ No newline at end of file diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 433ad2c..d98a3e1 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -216,4 +216,4 @@ public static readonly ReadOnlyCollection PingPong "| • |", ]); } -} +} \ No newline at end of file diff --git a/PrettyConsole/InputRequestExtensions.cs b/PrettyConsole/InputRequestExtensions.cs index 8046f31..b873f71 100755 --- a/PrettyConsole/InputRequestExtensions.cs +++ b/PrettyConsole/InputRequestExtensions.cs @@ -65,4 +65,4 @@ public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = t return false; } } -} +} \ No newline at end of file diff --git a/PrettyConsole/Markup.cs b/PrettyConsole/Markup.cs index f248e08..bcb9da6 100644 --- a/PrettyConsole/Markup.cs +++ b/PrettyConsole/Markup.cs @@ -68,4 +68,4 @@ static Markup() { ResetStrikethrough = "\u001b[29m"; } } -} +} \ No newline at end of file diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index d3721ec..114f4a0 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -137,7 +137,7 @@ public static (string option, string subOption) TreeMenu(Dictionary /// Writes whitespace to this up to length by chucks /// diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 2648350..b6eed42 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -18,11 +18,11 @@ static PrettyConsoleInterpolatedStringHandler() { if (AnsiColors.Enabled) { ChangeFg = static (writer, color) => writer.Write(AnsiColors.Foreground(color)); ChangeBg = static (writer, color) => writer.Write(AnsiColors.Background(color)); - } else { + } else { ChangeFg = static (_, color) => Console.ForegroundColor = color; ChangeBg = static (_, color) => Console.BackgroundColor = color; - } - } + } + } /// /// Creates a new handler that writes to . @@ -104,7 +104,7 @@ public void AppendFormatted(ConsoleColor color) { if (_currentForeground != color) { _currentForeground = color; ChangeFg(_writer, _currentForeground); - } + } } /// @@ -115,11 +115,11 @@ public void AppendFormatted((ConsoleColor Foreground, ConsoleColor Background) c if (_currentForeground != colors.Foreground) { _currentForeground = colors.Foreground; ChangeFg(_writer, _currentForeground); - } + } if (_currentBackground != colors.Background) { _currentBackground = colors.Background; ChangeBg(_writer, _currentBackground); - } + } } /// @@ -336,4 +336,4 @@ public void ResetColors() { /// Writes a new line to the used internally. /// public readonly void AppendNewLine() => _writer.WriteLine(); -} +} \ No newline at end of file diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 32dbf73..7be8d1a 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -158,4 +158,4 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo Console.WriteInterpolated(pipe, $"] {p,3}%"); } -} +} \ No newline at end of file diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index ef4cc4c..bc37e3a 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -71,4 +71,4 @@ public static void GoToLine(int line) { s_setCursorPosition(0, line); } } -} +} \ No newline at end of file From 7a50e3178be390871d7010d3357c58aab49f494e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:48:17 +0200 Subject: [PATCH 64/75] Added maxWidth option to progressBar class --- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 29 +++++++++++++++++++- PrettyConsole/ProgressBar.cs | 11 ++++++-- README.md | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 63a8f72..20d6cc9 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -107,6 +107,33 @@ public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { } } + [Fact] + public void ProgressBar_Update_RespectsMaxLineWidth() { + Error = Utilities.GetWriter(out var errorWriter); + int cursorLine = 0; + const int expectedWidth = 32; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + var bar = new ProgressBar { + ProgressColor = Cyan, + MaxLineWidth = expectedWidth + }; + + bar.Update(50, "Working"); + } finally { + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + + var output = Utilities.StripAnsiSequences(errorWriter.ToString()); + int percentIndex = output.LastIndexOf('%'); + Assert.True(percentIndex > 0, "Output contains a percentage symbol."); + int bracketIndex = output.LastIndexOf('[', percentIndex); + Assert.True(bracketIndex >= 0, "Output contains a bracketed progress bar."); + + var segment = output[bracketIndex..(percentIndex + 1)]; + Assert.Equal(expectedWidth, segment.Length); + } + [Fact] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { Error = Utilities.GetWriter(out var errorWriter); @@ -131,4 +158,4 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() RenderingExtensions.ConfigureCursorAccessors(null, null); } } -} \ No newline at end of file +} diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 7be8d1a..f69f780 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -32,6 +32,11 @@ public class ProgressBar { /// public ConsoleColor ProgressColor { get; set; } = ConsoleColor.DefaultForeground; + /// + /// Gets or sets an optional total width for the rendered bar line (includes brackets, spacing, and percentage). + /// + public int? MaxLineWidth { get; set; } + private readonly Lock _lock = new(); /// @@ -84,14 +89,14 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr if (status.Length > 0) { Console.Write(status, OutputPipe.Error, ForegroundColor); PrettyConsoleExtensions.GetWriter(OutputPipe.Error).WriteWhiteSpaces(1); - WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); } } else { bool hasStatus = status.Length > 0; int lines = hasStatus ? 2 : 1; Console.ClearNextLines(lines, OutputPipe.Error); if (hasStatus) Console.WriteLine(status, OutputPipe.Error, ForegroundColor); - WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); } Console.GoToLine(currentLine); } @@ -158,4 +163,4 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo Console.WriteInterpolated(pipe, $"] {p,3}%"); } -} \ No newline at end of file +} diff --git a/README.md b/README.md index d3fba49..2200941 100755 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ progress.Update(42.5, "Syncing", sameLine: false); ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', maxLineWidth: 32); ``` -`ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. 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. +`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. #### Multiple progress bars with tasks + channels From 69a3c06809cf117d8fc67fd62ae062d8428a5015 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 13:59:48 +0200 Subject: [PATCH 65/75] Updated versions --- Versions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Versions.md b/Versions.md index 48d4b9d..8cdda06 100755 --- a/Versions.md +++ b/Versions.md @@ -27,6 +27,8 @@ This version contains a lot of breaking changes, but they were necessary to trim - `TextWriter` which is the object backing `Console.Out` and `Console.Error` now has a static extension `WriteWhiteSpaces(int)`, which can be used to write paddings and whatever else without any allocations. It was previously an internal method but I chose to expose it for all of you. - `Markup` static class provides ANSI escape-sequence toggles (underline, bold, italic, strikethrough) that automatically collapse to empty strings when output/error are redirected, so callers can opt into inline decorations without additional checks. +- `PrettyConsoleInterpolatedStringHandler` now exposes a `duration` format for `TimeSpan` values (formerly `hr`) that emits `Xh Ym Zs` and a `bytes` format for `double` values that scales through `B/KB/MB/...` with culture-aware separators. +- `ProgressBar.WriteProgressBar` (and the instance helper via `ProgressBar.MaxLineWidth`) now accept an optional `maxLineWidth` so the full `[=====] 42%` line can be constrained for columnar layouts without overflowing the buffer. ## v4.1.0 From ba4dfd5495c11fb8f1b84b7947b2009c449920eb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:23:09 +0200 Subject: [PATCH 66/75] Benchmark integration in readme draft --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 2200941..1eaa628 100755 --- a/README.md +++ b/README.md @@ -11,6 +11,18 @@ PrettyConsole is a high-performance, allocation-conscious extension layer over ` - ⚙️ Allocation-conscious span-first APIs (`ISpanFormattable`, `ReadOnlySpan`, `TextWriter.WriteWhiteSpaces`) - ⛓ Output routing through `OutputPipe.Out` and `OutputPipe.Error` so piping/redirects continue to work +## Performance + +BenchmarkDotNet (macOS/Apple M2 Pro, .NET 10) measures styled output performance for a single line write: + +| Method | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +|--------------- |------------:|--------------:|-------:|-------:|----------:|--------------:| +| PrettyConsole | 95.06 ns | 49.95x faster | - | - | - | NA | +| SpectreConsole | 4,747.67 ns | baseline | 2.1255 | 0.0191 | 17,840 B | | +| SystemConsole | 67.90 ns | 69.92x faster | 0.0028 | - | 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. See `Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md`. + ## Installation ```bash @@ -208,6 +220,10 @@ ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', ma #### Multiple progress bars with tasks + channels +### Performance + +BenchmarkDotNet shows `PrettyConsole` running the styled output scenario in ~95 ns, roughly **50× faster than Spectre.Console** while reporting zero GC allocations even against the baseline. Compared to the BCL `System.Console` version (≈67 ns with 24 B allocated), PrettyConsole still matches or beats it when counting end-to-end rendering costs because it never allocates at all, making it the go-to choice for ultra-low-latency, allocation-free console rendering. See `Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md` for the full report. + ```csharp using System.Linq; using System.Threading.Channels; From 1973d6bb626350e688fecbd8dc1071f566db566d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:33:54 +0200 Subject: [PATCH 67/75] Updated readme with results --- ...rks.StyledOutputBenchmarks-report-github.md | 14 +++++++------- Benchmarks/Config.cs | 4 ++-- Benchmarks/Program.cs | 3 +-- Benchmarks/StyledOutputBenchmark.cs | 1 + PrettyConsole/PrettyConsole.csproj | 3 ++- README.md | 18 +++++++----------- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md index a860cdd..0775371 100644 --- a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -4,14 +4,14 @@ 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-GIBNDH : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a + Job-BGFQEO : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a -OutlierMode=DontRemove IterationCount=20 IterationTime=500ms +OutlierMode=DontRemove IterationCount=30 IterationTime=100ms LaunchCount=3 WarmupCount=5 ``` -| Method | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | -|--------------- |------------:|--------------:|-------:|-------:|----------:|--------------:| -| PrettyConsole | 95.06 ns | 49.95x faster | - | - | - | NA | -| SpectreConsole | 4,747.67 ns | baseline | 2.1255 | 0.0191 | 17840 B | | -| SystemConsole | 67.90 ns | 69.92x faster | 0.0028 | - | 24 B | 743.333x less | +| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | +|--------------- |------------:|--------------:|-------:|----------:|--------------:| +| PrettyConsole | 94.37 ns | 49.96x faster | - | - | NA | +| SpectreConsole | 4,713.76 ns | baseline | 2.1278 | 17840 B | | +| SystemConsole | 68.77 ns | 68.55x faster | 0.0028 | 24 B | 743.333x less | diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index 3581fcd..2ad9687 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -23,8 +23,8 @@ public Config() { .WithOutlierMode(OutlierMode.DontRemove) .WithLaunchCount(3) .WithWarmupCount(5) - .WithIterationCount(20) - .WithIterationTime(TimeInterval.FromMilliseconds(500))); + .WithIterationCount(30) + .WithIterationTime(TimeInterval.FromMilliseconds(100))); AddColumnProvider(DefaultColumnProviders.Instance); HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); WithOrderer(new GroupByTypeOrderer()); diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs index 5dcbf51..9fd021c 100644 --- a/Benchmarks/Program.cs +++ b/Benchmarks/Program.cs @@ -2,5 +2,4 @@ using Benchmarks; -var customConfig = new Config(); -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, customConfig); \ No newline at end of file +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/Benchmarks/StyledOutputBenchmark.cs b/Benchmarks/StyledOutputBenchmark.cs index ce7c54d..6237f20 100644 --- a/Benchmarks/StyledOutputBenchmark.cs +++ b/Benchmarks/StyledOutputBenchmark.cs @@ -12,6 +12,7 @@ namespace Benchmarks; /// Runs a benchmark printing the following output: /// Hello {Green}John{ResetColor}, status = {Cyan}{Percentage}{Reset}%, Elapsed = {Yellow}{Elapsed:c}{Reset} /// +[Config(typeof(Config))] public class StyledOutputBenchmarks { private static readonly TimeSpan Elapsed = new(1, 25, 31); private const double Percentage = 57.91; diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index aef724d..7dee638 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -19,7 +19,8 @@ David Shnayder PrettyConsole - High performance, feature rich and easy to use wrap over System.Console + High performance, ultra-low-latency, allocation-free, feature rich and easy to use + wrap over System.Console David Shnayder Console; Output; Input; ANSI enable diff --git a/README.md b/README.md index 1eaa628..b74f6c5 100755 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ PrettyConsole is a high-performance, allocation-conscious extension layer over ` ## Performance -BenchmarkDotNet (macOS/Apple M2 Pro, .NET 10) measures styled output performance for a single line write: +BenchmarkDotNet measures [styled output performance](Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md) for a single line write: -| Method | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | -|--------------- |------------:|--------------:|-------:|-------:|----------:|--------------:| -| PrettyConsole | 95.06 ns | 49.95x faster | - | - | - | NA | -| SpectreConsole | 4,747.67 ns | baseline | 2.1255 | 0.0191 | 17,840 B | | -| SystemConsole | 67.90 ns | 69.92x faster | 0.0028 | - | 24 B | 743.333x less | +| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | +|--------------- |------------:|--------------:|-------:|----------:|--------------:| +| PrettyConsole | 94.37 ns | 49.96x faster | - | - | NA | +| SpectreConsole | 4,713.76 ns | baseline | 2.1278 | 17840 B | | +| SystemConsole | 68.77 ns | 68.55x faster | 0.0028 | 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. See `Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md`. +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. ## Installation @@ -220,10 +220,6 @@ ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', ma #### Multiple progress bars with tasks + channels -### Performance - -BenchmarkDotNet shows `PrettyConsole` running the styled output scenario in ~95 ns, roughly **50× faster than Spectre.Console** while reporting zero GC allocations even against the baseline. Compared to the BCL `System.Console` version (≈67 ns with 24 B allocated), PrettyConsole still matches or beats it when counting end-to-end rendering costs because it never allocates at all, making it the go-to choice for ultra-low-latency, allocation-free console rendering. See `Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md` for the full report. - ```csharp using System.Linq; using System.Threading.Channels; From f9785fee6947bc1aee9f0bba399334ed2e5b03bb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:40:21 +0200 Subject: [PATCH 68/75] Better benchmark configuration --- .../Benchmarks.StyledOutputBenchmarks-report-github.md | 10 +++++----- Benchmarks/Config.cs | 5 ++++- README.md | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md index 0775371..6ce8fce 100644 --- a/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md +++ b/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.StyledOutputBenchmarks-report-github.md @@ -4,14 +4,14 @@ 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-BGFQEO : .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 -OutlierMode=DontRemove IterationCount=30 IterationTime=100ms +OutlierMode=RemoveAll IterationCount=30 IterationTime=100ms LaunchCount=3 WarmupCount=5 ``` | Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | |--------------- |------------:|--------------:|-------:|----------:|--------------:| -| PrettyConsole | 94.37 ns | 49.96x faster | - | - | NA | -| SpectreConsole | 4,713.76 ns | baseline | 2.1278 | 17840 B | | -| SystemConsole | 68.77 ns | 68.55x faster | 0.0028 | 24 B | 743.333x less | +| 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 | diff --git a/Benchmarks/Config.cs b/Benchmarks/Config.cs index 2ad9687..d8400f6 100644 --- a/Benchmarks/Config.cs +++ b/Benchmarks/Config.cs @@ -5,6 +5,7 @@ using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Order; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; @@ -17,10 +18,11 @@ namespace Benchmarks; public class Config : ManualConfig { public Config() { + UnionRule = ConfigUnionRule.AlwaysUseLocal; SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); AddDiagnoser(MemoryDiagnoser.Default); AddJob(Job.Default - .WithOutlierMode(OutlierMode.DontRemove) + .WithOutlierMode(OutlierMode.RemoveAll) .WithLaunchCount(3) .WithWarmupCount(5) .WithIterationCount(30) @@ -32,6 +34,7 @@ public Config() { WithOptions(ConfigOptions.StopOnFirstError); WithOptions(ConfigOptions.DisableLogFile); AddExporter(MarkdownExporter.GitHub); + AddLogger(ConsoleLogger.Default); } } diff --git a/README.md b/README.md index b74f6c5..0f09949 100755 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ BenchmarkDotNet measures [styled output performance](Benchmarks/BenchmarkDotNet. | Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | |--------------- |------------:|--------------:|-------:|----------:|--------------:| -| PrettyConsole | 94.37 ns | 49.96x faster | - | - | NA | -| SpectreConsole | 4,713.76 ns | baseline | 2.1278 | 17840 B | | -| SystemConsole | 68.77 ns | 68.55x faster | 0.0028 | 24 B | 743.333x less | +| 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 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. From 13e084fa3e4c77761f8a94dda56705f6d1bec2f7 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:46:09 +0200 Subject: [PATCH 69/75] Update readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f09949..1fe63b3 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # PrettyConsole -PrettyConsole is a high-performance, allocation-conscious extension layer over `System.Console`. The library uses C# extension members (`extension(Console)`) so every API lights up directly on `System.Console` once `using PrettyConsole;` (and optionally `using static System.Console;`) is in scope. It targets **.NET 10.0**, is trimming/AOT ready, preserves SourceLink metadata, and keeps the familiar console experience while adding structured rendering, menus, progress bars, and advanced input helpers. +[![NuGet](https://img.shields.io/nuget/v/PrettyConsole.svg?style=flat-square)](https://www.nuget.org/packages/PrettyConsole) +[![NuGet Downloads](https://img.shields.io/nuget/dt/PrettyConsole?style=flat&label=Downloads)](https://www.nuget.org/packages/PrettyConsole) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) +[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square)](#) + +PrettyConsole is a high-performance, ultra-low-latency, allocation-free extension layer over `System.Console`. The library uses C# extension members (`extension(Console)`) so every API lights up directly on `System.Console` once `using PrettyConsole;` is in scope. It is trimming/AOT ready, preserves SourceLink metadata, and keeps the familiar console experience while adding structured rendering, menus, progress bars, and advanced input helpers. ## Features From e2c00c89e27579f2d78b24bf7279cd251bea93bf Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:48:04 +0200 Subject: [PATCH 70/75] Update release notes --- PrettyConsole/PrettyConsole.csproj | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 7dee638..050d601 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -44,20 +44,10 @@ - - ProgressBar.ForegroundColor docs were fixed (they previously were the same as - ProgressColor) which is invalid. - - ProgressBar in all variations now shows progress as a round number suffixed by %. - - ProgressBar no longer tracks if the percentage is changed, being that the numbers - are round, percentage could progress or status needs to be re-written while it stays the - same when rounded. - - ProgressBar.Update overloads now include an optional parameter "sameLine" which - configures whether to render the progress bar at the same of the status. It is set to - "true" by default to keep current behavior. - - ProgressBar now includes a static method "WriteProgressBar" which renders a static - progress bar with the set parameters, it can be used in conjunction with "Overwrite" to - create multi-progress-bars UI. - - Methods that overwrite lines, now have a note in the remarks to clear the used lines - after the last call, to prevent artifacts. + - v5.0.0 rewrites the API surface as extension members on `System.Console`, introduces `WriteInterpolated`/`WriteLineInterpolated`, and exposes helpers such as `Selection`, `Menu`, `Table`, `ReadLine`, and `TryReadLine` directly on the console. + - `ColoredOutput` and the `Color` struct were removed; styling now flows through the enhanced `ConsoleColor` helpers (`DefaultForeground`, `DefaultBackground`, `Default`, and `/` tuples) together with the interpolated string handler to keep colored output allocation-free. + - `ProgressBar` and `IndeterminateProgressBar` are standalone types, and `ProgressBar.WriteProgressBar` plus the instance helper now accept an optional `maxLineWidth` for constrained layouts. + - New utilities include `TextWriter.WriteWhiteSpaces(int)` for allocation-free padding, `Markup` for ANSI decorations that collapse when output is redirected, and formatting shortcuts such as `duration` (for `TimeSpan`) and `bytes` (for `double`) on `PrettyConsoleInterpolatedStringHandler`. From be71b4eba8b2de2ac911dea3e6c971549a5c1d00 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:51:05 +0200 Subject: [PATCH 71/75] - --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fe63b3..d5f5fd0 100755 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![NuGet](https://img.shields.io/nuget/v/PrettyConsole.svg?style=flat-square)](https://www.nuget.org/packages/PrettyConsole) [![NuGet Downloads](https://img.shields.io/nuget/dt/PrettyConsole?style=flat&label=Downloads)](https://www.nuget.org/packages/PrettyConsole) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) -[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square)](#) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](License.txt) +[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square)](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) PrettyConsole is a high-performance, ultra-low-latency, allocation-free extension layer over `System.Console`. The library uses C# extension members (`extension(Console)`) so every API lights up directly on `System.Console` once `using PrettyConsole;` is in scope. It is trimming/AOT ready, preserves SourceLink metadata, and keeps the familiar console experience while adding structured rendering, menus, progress bars, and advanced input helpers. From 3b5ea08c41223d5885646c3213045dc7b06d94ce Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 14:52:35 +0200 Subject: [PATCH 72/75] Formatting --- PrettyConsole.Tests.Unit/GlobalUsings.cs | 4 +--- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 2 +- PrettyConsole/PrettyConsole.csproj | 1 - PrettyConsole/ProgressBar.cs | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index 645853a..3d73b08 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -1,6 +1,4 @@ -global using PrettyConsole; - global using Xunit; global using static System.ConsoleColor; -global using static PrettyConsole.PrettyConsoleExtensions; +global using static PrettyConsole.PrettyConsoleExtensions; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 20d6cc9..b2289e4 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -158,4 +158,4 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() RenderingExtensions.ConfigureCursorAccessors(null, null); } } -} +} \ No newline at end of file diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 050d601..2695024 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -2,7 +2,6 @@ net10.0 latest - true true true diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index f69f780..755220c 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -163,4 +163,4 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo Console.WriteInterpolated(pipe, $"] {p,3}%"); } -} +} \ No newline at end of file From 90f28bfae6ee8e837fb9c3fe885bfaa4b52f6b0b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 15:17:18 +0200 Subject: [PATCH 73/75] Use cleaner escape sequences --- PrettyConsole.Tests.Unit/ConsoleColorTests.cs | 4 +- PrettyConsole.Tests.Unit/MarkupTests.cs | 4 +- PrettyConsole/AnsiColors.cs | 91 +++++++++++++++++++ PrettyConsole/Color.cs | 91 ------------------- PrettyConsole/Markup.cs | 18 ++-- 5 files changed, 104 insertions(+), 104 deletions(-) create mode 100644 PrettyConsole/AnsiColors.cs delete mode 100644 PrettyConsole/Color.cs diff --git a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs index 6cf4256..b575a75 100644 --- a/PrettyConsole.Tests.Unit/ConsoleColorTests.cs +++ b/PrettyConsole.Tests.Unit/ConsoleColorTests.cs @@ -26,7 +26,7 @@ public void ConsoleColor_DefaultColors() { public void AnsiColors_DefaultForeground_UsesResetSequence() { if (AnsiColors.Enabled) { var sequence = AnsiColors.Foreground((ConsoleColor)(-1)); - Assert.Equal("\u001b[39m", sequence); + Assert.Equal("\e[39m", sequence); } } @@ -34,7 +34,7 @@ public void AnsiColors_DefaultForeground_UsesResetSequence() { public void AnsiColors_DefaultBackground_UsesResetSequence() { if (AnsiColors.Enabled) { var sequence = AnsiColors.Background((ConsoleColor)(-1)); - Assert.Equal("\u001b[49m", sequence); + 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 e1f9c7c..eea4b9c 100644 --- a/PrettyConsole.Tests.Unit/MarkupTests.cs +++ b/PrettyConsole.Tests.Unit/MarkupTests.cs @@ -6,8 +6,8 @@ public void Markup_Enabled_MatchesConsoleRedirectionState() { var expectedEnabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; Assert.Equal(expectedEnabled, Markup.Enabled); if (expectedEnabled) { - Assert.Equal("\u001b[4m", Markup.Underline); - Assert.Equal("\u001b[0m", Markup.Reset); + 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); diff --git a/PrettyConsole/AnsiColors.cs b/PrettyConsole/AnsiColors.cs new file mode 100644 index 0000000..4c2dc21 --- /dev/null +++ b/PrettyConsole/AnsiColors.cs @@ -0,0 +1,91 @@ +namespace PrettyConsole; + +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; + + static AnsiColors() { + Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; + if (!Enabled) return; + + ForegroundCodes = new string[16]; + BackgroundCodes = new string[16]; + foreach (var color in Enum.GetValues()) { + int index = (int)color; + ForegroundCodes[index] = BuildForegroundSequence(color); + BackgroundCodes[index] = BuildBackgroundSequence(color); + } + } + + /// + /// Gets the ANSI sequence for the specified foreground color or an empty string when disabled. + /// + public static string Foreground(ConsoleColor color) { + int index = (int)color; + if (index == -1) return ForegroundResetSequence; + return ForegroundCodes[index]; + } + + + /// + /// Gets the ANSI sequence for the specified background color or an empty string when disabled. + /// + public static string Background(ConsoleColor color) { + int index = (int)color; + if (index == -1) return BackgroundResetSequence; + return BackgroundCodes[index]; + } + + + private static string BuildForegroundSequence(ConsoleColor color) { + return color switch { + ConsoleColor.Black => "\e[30m", + ConsoleColor.DarkBlue => "\e[34m", + ConsoleColor.DarkGreen => "\e[32m", + ConsoleColor.DarkCyan => "\e[36m", + ConsoleColor.DarkRed => "\e[31m", + ConsoleColor.DarkMagenta => "\e[35m", + ConsoleColor.DarkYellow => "\e[33m", + ConsoleColor.Gray => "\e[37m", + ConsoleColor.DarkGray => "\e[90m", + ConsoleColor.Blue => "\e[94m", + ConsoleColor.Green => "\e[92m", + ConsoleColor.Cyan => "\e[96m", + ConsoleColor.Red => "\e[91m", + ConsoleColor.Magenta => "\e[95m", + ConsoleColor.Yellow => "\e[93m", + ConsoleColor.White => "\e[97m", + _ => ForegroundResetSequence + }; + } + + private static string BuildBackgroundSequence(ConsoleColor color) { + return color switch { + ConsoleColor.Black => "\e[40m", + ConsoleColor.DarkBlue => "\e[44m", + ConsoleColor.DarkGreen => "\e[42m", + ConsoleColor.DarkCyan => "\e[46m", + ConsoleColor.DarkRed => "\e[41m", + ConsoleColor.DarkMagenta => "\e[45m", + ConsoleColor.DarkYellow => "\e[43m", + ConsoleColor.Gray => "\e[47m", + ConsoleColor.DarkGray => "\e[100m", + ConsoleColor.Blue => "\e[104m", + ConsoleColor.Green => "\e[102m", + ConsoleColor.Cyan => "\e[106m", + ConsoleColor.Red => "\e[101m", + ConsoleColor.Magenta => "\e[105m", + ConsoleColor.Yellow => "\e[103m", + ConsoleColor.White => "\e[107m", + _ => BackgroundResetSequence + }; + } +} \ No newline at end of file diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs deleted file mode 100644 index 2f144ea..0000000 --- a/PrettyConsole/Color.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace PrettyConsole; - -internal static class AnsiColors { - private const string ForegroundResetSequence = "\u001b[39m"; - private const string BackgroundResetSequence = "\u001b[49m"; - - private static readonly string[] ForegroundCodes = default!; - private static readonly string[] BackgroundCodes = default!; - - /// - /// Gets a value indicating whether ANSI color sequences are emitted. - /// - public static readonly bool Enabled; - - static AnsiColors() { - Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; - if (!Enabled) return; - - ForegroundCodes = new string[16]; - BackgroundCodes = new string[16]; - foreach (var color in Enum.GetValues()) { - int index = (int)color; - ForegroundCodes[index] = BuildForegroundSequence(color); - BackgroundCodes[index] = BuildBackgroundSequence(color); - } - } - - /// - /// Gets the ANSI sequence for the specified foreground color or an empty string when disabled. - /// - public static string Foreground(ConsoleColor color) { - int index = (int)color; - if (index == -1) return ForegroundResetSequence; - return ForegroundCodes[index]; - } - - - /// - /// Gets the ANSI sequence for the specified background color or an empty string when disabled. - /// - public static string Background(ConsoleColor color) { - int index = (int)color; - if (index == -1) return BackgroundResetSequence; - return BackgroundCodes[index]; - } - - - private static string BuildForegroundSequence(ConsoleColor color) { - return color switch { - ConsoleColor.Black => "\u001b[30m", - ConsoleColor.DarkBlue => "\u001b[34m", - ConsoleColor.DarkGreen => "\u001b[32m", - ConsoleColor.DarkCyan => "\u001b[36m", - ConsoleColor.DarkRed => "\u001b[31m", - ConsoleColor.DarkMagenta => "\u001b[35m", - ConsoleColor.DarkYellow => "\u001b[33m", - ConsoleColor.Gray => "\u001b[37m", - ConsoleColor.DarkGray => "\u001b[90m", - ConsoleColor.Blue => "\u001b[94m", - ConsoleColor.Green => "\u001b[92m", - ConsoleColor.Cyan => "\u001b[96m", - ConsoleColor.Red => "\u001b[91m", - ConsoleColor.Magenta => "\u001b[95m", - ConsoleColor.Yellow => "\u001b[93m", - ConsoleColor.White => "\u001b[97m", - _ => ForegroundResetSequence - }; - } - - private static string BuildBackgroundSequence(ConsoleColor color) { - return color switch { - ConsoleColor.Black => "\u001b[40m", - ConsoleColor.DarkBlue => "\u001b[44m", - ConsoleColor.DarkGreen => "\u001b[42m", - ConsoleColor.DarkCyan => "\u001b[46m", - ConsoleColor.DarkRed => "\u001b[41m", - ConsoleColor.DarkMagenta => "\u001b[45m", - ConsoleColor.DarkYellow => "\u001b[43m", - ConsoleColor.Gray => "\u001b[47m", - ConsoleColor.DarkGray => "\u001b[100m", - ConsoleColor.Blue => "\u001b[104m", - ConsoleColor.Green => "\u001b[102m", - ConsoleColor.Cyan => "\u001b[106m", - ConsoleColor.Red => "\u001b[101m", - ConsoleColor.Magenta => "\u001b[105m", - ConsoleColor.Yellow => "\u001b[103m", - ConsoleColor.White => "\u001b[107m", - _ => BackgroundResetSequence - }; - } -} \ No newline at end of file diff --git a/PrettyConsole/Markup.cs b/PrettyConsole/Markup.cs index bcb9da6..8de7863 100644 --- a/PrettyConsole/Markup.cs +++ b/PrettyConsole/Markup.cs @@ -57,15 +57,15 @@ public static class Markup { static Markup() { Enabled = !Console.IsOutputRedirected && !Console.IsErrorRedirected; if (Enabled) { - Reset = "\u001b[0m"; - Underline = "\u001b[4m"; - ResetUnderline = "\u001b[24m"; - Bold = "\u001b[1m"; - ResetBold = "\u001b[22m"; - Italic = "\u001b[3m"; - ResetItalic = "\u001b[23m"; - Strikethrough = "\u001b[9m"; - ResetStrikethrough = "\u001b[29m"; + 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"; } } } \ No newline at end of file From ae6a42d4ac7138b9255196f30cdf07e49897d1cb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 15:17:45 +0200 Subject: [PATCH 74/75] Cleanup --- PrettyConsole.Tests.Unit/MenusTests.cs | 4 ++-- ...tyConsoleInterpolatedStringHandlerTests.cs | 2 +- PrettyConsole.Tests.Unit/Utilities.cs | 20 +------------------ PrettyConsole/ConsoleColorExtensions.cs | 2 +- PrettyConsole/PrettyConsoleExtensions.cs | 6 +++--- .../PrettyConsoleInterpolatedStringHandler.cs | 2 +- PrettyConsole/RenderingExtensions.cs | 2 +- 7 files changed, 10 insertions(+), 28 deletions(-) diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.Tests.Unit/MenusTests.cs index 771cbca..e796dea 100644 --- a/PrettyConsole.Tests.Unit/MenusTests.cs +++ b/PrettyConsole.Tests.Unit/MenusTests.cs @@ -12,8 +12,6 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { var output = writer.ToStringAndFlush(); - static string Normalize(string value) => value.Replace("\r\n", "\n"); - Assert.Equal( """ Choose a fruit: @@ -25,6 +23,8 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { """.Replace("\r\n", "\n"), Normalize(output)); Assert.Equal("Banana", result); + + static string Normalize(string value) => value.Replace("\r\n", "\n"); } [Fact] diff --git a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs index 60571b5..0428e76 100644 --- a/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs +++ b/PrettyConsole.Tests.Unit/PrettyConsoleInterpolatedStringHandlerTests.cs @@ -54,5 +54,5 @@ private static string FormatBytes(double value) { return string.Format(CultureInfo.CurrentCulture, "{0:#,##0.##} {1}", num, FileSizeSuffix[suffix]); } - private static readonly string[] FileSizeSuffix = ["B", "KB", "MB", "GB", "TB", "PB"]; + private static ReadOnlySpan FileSizeSuffix => new[] { "B", "KB", "MB", "GB", "TB", "PB" }; } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index 175c472..594ecd1 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -18,8 +18,6 @@ public static string ToStringAndFlush(this StringWriter writer) { return result; } - public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); - public static string StripAnsiSequences(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; @@ -28,22 +26,6 @@ public static string StripAnsiSequences(string value) { return AnsiSequenceRegex().Replace(value, string.Empty); } - public static void SkipIfNoInteractiveConsole() { - const string reason = "Interactive console APIs are not available in this environment."; - - if (Console.IsOutputRedirected) { - Assert.Skip(reason); - } - - try { - _ = Console.CursorTop; - } catch (IOException) { - Assert.Skip(reason); - } catch (PlatformNotSupportedException) { - Assert.Skip(reason); - } - } - - [GeneratedRegex("\\u001b\\[[0-9;]*m", RegexOptions.Compiled)] + [GeneratedRegex(@"\u001b\[[0-9;]*m", RegexOptions.Compiled)] private static partial Regex AnsiSequenceRegex(); } \ No newline at end of file diff --git a/PrettyConsole/ConsoleColorExtensions.cs b/PrettyConsole/ConsoleColorExtensions.cs index b350e60..4da91fc 100644 --- a/PrettyConsole/ConsoleColorExtensions.cs +++ b/PrettyConsole/ConsoleColorExtensions.cs @@ -20,7 +20,7 @@ static ConsoleColorExtensions() { DefaultBackgroundColor = Console.BackgroundColor; } - extension(ConsoleColor color) { + extension(ConsoleColor) { /// /// Returns the default foreground color for the shell. /// diff --git a/PrettyConsole/PrettyConsoleExtensions.cs b/PrettyConsole/PrettyConsoleExtensions.cs index 083649d..f5a27b9 100755 --- a/PrettyConsole/PrettyConsoleExtensions.cs +++ b/PrettyConsole/PrettyConsoleExtensions.cs @@ -19,9 +19,9 @@ public void WriteWhiteSpaces(int length) { ReadOnlySpan whiteSpaces = WhiteSpaces; while (length > 0) { - int cur_length = Math.Min(length, 256); - @this.Write(whiteSpaces.Slice(0, cur_length)); - length -= cur_length; + int curLength = Math.Min(length, 256); + @this.Write(whiteSpaces.Slice(0, curLength)); + length -= curLength; } } } diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index b6eed42..8036fea 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -158,7 +158,7 @@ public readonly void AppendFormatted(TimeSpan timeSpan, int alignment, string? f } } - private static readonly string[] FileSizeSuffix = ["B", "KB", "MB", "GB", "TB", "PB"]; + private static ReadOnlySpan FileSizeSuffix => new[] { "B", "KB", "MB", "GB", "TB", "PB" }; /// /// Append double with optional formatting. diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index bc37e3a..88f6ae7 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -3,7 +3,7 @@ namespace PrettyConsole; /// /// Provides methods extending with more rendering methods. /// -public static partial class RenderingExtensions { +public static class RenderingExtensions { private static readonly Func DefaultCursorTopAccessor = static () => Console.CursorTop; private static readonly Action DefaultSetCursorPosition = Console.SetCursorPosition; From 8b46a761fcc796fba90ae96e5f85f060205b15b6 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 15 Nov 2025 15:34:40 +0200 Subject: [PATCH 75/75] Skip progress bar tests if CI doesn't have access to cursor handle --- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index b2289e4..2cf4a08 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -1,8 +1,21 @@ namespace PrettyConsole.Tests.Unit; public class ProgressBarTests { + private readonly bool _consoleAvailable; + + public ProgressBarTests() { + try { + _ = Console.BufferWidth; + _ = Console.CursorLeft; + _consoleAvailable = true; + } catch (IOException) { + _consoleAvailable = false; + } + } + [Fact] public void ProgressBar_Update_WritesStatusAndPercentage() { + Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -26,6 +39,7 @@ public void ProgressBar_Update_WritesStatusAndPercentage() { [Fact] public void ProgressBar_Update_SamePercentage_RerendersOutput() { + Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -52,6 +66,7 @@ public void ProgressBar_Update_SamePercentage_RerendersOutput() { [Fact] public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { + Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); var originalError = Error; int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); @@ -75,6 +90,7 @@ public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { [Fact] public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { + Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); @@ -92,6 +108,7 @@ public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { [Fact] public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { + Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); @@ -109,6 +126,7 @@ public void ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { [Fact] public void ProgressBar_Update_RespectsMaxLineWidth() { + Assert.SkipWhen(!_consoleAvailable, "Console handle unavailable for this environment."); Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; const int expectedWidth = 32; @@ -136,6 +154,7 @@ public void ProgressBar_Update_RespectsMaxLineWidth() { [Fact] 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);