From 544d682f8e2ae68f3a31f898216fffde35c3d02b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 16:36:58 +0300 Subject: [PATCH 001/108] - --- .gitattributes | 63 -------------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100755 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100755 index 1ff0c42..0000000 --- a/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain From 39792c0580e2fed87c3d769b65efde820008a81e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 16:37:04 +0300 Subject: [PATCH 002/108] update rules --- .editorconfig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1b7d88b..30024f0 100755 --- a/.editorconfig +++ b/.editorconfig @@ -77,12 +77,17 @@ dotnet_remove_unnecessary_suppression_exclusions = none #### C# Coding Conventions #### [*.cs] # analyzers -dotnet_diagnostic.IDE0290.severity = none # use primary constuctor +dotnet_diagnostic.IDE0290.severity = none # use primary constructor dotnet_diagnostic.IDE0028.severity = none # use collection expression dotnet_diagnostic.IDE0056.severity = none # simplify index operator dotnet_diagnostic.IDE0057.severity = none # use range operator +dotnet_diagnostic.IDE0301.severity = none # simplify collection initialization +dotnet_diagnostic.IDE0053.severity = none # expression body lambda +dotnet_diagnostic.IDE0046.severity = none # simplify if(s) - conditional operator +dotnet_diagnostic.IDE0305.severity = none # [, ...] instead of .ToArray() -# namespace decleration + +# namespace declaration csharp_style_namespace_declarations = file_scoped:warning # var preferences From ae2b5ef433324fc237a14b5a6f87c8d233c7ce88 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 16:38:10 +0300 Subject: [PATCH 003/108] - --- PrettyConsole/PrettyConsole.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 2bfc794..6e672f1 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -26,6 +26,12 @@ + + + + + + From 3f439be034415336e35d0c99906c99f4ea73bbdc Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 16:38:39 +0300 Subject: [PATCH 004/108] - --- PrettyConsole/PrettyConsole.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 6e672f1..bc99b49 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -1,5 +1,4 @@  - net9.0;net8.0 David Shnayder From a2712c7dfdcdfebdf412c6149c444f857bb1a712 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 16:44:13 +0300 Subject: [PATCH 005/108] clamp percentage --- PrettyConsole/ProgressBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 6c51a4e..18a21a1 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -71,6 +71,8 @@ public ProgressBar() { /// The status text to be displayed after the progress bar. public void Update(double percentage, ReadOnlySpan status) { lock (_lock) { + percentage = Math.Clamp(percentage, 0, 100); + if (status.Length is 0) { status = Utils.FormatPercentage(percentage, _percentageBuffer); } From c8e7616ca775ccc28bcfc76c4a0a172240207066 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:21:21 +0300 Subject: [PATCH 006/108] Optimizations and safe-guards --- PrettyConsole/ProgressBar.cs | 41 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 18a21a1..1208a90 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Buffers; +using System.Runtime.CompilerServices; namespace PrettyConsole; @@ -30,26 +31,16 @@ public class ProgressBar { /// public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; - private readonly char[] _percentageBuffer; + private readonly char[] _percentageBuffer = new char[20]; private int _currentProgress = 0; - private readonly char[] _pBuffer; - #if NET9_0_OR_GREATER private readonly Lock _lock = new(); #else private readonly object _lock = new(); #endif - /// - /// Represents a progress bar that can be displayed in the console. - /// - public ProgressBar() { - _pBuffer = new char[baseConsole.BufferWidth]; - _percentageBuffer = new char[20]; - } - /// /// Updates the progress bar with the specified percentage. /// @@ -72,27 +63,39 @@ public ProgressBar() { public void Update(double percentage, ReadOnlySpan status) { lock (_lock) { percentage = Math.Clamp(percentage, 0, 100); + var bufferWidth = baseConsole.BufferWidth; + using var buffer = MemoryPool.Shared.Rent(bufferWidth); if (status.Length is 0) { status = Utils.FormatPercentage(percentage, _percentageBuffer); } - int pLength = baseConsole.BufferWidth - status.Length - 5; - var p = (int)(pLength * percentage * 0.01); + int pLength = Math.Max(0, bufferWidth - status.Length - 5); + var p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); if (p == _currentProgress) { return; } _currentProgress = p; + ResetColors(); baseConsole.ForegroundColor = ForegroundColor; var currentLine = GetCurrentLine(); ClearNextLines(1, OutputPipe.Error); Error.Write('['); baseConsole.ForegroundColor = ProgressColor; - Span span = _pBuffer.AsSpan(0, p); - span.Fill(ProgressChar); - Span end = WhiteSpace.AsSpan(0, pLength - p); - Error.Write(span); - Error.Write(end); + + Span span = buffer.Memory.Span; + Span progressSpan = span.Slice(0, p); + progressSpan.Fill(ProgressChar); + + var tailLength = Math.Max(0, pLength - p); + if (tailLength > 0) { + Span whiteSpaceSpan = span.Slice(p, tailLength); + whiteSpaceSpan.Fill(' '); + p += tailLength; + } + + Error.Write(span.Slice(0, p)); + baseConsole.ForegroundColor = ForegroundColor; Error.Write("] "); Error.Write(status); From 7ec4e80ee9143937431b0ea3e33f7bc4a5564d1c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:21:32 +0300 Subject: [PATCH 007/108] Removed whitespace buffer --- PrettyConsole/Console.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/PrettyConsole/Console.cs b/PrettyConsole/Console.cs index 495994b..799d075 100755 --- a/PrettyConsole/Console.cs +++ b/PrettyConsole/Console.cs @@ -9,10 +9,4 @@ namespace PrettyConsole; [UnsupportedOSPlatform("browser")] [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] -public static partial class Console { - internal static readonly char[] WhiteSpace = new char[256]; - - static Console() { - WhiteSpace.AsSpan().Fill(' '); - } -} +public static partial class Console; \ No newline at end of file From 92ae2c4972b99ec25062d6e28bffac404c1eb4f4 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:21:46 +0300 Subject: [PATCH 008/108] Added constant whitespace and method for safe writing --- PrettyConsole/Utils.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 69df861..835aa86 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -31,6 +31,40 @@ internal static ReadOnlySpan FormatPercentage(double percentage, Span + /// Constant buffer filled with whitespaces + /// + private static readonly string WhiteSpaces = new(' ', 256); + + /// + /// Writes whitespace to a up to length by chucks + /// + /// + /// + internal static void WriteWhiteSpace(this TextWriter writer, int length) { + if (length <= 0) { + return; + } + + // Fast path: single call when length fits in the buffer + if (length <= WhiteSpaces.Length) { + writer.Write(WhiteSpaces.AsSpan(0, length)); + return; + } + + // Write full chunks + var full = WhiteSpaces; + while (length >= full.Length) { + writer.Write(full); // writes all 256 in one call + length -= full.Length; + } + + // Write the remainder + if (length > 0) { + writer.Write(full.AsSpan(0, length)); + } + } + /// /// Rents a memory owner from the shared memory pool /// From 442269bfd30785b582e807b44424d3c9650b9ec9 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:29:40 +0300 Subject: [PATCH 009/108] Added method to get writer based on OutputPipe --- PrettyConsole/ConsolePipes.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index 01f47c6..7a37850 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -15,4 +15,15 @@ public static partial class Console { /// The standard input stream. /// public static TextReader In { get; internal set; } = baseConsole.In; + + /// + /// Gets the appropriate based on + /// + /// + /// + internal static TextWriter GetWriter(OutputPipe pipe) + => pipe switch { + OutputPipe.Error => Error, + _ => Out + }; } From 356ba3f15a5ae019f7395c9b8beaf76b4be41481 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:29:57 +0300 Subject: [PATCH 010/108] Improved ClearNextLines and NewLine --- PrettyConsole/RenderingControls.cs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs index eb40eac..bbc5d66 100755 --- a/PrettyConsole/RenderingControls.cs +++ b/PrettyConsole/RenderingControls.cs @@ -10,18 +10,13 @@ public static partial class Console { /// Useful for clearing output of overriding functions, like the ProgressBar /// public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { - if (pipe == OutputPipe.Error) { - InternalClearNextLines(lines, Error); - return; - } - InternalClearNextLines(lines, Out); - return; + InternalClearNextLines(lines, GetWriter(pipe)); static void InternalClearNextLines(int lines, TextWriter writer) { - ReadOnlySpan emptyLine = WhiteSpace.AsSpan(0, baseConsole.BufferWidth); + var lineLength = baseConsole.BufferWidth; var currentLine = GetCurrentLine(); for (int i = 0; i < lines; i++) { - writer.Write(emptyLine); + writer.WriteWhiteSpace(lineLength); } GoToLine(currentLine); } @@ -30,19 +25,13 @@ static void InternalClearNextLines(int lines, TextWriter writer) { /// /// Used to clear all previous outputs to the console /// - public static void Clear() { - baseConsole.Clear(); - } + 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) { - if (pipe == OutputPipe.Out) { - Out.WriteLine(); - return; - } - Error.WriteLine(); + GetWriter(pipe).WriteLine(); } /// From 6ade6f74971d557ca7b542fb9425492f82449916 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:32:38 +0300 Subject: [PATCH 011/108] Added release note --- PrettyConsole/PrettyConsole.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index bc99b49..d353be5 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -27,7 +27,8 @@ - + - Fixed possible issues in progress bar that could by caused by writing to buffer out of bounds + - From d469e6cd8f56d888cd8c0222206207aa0fd1842f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:38:04 +0300 Subject: [PATCH 012/108] Fix bounds issue --- PrettyConsole/IndeterminateProgressBar.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index fc77ec5..b5a327c 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -16,8 +16,10 @@ public static partial class Console { /// /// public class IndeterminateProgressBar { - // Constant pattern containing the characters needed for the indeterminate progress bar - private const string Twirl = "-\\|/"; + /// + /// Contains the characters that will be iterated through while running + /// + public string Twirl { get; set; } = "-\\|/"; // A whitespace the length of 10 spaces private const string ExtraBuffer = " "; @@ -97,7 +99,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d ResetColors(); var originalColor = baseConsole.ForegroundColor; var startTime = Stopwatch.GetTimestamp(); - var lineNum = GetCurrentLine(); + // var lineNum = GetCurrentLine(); while (!task.IsCompleted && !token.IsCancellationRequested) { // Await until the TaskAwaiter informs of completion @@ -119,10 +121,11 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } Error.Write(ExtraBuffer); - GoToLine(lineNum); await Task.Delay(UpdateRate, token); // The update rate - Error.Write(WhiteSpace.AsSpan(0, baseConsole.BufferWidth)); - GoToLine(lineNum); + ClearNextLines(1, OutputPipe.Error); + // GoToLine(lineNum); + // Error.Write(WhiteSpace.AsSpan(0, baseConsole.BufferWidth)); + // GoToLine(lineNum); if (token.IsCancellationRequested) { return; } From 49852bffaf61002342f186933bb0ccf9c14a079a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:38:10 +0300 Subject: [PATCH 013/108] Update release note --- PrettyConsole/PrettyConsole.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index d353be5..48466c2 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -27,8 +27,8 @@ - - Fixed possible issues in progress bar that could by caused by writing to buffer out of bounds - - + - Fixed possible issues with out-of-bounds buffer writing that affected ClearNextLines, and both ProgressBar`s + - IndeterminateProgressBar.Twirl is now customizable From 7476203d852dcad50f7cc38e9aa56f3b58ff37a8 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:42:37 +0300 Subject: [PATCH 014/108] Removed StringBuffer from some menus --- PrettyConsole/Menus.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 033a22f..b2599c6 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -22,12 +22,8 @@ public static string Selection(ReadOnlySpan title, TList c Span buffer = stackalloc char[baseConsole.BufferWidth]; for (int i = 0; i < choices.Count; i++) { - var builder = StringBuffer.Create(buffer); - builder.Append(" "); - builder.Append(i + 1); - builder.Append(") "); - builder.Append(choices[i]); - Out.WriteLine(builder.WrittenSpan); + buffer.TryWrite($" {i + 1}) {choices[i]}", out var written); + Out.WriteLine(buffer.Slice(0, written)); } NewLine(); @@ -61,12 +57,8 @@ public static string[] MultiSelection(ReadOnlySpan title, Span buffer = stackalloc char[baseConsole.BufferWidth]; for (int i = 0; i < choices.Count; i++) { - var builder = StringBuffer.Create(buffer); - builder.Append(" "); - builder.Append(i + 1); - builder.Append(") "); - builder.Append(choices[i]); - Out.WriteLine(builder.WrittenSpan); + buffer.TryWrite($" {i + 1}) {choices[i]}", out var written); + Out.WriteLine(buffer.Slice(0, written)); } NewLine(); From ca10b3d969818763c10ec2056bb68d64038aed81 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:49:48 +0300 Subject: [PATCH 015/108] Updated treeMenu --- PrettyConsole/Menus.cs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index b2599c6..3443161 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -109,33 +109,29 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan x.Length) + 10; // Used to make sub-tree prefix spaces uniform - Span buffer = stackalloc char[baseConsole.BufferWidth]; - Span emptySpaces = stackalloc char[maxMainOption]; - emptySpaces.Fill(' '); + using var memOwner = MemoryPool.Shared.Rent(baseConsole.BufferWidth); + Span buffer = memOwner.Memory.Span; //Enumerate options and sub-options for (int i = 0; i < menuKeys.Length; i++) { var mainEntry = menuKeys[i]; var subChoices = menu[mainEntry]; - var builder = StringBuffer.Create(buffer); - builder.Append(" "); - builder.Append(i + 1); - builder.Append(") "); - builder.Append(mainEntry); - var remainingLength = maxMainOption - builder.Position; - builder.Append(emptySpaces.Slice(0, remainingLength)); - Out.Write(builder.WrittenSpan); + + buffer.TryWrite($" {i + 1}) {mainEntry}", out int written); + Out.Write(buffer.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.Write(emptySpaces); + Out.WriteWhiteSpaces(maxMainOption); } - builder.Reset(); - builder.Append(" "); - builder.Append(j + 1); - builder.Append(") "); - builder.Append(subChoices[j]); - Out.WriteLine(builder.WrittenSpan); + buffer.TryWrite($" {j + 1}) {subChoices[j]}", out written); + Out.WriteLine(buffer.Slice(0, written)); } NewLine(); From d75ccfa6625155ee3612033612c2cf3ad8eac0fe Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:49:59 +0300 Subject: [PATCH 016/108] Added s to WriteWhiteSpaces --- PrettyConsole/RenderingControls.cs | 2 +- PrettyConsole/Utils.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs index bbc5d66..e80a288 100755 --- a/PrettyConsole/RenderingControls.cs +++ b/PrettyConsole/RenderingControls.cs @@ -16,7 +16,7 @@ static void InternalClearNextLines(int lines, TextWriter writer) { var lineLength = baseConsole.BufferWidth; var currentLine = GetCurrentLine(); for (int i = 0; i < lines; i++) { - writer.WriteWhiteSpace(lineLength); + writer.WriteWhiteSpaces(lineLength); } GoToLine(currentLine); } diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 835aa86..7dc8e04 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -41,7 +41,7 @@ internal static ReadOnlySpan FormatPercentage(double percentage, Span /// /// - internal static void WriteWhiteSpace(this TextWriter writer, int length) { + internal static void WriteWhiteSpaces(this TextWriter writer, int length) { if (length <= 0) { return; } From 45dceeb760c7c6241700d0cdb1ca2ed3395c8080 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 17:53:16 +0300 Subject: [PATCH 017/108] Simplified some methods --- PrettyConsole/Write.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index e938e84..3501a3b 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -89,11 +89,7 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor /// background color public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { SetColors(foreground, background); - if (pipe == OutputPipe.Out) { - Out.Write(span); - } else { - Error.Write(span); - } + GetWriter(pipe).Write(span); ResetColors(); } @@ -107,11 +103,7 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor /// public static void Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out) { SetColors(output.ForegroundColor, output.BackgroundColor); - if (pipe == OutputPipe.Out) { - Out.Write(output.Value); - } else { - Error.Write(output.Value); - } + GetWriter(pipe).Write(output.Value); ResetColors(); } From e5741ebe637bdafe1dcac19dd267c095afa0e66d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 18:18:41 +0300 Subject: [PATCH 018/108] Removed Sharpify dependency --- PrettyConsole/IndeterminateProgressBar.cs | 6 +- PrettyConsole/Menus.cs | 2 - PrettyConsole/PrettyConsole.csproj | 6 +- PrettyConsole/Utils.cs | 72 +++++++++++++++++++---- 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index b5a327c..284c5fc 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -99,7 +99,6 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d ResetColors(); var originalColor = baseConsole.ForegroundColor; var startTime = Stopwatch.GetTimestamp(); - // var lineNum = GetCurrentLine(); while (!task.IsCompleted && !token.IsCancellationRequested) { // Await until the TaskAwaiter informs of completion @@ -116,16 +115,13 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d if (DisplayElapsedTime) { var elapsed = Stopwatch.GetElapsedTime(startTime); Error.Write(" [Elapsed: "); - Error.Write(Sharpify.Utils.DateAndTime.FormatTimeSpan(elapsed, _buffer)); + Error.Write(Utils.FormatTimeSpan(elapsed, _buffer)); Error.Write(']'); } Error.Write(ExtraBuffer); await Task.Delay(UpdateRate, token); // The update rate ClearNextLines(1, OutputPipe.Error); - // GoToLine(lineNum); - // Error.Write(WhiteSpace.AsSpan(0, baseConsole.BufferWidth)); - // GoToLine(lineNum); if (token.IsCancellationRequested) { return; } diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 3443161..795afc6 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -1,8 +1,6 @@ using System.Buffers; using System.Runtime.InteropServices; -using Sharpify.Collections; - namespace PrettyConsole; public static partial class Console { diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 48466c2..287504a 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -25,16 +25,16 @@ - + - Fixed possible issues with out-of-bounds buffer writing that affected ClearNextLines, and both ProgressBar`s - IndeterminateProgressBar.Twirl is now customizable + - Sharpify is no longer a dependency - + - diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 7dc8e04..5b8ba65 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -1,7 +1,5 @@ using System.Buffers; -using Sharpify.Collections; - namespace PrettyConsole; /// @@ -16,19 +14,67 @@ internal static class Utils { /// internal static ReadOnlySpan FormatPercentage(double percentage, Span buffer) { const int length = 5; - var rounded = Math.Round(percentage, 2); - var builder = StringBuffer.Create(buffer); - builder.Append(rounded); - if (builder.Position is length) { - return buffer.Slice(0, length); + percentage = Math.Round(Math.Clamp(percentage, 0, 100), 2, MidpointRounding.AwayFromZero); + + percentage.TryFormat(buffer, out int written); + if (written == length) { + return buffer.Slice(0, written); } - var padding = length - builder.Position; - builder.Reset(); - while (padding-- > 0) { - builder.Append(' '); + + var padding = length - written; + buffer.Slice(0, padding).Fill(' '); + percentage.TryFormat(buffer.Slice(padding), out written); + + return buffer.Slice(0, padding + written); + } + + /// + /// Formats + /// + /// + /// + /// + internal static ReadOnlySpan FormatTimeSpan(TimeSpan timeSpan, Span buffer) { + // < 1s → "500ms" + int written; + + if (timeSpan.TotalSeconds < 1) { + if (!timeSpan.Milliseconds.TryFormat(buffer, out written)) { + return ReadOnlySpan.Empty; + } + "ms".CopyTo(buffer.Slice(written)); + return buffer.Slice(0, written + 2); + } + + // < 60s → "SS:MMMs" (zero-padded) + if (timeSpan.TotalSeconds < 60) { + if (!buffer.TryWrite($"{timeSpan.Seconds:00}:{timeSpan.Milliseconds:000}s", out written)) { + return ReadOnlySpan.Empty; + } + return buffer.Slice(0, written); + } + + // < 1h → "MM:SSm" + if (timeSpan.TotalSeconds < 3600) { + if (!buffer.TryWrite($"{timeSpan.Minutes:00}:{timeSpan.Seconds:00}m", out written)) { + return ReadOnlySpan.Empty; + } + return buffer.Slice(0, written); + } + + // < 1d → "HH:MMhr" + if (timeSpan.TotalSeconds < 86400) { + if (!buffer.TryWrite($"{timeSpan.Hours:00}:{timeSpan.Minutes:00}hr", out written)) { + return ReadOnlySpan.Empty; + } + return buffer.Slice(0, written); + } + + // ≥ 1d → "DD:HHd" + if (!buffer.TryWrite($"{timeSpan.Days:00}:{timeSpan.Hours:00}d", out written)) { + return ReadOnlySpan.Empty; } - builder.Append(rounded); - return builder.WrittenSpan; + return buffer.Slice(0, written); } /// From 39aa445de08f7be4ba88125d9f9bba942cf45757 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 18:41:56 +0300 Subject: [PATCH 019/108] updated unit tests to Xunit3 --- PrettyConsole.Tests.Unit/AdvancedInputs.cs | 16 +++++----- PrettyConsole.Tests.Unit/AdvancedOutputs.cs | 4 +-- PrettyConsole.Tests.Unit/AssemblyInfo.cs | 1 - PrettyConsole.Tests.Unit/GlobalUsings.cs | 1 - .../PrettyConsole.Tests.Unit.csproj | 29 +++++++------------ PrettyConsole.Tests.Unit/ReadLine.cs | 16 +++++----- PrettyConsole.Tests.Unit/Write.cs | 16 +++++----- PrettyConsole.Tests.Unit/WriteLine.cs | 8 ++--- PrettyConsole.Tests.Unit/xunit.runner.json | 7 +++++ 9 files changed, 47 insertions(+), 51 deletions(-) delete mode 100755 PrettyConsole.Tests.Unit/AssemblyInfo.cs create mode 100644 PrettyConsole.Tests.Unit/xunit.runner.json diff --git a/PrettyConsole.Tests.Unit/AdvancedInputs.cs b/PrettyConsole.Tests.Unit/AdvancedInputs.cs index 074fdf3..e1d5a24 100755 --- a/PrettyConsole.Tests.Unit/AdvancedInputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedInputs.cs @@ -7,8 +7,8 @@ public void Confirm_Case_Y() { var reader = Utilities.GetReader("y"); In = reader; var res = Confirm(["Enter y" * Color.White]); - stringWriter.ToString().Should().Contain("Enter y"); - res.Should().BeTrue(); + Assert.Contains("Enter y", stringWriter.ToString()); + Assert.True(res); } [Fact] @@ -17,8 +17,8 @@ public void Confirm_Case_Yes() { var reader = Utilities.GetReader("yes"); In = reader; var res = Confirm(["Enter yes" * Color.White]); - stringWriter.ToString().Should().Contain("Enter yes"); - res.Should().BeTrue(); + Assert.Contains("Enter yes", stringWriter.ToString()); + Assert.True(res); } [Fact] @@ -27,8 +27,8 @@ public void Confirm_Case_Empty() { var reader = Utilities.GetReader(""); In = reader; var res = Confirm(["Enter yes" * Color.White]); - stringWriter.ToString().Should().Contain("Enter yes"); - res.Should().BeTrue(); + Assert.Contains("Enter yes", stringWriter.ToString()); + Assert.True(res); } [Fact] @@ -37,7 +37,7 @@ public void Confirm_Case_No() { var reader = Utilities.GetReader("no"); In = reader; var res = Confirm(["Enter no" * Color.White]); - stringWriter.ToString().Should().Contain("Enter no"); - res.Should().BeFalse(); + Assert.Contains("Enter no", stringWriter.ToString()); + Assert.False(res); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs index a85c0e4..1bea034 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs @@ -5,13 +5,13 @@ public class AdvancedOutputs { public async Task TypeWrite_Regular() { Out = Utilities.GetWriter(out var stringWriter); await TypeWrite("Hello world!" * Color.Green, 10); - stringWriter.ToString().Should().Contain("Hello world!"); + 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); - stringWriter.ToString().Should().Contain("Hello world!" + Environment.NewLine); + Assert.Contains("Hello world!" + Environment.NewLine, stringWriter.ToString()); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/AssemblyInfo.cs b/PrettyConsole.Tests.Unit/AssemblyInfo.cs deleted file mode 100755 index b0b47aa..0000000 --- a/PrettyConsole.Tests.Unit/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index cee7dfb..652c576 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -1,4 +1,3 @@ global using static PrettyConsole.Console; global using PrettyConsole; global using Xunit; -global using FluentAssertions; diff --git a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj index 26e5df1..89973e6 100755 --- a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj +++ b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj @@ -1,31 +1,22 @@ - net9.0;net8.0 enable enable - false - true - - true + Exe + net9.0;net8.0 + true + true - - Windows - + + + - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + diff --git a/PrettyConsole.Tests.Unit/ReadLine.cs b/PrettyConsole.Tests.Unit/ReadLine.cs index 40fef46..22ab539 100755 --- a/PrettyConsole.Tests.Unit/ReadLine.cs +++ b/PrettyConsole.Tests.Unit/ReadLine.cs @@ -6,7 +6,7 @@ public void ReadLine_String_NoOutput() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello world!"); In = reader; - ReadLine().Should().Be("Hello world!"); + Assert.Equal("Hello world!", ReadLine()); } [Fact] @@ -14,7 +14,7 @@ public void ReadLine_String_WithOutput() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello world!"); In = reader; - ReadLine(["Enter something:"]).Should().Be("Hello world!"); + Assert.Equal("Hello world!", ReadLine(["Enter something:"])); } [Fact] @@ -22,7 +22,7 @@ public void ReadLine_Int() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("5"); In = reader; - ReadLine(["Enter num:"]).Should().Be(5); + Assert.Equal(5, ReadLine(["Enter num:"])); } [Fact] @@ -30,7 +30,7 @@ public void ReadLine_Int_InvalidWithDefault() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello"); In = reader; - ReadLine(["Enter num:"], 5).Should().Be(5); + Assert.Equal(5, ReadLine(["Enter num:"], 5)); } [Fact] @@ -38,8 +38,8 @@ public void TryReadLine_Int_InvalidWithDefault() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello"); In = reader; - TryReadLine(["Enter num:"], 5, out int num).Should().BeFalse(); - num.Should().Be(5); + Assert.False(TryReadLine(["Enter num:"], 5, out int num)); + Assert.Equal(5, num); } [Fact] @@ -47,7 +47,7 @@ public void TryReadLine_Enum_IgnoreCase() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("bLack"); In = reader; - TryReadLine(["Enter color:"], true, out ConsoleColor color).Should().BeTrue(); - color.Should().Be(ConsoleColor.Black); + Assert.True(TryReadLine(["Enter color:"], true, out ConsoleColor color)); + Assert.Equal(ConsoleColor.Black, color); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/Write.cs b/PrettyConsole.Tests.Unit/Write.cs index aad3ef8..220ebd7 100755 --- a/PrettyConsole.Tests.Unit/Write.cs +++ b/PrettyConsole.Tests.Unit/Write.cs @@ -12,48 +12,48 @@ public Write() { [Fact] public void Write_SpanFormattable_NoColors() { Write(3.14); - _writer.ToStringAndFlush().Should().Be("3.14"); + Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_ForegroundColor() { Write(3.14, OutputPipe.Out, Color.White); - _writer.ToStringAndFlush().Should().Be("3.14"); + Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_ForegroundAndBackgroundColor() { Write(3.14, OutputPipe.Out, Color.White, Color.Black); - _writer.ToStringAndFlush().Should().Be("3.14"); + Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_ColoredOutput_Single() { Write("Hello world!" * Color.Green); - _writer.ToStringAndFlush().Should().Be("Hello world!"); + Assert.Equal("Hello world!", _writer.ToStringAndFlush()); } [Fact] public void Write_ColoredOutput_Multiple() { Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"]); - _writer.ToStringAndFlush().Should().Be("Hello David!"); + Assert.Equal("Hello David!", _writer.ToStringAndFlush()); } [Fact] public void WriteError_ColoredOutput_Single() { Write("Hello world!" * Color.Yellow, OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello world!"); + Assert.Equal("Hello world!", _errorWriter.ToStringAndFlush()); } [Fact] public void WriteError_ColoredOutput_Single2() { Write(["Hello world!" * Color.Green], OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello world!"); + Assert.Equal("Hello world!", _errorWriter.ToStringAndFlush()); } [Fact] public void WriteError_ColoredOutput_Multiple() { Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello David!"); + Assert.Equal("Hello David!", _errorWriter.ToStringAndFlush()); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/WriteLine.cs b/PrettyConsole.Tests.Unit/WriteLine.cs index 3b2a648..01512b2 100755 --- a/PrettyConsole.Tests.Unit/WriteLine.cs +++ b/PrettyConsole.Tests.Unit/WriteLine.cs @@ -12,24 +12,24 @@ public WriteLine() { [Fact] public void WriteLine_ColoredOutput_Single() { WriteLine("Hello world!" * Color.Green); - _writer.ToStringAndFlush().Should().Be("Hello world!".WithNewLine()); + Assert.Equal("Hello world!".WithNewLine(), _writer.ToStringAndFlush()); } [Fact] public void WriteLine_ColoredOutput_Multiple() { WriteLine(["Hello " * Color.Green, "David" * Color.Yellow, "!"]); - _writer.ToStringAndFlush().Should().Be("Hello David!".WithNewLine()); + Assert.Equal("Hello David!".WithNewLine(), _writer.ToStringAndFlush()); } [Fact] public void WriteLineError_ColoredOutput_Single() { WriteLine("Hello world!" * Color.Green, OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello world!".WithNewLine()); + Assert.Equal("Hello world!".WithNewLine(), _errorWriter.ToStringAndFlush()); } [Fact] public void WriteLineError_ColoredOutput_Multiple() { WriteLine(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello David!".WithNewLine()); + Assert.Equal("Hello David!".WithNewLine(), _errorWriter.ToStringAndFlush()); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/xunit.runner.json b/PrettyConsole.Tests.Unit/xunit.runner.json new file mode 100644 index 0000000..9acde45 --- /dev/null +++ b/PrettyConsole.Tests.Unit/xunit.runner.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true, + "parallelizeAssembly": false, + "parallelizeTestCollections": false, + "showLiveOutput": true +} \ No newline at end of file From d3a9347debd8eb6d791b34f709ece0ac8a6b69f6 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 18:42:33 +0300 Subject: [PATCH 020/108] Updated version --- PrettyConsole/PrettyConsole.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 287504a..9d463c0 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -10,7 +10,7 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 3.1.0 + 3.2.0 enable true MIT From 5ac5d95c3b81816c91d5ec26397b7b326b7049a8 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 18:46:03 +0300 Subject: [PATCH 021/108] Updated GitHub workflow --- .github/workflows/UnitTests.yaml | 57 +++++--------------------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/.github/workflows/UnitTests.yaml b/.github/workflows/UnitTests.yaml index 9cb4634..ae68182 100755 --- a/.github/workflows/UnitTests.yaml +++ b/.github/workflows/UnitTests.yaml @@ -2,56 +2,17 @@ name: Unit Tests on: pull_request: - workflow_dispatch: # Allows the workflow to be triggered manually + workflow_dispatch: jobs: - - run-tests: - - runs-on: ${{ matrix.os }} + unit-tests: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - configuration: [Debug, Release] - - env: - # Define the path to project and test project - PROJECT_PATH: PrettyConsole/PrettyConsole.csproj - TEST_PROJECT_PATH: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj - - steps: - # 1. Checkout the repository code - - name: Checkout Repository - uses: actions/checkout@v4 - - # 2. Cache NuGet packages - - name: Cache NuGet Packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-nuget- - - # 3. Setup .NET - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - # 4. Clean - - name: Clean - run: | - dotnet clean ${{ env.PROJECT_PATH }} -c ${{ matrix.configuration }} - dotnet clean ${{ env.TEST_PROJECT_PATH }} -c ${{ matrix.configuration }} - - # 5. Restore dependencies - - name: Restore Dependencies - run: | - dotnet restore ${{ env.PROJECT_PATH }} - dotnet restore ${{ env.TEST_PROJECT_PATH }} - - # 6. Run Unit Tests - - name: Run Unit Tests - run: dotnet test ${{ env.TEST_PROJECT_PATH }} -c ${{ matrix.configuration }} \ No newline at end of file + platform: [ubuntu-latest, windows-latest, macos-latest] + framework: [8.0.x, 9.0.x] + uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main + with: + platform: ${{ matrix.platform }} + dotnet-version: ${{ matrix.framework }} + test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj \ No newline at end of file From 3fd8081e7f2f1052b52be32755f0d5b89fd1e4e5 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 19:39:49 +0300 Subject: [PATCH 022/108] More array pooling to reduce stack pressure on large console buffers --- PrettyConsole/Menus.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 795afc6..13d597b 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -17,7 +17,8 @@ public static string Selection(ReadOnlySpan title, TList c where TList : IList { WriteLine(title); - Span buffer = stackalloc char[baseConsole.BufferWidth]; + using var memoryOwner = MemoryPool.Shared.Rent(baseConsole.BufferWidth); + Span buffer = memoryOwner.Memory.Span; for (int i = 0; i < choices.Count; i++) { buffer.TryWrite($" {i + 1}) {choices[i]}", out var written); @@ -52,7 +53,8 @@ public static string[] MultiSelection(ReadOnlySpan title, where TList : IList { WriteLine(title); - Span buffer = stackalloc char[baseConsole.BufferWidth]; + using var memoryOwner = MemoryPool.Shared.Rent(baseConsole.BufferWidth); + Span buffer = memoryOwner.Memory.Span; for (int i = 0; i < choices.Count; i++) { buffer.TryWrite($" {i + 1}) {choices[i]}", out var written); From 81142f426f029926fb824621b58b9909dd3c9e9f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 19:39:59 +0300 Subject: [PATCH 023/108] Moved versions to root --- PrettyConsole/Versions.md => Versions.md | 208 ++++++++++++----------- 1 file changed, 108 insertions(+), 100 deletions(-) rename PrettyConsole/Versions.md => Versions.md (55%) diff --git a/PrettyConsole/Versions.md b/Versions.md similarity index 55% rename from PrettyConsole/Versions.md rename to Versions.md index 5aa7f5d..c24dad7 100755 --- a/PrettyConsole/Versions.md +++ b/Versions.md @@ -1,107 +1,115 @@ # CHANGELOG +## v3.2.0 + +- Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. Possibly effected: + - `ProgressBar` and `IndeterminateProgressBar` + - `ClearNextLines` +- `IndeterminateProgressBar` will no allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. +- Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient + ## v3.1.0 -* Updated to support .NET 9.0 -* Updated to use `Sharpify 2.5.0` -* `ProgressBar` was redesigned since it randomly produced artifacts such as printing the header multiple times. - * Now the buffer area of `ProgressBar` is only 1 line, the (now "status") is printed on the same line, after the bar - * Only if the header is empty, the percentage is printed instead. - * An internal lock is now also used to prevent race conditions. -* `ClearNextLines` show now work properly, previously it could actually clear 1 line too many. +- Updated to support .NET 9.0 +- Updated to use `Sharpify 2.5.0` +- `ProgressBar` was redesigned since it randomly produced artifacts such as printing the header multiple times. + - Now the buffer area of `ProgressBar` is only 1 line, the (now "status") is printed on the same line, after the bar + - Only if the header is empty, the percentage is printed instead. + - An internal lock is now also used to prevent race conditions. +- `ClearNextLines` show now work properly, previously it could actually clear 1 line too many. ### Breaking changes -* `OutputPipe` enum was added to unify APIs -* `ClearNextLinesError` was removed, use `ClearNextLines` with `OutputPipe.Error` instead -* `NewLineError` was removed, use `NewLine` with `OutputPipe.Error` instead -* `WriteError` was removed, use `Write` with `OutputPipe.Error` instead -* `WriteLineError` was removed, use `WriteLine` with `OutputPipe.Error` instead -* `OverrideCurrentLine` now has an option to choose the output pipe, use `OutputPipe.Error` by default -* `Write` had the same treatment, now it has an option to choose the output pipe, use `OutputPipe.Out` by default, so the overload of `WriteError` were removed -* `WriteLine` and `WriteLine(ReadOnlySpan)` were introduced thanks to this change +- `OutputPipe` enum was added to unify APIs +- `ClearNextLinesError` was removed, use `ClearNextLines` with `OutputPipe.Error` instead +- `NewLineError` was removed, use `NewLine` with `OutputPipe.Error` instead +- `WriteError` was removed, use `Write` with `OutputPipe.Error` instead +- `WriteLineError` was removed, use `WriteLine` with `OutputPipe.Error` instead +- `OverrideCurrentLine` now has an option to choose the output pipe, use `OutputPipe.Error` by default +- `Write` had the same treatment, now it has an option to choose the output pipe, use `OutputPipe.Out` by default, so the overload of `WriteError` were removed +- `WriteLine` and `WriteLine(ReadOnlySpan)` were introduced thanks to this change ## v3.0.0 -* Removed `Write` and `WriteLine` overloads that contains multiple `ColoredOutput`s, when using the overload with `ReadOnlySpan` and `CollectionExpression`, the compiler generates an inline array to hold the elements, this is very efficient, and the reduced complexity allows usage of multiple `ColoredOutput`s in more places. -* All overloads of all functions that previously took only one `ColoredOutput` now take a `ReadOnlySpan` instead, as noted it will use an inline array, and the internal implementation also has fast paths for size 1. -* All fast changing outputs (`ProgressBar`, `IndeterminateProgressBar`, `OverrideCurrentLine`) now uses the error output pipe by default, this means that if the consumer will pipe the output to another cli, it won't be filled with garbage and retain only the valuable stuff. -* Added `ReadOnlySpan` overloads to `WriteError` and `WriteLineError`. -* Added overloads for all variants of `ReadLine` and `TryReadLine` that support `T @default` that will be returned if parsing fails. -* `Write`, `WriteLine`, `WriteError` and `WriteLineError` no longer have `params ColoredOutput[]` overloads, they instead have a `ReadOnlySpan` overload. This performs even better as it uses an inline array, and the removal of the restrictions on where `params` can be used, allowed `ReadOnlySpan` to replace virtually allow single `ColoredOutput` methods as well. Allowing greater customization in any function. -* You can now access the `In`, `Out`, and `Error` streams of `System.Console` directly from `PrettyConsole.Console` by using the properties `Out`, `Error`, and `In`. This reduces verbosity since the names of the classes have collisions. -* Added `SetColors` to allow setting the colors of the console output. -* `ClearNextLines` now also has a `ClearNextLinesError` brother which does the same for the `Error` stream. -* `NewLineError` was also added for this. -* To enhance customization in extreme high perf scenarios where you write a `ReadOnlySpan` directly to the output stream, the `Write` and `WriteError` methods now have overloads that take a `ReadOnlySpan` instead of a `ReadOnlySpan`, along with foreground and background colors. -* Added `Write` and `WriteError` variants for `T : ISpanFormattable` as well. -* `ProgressBar` should now be even more performant as internal progress tracking allowed cutting the required operations by 20%. -* `Color` will now contain `static readonly ConsoleColor` fields for both `Foreground` and `Background` colors, they will be initialized during runtime to support all platforms (fixing the instant crashes on Windows). - * You can also refer them when you want to use the defaults for any reason. -* The base methods which are used for outputting `ReadOnlySpan` have been re-written to reduce assembly instructions, leading to about 15-20% runtime improvements across the board, and reducing by the binary size by a few bytes too lol. -* `Console` and `Color` now have the correct attributes to disallow compilation on unsupported platforms, if anyone tries to use them now (even thought it should've been obvious that they shouldn't be used on unsupported platforms), it should display the right message at build time. -* Removed all overloads that have options to change the input colors, it invites non-streamlined usage, and just increases the code complexity to maintain. Without them the code is simpler and more performant. -* A lot of methods that used white-spaces in any shape or form were now optimized using pre-initialized static buffer. -* `IndeterminateProgressBar` now has overloads that accept a `string header` that can be displayed before the progress char. There also was a change involving a secondary addition of catching the `CancellationToken`, removing up to 5 internal iterations. -* Added `GetCurrentLine` and `GoToLine` methods to allow efficiently using the same space in the console for continuous output, such as progress outputting, and general control flow. +- Removed `Write` and `WriteLine` overloads that contains multiple `ColoredOutput`s, when using the overload with `ReadOnlySpan` and `CollectionExpression`, the compiler generates an inline array to hold the elements, this is very efficient, and the reduced complexity allows usage of multiple `ColoredOutput`s in more places. +- All overloads of all functions that previously took only one `ColoredOutput` now take a `ReadOnlySpan` instead, as noted it will use an inline array, and the internal implementation also has fast paths for size 1. +- All fast changing outputs (`ProgressBar`, `IndeterminateProgressBar`, `OverrideCurrentLine`) now uses the error output pipe by default, this means that if the consumer will pipe the output to another cli, it won't be filled with garbage and retain only the valuable stuff. +- Added `ReadOnlySpan` overloads to `WriteError` and `WriteLineError`. +- Added overloads for all variants of `ReadLine` and `TryReadLine` that support `T @default` that will be returned if parsing fails. +- `Write`, `WriteLine`, `WriteError` and `WriteLineError` no longer have `params ColoredOutput[]` overloads, they instead have a `ReadOnlySpan` overload. This performs even better as it uses an inline array, and the removal of the restrictions on where `params` can be used, allowed `ReadOnlySpan` to replace virtually allow single `ColoredOutput` methods as well. Allowing greater customization in any function. +- You can now access the `In`, `Out`, and `Error` streams of `System.Console` directly from `PrettyConsole.Console` by using the properties `Out`, `Error`, and `In`. This reduces verbosity since the names of the classes have collisions. +- Added `SetColors` to allow setting the colors of the console output. +- `ClearNextLines` now also has a `ClearNextLinesError` brother which does the same for the `Error` stream. +- `NewLineError` was also added for this. +- To enhance customization in extreme high perf scenarios where you write a `ReadOnlySpan` directly to the output stream, the `Write` and `WriteError` methods now have overloads that take a `ReadOnlySpan` instead of a `ReadOnlySpan`, along with foreground and background colors. +- Added `Write` and `WriteError` variants for `T : ISpanFormattable` as well. +- `ProgressBar` should now be even more performant as internal progress tracking allowed cutting the required operations by 20%. +- `Color` will now contain `static readonly ConsoleColor` fields for both `Foreground` and `Background` colors, they will be initialized during runtime to support all platforms (fixing the instant crashes on Windows). + - You can also refer them when you want to use the defaults for any reason. +- The base methods which are used for outputting `ReadOnlySpan` have been re-written to reduce assembly instructions, leading to about 15-20% runtime improvements across the board, and reducing by the binary size by a few bytes too lol. +- `Console` and `Color` now have the correct attributes to disallow compilation on unsupported platforms, if anyone tries to use them now (even thought it should've been obvious that they shouldn't be used on unsupported platforms), it should display the right message at build time. +- Removed all overloads that have options to change the input colors, it invites non-streamlined usage, and just increases the code complexity to maintain. Without them the code is simpler and more performant. +- A lot of methods that used white-spaces in any shape or form were now optimized using pre-initialized static buffer. +- `IndeterminateProgressBar` now has overloads that accept a `string header` that can be displayed before the progress char. There also was a change involving a secondary addition of catching the `CancellationToken`, removing up to 5 internal iterations. +- Added `GetCurrentLine` and `GoToLine` methods to allow efficiently using the same space in the console for continuous output, such as progress outputting, and general control flow. ## v2.1.1 -* Removed redirection of `ReadOnlySpan` print overload that degraded performance. -* Improved internal handling of array and memory pooling. -* Decreased formatted length limit for `ISpanFormattable` to 50 characters, as it was too long and frequent calls could cause the os to lag. Also changed to use rented buffer instead of stack allocation. +- Removed redirection of `ReadOnlySpan` print overload that degraded performance. +- Improved internal handling of array and memory pooling. +- Decreased formatted length limit for `ISpanFormattable` to 50 characters, as it was too long and frequent calls could cause the os to lag. Also changed to use rented buffer instead of stack allocation. ## v2.1.0 -* Fixed default colors, previously colors where defaulted to `Color.Gray` for foreground and `Color.Black` for background, however many shells have custom colors that they render by default, which means the default colors seemed to render in a non ordinary fashion. The default colors now are `Color.Unknown` which will actually use the colors of the shell. -* `ProgressBar` now implements `IDisposable` since the implementation has been optimized, make sure to dispose of it, lest you induce a penalty on other such optimization which are now spread across the library. -* Introduced an overload to `Write` that accepts any `T : ISpanFormattable`. In many cases users might want to print structs or other types that implement this interface such as `int`, `double`, `DateTime` and more... splitting the output into a few lines and using this overload will enable you completely avoid the string allocation for this object, the overload is very optimized, writes it directly to the stack and prints it using a span. However, this limitation means that if the formatted item is longer than 256 characters, an exception will be thrown indicating that you should use a different overload. +- Fixed default colors, previously colors where defaulted to `Color.Gray` for foreground and `Color.Black` for background, however many shells have custom colors that they render by default, which means the default colors seemed to render in a non ordinary fashion. The default colors now are `Color.Unknown` which will actually use the colors of the shell. +- `ProgressBar` now implements `IDisposable` since the implementation has been optimized, make sure to dispose of it, lest you induce a penalty on other such optimization which are now spread across the library. +- Introduced an overload to `Write` that accepts any `T : ISpanFormattable`. In many cases users might want to print structs or other types that implement this interface such as `int`, `double`, `DateTime` and more... splitting the output into a few lines and using this overload will enable you completely avoid the string allocation for this object, the overload is very optimized, writes it directly to the stack and prints it using a span. However, this limitation means that if the formatted item is longer than 256 characters, an exception will be thrown indicating that you should use a different overload. ## v2.0.0 -* All previous variants of output types were removed, now the main output type is `ColoredOutput` (which includes a string, foreground color and background color). +- All previous variants of output types were removed, now the main output type is `ColoredOutput` (which includes a string, foreground color and background color). Thanks to implicit and other converters and the added supporting `Color` class, usage is even simpler, and much more performant. -* Overloads of many functions were changed, many were deleted. -* `ReadLine` and new overloads `TryReadLine` now support reflection free parsing for any `IParsable` +- Overloads of many functions were changed, many were deleted. +- `ReadLine` and new overloads `TryReadLine` now support reflection free parsing for any `IParsable` implementing types (which are most of the base types, and you can create any custom implementations if you choose so). -* `ReadLine` now also has an `Enum` overload that can parse for enums, with configurable case sensitivity. -* Many functions, especially more advance outputs such as selections, menus and progress bar, had undergone tremendous performance optimizations, and show now perform extremely efficiently. -* A new table view implementation was added. -* The progress bar types `ProgressBar` and `IndeterminateProgressBar` are now classes, and also have been substantially optimized. +- `ReadLine` now also has an `Enum` overload that can parse for enums, with configurable case sensitivity. +- Many functions, especially more advance outputs such as selections, menus and progress bar, had undergone tremendous performance optimizations, and show now perform extremely efficiently. +- A new table view implementation was added. +- The progress bar types `ProgressBar` and `IndeterminateProgressBar` are now classes, and also have been substantially optimized. ## v1.6.1 -* `TextRenderingScheme` type was added for better handling of outputs consisting of mixed colors -* `TypeWrite` now has a `TextRenderingScheme` overload and the `delay` in all overloads was increased to 200 (ms) to look more natural -* `Write`, `WriteLine`, `ReadLine` and `OverrideCurrentLine` now also have overloads with `TextRenderingScheme`, and they are the recommended ones for mixed color use, using them instead of the `params array` overload may allow further optimization during JIT compilation -* `Write` and `WriteLine` now have overloads for `ReadOnlySpan` for even better performance -* More `try-finally` blocks have been implemented to further reduce the possibility of render failure upon exceptions -* More methods have been re-implemented to call their overloads for better maintainability and consistency -* More methods were marked as `Pure` to provide more information to the end users -* After testing it became rather clear, the best way to avoid glitches in frequently updated outputs inside event handlers, such as `ProgressBar`, `OverrideCurrentLine` and the likes is to firstly create an early return based on elapsed time after previous render, secondly, Use the `[MethodImpl(MethodImplOptions.Synchronized)]` On the event, and if the output is a Task, use `.Wait` instead of making the event async and awaiting it -* `ProgressBarDisplay` was modified to be a `record struct` instead of `ref struct`, This is aimed to increase usage flexibility which is limited with `ref struct`s, Also it may increase performance in edge cases due to higher potential for compiler optimization +- `TextRenderingScheme` type was added for better handling of outputs consisting of mixed colors +- `TypeWrite` now has a `TextRenderingScheme` overload and the `delay` in all overloads was increased to 200 (ms) to look more natural +- `Write`, `WriteLine`, `ReadLine` and `OverrideCurrentLine` now also have overloads with `TextRenderingScheme`, and they are the recommended ones for mixed color use, using them instead of the `params array` overload may allow further optimization during JIT compilation +- `Write` and `WriteLine` now have overloads for `ReadOnlySpan` for even better performance +- More `try-finally` blocks have been implemented to further reduce the possibility of render failure upon exceptions +- More methods have been re-implemented to call their overloads for better maintainability and consistency +- More methods were marked as `Pure` to provide more information to the end users +- After testing it became rather clear, the best way to avoid glitches in frequently updated outputs inside event handlers, such as `ProgressBar`, `OverrideCurrentLine` and the likes is to firstly create an early return based on elapsed time after previous render, secondly, Use the `[MethodImpl(MethodImplOptions.Synchronized)]` On the event, and if the output is a Task, use `.Wait` instead of making the event async and awaiting it +- `ProgressBarDisplay` was modified to be a `record struct` instead of `ref struct`, This is aimed to increase usage flexibility which is limited with `ref struct`s, Also it may increase performance in edge cases due to higher potential for compiler optimization ## v1.6.0 -* Re-structured `Console` as a `static partial class` into many files to separate the code into categorized section for better maintainability and improved workflow for adding new features -* Removed `synchronized method` compiler enforcements that could in some case limit usage flexibility when intentionally using `Task`s, if that feature is needed, simply create your own wrapper method with the attribute -* Update all places where the color of the output is changed to use `try-finally` block to ensure color reset even if an exception was thrown, which before could cause bugged colors -* Added more safeguards in key places -* Improved performance of various methods -* Merged closely related method implementations to reduce possibility of future errors -* **[POSSIBLE BREAKING CHANGE]** `ProgressBarDisplay` has been restructured to safeguard against improper initialization. Also a `ProgressChar` property was added to allow further customization of the progress bar look -* Added `TypeWrite` and `TypeWriteLine` methods that provide a simple type-writer effect for text. -* Fixed issue in `RequestAnyInput` that could read a pre-existing character from the input stream and accept it, basically skipping the entire request. +- Re-structured `Console` as a `static partial class` into many files to separate the code into categorized section for better maintainability and improved workflow for adding new features +- Removed `synchronized method` compiler enforcements that could in some case limit usage flexibility when intentionally using `Task`s, if that feature is needed, simply create your own wrapper method with the attribute +- Update all places where the color of the output is changed to use `try-finally` block to ensure color reset even if an exception was thrown, which before could cause bugged colors +- Added more safeguards in key places +- Improved performance of various methods +- Merged closely related method implementations to reduce possibility of future errors +- **[POSSIBLE BREAKING CHANGE]** `ProgressBarDisplay` has been restructured to safeguard against improper initialization. Also a `ProgressChar` property was added to allow further customization of the progress bar look +- Added `TypeWrite` and `TypeWriteLine` methods that provide a simple type-writer effect for text. +- Fixed issue in `RequestAnyInput` that could read a pre-existing character from the input stream and accept it, basically skipping the entire request. ## v1.5.2 -* **FROM THIS VERSION ON, THE PACKAGE WILL BE SIGNED** -* Updated many string interpolation instances to use `string.Concat` instead to improve performance -* Modified calculations in progress bars to use functions which are more hardware optimized -* Modified evaluated strings in progress bars to only be printed when done -* Added direction to synchronize progress bars to reduce buggy outputs -* Updated csproj file for better compatibility with the nuget website +- **FROM THIS VERSION ON, THE PACKAGE WILL BE SIGNED** +- Updated many string interpolation instances to use `string.Concat` instead to improve performance +- Modified calculations in progress bars to use functions which are more hardware optimized +- Modified evaluated strings in progress bars to only be printed when done +- Added direction to synchronize progress bars to reduce buggy outputs +- Updated csproj file for better compatibility with the nuget website This is a quality of life update, in most use cases the performance benefit, whether time wise or memory allocation will be visible and sometimes very significant. @@ -109,54 +117,54 @@ This update doesn't introduce any breaking changes so updating is highly encoura ## v1.5.1 -* Fixed issues with progress bar where the next override could keep artifacts of previous render. -* Added `OverrideCurrentLine()` which allows showing of progress with string only. -* Added `ClearNextLines(num)` which allows to remove next `num` lines, this is useful when something overriding was used, such as progress bar, but after parts of are left because the new output is shorter. +- Fixed issues with progress bar where the next override could keep artifacts of previous render. +- Added `OverrideCurrentLine()` which allows showing of progress with string only. +- Added `ClearNextLines(num)` which allows to remove next `num` lines, this is useful when something overriding was used, such as progress bar, but after parts of are left because the new output is shorter. ## v1.5.0 -* Optimized code across the board for better performance and less memory allocation -* Improved organization and documentation -* Added more method overloads -* Added options to output errors to error pipeline to improve stds down the line -* Added trimming safe overloads for generic methods -* Added trim warnings for generic and reflection methods -* Added optional header to the progress bar +- Optimized code across the board for better performance and less memory allocation +- Improved organization and documentation +- Added more method overloads +- Added options to output errors to error pipeline to improve stds down the line +- Added trimming safe overloads for generic methods +- Added trim warnings for generic and reflection methods +- Added optional header to the progress bar ## v1.4.0 -* Greatly improved performance by taking generics or strings into output methods instead of objects to avoid boxing. +- Greatly improved performance by taking generics or strings into output methods instead of objects to avoid boxing. ### Upgrade notes -* Most methods, including `Write` and `WriteLine` with single parameter should not require any code modification as they will use generics -* Outputs methods which use `tuples` will require modification to enjoy the performance upgrade, with that said, existing code will be break because legacy `object` implementation was not removed but rather just marked as obsolete for now. +- Most methods, including `Write` and `WriteLine` with single parameter should not require any code modification as they will use generics +- Outputs methods which use `tuples` will require modification to enjoy the performance upgrade, with that said, existing code will be break because legacy `object` implementation was not removed but rather just marked as obsolete for now. ## v1.3.0 -* Fixed memory leak in `TreeMenu` -* Improved performance and reduced complexity -* Removed `Colors.Primary` - it reduced uniformity as default was very close in color. Now default will be used in place of both, if you like primary more, consider overriding the `Colors.Default` to `ConsoleColor.White` -* Added Labels - outputs with configurable background color. +- Fixed memory leak in `TreeMenu` +- Improved performance and reduced complexity +- Removed `Colors.Primary` - it reduced uniformity as default was very close in color. Now default will be used in place of both, if you like primary more, consider overriding the `Colors.Default` to `ConsoleColor.White` +- Added Labels - outputs with configurable background color. ## v1.2.0 -* Removed `Color` enum, use the `Colors` property instead -* Added progress bars, both regular and indeterminate -* Added documentation file so summaries will be visible +- Removed `Color` enum, use the `Colors` property instead +- Added progress bars, both regular and indeterminate +- Added documentation file so summaries will be visible ## v1.1.0 -* Added indeterminate progress-bar -* Added regular progress-bar -* Changed parameter-less `Write` and `WriteLine` to use color version with default values to trim unnecessary code. +- Added indeterminate progress-bar +- Added regular progress-bar +- Changed parameter-less `Write` and `WriteLine` to use color version with default values to trim unnecessary code. ## v1.0.2 -* Changed secondary color to be called the default color and also act as such, meaning by default `Write` and `WriteLine` will use it -* Internal outputs such as ReadLine or others will still use primary color +- Changed secondary color to be called the default color and also act as such, meaning by default `Write` and `WriteLine` will use it +- Internal outputs such as ReadLine or others will still use primary color ## v1.0.1 -* Moved `Color` enum into Console to reduce using statements -* Changed accessibility of extensions, they were meant to be used internally +- Moved `Color` enum into Console to reduce using statements +- Changed accessibility of extensions, they were meant to be used internally From 50465a95218fe0527b54f02f12b82affcb9b84d6 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 19:40:07 +0300 Subject: [PATCH 024/108] Removed changelog - now releasenotes --- PrettyConsole/CHANGELOG.md | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100755 PrettyConsole/CHANGELOG.md diff --git a/PrettyConsole/CHANGELOG.md b/PrettyConsole/CHANGELOG.md deleted file mode 100755 index 0670b87..0000000 --- a/PrettyConsole/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# Changelog - -## v3.1.0 - -* Updated to support .NET 9.0 -* Updated to use `Sharpify 2.5.0` -* `ProgressBar` was redesigned since it randomly produced artifacts such as printing the header multiple times. - * Now the buffer area of `ProgressBar` is only 1 line, the (now "status") is printed on the same line, after the bar - * Only if the header is empty, the percentage is printed instead. - * An internal lock is now also used to prevent race conditions. -* `ClearNextLines` show now work properly, previously it could actually clear 1 line too many. - -### Breaking changes - -* `OutputPipe` enum was added to unify APIs -* `ClearNextLinesError` was removed, use `ClearNextLines` with `OutputPipe.Error` instead -* `NewLineError` was removed, use `NewLine` with `OutputPipe.Error` instead -* `WriteError` was removed, use `Write` with `OutputPipe.Error` instead -* `WriteLineError` was removed, use `WriteLine` with `OutputPipe.Error` instead -* `OverrideCurrentLine` now has an option to choose the output pipe, use `OutputPipe.Error` by default -* `Write` had the same treatment, now it has an option to choose the output pipe, use `OutputPipe.Out` by default, so the overload of `WriteError` were removed -* `WriteLine` and `WriteLine(ReadOnlySpan)` were introduced thanks to this change From ba043012e99fdeb868e4c6df481f26098b16f017 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 19:42:37 +0300 Subject: [PATCH 025/108] Removed duplicate - better root link --- PrettyConsole/README.md | 182 ---------------------------------------- 1 file changed, 182 deletions(-) delete mode 100755 PrettyConsole/README.md diff --git a/PrettyConsole/README.md b/PrettyConsole/README.md deleted file mode 100755 index cf487b4..0000000 --- a/PrettyConsole/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# 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. - -## Features - -* 🚀 High performance, Low memory usage and allocation -* 🪶 Very lightweight (No external dependencies) -* Easy to use (no need to learn a new syntax while still writing less boilerplate code) -* 💾 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. - -## Installation [![NUGET DOWNLOADS](https://img.shields.io/nuget/dt/PrettyConsole?label=Downloads)](https://www.nuget.org/packages/PrettyConsole/) - -> dotnet add package PrettyConsole - -## Usage - -Everything starts off with the using statements, I recommend using the `Console` statically - -```csharp -using static PrettyConsole.Console; // Access to all Console methods -using PrettyConsole; // Access to the Color struct and OutputPipe enum -``` - -### ColoredOutput - -PrettyConsole uses an equation inspired syntax to colorize text. The syntax is as follows: - -```csharp -WriteLine("Test" * Color.Red / Color.Blue); -``` - -i.e `TEXT * FOREGROUND / BACKGROUND` - -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 - -The most basic method for outputting is `Write`, which has multiple overloads: - -```csharp -Write(ColoredOutput, OutputPipe pipe = OutputPipe.Out); -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // use collections expression for the compiler to inline the array -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPip, ConsoleColor); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPipe, ConsoleColor, ConsoleColor); -Write(T , OutputPipe pipe = OutputPipe.Out); // no string allocation with T : ISpanFormattable -Write(T, OutputPipe, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor, ReadOnlySpan, IFormatProvider?); -``` - -Overload for `WriteLine` are available with the same parameters - -### Basic Inputs - -These are the methods for reading user input: - -```csharp -string? ReadLine(); // ReadLine -string? ReadLine(ReadOnlySpan); -T? ReadLine(ReadOnlySpan); // T : IParsable -T ReadLine(ReadOnlySpan, T @default); // @default will be returned if parsing fails -bool TryReadLine(ReadOnlySpan, out T?); // T : IParsable -bool TryReadLine(ReadOnlySpan, T @default, out T); // @default will be returned if parsing fails -bool TryReadLine(ReadOnlySpan, bool ignoreCase, out TEnum?); // TEnum : struct, Enum -bool TryReadLine(ReadOnlySpan, bool ignoreCase, TEnum @default, out TEnum); // @default will be returned if parsing fails -``` - -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. - -### Advanced Inputs - -These are some special methods for 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); -// 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); -``` - -### Rendering Controls - -To aid in rendering and building your own complex outputs, there are many methods that simplify some processes. - -```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 -``` - -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 - -```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 OverrideCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error); -// 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); -``` - -### Menus - -```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 {} -``` - -### Progress Bars - -There are two types of progress bars here, they both are implemented using a class to maintain states. - -#### IndeterminateProgressBar - -```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 -``` - -#### ProgressBar - -`ProgressBar` is a more powerful version, but requires a percentage of progress. - -```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) -// 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 -``` - -### Pipes - -`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. - -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. - -## Contributing - -This project uses an MIT license, if you want to contribute, you can do so by forking the repository and creating a pull request. - -If you have feature requests or bug reports, please create an issue. - -## Contact - -For bug reports, feature requests or offers of support/sponsorship contact - -> This project is proudly made in Israel 🇮🇱 for the benefit of mankind. From 548ad40c85209804787d360af314ff5c8c97b5f6 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 19:42:50 +0300 Subject: [PATCH 026/108] Better naming --- PrettyConsole/IndeterminateProgressBar.cs | 32 +++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 284c5fc..ecc5b28 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -19,7 +19,10 @@ public class IndeterminateProgressBar { /// /// Contains the characters that will be iterated through while running /// - public string Twirl { get; set; } = "-\\|/"; + /// + /// You can also choose from some defaults in + /// + public string AnimationSequence { get; set; } = Patterns.Twirl; // A whitespace the length of 10 spaces private const string ExtraBuffer = " "; @@ -102,7 +105,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d while (!task.IsCompleted && !token.IsCancellationRequested) { // Await until the TaskAwaiter informs of completion - foreach (var c in Twirl) { + foreach (var c in AnimationSequence) { if (header.Length > 0) { Error.Write(header); Error.Write(' '); @@ -134,4 +137,29 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d [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 const string Twirl = "|/-\\"; + + /// + /// A bounce animation sequence + /// + public const string Bounce = "<>==<>"; + + /// + /// A dots animation sequence + /// + public const string Dots = ".oO°Oo."; + + /// + /// A braille animation sequence + /// + public const string Braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; + } } \ No newline at end of file From 661592378f1c1218dd0ed5d8d3e95be4c3152c2b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 19:43:03 +0300 Subject: [PATCH 027/108] Updated props --- PrettyConsole/PrettyConsole.csproj | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 9d463c0..21944ae 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -15,21 +15,24 @@ true MIT True - CHANGELOG.md + README.md true - - - + - - Fixed possible issues with out-of-bounds buffer writing that affected ClearNextLines, and both ProgressBar`s - - IndeterminateProgressBar.Twirl is now customizable - - Sharpify is no longer a dependency + - Fixed issue that could sometimes cause writing into buffers beyond their bounds - + throwing an exception. Possibly effected: + - `ProgressBar` and `IndeterminateProgressBar` + - `ClearNextLines` + - `IndeterminateProgressBar` will no allow customization of the animated sequence via + the property `AnimationSequence`, and it also includes an inner class `Patterns` that + contains some constant sequences that could be used with it. + - Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient From 58292b3448b42d7d58d663f2a4d623765c063033 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 20:09:28 +0300 Subject: [PATCH 028/108] Added patterns to reduced binary size --- PrettyConsole/IndeterminateProgressBar.cs | 71 ++++++++++++----------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index ecc5b28..93e58c1 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -24,8 +24,8 @@ public class IndeterminateProgressBar { /// public string AnimationSequence { get; set; } = Patterns.Twirl; - // A whitespace the length of 10 spaces - private const string ExtraBuffer = " "; + // A length of whitespace padding to the end + private const int PaddingLength = 10; /// /// Gets or sets the foreground color of the progress bar. @@ -40,15 +40,10 @@ public class IndeterminateProgressBar { /// /// Gets or sets the update rate (in ms) of the indeterminate progress bar. /// + /// Default = 50 public int UpdateRate { get; set; } = 50; - private readonly char[] _buffer; - /// - /// Represents an indeterminate progress bar that continuously animates without a specific progress value. - /// - public IndeterminateProgressBar() { - _buffer = new char[20]; - } + private static readonly char[] TempBuffer = new char[20]; /// /// Runs the indeterminate progress bar while the specified task is running. @@ -118,11 +113,11 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d if (DisplayElapsedTime) { var elapsed = Stopwatch.GetElapsedTime(startTime); Error.Write(" [Elapsed: "); - Error.Write(Utils.FormatTimeSpan(elapsed, _buffer)); + Error.Write(Utils.FormatTimeSpan(elapsed, TempBuffer)); Error.Write(']'); } - Error.Write(ExtraBuffer); + Error.WriteWhiteSpaces(PaddingLength); await Task.Delay(UpdateRate, token); // The update rate ClearNextLines(1, OutputPipe.Error); if (token.IsCancellationRequested) { @@ -136,30 +131,40 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d [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 const string Twirl = "|/-\\"; - - /// - /// A bounce animation sequence - /// - public const string Bounce = "<>==<>"; /// - /// A dots animation sequence + /// Provides constant animation sequences that can be used for /// - public const string Dots = ".oO°Oo."; - - /// - /// A braille animation sequence - /// - public const string Braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; + public static class Patterns { + /// + /// A twirl animation sequence + /// + public const string Twirl = "|/-\\"; + + /// + /// A bounce animation sequence + /// + public const string Bounce = "<>==<>"; + + /// + /// A dots animation sequence + /// + public const string Dots = ".oO°Oo."; + + /// + /// A braille animation sequence + /// + public const string Braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; + + /// + /// An arrow animation sequence + /// + public const string Arrow = "--~~>>"; + + /// + /// A brackets animation sequence + /// + public const string Brackets = "<<(([[{{}}]]))>>"; + } } } \ No newline at end of file From dba44a99dae4f75f35926bcf622e24c50bccebcd Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 20:09:40 +0300 Subject: [PATCH 029/108] Fix current line clearing --- PrettyConsole/RenderingControls.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs index e80a288..701d32f 100755 --- a/PrettyConsole/RenderingControls.cs +++ b/PrettyConsole/RenderingControls.cs @@ -15,6 +15,7 @@ public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) static void InternalClearNextLines(int lines, TextWriter writer) { var lineLength = baseConsole.BufferWidth; var currentLine = GetCurrentLine(); + GoToLine(currentLine); for (int i = 0; i < lines; i++) { writer.WriteWhiteSpaces(lineLength); } From a4735e6f104f598a7b9c0b7261249ee59a7c5fe0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 20:40:17 +0300 Subject: [PATCH 030/108] Updated release notes --- PrettyConsole/PrettyConsole.csproj | 11 +++++------ Versions.md | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 21944ae..d741827 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -26,13 +26,12 @@ - Fixed issue that could sometimes cause writing into buffers beyond their bounds - - throwing an exception. Possibly effected: - - `ProgressBar` and `IndeterminateProgressBar` - - `ClearNextLines` - - `IndeterminateProgressBar` will no allow customization of the animated sequence via - the property `AnimationSequence`, and it also includes an inner class `Patterns` that + throwing an exception + - IndeterminateProgressBar will no allow customization of the animated sequence via + the property AnimationSequence, and it also includes an inner class Patterns that contains some constant sequences that could be used with it. - - Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient + - IndeterminateProgressBar.UpdateRate is 200 ms by default. + - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient diff --git a/Versions.md b/Versions.md index c24dad7..0f27463 100755 --- a/Versions.md +++ b/Versions.md @@ -6,7 +6,8 @@ - `ProgressBar` and `IndeterminateProgressBar` - `ClearNextLines` - `IndeterminateProgressBar` will no allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. -- Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient +- `IndeterminateProgressBar.UpdateRate` is 200 ms by default. +- Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient. ## v3.1.0 From b83b5019e88e66da778edb86e99c77143f6b2ac2 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 20:40:42 +0300 Subject: [PATCH 031/108] Added new patterns to modified to allow emoji --- PrettyConsole/IndeterminateProgressBar.cs | 37 +++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 93e58c1..4db391d 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -22,7 +23,7 @@ public class IndeterminateProgressBar { /// /// You can also choose from some defaults in /// - public string 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; @@ -40,8 +41,8 @@ public class IndeterminateProgressBar { /// /// Gets or sets the update rate (in ms) of the indeterminate progress bar. /// - /// Default = 50 - public int UpdateRate { get; set; } = 50; + /// Default = 200 + public int UpdateRate { get; set; } = 200; private static readonly char[] TempBuffer = new char[20]; @@ -139,32 +140,50 @@ public static class Patterns { /// /// A twirl animation sequence /// - public const string Twirl = "|/-\\"; + public static readonly ReadOnlyCollection Twirl + = new(["|", "/", "-", "\\"]); /// /// A bounce animation sequence /// - public const string Bounce = "<>==<>"; + public static readonly ReadOnlyCollection Bounce + = new(["<", ">", "=", "=", "<", ">"]); /// /// A dots animation sequence /// - public const string Dots = ".oO°Oo."; + public static readonly ReadOnlyCollection Dots + = new([".", "o", "O", "°", "O", "o", "."]); /// /// A braille animation sequence /// - public const string Braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; + public static readonly ReadOnlyCollection Braille + = new(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); /// /// An arrow animation sequence /// - public const string Arrow = "--~~>>"; + public static readonly ReadOnlyCollection Arrow + = new(["-", "~", ">"]); /// /// A brackets animation sequence /// - public const string Brackets = "<<(([[{{}}]]))>>"; + public static readonly ReadOnlyCollection Brackets + = new(["<", "(", "[", "{", "}", "]", ")", ">"]); + + /// + /// A running person animation sequence + /// + public static readonly ReadOnlyCollection RunningPerson + = new(["🧍", "🚶‍➡️", "🏃‍➡️"]); + + /// + /// A sad smiley animation sequence + /// + public static readonly ReadOnlyCollection SadSmiley + = new(["😞", "😣", "😖", "😫", "😩"]); } } } \ No newline at end of file From dc660eee8a1d83e97389a150962c1b0cc6119a13 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 14 Aug 2025 21:30:07 +0300 Subject: [PATCH 032/108] Updated animations --- PrettyConsole/IndeterminateProgressBar.cs | 51 ++++++++++++----------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 4db391d..bdb9cd4 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -143,18 +143,6 @@ public static class Patterns { public static readonly ReadOnlyCollection Twirl = new(["|", "/", "-", "\\"]); - /// - /// A bounce animation sequence - /// - public static readonly ReadOnlyCollection Bounce - = new(["<", ">", "=", "=", "<", ">"]); - - /// - /// A dots animation sequence - /// - public static readonly ReadOnlyCollection Dots - = new([".", "o", "O", "°", "O", "o", "."]); - /// /// A braille animation sequence /// @@ -162,28 +150,43 @@ public static readonly ReadOnlyCollection Braille = new(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); /// - /// An arrow animation sequence + /// A running person animation sequence /// - public static readonly ReadOnlyCollection Arrow - = new(["-", "~", ">"]); + public static readonly ReadOnlyCollection RunningPerson + = new(["🧎‍➡️", "🧍", "🚶‍➡️", "🏃‍➡️", " "]); /// - /// A brackets animation sequence + /// A sad smiley animation sequence ("what's taking so long??") /// - public static readonly ReadOnlyCollection Brackets - = new(["<", "(", "[", "{", "}", "]", ")", ">"]); + public static readonly ReadOnlyCollection SadSmiley + = new(["😞", "😣", "😖", "😫", "😩", " "]); /// - /// A running person animation sequence + /// A pulse animation sequence /// - public static readonly ReadOnlyCollection RunningPerson - = new(["🧍", "🚶‍➡️", "🏃‍➡️"]); + public static readonly ReadOnlyCollection Pulse + = new(["•", "●", "•", " "]); /// - /// A sad smiley animation sequence + /// A loading-bar animation sequence /// - public static readonly ReadOnlyCollection SadSmiley - = new(["😞", "😣", "😖", "😫", "😩"]); + public static readonly ReadOnlyCollection LoadingBar + = new(["[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]", "[ ]"]); + + /// + /// An ASCII ping-pong animation sequence + /// + public static readonly ReadOnlyCollection PingPongAscii + = new([ + "|o |", + "| o |", + "| o |", + "| o |", + "| o|", + "| o |", + "| o |", + "| o |", + ]); } } } \ No newline at end of file From 3d29f47abe00a80ccde521f32c72a740efb8a241 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 09:18:35 +0300 Subject: [PATCH 033/108] - --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1253c88..3cc9b82 100755 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ *.userosscache *.sln.docstates PrettyConsole.Tests.Integration/ +WARP.md # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs From 4cd861d6cf66a6c8906bf3c5798733784fad9da0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 09:48:39 +0300 Subject: [PATCH 034/108] Multiple improvements to syncronization and perf --- PrettyConsole/IndeterminateProgressBar.cs | 86 ++++++++++++++++------- 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index bdb9cd4..477b3f8 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -44,7 +44,7 @@ public class IndeterminateProgressBar { /// Default = 200 public int UpdateRate { get; set; } = 200; - private static readonly char[] TempBuffer = new char[20]; + private static readonly char[] TempBuffer = new char[128]; /// /// Runs the indeterminate progress bar while the specified task is running. @@ -96,35 +96,73 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } ResetColors(); - var originalColor = baseConsole.ForegroundColor; - var startTime = Stopwatch.GetTimestamp(); + ConsoleColor originalColor = baseConsole.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; + + 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); while (!task.IsCompleted && !token.IsCancellationRequested) { - // Await until the TaskAwaiter informs of completion - foreach (var c in AnimationSequence) { - if (header.Length > 0) { - Error.Write(header); - Error.Write(' '); - } + // Render a single frame + if (header.Length > 0) { + Error.Write(header); + Error.Write(' '); + } - // Cycle through the characters of twirl - baseConsole.ForegroundColor = ForegroundColor; - Error.Write(c); - baseConsole.ForegroundColor = originalColor; - if (DisplayElapsedTime) { - var elapsed = Stopwatch.GetElapsedTime(startTime); - Error.Write(" [Elapsed: "); - Error.Write(Utils.FormatTimeSpan(elapsed, TempBuffer)); - Error.Write(']'); - } + baseConsole.ForegroundColor = ForegroundColor; + Error.Write(AnimationSequence[seqIndex]); + baseConsole.ForegroundColor = originalColor; + + if (DisplayElapsedTime) { + var elapsed = Stopwatch.GetElapsedTime(startTime); + const string elapsedLabel = " [Elapsed: "; + Span buf = TempBuffer; + elapsedLabel.CopyTo(buf); + int length = elapsedLabel.Length; + length += Utils.FormatTimeSpan(elapsed, buf.Slice(length)); + buf.Slice(length)[0] = ']'; + length += 1; + Error.Write(buf.Slice(0, length)); + } - Error.WriteWhiteSpaces(PaddingLength); - await Task.Delay(UpdateRate, token); // The update rate - ClearNextLines(1, OutputPipe.Error); - if (token.IsCancellationRequested) { - return; + Error.WriteWhiteSpaces(PaddingLength); + + // Compute sleep to maintain UpdateRate between frame starts + var now = Stopwatch.GetTimestamp(); + nextTick += updateRateAsTicks; + var remaining = nextTick - now; + + if (remaining > 0) { + try { + var remainingTimeSpan = TimeSpan.FromTicks(remaining); + if (remainingTimeSpan.TotalMilliseconds > 0) { + await Task.Delay(remainingTimeSpan, linkedCts.Token).ConfigureAwait(false); + } + } catch (OperationCanceledException) { + // Either external cancellation or task completed } } + + // Always clear once per frame + ClearNextLines(1, OutputPipe.Error); + + if (token.IsCancellationRequested || task.IsCompleted) { + break; + } + + // Advance animation sequence index without allocations + seqIndex++; + if (seqIndex == AnimationSequence.Count) { + seqIndex = 0; + } } ResetColors(); From 5690fd9335ab0dc6016add1bf46bd58afc8342cc Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 09:48:46 +0300 Subject: [PATCH 035/108] use langversion 13 --- PrettyConsole/PrettyConsole.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index d741827..79cfb9b 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -1,6 +1,7 @@  net9.0;net8.0 + 13 David Shnayder PrettyConsole High performance, feature rich and easy to use wrap over System.Console From fb20f874eebdb650544251a542a5a30969e74c49 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 09:50:01 +0300 Subject: [PATCH 036/108] FormatTimeSpan API works better now with non-managed buffers --- PrettyConsole/Utils.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 5b8ba65..a70c170 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -33,48 +33,48 @@ internal static ReadOnlySpan FormatPercentage(double percentage, Span /// /// - /// - internal static ReadOnlySpan FormatTimeSpan(TimeSpan timeSpan, Span buffer) { + /// The number of characters written to + internal static int FormatTimeSpan(TimeSpan timeSpan, Span buffer) { // < 1s → "500ms" int written; if (timeSpan.TotalSeconds < 1) { if (!timeSpan.Milliseconds.TryFormat(buffer, out written)) { - return ReadOnlySpan.Empty; + return 0; } "ms".CopyTo(buffer.Slice(written)); - return buffer.Slice(0, written + 2); + return written + 2; } // < 60s → "SS:MMMs" (zero-padded) if (timeSpan.TotalSeconds < 60) { if (!buffer.TryWrite($"{timeSpan.Seconds:00}:{timeSpan.Milliseconds:000}s", out written)) { - return ReadOnlySpan.Empty; + return 0; } - return buffer.Slice(0, written); + return written; } // < 1h → "MM:SSm" if (timeSpan.TotalSeconds < 3600) { if (!buffer.TryWrite($"{timeSpan.Minutes:00}:{timeSpan.Seconds:00}m", out written)) { - return ReadOnlySpan.Empty; + return 0; } - return buffer.Slice(0, written); + return written; } // < 1d → "HH:MMhr" if (timeSpan.TotalSeconds < 86400) { if (!buffer.TryWrite($"{timeSpan.Hours:00}:{timeSpan.Minutes:00}hr", out written)) { - return ReadOnlySpan.Empty; + return 0; } - return buffer.Slice(0, written); + return written; } // ≥ 1d → "DD:HHd" if (!buffer.TryWrite($"{timeSpan.Days:00}:{timeSpan.Hours:00}d", out written)) { - return ReadOnlySpan.Empty; + return 0; } - return buffer.Slice(0, written); + return written; } /// From c893651cb48d8c9054179b2bf90fd078affa051f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 09:59:57 +0300 Subject: [PATCH 037/108] Improve update steadiness to reduce animation jitter --- PrettyConsole/IndeterminateProgressBar.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 477b3f8..0a3fc6b 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -140,11 +140,22 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d nextTick += updateRateAsTicks; var remaining = nextTick - now; - if (remaining > 0) { + 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 { - var remainingTimeSpan = TimeSpan.FromTicks(remaining); - if (remainingTimeSpan.TotalMilliseconds > 0) { - await Task.Delay(remainingTimeSpan, linkedCts.Token).ConfigureAwait(false); + // 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 From bdf3ff77d5116797b8899f4061006b7af0cd0374 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 10:02:06 +0300 Subject: [PATCH 038/108] Improve ping pong animation and remove pulse --- PrettyConsole/IndeterminateProgressBar.cs | 24 +++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 0a3fc6b..629d43e 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -210,12 +210,6 @@ public static readonly ReadOnlyCollection RunningPerson public static readonly ReadOnlyCollection SadSmiley = new(["😞", "😣", "😖", "😫", "😩", " "]); - /// - /// A pulse animation sequence - /// - public static readonly ReadOnlyCollection Pulse - = new(["•", "●", "•", " "]); - /// /// A loading-bar animation sequence /// @@ -225,16 +219,16 @@ public static readonly ReadOnlyCollection LoadingBar /// /// An ASCII ping-pong animation sequence /// - public static readonly ReadOnlyCollection PingPongAscii + public static readonly ReadOnlyCollection PingPong = new([ - "|o |", - "| o |", - "| o |", - "| o |", - "| o|", - "| o |", - "| o |", - "| o |", + "|• |", + "| • |", + "| • |", + "| • |", + "| •|", + "| • |", + "| • |", + "| • |", ]); } } From 72feb90955d03347bf28fbb3b4f8da240200d340 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 10:29:50 +0300 Subject: [PATCH 039/108] Improve perf by using local buffer --- PrettyConsole/ProgressBar.cs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 1208a90..c9bc975 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,5 +1,5 @@ -using System.Buffers; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace PrettyConsole; @@ -31,8 +31,12 @@ public class ProgressBar { /// public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; + // small buffer to write percentages private readonly char[] _percentageBuffer = new char[20]; + // The buffer used for writing the progress + private readonly List _buffer = new(256); + private int _currentProgress = 0; #if NET9_0_OR_GREATER @@ -63,14 +67,18 @@ public class ProgressBar { public void Update(double percentage, ReadOnlySpan status) { lock (_lock) { percentage = Math.Clamp(percentage, 0, 100); - var bufferWidth = baseConsole.BufferWidth; - using var buffer = MemoryPool.Shared.Rent(bufferWidth); + int bufferWidth = GetWidthOrDefault(); + + // Ensure buffer capacity + _buffer.EnsureCapacity(bufferWidth); + CollectionsMarshal.SetCount(_buffer, bufferWidth); + Span buf = CollectionsMarshal.AsSpan(_buffer); if (status.Length is 0) { status = Utils.FormatPercentage(percentage, _percentageBuffer); } int pLength = Math.Max(0, bufferWidth - status.Length - 5); - var p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); + int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); if (p == _currentProgress) { return; } @@ -83,24 +91,26 @@ public void Update(double percentage, ReadOnlySpan status) { Error.Write('['); baseConsole.ForegroundColor = ProgressColor; - Span span = buffer.Memory.Span; - Span progressSpan = span.Slice(0, p); + Span progressSpan = buf.Slice(0, p); progressSpan.Fill(ProgressChar); - var tailLength = Math.Max(0, pLength - p); + int tailLength = Math.Max(0, pLength - p); if (tailLength > 0) { - Span whiteSpaceSpan = span.Slice(p, tailLength); + Span whiteSpaceSpan = buf.Slice(p, tailLength); whiteSpaceSpan.Fill(' '); p += tailLength; } - Error.Write(span.Slice(0, p)); + Error.Write(buf.Slice(0, p)); baseConsole.ForegroundColor = ForegroundColor; Error.Write("] "); Error.Write(status); ResetColors(); GoToLine(currentLine); + + // Reset buffer + _buffer.Clear(); } } } From 26e64e87a47bd70a0627383f67ae7da62caf6904 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 10:30:21 +0300 Subject: [PATCH 040/108] Enhance flexibility by using default buffer width if Output is redirected --- PrettyConsole/ConsolePipes.cs | 14 +++++++++++++- PrettyConsole/Menus.cs | 6 +++--- PrettyConsole/RenderingControls.cs | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index 7a37850..9e640e9 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -17,7 +17,7 @@ public static partial class Console { public static TextReader In { get; internal set; } = baseConsole.In; /// - /// Gets the appropriate based on + /// Gets the appropriate based on /// /// /// @@ -26,4 +26,16 @@ internal static TextWriter GetWriter(OutputPipe pipe) OutputPipe.Error => Error, _ => Out }; + + /// + /// Returns the current console buffer width or if + /// + /// + /// + internal static int GetWidthOrDefault(int defaultWidth = 120) { + if (baseConsole.IsOutputRedirected) { + return defaultWidth; + } + return baseConsole.BufferWidth; + } } diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 13d597b..c30888b 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -17,7 +17,7 @@ public static string Selection(ReadOnlySpan title, TList c where TList : IList { WriteLine(title); - using var memoryOwner = MemoryPool.Shared.Rent(baseConsole.BufferWidth); + using var memoryOwner = MemoryPool.Shared.Rent(GetWidthOrDefault()); Span buffer = memoryOwner.Memory.Span; for (int i = 0; i < choices.Count; i++) { @@ -53,7 +53,7 @@ public static string[] MultiSelection(ReadOnlySpan title, where TList : IList { WriteLine(title); - using var memoryOwner = MemoryPool.Shared.Rent(baseConsole.BufferWidth); + using var memoryOwner = MemoryPool.Shared.Rent(GetWidthOrDefault()); Span buffer = memoryOwner.Memory.Span; for (int i = 0; i < choices.Count; i++) { @@ -109,7 +109,7 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan x.Length) + 10; // Used to make sub-tree prefix spaces uniform - using var memOwner = MemoryPool.Shared.Rent(baseConsole.BufferWidth); + using var memOwner = MemoryPool.Shared.Rent(GetWidthOrDefault()); Span buffer = memOwner.Memory.Span; //Enumerate options and sub-options diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs index 701d32f..c717f20 100755 --- a/PrettyConsole/RenderingControls.cs +++ b/PrettyConsole/RenderingControls.cs @@ -13,7 +13,7 @@ public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) InternalClearNextLines(lines, GetWriter(pipe)); static void InternalClearNextLines(int lines, TextWriter writer) { - var lineLength = baseConsole.BufferWidth; + var lineLength = GetWidthOrDefault(); var currentLine = GetCurrentLine(); GoToLine(currentLine); for (int i = 0; i < lines; i++) { From fc1729d5530d10b8ca04344c2e2ed031312a48c3 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 14:26:18 +0300 Subject: [PATCH 041/108] Moved status to the left --- PrettyConsole/ProgressBar.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index c9bc975..76b9903 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -67,18 +67,16 @@ public class ProgressBar { public void Update(double percentage, ReadOnlySpan status) { lock (_lock) { percentage = Math.Clamp(percentage, 0, 100); - int bufferWidth = GetWidthOrDefault(); + int bufferWidth = GetWidthOrDefault(); // Ensure buffer capacity _buffer.EnsureCapacity(bufferWidth); CollectionsMarshal.SetCount(_buffer, bufferWidth); Span buf = CollectionsMarshal.AsSpan(_buffer); - if (status.Length is 0) { - status = Utils.FormatPercentage(percentage, _percentageBuffer); - } - int pLength = Math.Max(0, bufferWidth - status.Length - 5); - int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); + var percentageSpan = Utils.FormatPercentage(percentage, _percentageBuffer); + int pLength = Math.Max(0, bufferWidth - status.Length - 12); + int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); if (p == _currentProgress) { return; } @@ -88,13 +86,16 @@ public void Update(double percentage, ReadOnlySpan status) { baseConsole.ForegroundColor = ForegroundColor; var currentLine = GetCurrentLine(); ClearNextLines(1, OutputPipe.Error); - Error.Write('['); + if (status.Length != 0) { + Error.Write(status); + } + Error.Write(" ["); baseConsole.ForegroundColor = ProgressColor; Span progressSpan = buf.Slice(0, p); progressSpan.Fill(ProgressChar); - int tailLength = Math.Max(0, pLength - p); + int tailLength = Math.Max(0, pLength - p); if (tailLength > 0) { Span whiteSpaceSpan = buf.Slice(p, tailLength); whiteSpaceSpan.Fill(' '); @@ -105,7 +106,7 @@ public void Update(double percentage, ReadOnlySpan status) { baseConsole.ForegroundColor = ForegroundColor; Error.Write("] "); - Error.Write(status); + Error.Write(percentageSpan); ResetColors(); GoToLine(currentLine); From c5585c474ffae8af788b5c53c073bd2b07bde764 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 14:38:39 +0300 Subject: [PATCH 042/108] optimizations1 --- PrettyConsole/ProgressBar.cs | 64 +++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 76b9903..6eec90b 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -74,44 +74,48 @@ public void Update(double percentage, ReadOnlySpan status) { CollectionsMarshal.SetCount(_buffer, bufferWidth); Span buf = CollectionsMarshal.AsSpan(_buffer); - var percentageSpan = Utils.FormatPercentage(percentage, _percentageBuffer); - int pLength = Math.Max(0, bufferWidth - status.Length - 12); + int pLength = Math.Max(0, bufferWidth - status.Length - 8); int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); if (p == _currentProgress) { return; } _currentProgress = p; - ResetColors(); - baseConsole.ForegroundColor = ForegroundColor; - var currentLine = GetCurrentLine(); - ClearNextLines(1, OutputPipe.Error); - if (status.Length != 0) { - Error.Write(status); - } - Error.Write(" ["); - baseConsole.ForegroundColor = ProgressColor; - - Span progressSpan = buf.Slice(0, p); - progressSpan.Fill(ProgressChar); + // Defer percentage formatting until after we know we'll render + var percentageSpan = Utils.FormatPercentage(percentage, _percentageBuffer); - int tailLength = Math.Max(0, pLength - p); - if (tailLength > 0) { - Span whiteSpaceSpan = buf.Slice(p, tailLength); - whiteSpaceSpan.Fill(' '); - p += tailLength; + var currentLine = GetCurrentLine(); + try { + ResetColors(); + baseConsole.ForegroundColor = ForegroundColor; + ClearNextLines(1, OutputPipe.Error); + if (status.Length != 0) { + Error.Write(status); + } + Error.Write(" ["); + baseConsole.ForegroundColor = ProgressColor; + + Span progressSpan = buf.Slice(0, p); + progressSpan.Fill(ProgressChar); + + int tailLength = Math.Max(0, pLength - p); + if (tailLength > 0) { + Span whiteSpaceSpan = buf.Slice(p, tailLength); + whiteSpaceSpan.Fill(' '); + p += tailLength; + } + + Error.Write(buf.Slice(0, p)); + + baseConsole.ForegroundColor = ForegroundColor; + Error.Write("] "); + Error.Write(percentageSpan); + GoToLine(currentLine); + } finally { + // Ensure colors and buffer are reset even if an exception occurs mid-render + ResetColors(); + _buffer.Clear(); } - - Error.Write(buf.Slice(0, p)); - - baseConsole.ForegroundColor = ForegroundColor; - Error.Write("] "); - Error.Write(percentageSpan); - ResetColors(); - GoToLine(currentLine); - - // Reset buffer - _buffer.Clear(); } } } From 3dc152decf57d5eb2a9e497a995e707c4ff17d80 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 14:45:47 +0300 Subject: [PATCH 043/108] optimization2 --- PrettyConsole/ProgressBar.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 6eec90b..cc35032 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -65,8 +65,17 @@ public class ProgressBar { /// The percentage value (0-100) representing the progress. /// The status text to be displayed after the progress bar. public void Update(double percentage, ReadOnlySpan status) { + // Non-locking fast path: compute the desired progress and early-return if unchanged. + percentage = Math.Clamp(percentage, 0, 100); + int predictedWidth = GetWidthOrDefault(); + int predictedLength = Math.Max(0, predictedWidth - status.Length - 8); + int predictedP = Math.Clamp((int)(predictedLength * percentage * 0.01), 0, predictedLength); + if (predictedP == Volatile.Read(ref _currentProgress)) { + return; + } + lock (_lock) { - percentage = Math.Clamp(percentage, 0, 100); + // Recompute under lock to avoid races and use a consistent width/buffer state. int bufferWidth = GetWidthOrDefault(); // Ensure buffer capacity From 8b2ebb41d907ed3a08613348bcb3b03cb9f50a34 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 14:54:39 +0300 Subject: [PATCH 044/108] optimization3 --- PrettyConsole/ProgressBar.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index cc35032..8c04097 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; namespace PrettyConsole; @@ -83,16 +84,16 @@ public void Update(double percentage, ReadOnlySpan status) { CollectionsMarshal.SetCount(_buffer, bufferWidth); Span buf = CollectionsMarshal.AsSpan(_buffer); - int pLength = Math.Max(0, bufferWidth - status.Length - 8); + // Format percentage now to get its exact length and avoid overflow + var percentageSpan = Utils.FormatPercentage(percentage, _percentageBuffer); + // Compute pLength using exact overhead: " [" (2) + "] " (2) + percentage length + int pLength = Math.Max(0, bufferWidth - status.Length - 4 - percentageSpan.Length); int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); if (p == _currentProgress) { return; } _currentProgress = p; - // Defer percentage formatting until after we know we'll render - var percentageSpan = Utils.FormatPercentage(percentage, _percentageBuffer); - var currentLine = GetCurrentLine(); try { ResetColors(); From 3a4ddbef4af7e436a736aafcb36d520b39abef5e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 15:25:39 +0300 Subject: [PATCH 045/108] Optimization-final --- PrettyConsole/ProgressBar.cs | 47 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 8c04097..80ce19c 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading; namespace PrettyConsole; @@ -68,30 +67,27 @@ public class ProgressBar { public void Update(double percentage, ReadOnlySpan status) { // Non-locking fast path: compute the desired progress and early-return if unchanged. percentage = Math.Clamp(percentage, 0, 100); - int predictedWidth = GetWidthOrDefault(); - int predictedLength = Math.Max(0, predictedWidth - status.Length - 8); - int predictedP = Math.Clamp((int)(predictedLength * percentage * 0.01), 0, predictedLength); - if (predictedP == Volatile.Read(ref _currentProgress)) { + + int bufferWidth = GetWidthOrDefault(); + // Compute pLength using exact overhead: " [" (2) + "] " (2) + percentage length (5) + int pLength = Math.Max(0, bufferWidth - status.Length - 4 - 5); + int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); + + if (p == Volatile.Read(ref _currentProgress)) { return; } lock (_lock) { - // Recompute under lock to avoid races and use a consistent width/buffer state. - int bufferWidth = GetWidthOrDefault(); - - // Ensure buffer capacity - _buffer.EnsureCapacity(bufferWidth); - CollectionsMarshal.SetCount(_buffer, bufferWidth); - Span buf = CollectionsMarshal.AsSpan(_buffer); - - // Format percentage now to get its exact length and avoid overflow - var percentageSpan = Utils.FormatPercentage(percentage, _percentageBuffer); - // Compute pLength using exact overhead: " [" (2) + "] " (2) + percentage length - int pLength = Math.Max(0, bufferWidth - status.Length - 4 - percentageSpan.Length); - int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); + // It's possible another thread updated while we were waiting; re-check only the progress value. if (p == _currentProgress) { return; } + + // Prepare the buffer exactly for the characters we will write for the bar (pLength) + _buffer.EnsureCapacity(pLength); + CollectionsMarshal.SetCount(_buffer, pLength); + Span buf = CollectionsMarshal.AsSpan(_buffer); + _currentProgress = p; var currentLine = GetCurrentLine(); @@ -105,21 +101,26 @@ public void Update(double percentage, ReadOnlySpan status) { Error.Write(" ["); baseConsole.ForegroundColor = ProgressColor; - Span progressSpan = buf.Slice(0, p); - progressSpan.Fill(ProgressChar); + // Fill the progress portion + if (p > 0) { + Span progressSpan = buf.Slice(0, p); + progressSpan.Fill(ProgressChar); + } + // Fill the remaining tail with spaces int tailLength = Math.Max(0, pLength - p); if (tailLength > 0) { Span whiteSpaceSpan = buf.Slice(p, tailLength); whiteSpaceSpan.Fill(' '); - p += tailLength; } - Error.Write(buf.Slice(0, p)); + // Write the entire bar (progress + tail) + Error.Write(buf.Slice(0, pLength)); baseConsole.ForegroundColor = ForegroundColor; Error.Write("] "); - Error.Write(percentageSpan); + // Write percentage + Error.Write(Utils.FormatPercentage(percentage, _percentageBuffer)); GoToLine(currentLine); } finally { // Ensure colors and buffer are reset even if an exception occurs mid-render From 442cca3e7e492e26a117a870b549850f99399356 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 15:26:53 +0300 Subject: [PATCH 046/108] Updated release notes --- PrettyConsole/PrettyConsole.csproj | 5 +++-- Versions.md | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 79cfb9b..6c98a7f 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -27,12 +27,13 @@ - Fixed issue that could sometimes cause writing into buffers beyond their bounds - - throwing an exception + throwing an exception. - IndeterminateProgressBar will no allow customization of the animated sequence via the property AnimationSequence, and it also includes an inner class Patterns that contains some constant sequences that could be used with it. - IndeterminateProgressBar.UpdateRate is 200 ms by default. - - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient + - ProgressBar had numeral optimizations and should perform better in all scenarios. + - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. diff --git a/Versions.md b/Versions.md index 0f27463..cee7823 100755 --- a/Versions.md +++ b/Versions.md @@ -7,6 +7,7 @@ - `ClearNextLines` - `IndeterminateProgressBar` will no allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. - `IndeterminateProgressBar.UpdateRate` is 200 ms by default. +- `ProgressBar` had numeral optimizations and should perform better in all scenarios. - Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient. ## v3.1.0 From 6519d70e06de3b1bb18907789c828bd07fefa3a4 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 15:31:38 +0300 Subject: [PATCH 047/108] moved animation to leftmost side --- PrettyConsole/IndeterminateProgressBar.cs | 34 ++++++++++++----------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 629d43e..ec8e808 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -111,27 +111,29 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); while (!task.IsCompleted && !token.IsCancellationRequested) { - // Render a single frame + try { + baseConsole.ForegroundColor = ForegroundColor; + Error.Write(AnimationSequence[seqIndex]); + } finally { + baseConsole.ForegroundColor = originalColor; + } + if (header.Length > 0) { - Error.Write(header); Error.Write(' '); + Error.Write(header); } - baseConsole.ForegroundColor = ForegroundColor; - Error.Write(AnimationSequence[seqIndex]); - baseConsole.ForegroundColor = originalColor; - if (DisplayElapsedTime) { - var elapsed = Stopwatch.GetElapsedTime(startTime); - const string elapsedLabel = " [Elapsed: "; - Span buf = TempBuffer; - elapsedLabel.CopyTo(buf); - int length = elapsedLabel.Length; - length += Utils.FormatTimeSpan(elapsed, buf.Slice(length)); - buf.Slice(length)[0] = ']'; - length += 1; - Error.Write(buf.Slice(0, length)); - } + var elapsed = Stopwatch.GetElapsedTime(startTime); + const string elapsedLabel = " [Elapsed: "; + Span buf = TempBuffer; + elapsedLabel.CopyTo(buf); + int length = elapsedLabel.Length; + length += Utils.FormatTimeSpan(elapsed, buf.Slice(length)); + buf.Slice(length)[0] = ']'; + length += 1; + Error.Write(buf.Slice(0, length)); + } Error.WriteWhiteSpaces(PaddingLength); From d940512c81610021081a989f3929414e56329e6a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 15:31:54 +0300 Subject: [PATCH 048/108] Updated release notes --- PrettyConsole/PrettyConsole.csproj | 2 ++ Versions.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 6c98a7f..a1be76a 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -32,6 +32,8 @@ the property AnimationSequence, and it also includes an inner class Patterns that contains some constant sequences that could be used with it. - IndeterminateProgressBar.UpdateRate is 200 ms by default. + - IndeterminateProgressBar header is now positioned right of the animation. Similar to + common CLIs. - ProgressBar had numeral optimizations and should perform better in all scenarios. - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. diff --git a/Versions.md b/Versions.md index cee7823..78047b7 100755 --- a/Versions.md +++ b/Versions.md @@ -7,7 +7,9 @@ - `ClearNextLines` - `IndeterminateProgressBar` will no allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. - `IndeterminateProgressBar.UpdateRate` is 200 ms by default. +- `IndeterminateProgressBar` header is now positioned right of the animation. Similar to common CLIs. - `ProgressBar` had numeral optimizations and should perform better in all scenarios. + - Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient. ## v3.1.0 From 243c423fd67252df99d2c0a0ae817b92a9d1b89c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 15:32:22 +0300 Subject: [PATCH 049/108] Better test scenario --- .../Features/IndeterminateProgressBarTest.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs index 453c0cb..6017716 100755 --- a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs @@ -7,9 +7,11 @@ public sealed class IndeterminateProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new IndeterminateProgressBar { - ForegroundColor = Color.Red, + AnimationSequence = IndeterminateProgressBar.Patterns.Braille, + ForegroundColor = Color.Magenta, + // UpdateRate = 120, DisplayElapsedTime = true }; - await prg.RunAsync(Task.Delay(1_000), "running..."); + await prg.RunAsync(Task.Delay(5_000), "running..."); } } \ No newline at end of file From 8b652a879e213ad5241a4c440ae6377c248c76ab Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 15 Aug 2025 15:32:37 +0300 Subject: [PATCH 050/108] More accurate test scenario --- PrettyConsole.Tests/Features/ProgressBarTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PrettyConsole.Tests/Features/ProgressBarTest.cs b/PrettyConsole.Tests/Features/ProgressBarTest.cs index f84989e..0110227 100755 --- a/PrettyConsole.Tests/Features/ProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarTest.cs @@ -7,15 +7,15 @@ public sealed class ProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new ProgressBar { - ProgressColor = Color.Yellow, - ProgressChar = '»' + ProgressColor = Color.Magenta, + // ProgressChar = '🧎‍♂️‍➡️' }; - const int count = 1_000; + const int count = 333; var currentLine = GetCurrentLine(); for (int i = 1; i <= count; i++) { double percentage = 100 * (double)i / count; - prg.Update(percentage); - await Task.Delay(1); + prg.Update(percentage, "TESTING"); + await Task.Delay(15); } ClearNextLines(1, OutputPipe.Error); GoToLine(currentLine); From 578da7feda1fb37987768954fbea8364d33728e1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 14:06:25 +0300 Subject: [PATCH 051/108] Implicit operators with object --- PrettyConsole/Color.cs | 88 +++++++++++++++++++----------- PrettyConsole/ColoredOutput.cs | 4 ++ PrettyConsole/PrettyConsole.csproj | 4 +- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index fcce47f..b62dce8 100755 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -11,39 +11,61 @@ namespace PrettyConsole; [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 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 /(string value, Color color) { - return new(value, DefaultForegroundColor, color); - } - - /// - /// Gets a object representing the color black. - /// - public static readonly Color Black = new(ConsoleColor.Black); + /// + /// 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 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. + /// + public static readonly Color Black = new(ConsoleColor.Black); /// /// Gets a object representing the color dark blue. diff --git a/PrettyConsole/ColoredOutput.cs b/PrettyConsole/ColoredOutput.cs index 8660359..1041405 100755 --- a/PrettyConsole/ColoredOutput.cs +++ b/PrettyConsole/ColoredOutput.cs @@ -37,6 +37,10 @@ public static implicit operator ColoredOutput(ReadOnlySpan buffer) { return new(new string(buffer)); } + // public static implicit operator ColoredOutput(object? value) { + // return value is null ? new(string.Empty) : new(value.ToString() ?? string.Empty); + // } + /// /// Creates a new instance of with a different background color /// diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index a1be76a..f0bc503 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -1,7 +1,7 @@  net9.0;net8.0 - 13 + latest David Shnayder PrettyConsole High performance, feature rich and easy to use wrap over System.Console @@ -36,6 +36,8 @@ common CLIs. - ProgressBar had numeral optimizations and should perform better in all scenarios. - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. + - Color received 2 new implicit operators, enabling it to convert a combination with any object + to ColoredOutput, which means you can write much less verbose statements with .ToString() calls. From 552958780c42cdddaf7fdefd9633b5e9dd32fcf2 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 17:29:19 +0300 Subject: [PATCH 052/108] Update workflow to run once in debug configuration as well --- .github/workflows/UnitTests.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UnitTests.yaml b/.github/workflows/UnitTests.yaml index ae68182..56eacc4 100755 --- a/.github/workflows/UnitTests.yaml +++ b/.github/workflows/UnitTests.yaml @@ -5,7 +5,7 @@ on: workflow_dispatch: jobs: - unit-tests: + unit-tests-matrix: strategy: fail-fast: false matrix: @@ -15,4 +15,12 @@ jobs: with: platform: ${{ matrix.platform }} dotnet-version: ${{ matrix.framework }} - test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj \ No newline at end of file + 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 + test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj + use-debug: true \ No newline at end of file From 73df9ac06f8cc90691026108095be6be2158b919 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 17:47:15 +0300 Subject: [PATCH 053/108] Addressed multiple analyzer warnings --- PrettyConsole/AdvancedInputs.cs | 2 +- PrettyConsole/PrettyConsole.csproj | 23 +++++++++++++++++++++-- PrettyConsole/ProgressBar.cs | 5 +++-- PrettyConsole/Utils.cs | 8 +++++--- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/PrettyConsole/AdvancedInputs.cs b/PrettyConsole/AdvancedInputs.cs index 5526efd..3e32462 100755 --- a/PrettyConsole/AdvancedInputs.cs +++ b/PrettyConsole/AdvancedInputs.cs @@ -48,7 +48,7 @@ public static bool Confirm(ReadOnlySpan message, ReadOnlySpan net9.0;net8.0 latest + + true + true + true + true + latest-recommended + true + true + true + true + true + true + David Shnayder PrettyConsole High performance, feature rich and easy to use wrap over System.Console @@ -13,11 +26,9 @@ git 3.2.0 enable - true MIT True README.md - true @@ -45,6 +56,14 @@ + + portable + true + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + <_Parameter1>PrettyConsole.Tests.Unit diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 80ce19c..363ff86 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -37,12 +37,13 @@ public class ProgressBar { // The buffer used for writing the progress private readonly List _buffer = new(256); - private int _currentProgress = 0; + private int _currentProgress; + #if NET9_0_OR_GREATER private readonly Lock _lock = new(); #else - private readonly object _lock = new(); + private readonly object _lock = new(); #endif /// diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index a70c170..5409c87 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Globalization; namespace PrettyConsole; @@ -15,15 +16,16 @@ internal static class Utils { internal static ReadOnlySpan FormatPercentage(double percentage, Span buffer) { const int length = 5; percentage = Math.Round(Math.Clamp(percentage, 0, 100), 2, MidpointRounding.AwayFromZero); + var currentCulture = CultureInfo.CurrentCulture; - percentage.TryFormat(buffer, out int written); + percentage.TryFormat(buffer, out int written, provider: currentCulture); if (written == length) { return buffer.Slice(0, written); } var padding = length - written; buffer.Slice(0, padding).Fill(' '); - percentage.TryFormat(buffer.Slice(padding), out written); + percentage.TryFormat(buffer.Slice(padding), out written, provider: currentCulture); return buffer.Slice(0, padding + written); } @@ -39,7 +41,7 @@ internal static int FormatTimeSpan(TimeSpan timeSpan, Span buffer) { int written; if (timeSpan.TotalSeconds < 1) { - if (!timeSpan.Milliseconds.TryFormat(buffer, out written)) { + if (!timeSpan.Milliseconds.TryFormat(buffer, out written, provider: CultureInfo.CurrentCulture)) { return 0; } "ms".CopyTo(buffer.Slice(written)); From 2acead930bf197d814f8ffc14dfcad086d533c89 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 19:34:54 +0300 Subject: [PATCH 054/108] pooling improvements --- PrettyConsole.Tests/Program.cs | 2 + PrettyConsole/BufferPool.cs | 63 +++++++++++++++++++++++ PrettyConsole/ConsolePipes.cs | 26 ++++++---- PrettyConsole/IndeterminateProgressBar.cs | 26 +++++----- PrettyConsole/Menus.cs | 56 +++++++++++--------- PrettyConsole/PrettyConsole.csproj | 1 + PrettyConsole/ProgressBar.cs | 11 ++-- PrettyConsole/RenderingControls.cs | 17 +++--- PrettyConsole/Utils.cs | 7 --- PrettyConsole/Write.cs | 41 ++++++++++----- PrettyConsole/WriteLine.cs | 16 ++++-- 11 files changed, 178 insertions(+), 88 deletions(-) create mode 100644 PrettyConsole/BufferPool.cs diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 8dd4c7c..c8fbbfb 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -12,6 +12,8 @@ // .Select(x => (IPrettyConsoleTest)Activator.CreateInstance(x)!) // .ToArray(); +Write(5); + var tests = new IPrettyConsoleTest[] { new ColoredOutputTest(), new SelectionTest(), diff --git a/PrettyConsole/BufferPool.cs b/PrettyConsole/BufferPool.cs new file mode 100644 index 0000000..b706149 --- /dev/null +++ b/PrettyConsole/BufferPool.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace PrettyConsole; + +internal sealed class BufferPool { + internal static readonly BufferPool Shared = new(); + + private readonly ConcurrentQueue> _collection; + private long _poolSize; + private readonly int _maxCapacity; + private List? _fastItem; + + public const int ListStartingSize = 256; + + private BufferPool() { + _collection = new(); + _maxCapacity = Environment.ProcessorCount * 2; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public RentedBufferOwner Rent() { + var buffer = _fastItem; + if (buffer is null || Interlocked.CompareExchange(ref _fastItem, null, buffer) != buffer) { + if (_collection.TryDequeue(out buffer)) { + Interlocked.Decrement(ref _poolSize); + // return item; + return new RentedBufferOwner(this, buffer); + } + + return new RentedBufferOwner(this, new(ListStartingSize)); + } + return new RentedBufferOwner(this, buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private void Return(List buffer) { + if (buffer.Count > 4096) { + return; + } + if (_fastItem is not null || Interlocked.CompareExchange(ref _fastItem, buffer, null) != null) { + if (Interlocked.Increment(ref _poolSize) <= _maxCapacity) { + buffer.Clear(); + _collection.Enqueue(buffer); + } else { + // no room, clean up the count and drop the object on the floor + Interlocked.Decrement(ref _poolSize); + } + } + } + + internal readonly struct RentedBufferOwner : IDisposable { + private readonly BufferPool _pool; + public readonly List Buffer; + + public RentedBufferOwner(BufferPool pool, List buffer) { + _pool = pool; + Buffer = buffer; + } + + public readonly void Dispose() => _pool.Return(Buffer); + } +} \ No newline at end of file diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index 9e640e9..d8ccf0d 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -1,4 +1,6 @@ -namespace PrettyConsole; +using System.Runtime.CompilerServices; + +namespace PrettyConsole; public static partial class Console { /// @@ -21,6 +23,7 @@ public static partial class Console { /// /// /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] internal static TextWriter GetWriter(OutputPipe pipe) => pipe switch { OutputPipe.Error => Error, @@ -28,14 +31,15 @@ internal static TextWriter GetWriter(OutputPipe pipe) }; /// - /// Returns the current console buffer width or if - /// - /// - /// - internal static int GetWidthOrDefault(int defaultWidth = 120) { - if (baseConsole.IsOutputRedirected) { - return defaultWidth; - } - return baseConsole.BufferWidth; - } + /// Returns the current console buffer width or if + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + internal static int GetWidthOrDefault(int defaultWidth = 120) { + if (baseConsole.IsOutputRedirected) { + return defaultWidth; + } + return baseConsole.BufferWidth; + } } diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index ec8e808..107148f 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace PrettyConsole; @@ -44,8 +45,6 @@ public class IndeterminateProgressBar { /// Default = 200 public int UpdateRate { get; set; } = 200; - private static readonly char[] TempBuffer = new char[128]; - /// /// Runs the indeterminate progress bar while the specified task is running. /// @@ -124,16 +123,19 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } if (DisplayElapsedTime) { - var elapsed = Stopwatch.GetElapsedTime(startTime); - const string elapsedLabel = " [Elapsed: "; - Span buf = TempBuffer; - elapsedLabel.CopyTo(buf); - int length = elapsedLabel.Length; - length += Utils.FormatTimeSpan(elapsed, buf.Slice(length)); - buf.Slice(length)[0] = ']'; - length += 1; - Error.Write(buf.Slice(0, length)); - } + var elapsed = Stopwatch.GetElapsedTime(startTime); + const string elapsedLabel = " [Elapsed: "; + using var bufferOwner = BufferPool.Shared.Rent(); + var buf = bufferOwner.Buffer; + CollectionsMarshal.SetCount(buf, 256); // starting size is 256 + Span span = CollectionsMarshal.AsSpan(buf); + elapsedLabel.CopyTo(span); + int length = elapsedLabel.Length; + length += Utils.FormatTimeSpan(elapsed, span.Slice(length)); + span.Slice(length)[0] = ']'; + length += 1; + Error.Write(buf.Slice(0, length)); + } Error.WriteWhiteSpaces(PaddingLength); diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index c30888b..7dff9d8 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -17,12 +17,16 @@ public static string Selection(ReadOnlySpan title, TList c where TList : IList { WriteLine(title); - using var memoryOwner = MemoryPool.Shared.Rent(GetWidthOrDefault()); - Span buffer = memoryOwner.Memory.Span; + using var bufferOwner = BufferPool.Shared.Rent(); + var buffer = bufferOwner.Buffer; + var width = GetWidthOrDefault(); + buffer.EnsureCapacity(width); + CollectionsMarshal.SetCount(buffer, width); + var span = CollectionsMarshal.AsSpan(buffer); for (int i = 0; i < choices.Count; i++) { - buffer.TryWrite($" {i + 1}) {choices[i]}", out var written); - Out.WriteLine(buffer.Slice(0, written)); + span.TryWrite($" {i + 1}) {choices[i]}", out var written); + Out.WriteLine(span.Slice(0, written)); } NewLine(); @@ -53,12 +57,16 @@ public static string[] MultiSelection(ReadOnlySpan title, where TList : IList { WriteLine(title); - using var memoryOwner = MemoryPool.Shared.Rent(GetWidthOrDefault()); - Span buffer = memoryOwner.Memory.Span; + using var bufferOwner = BufferPool.Shared.Rent(); + var buffer = bufferOwner.Buffer; + var width = GetWidthOrDefault(); + buffer.EnsureCapacity(width); + CollectionsMarshal.SetCount(buffer, width); + var span = CollectionsMarshal.AsSpan(buffer); for (int i = 0; i < choices.Count; i++) { - buffer.TryWrite($" {i + 1}) {choices[i]}", out var written); - Out.WriteLine(buffer.Slice(0, written)); + span.TryWrite($" {i + 1}) {choices[i]}", out var written); + Out.WriteLine(span.Slice(0, written)); } NewLine(); @@ -109,16 +117,20 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan x.Length) + 10; // Used to make sub-tree prefix spaces uniform - using var memOwner = MemoryPool.Shared.Rent(GetWidthOrDefault()); - Span buffer = memOwner.Memory.Span; + using var bufferOwner = BufferPool.Shared.Rent(); + var buffer = bufferOwner.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]; - buffer.TryWrite($" {i + 1}) {mainEntry}", out int written); - Out.Write(buffer.Slice(0, written)); + span.TryWrite($" {i + 1}) {mainEntry}", out int written); + Out.Write(span.Slice(0, written)); var remainingLength = maxMainOption - written; if (remainingLength > 0) { @@ -130,8 +142,8 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan(TList headers, ReadOnlySpan columns) wher var columnsLength = columns.Length; - using var memoryOwner = MemoryPool.Shared.Rent(columnsLength); + List buffer = new(columnsLength); for (int i = 0; i < columnsLength; i++) { - memoryOwner.Memory.Span[i] = headers[i].PadRight(lengths[i]); + buffer.Add(headers[i].PadRight(lengths[i])); } - ReadOnlyMemory slice = memoryOwner.Memory.Slice(0, columnsLength); - var enumerable = MemoryMarshal.ToEnumerable(slice); - var header = string.Join(columnSeparator, enumerable); + var header = string.Join(columnSeparator, buffer); + buffer.Clear(); Span rowSeparation = stackalloc char[header.Length]; rowSeparation.Fill(rowSeparator); @@ -206,12 +217,11 @@ public static void Table(TList headers, ReadOnlySpan columns) wher Out.WriteLine(rowSeparation); for (int row = 0; row < height; row++) { for (int i = 0; i < columnsLength; i++) { - memoryOwner.Memory.Span[i] = columns[i][row].PadRight(lengths[i]); + buffer.Add(columns[i][row].PadRight(lengths[i])); } - slice = memoryOwner.Memory.Slice(0, columnsLength); - enumerable = MemoryMarshal.ToEnumerable(slice); - var line = string.Join(columnSeparator, enumerable); + var line = string.Join(columnSeparator, buffer); + buffer.Clear(); Out.WriteLine(line); } diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 9448778..07946cf 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -49,6 +49,7 @@ - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. - Color received 2 new implicit operators, enabling it to convert a combination with any object to ColoredOutput, which means you can write much less verbose statements with .ToString() calls. + - Internal buffer pooling system was upgraded to further reduce allocations and relief more pressure from the GC. diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 363ff86..309757e 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -34,9 +34,6 @@ public class ProgressBar { // small buffer to write percentages private readonly char[] _percentageBuffer = new char[20]; - // The buffer used for writing the progress - private readonly List _buffer = new(256); - private int _currentProgress; @@ -85,9 +82,10 @@ public void Update(double percentage, ReadOnlySpan status) { } // Prepare the buffer exactly for the characters we will write for the bar (pLength) - _buffer.EnsureCapacity(pLength); - CollectionsMarshal.SetCount(_buffer, pLength); - Span buf = CollectionsMarshal.AsSpan(_buffer); + using var listOwner = BufferPool.Shared.Rent(); + listOwner.Buffer.EnsureCapacity(pLength); + CollectionsMarshal.SetCount(listOwner.Buffer, pLength); + Span buf = CollectionsMarshal.AsSpan(listOwner.Buffer); _currentProgress = p; @@ -126,7 +124,6 @@ public void Update(double percentage, ReadOnlySpan status) { } finally { // Ensure colors and buffer are reset even if an exception occurs mid-render ResetColors(); - _buffer.Clear(); } } } diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs index c717f20..da27206 100755 --- a/PrettyConsole/RenderingControls.cs +++ b/PrettyConsole/RenderingControls.cs @@ -10,17 +10,14 @@ public static partial class Console { /// Useful for clearing output of overriding functions, like the ProgressBar /// public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { - InternalClearNextLines(lines, GetWriter(pipe)); - - static void InternalClearNextLines(int lines, TextWriter writer) { - var lineLength = GetWidthOrDefault(); - var currentLine = GetCurrentLine(); - GoToLine(currentLine); - for (int i = 0; i < lines; i++) { - writer.WriteWhiteSpaces(lineLength); - } - GoToLine(currentLine); + var textWriter = GetWriter(pipe); + var lineLength = GetWidthOrDefault(); + var currentLine = GetCurrentLine(); + GoToLine(currentLine); + for (int i = 0; i < lines; i++) { + textWriter.WriteWhiteSpaces(lineLength); } + GoToLine(currentLine); } /// diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 5409c87..5342b41 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -1,4 +1,3 @@ -using System.Buffers; using System.Globalization; namespace PrettyConsole; @@ -112,10 +111,4 @@ internal static void WriteWhiteSpaces(this TextWriter writer, int length) { writer.Write(full.AsSpan(0, length)); } } - - /// - /// Rents a memory owner from the shared memory pool - /// - /// The minimum length - internal static IMemoryOwner ObtainMemory(int length) => MemoryPool.Shared.Rent(length); } diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index 3501a3b..bd61062 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -1,18 +1,17 @@ +using System.Runtime.InteropServices; + namespace PrettyConsole; public static partial class Console { - /// - /// The size of the buffer used for items - /// - private const int SpanFormattableBufferSize = 256; - /// /// Writes an item that implements without boxing directly to the output writer /// /// /// The output pipe to use /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { Write(item, pipe, Color.DefaultForegroundColor, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); } @@ -25,7 +24,9 @@ public static void Write(T item, OutputPipe pipe = OutputPipe.Out) where T : /// The output pipe to use /// foreground color /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { Write(item, pipe, foreground, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); } @@ -39,7 +40,9 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) wh /// foreground color /// background color /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { Write(item, pipe, foreground, background, ReadOnlySpan.Empty, null); @@ -56,16 +59,26 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, /// item format /// format provider /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { - using var memoryOwner = Utils.ObtainMemory(SpanFormattableBufferSize); - var span = memoryOwner.Memory.Span; - if (!item.TryFormat(span, out int charsWritten, format, formatProvider)) { - throw new ArgumentException($"Formatted item length > {SpanFormattableBufferSize}, please use a different overload", nameof(item)); + using var listOwner = BufferPool.Shared.Rent(); + int upperBound = BufferPool.ListStartingSize; + var lst = listOwner.Buffer; + 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; + } } - Write(span.Slice(0, charsWritten), pipe, foreground, background); } /// diff --git a/PrettyConsole/WriteLine.cs b/PrettyConsole/WriteLine.cs index fe0db64..ff6f435 100755 --- a/PrettyConsole/WriteLine.cs +++ b/PrettyConsole/WriteLine.cs @@ -37,7 +37,9 @@ public static void WriteLine(ReadOnlySpan outputs, OutputPipe pip /// /// The output pipe to use /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { WriteLine(item, pipe, Color.DefaultForegroundColor, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); } @@ -50,7 +52,9 @@ public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) where /// The output pipe to use /// foreground color /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { WriteLine(item, pipe, foreground, Color.DefaultBackgroundColor, ReadOnlySpan.Empty, null); } @@ -64,7 +68,9 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// foreground color /// background color /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { WriteLine(item, pipe, foreground, background, ReadOnlySpan.Empty, null); @@ -81,7 +87,9 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// item format /// format provider /// - /// If the result of formatted item length is > 256 characters + /// + /// 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 { From aefc9673d672e23a24d1f6fd5a7edc59c1bacd8f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 19:54:11 +0300 Subject: [PATCH 055/108] Add tests for long ISpanFormattable implementations --- PrettyConsole.Tests.Unit/Write.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/PrettyConsole.Tests.Unit/Write.cs b/PrettyConsole.Tests.Unit/Write.cs index 220ebd7..37c9dcf 100755 --- a/PrettyConsole.Tests.Unit/Write.cs +++ b/PrettyConsole.Tests.Unit/Write.cs @@ -27,6 +27,13 @@ public void Write_SpanFormattable_ForegroundAndBackgroundColor() { Assert.Equal("3.14", _writer.ToStringAndFlush()); } + [Fact] + public void Write_SpanFormattable_VeryLongObjectFormat() { + var obj = new LongFormatStud(); + Write(obj); + Assert.Equal(new string('X', LongFormatStud.Length), _writer.ToStringAndFlush()); + } + [Fact] public void Write_ColoredOutput_Single() { Write("Hello world!" * Color.Green); @@ -56,4 +63,23 @@ public void WriteError_ColoredOutput_Multiple() { Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); Assert.Equal("Hello David!", _errorWriter.ToStringAndFlush()); } + + private readonly ref struct LongFormatStud : ISpanFormattable { + public const int Length = 1024; + + public string ToString(string? format, IFormatProvider? formatProvider) { + return new string('X', Length); + } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) { + if (destination.Length < Length) { + charsWritten = 0; + return false; + } + var slice = destination.Slice(0, Length); + slice.Fill('X'); + charsWritten = Length; + return true; + } + } } \ No newline at end of file From f6a2dddf5daf6a5e74a5f81f487665575e55696a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 19:54:42 +0300 Subject: [PATCH 056/108] Removed dotnet 8 targeting from this release onward to support better perf features --- .github/workflows/UnitTests.yaml | 3 +-- PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj | 2 +- PrettyConsole/PrettyConsole.csproj | 5 +++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/UnitTests.yaml b/.github/workflows/UnitTests.yaml index 56eacc4..0ff99eb 100755 --- a/.github/workflows/UnitTests.yaml +++ b/.github/workflows/UnitTests.yaml @@ -10,11 +10,10 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - framework: [8.0.x, 9.0.x] uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main with: platform: ${{ matrix.platform }} - dotnet-version: ${{ matrix.framework }} + dotnet-version: 9.0.x test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj unit-tests-debug: diff --git a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj index 89973e6..e8e366e 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;net8.0 + net9.0 true true diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 07946cf..f5d354c 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -1,6 +1,6 @@  - net9.0;net8.0 + net9.0 latest true @@ -37,6 +37,7 @@ + - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. - Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. - IndeterminateProgressBar will no allow customization of the animated sequence via @@ -46,10 +47,10 @@ - IndeterminateProgressBar header is now positioned right of the animation. Similar to common CLIs. - ProgressBar had numeral optimizations and should perform better in all scenarios. - - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. - Color received 2 new implicit operators, enabling it to convert a combination with any object to ColoredOutput, which means you can write much less verbose statements with .ToString() calls. - Internal buffer pooling system was upgraded to further reduce allocations and relief more pressure from the GC. + - Write/WriteLine overload for T, will no longer throw an exception when a capacity above 256 characters is required to format the type, and also support ref structs. From 8695a36f566dfc312cae5a412c53c9d92ec10623 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 19:55:01 +0300 Subject: [PATCH 057/108] Removed net8 conditional which is no longer possible --- PrettyConsole/ProgressBar.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 309757e..61f67e7 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -36,12 +36,7 @@ public class ProgressBar { private int _currentProgress; - -#if NET9_0_OR_GREATER private readonly Lock _lock = new(); -#else - private readonly object _lock = new(); -#endif /// /// Updates the progress bar with the specified percentage. From 3264886cf60ea1a362add28b059419811e23f3a7 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 19:55:20 +0300 Subject: [PATCH 058/108] Allow ref struct parameters to ISpanFormattable writers --- PrettyConsole/Write.cs | 12 +++++++----- PrettyConsole/WriteLine.cs | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index bd61062..d814c0d 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -12,7 +12,8 @@ public static partial class Console { /// /// 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 { + 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); } @@ -27,7 +28,8 @@ public static void Write(T item, OutputPipe pipe = OutputPipe.Out) where T : /// /// 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 { + 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); } @@ -43,8 +45,8 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) wh /// /// 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 { + 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); } @@ -64,7 +66,7 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, /// public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) - where T : ISpanFormattable { + where T : ISpanFormattable, allows ref struct { using var listOwner = BufferPool.Shared.Rent(); int upperBound = BufferPool.ListStartingSize; var lst = listOwner.Buffer; diff --git a/PrettyConsole/WriteLine.cs b/PrettyConsole/WriteLine.cs index ff6f435..0f4ab94 100755 --- a/PrettyConsole/WriteLine.cs +++ b/PrettyConsole/WriteLine.cs @@ -40,7 +40,8 @@ public static void WriteLine(ReadOnlySpan outputs, OutputPipe pip /// /// 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 { + 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); } @@ -55,7 +56,8 @@ public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) where /// /// 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 { + 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); } @@ -72,7 +74,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// 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 { + ConsoleColor background) where T : ISpanFormattable, allows ref struct { WriteLine(item, pipe, foreground, background, ReadOnlySpan.Empty, null); } @@ -92,7 +94,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) - where T : ISpanFormattable { + where T : ISpanFormattable, allows ref struct { Write(item, pipe, foreground, background, format, formatProvider); NewLine(pipe); } From 4d844a9eaec9eafec22535127e26cdda32c33b51 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 21:23:07 +0300 Subject: [PATCH 059/108] Add coverage + updates --- .../PrettyConsole.Tests.Unit.csproj | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj index e8e366e..79e6075 100755 --- a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj +++ b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj @@ -14,9 +14,16 @@ - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -27,4 +34,4 @@ - \ No newline at end of file + From 6a4350966c5077295eeefcef1ab70ae26df183dc Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 21:23:24 +0300 Subject: [PATCH 060/108] Removed commented out code --- PrettyConsole/ColoredOutput.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/PrettyConsole/ColoredOutput.cs b/PrettyConsole/ColoredOutput.cs index 1041405..8660359 100755 --- a/PrettyConsole/ColoredOutput.cs +++ b/PrettyConsole/ColoredOutput.cs @@ -37,10 +37,6 @@ public static implicit operator ColoredOutput(ReadOnlySpan buffer) { return new(new string(buffer)); } - // public static implicit operator ColoredOutput(object? value) { - // return value is null ? new(string.Empty) : new(value.ToString() ?? string.Empty); - // } - /// /// Creates a new instance of with a different background color /// From e7298799c643ea661ad3b27af788586d0bd223b5 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 21:23:47 +0300 Subject: [PATCH 061/108] Added tests to cover Color and ColoredOutput --- PrettyConsole.Tests.Unit/ColorTests.cs | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/ColorTests.cs diff --git a/PrettyConsole.Tests.Unit/ColorTests.cs b/PrettyConsole.Tests.Unit/ColorTests.cs new file mode 100644 index 0000000..619a1f0 --- /dev/null +++ b/PrettyConsole.Tests.Unit/ColorTests.cs @@ -0,0 +1,105 @@ +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_AsteriskOperator() { + var coloredOutput = "Hello" * Color.Green; + Assert.Equal(ConsoleColor.Green, coloredOutput.ForegroundColor); + Assert.Equal(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + } + + [Fact] + public void Color_DivideOperator() { + var coloredOutput = "Hello" / Color.Red; + Assert.Equal(System.Console.ForegroundColor, 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 ColoredOutput_ForegroundCtor() { + var coloredOutput = new ColoredOutput("Hello", Color.Red); + Assert.Equal(ConsoleColor.Red, coloredOutput.ForegroundColor); + Assert.Equal(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + } + + [Fact] + public void ColoredOutput_StringOperator() { + ColoredOutput coloredOutput = "Hello"; + Assert.Equal("Hello", coloredOutput.Value); + Assert.Equal(System.Console.ForegroundColor, coloredOutput.ForegroundColor); + Assert.Equal(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + } + + [Fact] + public void ColoredOutput_ReadOnlySpanOperator() { + ColoredOutput coloredOutput = "Hello".AsSpan(); + Assert.Equal("Hello", coloredOutput.Value); + Assert.Equal(System.Console.ForegroundColor, coloredOutput.ForegroundColor); + Assert.Equal(System.Console.BackgroundColor, 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); + } +} From 4537d99c09ae2db4dbbdae22585d4c0b612baa64 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 21:23:56 +0300 Subject: [PATCH 062/108] Added tests for menus and trees --- PrettyConsole.Tests.Unit/MenusTests.cs | 156 +++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/MenusTests.cs diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.Tests.Unit/MenusTests.cs new file mode 100644 index 0000000..ce39dc5 --- /dev/null +++ b/PrettyConsole.Tests.Unit/MenusTests.cs @@ -0,0 +1,156 @@ +namespace PrettyConsole.Tests.Unit; + +public class MenusTests { + [Fact] + public void Selection_ReturnsSelectedChoice_WhenInputValid() { + Out = Utilities.GetWriter(out var writer); + In = Utilities.GetReader("2"); + + var choices = new List { "Apple", "Banana", "Cherry" }; + + var result = Selection(["Choose a fruit:"], choices); + + var output = writer.ToStringAndFlush(); + + // Do not remove extra whitespace + Assert.Equal( + """ + Choose a fruit: + 1) Apple + 2) Banana + 3) Cherry + + Enter your choice: + """ + , output); + Assert.Equal("Banana", result); + } + + [Fact] + public void Selection_InvalidNumber_ReturnsEmptyString() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("5"); + + var choices = new List { "One", "Two" }; + + var result = Selection(["Pick a number:"], choices); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public void Selection_NonNumericInput_ReturnsEmptyString() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("abc"); + + var choices = new List { "First", "Second" }; + + var result = Selection(["Pick a number:"], choices); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public void MultiSelection_ReturnsSelectedChoices_InOrder() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("3 1"); + + var choices = new List { "Mercury", "Venus", "Earth" }; + + var result = MultiSelection(["Planets:"], choices); + + Assert.Equal(["Earth", "Mercury"], result); + } + + [Fact] + public void MultiSelection_InvalidEntry_ReturnsEmptyArray() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("2 x"); + + var choices = new List { "Alpha", "Beta", "Gamma" }; + + var result = MultiSelection(["Letters:"], choices); + + Assert.Empty(result); + } + + [Fact] + public void MultiSelection_EmptyInput_ReturnsEmptyArray() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader(string.Empty); + + var choices = new List { "Alpha", "Beta" }; + + var result = MultiSelection(["Letters:"], choices); + + Assert.Empty(result); + } + + [Fact] + public void TreeMenu_ValidSelection_ReturnsTuple() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("2 1"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open", "Save" }, + ["Edit"] = new List { "Undo", "Redo" } + }; + + var (option, subOption) = TreeMenu(["Menu:"], menu); + + Assert.Equal("Edit", option); + Assert.Equal("Undo", subOption); + } + + [Fact] + public void TreeMenu_MissingSelectionParts_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("1"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open" } + }; + + Assert.Throws(() => TreeMenu(["Menu:"], menu)); + } + + [Fact] + public void TreeMenu_InvalidIndexes_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("3 1"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open" }, + ["Edit"] = new List { "Undo" } + }; + + Assert.Throws(() => TreeMenu(["Menu:"], menu)); + } + + [Fact] + public void Table_WritesHeaderAndRows() { + Out = Utilities.GetWriter(out var writer); + + var headers = new List { "Name", "Age" }; + var column1 = new List { "Alice", "Bob" }; + var column2 = new List { "30", "25" }; + + Table(headers, [column1, column2]); + + var output = writer.ToString(); + Assert.Contains("Name", output); + Assert.Contains("Age", output); + Assert.Contains("Alice", output); + Assert.Contains("Bob", output); + } + + [Fact] + public void Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + + var headers = new List { "Name", "Age" }; + var column1 = new List { "Alice", "Bob" }; + + Assert.Throws(() => Table(headers, [column1])); + } +} From 9a6edce09a1a019bc646980438d015b3715d766a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 21:24:04 +0300 Subject: [PATCH 063/108] Added tests for progressbars --- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/ProgressBarTests.cs diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs new file mode 100644 index 0000000..fda9dcc --- /dev/null +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -0,0 +1,55 @@ +namespace PrettyConsole.Tests.Unit; + +public class ProgressBarTests { + [Fact] + public void ProgressBar_Update_WritesStatusAndPercentage() { + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar { + ProgressChar = '#', + ForegroundColor = ConsoleColor.White, + ProgressColor = ConsoleColor.Green + }; + + bar.Update(50, "Loading"); + + var output = errorWriter.ToString(); + Assert.Contains("Loading", output); + Assert.Contains("#", output); + Assert.Contains("50", output); + } + + [Fact] + public void ProgressBar_Update_SamePercentage_NoAdditionalOutput() { + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar(); + + bar.Update(25); + errorWriter.ToStringAndFlush(); + + bar.Update(25); + + Assert.Equal(string.Empty, errorWriter.ToString()); + } + + [Fact] + public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { + Error = Utilities.GetWriter(out var errorWriter); + + 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); + + Assert.Equal(42, result); + Assert.NotEqual(string.Empty, errorWriter.ToString()); + } +} From cd5d05c1256ed54a7feb3d19462b0c024a933dda Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 7 Oct 2025 21:24:11 +0300 Subject: [PATCH 064/108] Added tests for utilities --- PrettyConsole.Tests.Unit/UtilityTests.cs | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/UtilityTests.cs diff --git a/PrettyConsole.Tests.Unit/UtilityTests.cs b/PrettyConsole.Tests.Unit/UtilityTests.cs new file mode 100644 index 0000000..61e4776 --- /dev/null +++ b/PrettyConsole.Tests.Unit/UtilityTests.cs @@ -0,0 +1,90 @@ +using System.Globalization; + +namespace PrettyConsole.Tests.Unit; + +public class UtilityTests { + [Fact] + public void FormatPercentage_PadsAndRoundsToFiveCharacters() { + var originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try { + Span buffer = stackalloc char[8]; + + var span = Utils.FormatPercentage(3, buffer); + Assert.Equal(" 3", span.ToString()); + + span = Utils.FormatPercentage(12.345, buffer); + Assert.Equal("12.35", span.ToString()); + } finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void FormatPercentage_ClampsWithinBounds() { + var originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try { + Span buffer = stackalloc char[8]; + + var span = Utils.FormatPercentage(150, buffer); + Assert.Equal(" 100", span.ToString()); + + span = Utils.FormatPercentage(-10, buffer); + Assert.Equal(" 0", span.ToString()); + } finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void FormatTimeSpan_AdaptsToDuration() { + Span buffer = stackalloc char[32]; + + var written = Utils.FormatTimeSpan(TimeSpan.FromMilliseconds(500), buffer); + Assert.Equal("500ms", new string(buffer[..written])); + + written = Utils.FormatTimeSpan(TimeSpan.FromMilliseconds(12_345), buffer); + Assert.Equal("12:345s", new string(buffer[..written])); + + written = Utils.FormatTimeSpan(TimeSpan.FromMinutes(5) + TimeSpan.FromSeconds(30), buffer); + Assert.Equal("05:30m", new string(buffer[..written])); + + written = Utils.FormatTimeSpan(TimeSpan.FromHours(2) + TimeSpan.FromMinutes(15), buffer); + Assert.Equal("02:15hr", new string(buffer[..written])); + + written = Utils.FormatTimeSpan(TimeSpan.FromDays(1) + TimeSpan.FromHours(3), buffer); + Assert.Equal("01:03d", new string(buffer[..written])); + } + + [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); + } +} From df560f811d6a9ef2b9ddd4c555d0107a04956451 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 21:49:27 +0300 Subject: [PATCH 065/108] Added operator and computed field for default colors --- PrettyConsole/Color.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index b62dce8..a5560b6 100755 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -20,6 +20,21 @@ 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); + } + + /// + /// Returns a tuple of the default foreground and background colors + /// + public static (ConsoleColor fg, ConsoleColor bg) Default => (DefaultForegroundColor, DefaultBackgroundColor); + /// /// Creates a object by combining a string value with a color. /// From 4dcee573cdf720ea70c237df5a3a38089309a1f8 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 21:51:02 +0300 Subject: [PATCH 066/108] Added custom interpolated string handler for hotpaths --- .../PrettyConsoleInterpolatedStringHandler.cs | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs new file mode 100644 index 0000000..3d4a3d1 --- /dev/null +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -0,0 +1,268 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +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 { + private readonly OutputPipe _pipe; + private readonly TextWriter _writer; + private readonly IFormatProvider? _provider; + + /// + /// Creates a new handler that writes to . + /// + /// Estimated literal length supplied by the compiler. + /// Formatted item count supplied by the compiler. + /// The pipe to stream the output to. + /// Always ; reserved for future short-circuiting. + public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, OutputPipe pipe, out bool shouldAppend) + : this(literalLength, formattedCount, pipe, provider: null, out shouldAppend) { + } + + /// + /// Creates a new handler that writes to using for formatting. + /// + /// Estimated literal length supplied by the compiler. + /// Formatted item count supplied by the compiler. + /// The pipe to stream the output to. + /// 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) { + _pipe = pipe; + _writer = Console.GetWriter(pipe); + _provider = provider; + shouldAppend = true; + } + + /// + /// Appends a literal segment supplied by the compiler. + /// + public void AppendLiteral(string value) { + if (!string.IsNullOrEmpty(value)) { + _writer.Write(value); + } + } + + /// + /// Appends a formatted string value. + /// + /// Formatted string. + /// Optional alignment as provided by the interpolation. + /// Unused string format specifier. + public void AppendFormatted(string? value, int alignment = 0, string? format = null) { + AppendString(value, alignment); + } + + /// + /// Appends a span segment without allocations. + /// + /// Characters to write. + /// Optional alignment as provided by the interpolation. + public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { + AppendSpan(value, alignment); + } + + /// + /// Appends a single character. + /// + /// Character to write. + /// Optional alignment as provided by the interpolation. + public void AppendFormatted(char value, int alignment = 0) { + if (alignment == 0) { + _writer.Write(value); + return; + } + + Span buffer = [value]; + AppendSpan(buffer, alignment); + } + + /// + /// Sets the console foreground color to . + /// + public void AppendFormatted(ConsoleColor color) { + // _ = _pipe; + Console.SetColors(color, baseConsole.BackgroundColor); + } + + /// + /// Sets the console foreground color to . + /// + public void AppendFormatted(Color color) { + // _ = _pipe; + Console.SetColors(color, baseConsole.BackgroundColor); + } + + /// + /// Sets the foreground and background colors of the console + /// + /// + public void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors) { + // _ = _pipe; + Console.SetColors(colors.foreground, colors.background); + } + + /// + /// Writes a segment and applies its colors for the duration of the write. + /// + /// Segment to write. + /// Optional alignment as provided by the interpolation. + public void AppendFormatted(ColoredOutput output, int alignment = 0) { + Console.Write(output, _pipe); + if (alignment != 0) { + AppendSpan(ReadOnlySpan.Empty, alignment); + } + } + + /// + /// Writes a buffer of items. + /// + /// Segments to write. + public void AppendFormatted(ReadOnlySpan outputs) { + if (outputs.Length is 0) { + return; + } + Console.Write(outputs, _pipe); + } + + /// + /// Appends a value type that implements without boxing. + /// + public void AppendFormatted(T value) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment: 0, format: null); + } + + /// + /// Appends a value type that implements without boxing while respecting alignment. + /// + public void AppendFormatted(T value, int alignment) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment, format: null); + } + + /// + /// Appends a value type that implements without boxing using the provided format string. + /// + public void AppendFormatted(T value, string? format) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment: 0, format); + } + + /// + /// Appends a value type that implements without boxing using alignment and format string. + /// + public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment, format); + } + + /// + /// Appends an object value when the compiler cannot resolve a more specific overload. + /// + /// Value to write. + /// Optional alignment as provided by the interpolation. + /// Optional format specifier. + public void AppendFormatted(object? value, int alignment = 0, string? format = null) { + if (value is null) { + AppendSpan(ReadOnlySpan.Empty, alignment); + return; + } + + if (value is ConsoleColor consoleColor) { + AppendFormatted(consoleColor); + return; + } + + if (value is Color color) { + AppendFormatted(color); + return; + } + + if (value is ColoredOutput coloredOutput) { + AppendFormatted(coloredOutput, alignment); + return; + } + + if (value is string str) { + AppendString(str, alignment); + return; + } + + if (value is IFormattable formattable) { + AppendString(formattable.ToString(format, _provider), alignment); + return; + } + + AppendString(value.ToString(), alignment); + } + + private void AppendSpanFormattable(T value, int alignment, string? format) + where T : ISpanFormattable { + using var owner = BufferPool.Shared.Rent(); + var buffer = owner.Buffer; + int upperBound = BufferPool.ListStartingSize; + var formatSpan = format is null ? ReadOnlySpan.Empty : format.AsSpan(); + + 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[..charsWritten], alignment); + break; + } + + upperBound *= 2; + } + } + + private void AppendString(string? value, int alignment) { + if (string.IsNullOrEmpty(value)) { + AppendSpan(ReadOnlySpan.Empty, alignment); + return; + } + + AppendSpan(value.AsSpan(), alignment); + } + + private void AppendSpan(scoped ReadOnlySpan span, int alignment) { + if (alignment != 0) { + 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); + } + return; + } + + if (!span.IsEmpty) { + _writer.Write(span); + } + } + + private void WritePadding(int count) { + if (count <= 0) { + return; + } + + Span spaces = stackalloc char[Math.Min(count, 32)]; + spaces.Fill(' '); + while (count > 0) { + int segmentLength = Math.Min(count, spaces.Length); + _writer.Write(spaces[..segmentLength]); + count -= segmentLength; + } + } +} +#pragma warning restore CA1822 // Mark members as static From 02b65cc1a5163199ed9b1e01de0452f0ea0cea41 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 21:51:24 +0300 Subject: [PATCH 067/108] Added entry paths for interpolated string handler --- PrettyConsole/WriteInterpolated.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 PrettyConsole/WriteInterpolated.cs diff --git a/PrettyConsole/WriteInterpolated.cs b/PrettyConsole/WriteInterpolated.cs new file mode 100644 index 0000000..7564b7a --- /dev/null +++ b/PrettyConsole/WriteInterpolated.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +namespace PrettyConsole; + +public static partial class Console { + /// + /// 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) { + _ = pipe; + _ = handler; + } + + /// + /// 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) { + _ = pipe; + _ = handler; + NewLine(pipe); + } +} From 43cbabebcd7815c121242aaf510e2f0eb61b5e44 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:10:46 +0300 Subject: [PATCH 068/108] Added alignment --- .../PrettyConsoleInterpolatedStringHandler.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 3d4a3d1..c3639f0 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -86,7 +86,6 @@ public void AppendFormatted(char value, int alignment = 0) { /// Sets the console foreground color to . /// public void AppendFormatted(ConsoleColor color) { - // _ = _pipe; Console.SetColors(color, baseConsole.BackgroundColor); } @@ -94,17 +93,19 @@ public void AppendFormatted(ConsoleColor color) { /// Sets the console foreground color to . /// public void AppendFormatted(Color color) { - // _ = _pipe; Console.SetColors(color, baseConsole.BackgroundColor); } /// - /// Sets the foreground and background colors of the console - /// - /// - public void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors) { - // _ = _pipe; + /// Sets the foreground and background colors of the console + /// + /// + /// + public void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors, int alignment = 0) { Console.SetColors(colors.foreground, colors.background); + if (alignment != 0) { + AppendSpan(ReadOnlySpan.Empty, alignment); + } } /// From 05afffb5693582226748ade96b3e03aea9e6a8bd Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:12:28 +0300 Subject: [PATCH 069/108] Moved Default to correct place --- PrettyConsole/Color.cs | 5 ----- PrettyConsole/ColorDefaults.cs | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index a5560b6..757d611 100755 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -30,11 +30,6 @@ public static (ConsoleColor fg, ConsoleColor bg) operator /(Color foreground, Co return (foreground, background); } - /// - /// Returns a tuple of the default foreground and background colors - /// - public static (ConsoleColor fg, ConsoleColor bg) Default => (DefaultForegroundColor, DefaultBackgroundColor); - /// /// Creates a object by combining a string value with a color. /// diff --git a/PrettyConsole/ColorDefaults.cs b/PrettyConsole/ColorDefaults.cs index be6b5ff..224ad4e 100755 --- a/PrettyConsole/ColorDefaults.cs +++ b/PrettyConsole/ColorDefaults.cs @@ -11,6 +11,11 @@ public readonly partial record struct Color { /// public static readonly ConsoleColor DefaultBackgroundColor; + /// + /// Returns a tuple of the default foreground and background colors + /// + public static readonly (ConsoleColor fg, ConsoleColor bg) Default = (DefaultForegroundColor, DefaultBackgroundColor); + static Color() { baseConsole.ResetColor(); DefaultForegroundColor = baseConsole.ForegroundColor; From e9cae41598be8013d68cdb83af5835cf32089a0d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:20:28 +0300 Subject: [PATCH 070/108] Improve perf of padding writing --- .../PrettyConsoleInterpolatedStringHandler.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index c3639f0..f6f776b 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -211,7 +211,7 @@ private void AppendSpanFormattable(T value, int alignment, string? format) CollectionsMarshal.SetCount(buffer, upperBound); var span = CollectionsMarshal.AsSpan(buffer); if (value.TryFormat(span, out int charsWritten, formatSpan, _provider)) { - AppendSpan(span[..charsWritten], alignment); + AppendSpan(span.Slice(0, charsWritten), alignment); break; } @@ -257,11 +257,12 @@ private void WritePadding(int count) { return; } - Span spaces = stackalloc char[Math.Min(count, 32)]; - spaces.Fill(' '); + const string paddingChunk = " "; + + ReadOnlySpan chunk = paddingChunk; while (count > 0) { - int segmentLength = Math.Min(count, spaces.Length); - _writer.Write(spaces[..segmentLength]); + int segmentLength = Math.Min(count, chunk.Length); + _writer.Write(chunk.Slice(0, segmentLength)); count -= segmentLength; } } From 40625c5ab2d2994c458aa85da9cbe38caff71e00 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:20:48 +0300 Subject: [PATCH 071/108] Return field to computed state --- PrettyConsole/ColorDefaults.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/ColorDefaults.cs b/PrettyConsole/ColorDefaults.cs index 224ad4e..ced547e 100755 --- a/PrettyConsole/ColorDefaults.cs +++ b/PrettyConsole/ColorDefaults.cs @@ -14,7 +14,7 @@ public readonly partial record struct Color { /// /// Returns a tuple of the default foreground and background colors /// - public static readonly (ConsoleColor fg, ConsoleColor bg) Default = (DefaultForegroundColor, DefaultBackgroundColor); + public static (ConsoleColor fg, ConsoleColor bg) Default => (DefaultForegroundColor, DefaultBackgroundColor); static Color() { baseConsole.ResetColor(); From dba4c2cb2b1835c9ac391474119aac6bf9095ada Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:22:47 +0300 Subject: [PATCH 072/108] Remove discards --- PrettyConsole/WriteInterpolated.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/WriteInterpolated.cs b/PrettyConsole/WriteInterpolated.cs index 7564b7a..cd78a39 100644 --- a/PrettyConsole/WriteInterpolated.cs +++ b/PrettyConsole/WriteInterpolated.cs @@ -3,14 +3,13 @@ namespace PrettyConsole; public static partial class Console { +#pragma warning disable IDE0060 // Remove unused parameter /// /// 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) { - _ = pipe; - _ = handler; } /// @@ -18,9 +17,10 @@ public static void WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandler /// /// Destination pipe. Defaults to . /// Interpolated string handler that streams the content. + public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { - _ = pipe; - _ = handler; NewLine(pipe); } +#pragma warning restore IDE0060 // Remove unused parameter + } From 15c374f30a6c6fdcbd6a607eafa71a6d8cb6f8f6 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:29:41 +0300 Subject: [PATCH 073/108] Added tests to cover interpolated handler and default colors and new operator --- PrettyConsole.Tests.Unit/ColorTests.cs | 14 +++++ .../InterpolatedStringHandlerTests.cs | 61 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs diff --git a/PrettyConsole.Tests.Unit/ColorTests.cs b/PrettyConsole.Tests.Unit/ColorTests.cs index 619a1f0..91ea006 100644 --- a/PrettyConsole.Tests.Unit/ColorTests.cs +++ b/PrettyConsole.Tests.Unit/ColorTests.cs @@ -53,6 +53,13 @@ public void Color_StaticField_EqualsConsoleColor() { } } + [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; @@ -73,6 +80,13 @@ public void Color_ObjectOperator() { Assert.Equal("3", coloredOutput.Value); } + [Fact] + public void Color_DefaultColors() { + Assert.Equal(System.Console.ForegroundColor, Color.DefaultForegroundColor); + Assert.Equal(System.Console.BackgroundColor, Color.DefaultBackgroundColor); + Assert.Equal((System.Console.ForegroundColor, System.Console.BackgroundColor), Color.Default); + } + [Fact] public void ColoredOutput_ForegroundCtor() { var coloredOutput = new ColoredOutput("Hello", Color.Red); diff --git a/PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs b/PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs new file mode 100644 index 0000000..4decd5d --- /dev/null +++ b/PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs @@ -0,0 +1,61 @@ +namespace PrettyConsole.Tests.Unit; + +public class InterpolatedStringHandlerTests { + [Fact] + public void WriteInterpolated_WritesFormattedContent_ToOutPipe() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + WriteInterpolated(OutputPipe.Out, $"Hello {42}"); + Assert.Equal("Hello 42", writer.ToString()); + } finally { + Out = originalOut; + } + } + + [Fact] + public void WriteInterpolated_WritesFormattedContent_ToErrorPipe() { + var originalError = Error; + var writer = new StringWriter(); + Error = writer; + + try { + WriteInterpolated(OutputPipe.Error, $"Error {123}"); + Assert.Equal("Error 123", writer.ToString()); + } finally { + Error = originalError; + } + } + + [Fact] + public void WriteLineInterpolated_AppendsNewLine() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + WriteLineInterpolated(OutputPipe.Out, $"Line {7}"); + Assert.Equal($"Line 7{writer.NewLine}", writer.ToString()); + } finally { + Out = originalOut; + } + } + + [Fact] + public void WriteInterpolated_IgnoresColorTokensInOutput() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + WriteInterpolated(OutputPipe.Out, + $"Colors {Color.Black / Color.Green}Green{Color.Default} {Color.Red}Red{Color.Default}"); + + Assert.Equal("Colors Green Red", writer.ToString()); + } finally { + Out = originalOut; + } + } +} From 1b73220724320baef9d37ae9d8d768deae77e1b8 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Fri, 17 Oct 2025 22:31:32 +0300 Subject: [PATCH 074/108] Added memory allocation measurement function --- PrettyConsole.Tests/Program.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index c8fbbfb..e776b96 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -1,5 +1,4 @@ -// See https://aka.ms/new-console-template for more information - +using PrettyConsole; using PrettyConsole.Tests; using PrettyConsole.Tests.Features; @@ -27,4 +26,16 @@ foreach (var test in tests) { await test.Render(); NewLine(); -} \ No newline at end of file +} + +#pragma warning disable CS8321 // Local function is declared but never used + +static void Measure(string label, Action action) { + long before = GC.GetAllocatedBytesForCurrentThread(); + action(); + long after = GC.GetAllocatedBytesForCurrentThread(); + NewLine(); + WriteLine($"{label} - allocated {after - before} bytes"); + NewLine(); +} +#pragma warning restore CS8321 // Local function is declared but never used \ No newline at end of file From ef5ed0056155f02be3f490659382365f80b6344e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 08:57:56 +0300 Subject: [PATCH 075/108] migrated sln to slnx --- .gitignore | 1 + PrettyConsole.sln | 37 ------------------------------------- PrettyConsole.slnx | 5 +++++ 3 files changed, 6 insertions(+), 37 deletions(-) delete mode 100755 PrettyConsole.sln create mode 100644 PrettyConsole.slnx diff --git a/.gitignore b/.gitignore index 3cc9b82..dc788bb 100755 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ *.sln.docstates PrettyConsole.Tests.Integration/ WARP.md +AGENTS.md # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/PrettyConsole.sln b/PrettyConsole.sln deleted file mode 100755 index e535e98..0000000 --- a/PrettyConsole.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31717.71 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PrettyConsole", "PrettyConsole\PrettyConsole.csproj", "{03ADF094-C7A1-4771-A4F1-4A8498D87B34}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrettyConsole.Tests", "PrettyConsole.Tests\PrettyConsole.Tests.csproj", "{128CE278-869A-4896-8977-1A588FFD6A15}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrettyConsole.Tests.Unit", "PrettyConsole.Tests.Unit\PrettyConsole.Tests.Unit.csproj", "{202848E2-F4F8-4DEE-904D-AF610CF30765}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Release|Any CPU.Build.0 = Release|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Release|Any CPU.Build.0 = Release|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Debug|Any CPU.Build.0 = Debug|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Release|Any CPU.ActiveCfg = Release|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {457DC559-ED77-4D40-AC98-77E3339516C9} - EndGlobalSection -EndGlobal diff --git a/PrettyConsole.slnx b/PrettyConsole.slnx new file mode 100644 index 0000000..61c5abd --- /dev/null +++ b/PrettyConsole.slnx @@ -0,0 +1,5 @@ + + + + + From d4f6bd06d937ec52dfa14d0b69fc3e75fe7a42c9 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 10:09:51 +0300 Subject: [PATCH 076/108] Vastly improved buffer pool --- PrettyConsole/BufferPool.cs | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/PrettyConsole/BufferPool.cs b/PrettyConsole/BufferPool.cs index b706149..862cb08 100644 --- a/PrettyConsole/BufferPool.cs +++ b/PrettyConsole/BufferPool.cs @@ -1,30 +1,30 @@ -using System.Collections.Concurrent; using System.Runtime.CompilerServices; +using System.Threading.Channels; namespace PrettyConsole; internal sealed class BufferPool { - internal static readonly BufferPool Shared = new(); + internal static readonly BufferPool Shared = new(); - private readonly ConcurrentQueue> _collection; - private long _poolSize; - private readonly int _maxCapacity; + private readonly Channel> _channel; private List? _fastItem; - public const int ListStartingSize = 256; + public const int ListStartingSize = 256; private BufferPool() { - _collection = new(); - _maxCapacity = Environment.ProcessorCount * 2; - } + _channel = Channel.CreateBounded> + (new BoundedChannelOptions(Environment.ProcessorCount * 2) { + SingleWriter = false, + SingleReader = false, + FullMode = BoundedChannelFullMode.DropWrite + }); + } [MethodImpl(MethodImplOptions.AggressiveOptimization)] public RentedBufferOwner Rent() { var buffer = _fastItem; if (buffer is null || Interlocked.CompareExchange(ref _fastItem, null, buffer) != buffer) { - if (_collection.TryDequeue(out buffer)) { - Interlocked.Decrement(ref _poolSize); - // return item; + if (_channel.Reader.TryRead(out buffer)) { return new RentedBufferOwner(this, buffer); } @@ -39,14 +39,10 @@ private void Return(List buffer) { return; } if (_fastItem is not null || Interlocked.CompareExchange(ref _fastItem, buffer, null) != null) { - if (Interlocked.Increment(ref _poolSize) <= _maxCapacity) { - buffer.Clear(); - _collection.Enqueue(buffer); - } else { - // no room, clean up the count and drop the object on the floor - Interlocked.Decrement(ref _poolSize); + if (_channel.Writer.TryWrite(buffer)) { + buffer.Clear(); } - } + } } internal readonly struct RentedBufferOwner : IDisposable { From b1285fbf74b3553d06fffe5b783a4075d59727a1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 13:03:07 +0300 Subject: [PATCH 077/108] Better more optimized buffer pool --- PrettyConsole/BufferPool.cs | 124 +++++++++++++----- PrettyConsole/IndeterminateProgressBar.cs | 3 +- PrettyConsole/Menus.cs | 9 +- .../PrettyConsoleInterpolatedStringHandler.cs | 3 +- PrettyConsole/ProgressBar.cs | 8 +- PrettyConsole/Write.cs | 3 +- 6 files changed, 100 insertions(+), 50 deletions(-) diff --git a/PrettyConsole/BufferPool.cs b/PrettyConsole/BufferPool.cs index 862cb08..7c33ce2 100644 --- a/PrettyConsole/BufferPool.cs +++ b/PrettyConsole/BufferPool.cs @@ -1,59 +1,115 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; namespace PrettyConsole; -internal sealed class BufferPool { - internal static readonly BufferPool Shared = new(); - +internal sealed class BufferPool : IDisposable { + private bool _disposed; private readonly Channel> _channel; - private List? _fastItem; + private readonly Func> _createPolicy; + private readonly Func, bool> _returnPolicy; + private readonly ThreadLocal?> _fastItem; + + internal const int ListStartingSize = 256; + internal const int ListMaxSize = 4096; - public const int ListStartingSize = 256; + public static readonly BufferPool Shared + = new(() => new(ListStartingSize), + item => { + if (item.Count > ListMaxSize) { + return false; + } + item.Clear(); + return true; + }); - private BufferPool() { - _channel = Channel.CreateBounded> - (new BoundedChannelOptions(Environment.ProcessorCount * 2) { - SingleWriter = false, - SingleReader = false, - FullMode = BoundedChannelFullMode.DropWrite - }); + private BufferPool(Func> createPolicy, Func, bool> returnPolicy) { + _channel = Channel.CreateBounded>(new BoundedChannelOptions(Environment.ProcessorCount * 2) { + SingleWriter = false, + SingleReader = false, + FullMode = BoundedChannelFullMode.DropWrite + }); + _createPolicy = createPolicy ?? throw new ArgumentNullException(nameof(createPolicy)); + _returnPolicy = returnPolicy ?? throw new ArgumentNullException(nameof(returnPolicy)); + _fastItem = new(() => null, trackAllValues: false); } - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public RentedBufferOwner Rent() { - var buffer = _fastItem; - if (buffer is null || Interlocked.CompareExchange(ref _fastItem, null, buffer) != buffer) { - if (_channel.Reader.TryRead(out buffer)) { - return new RentedBufferOwner(this, buffer); - } + [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 = _createPolicy(); + return new(this, value); + } - return new RentedBufferOwner(this, new(ListStartingSize)); + [MethodImpl(MethodImplOptions.NoInlining)] + private void Return(List item) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!_returnPolicy(item)) { + (item as IDisposable)?.Dispose(); + return; + } + if (_fastItem.Value is null) { + _fastItem.Value = item; + return; + } + if (!_channel.Writer.TryWrite(item)) { + (item as IDisposable)?.Dispose(); } - return new RentedBufferOwner(this, buffer); } - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private void Return(List buffer) { - if (buffer.Count > 4096) { + public void Dispose() { + if (_disposed) { return; } - if (_fastItem is not null || Interlocked.CompareExchange(ref _fastItem, buffer, null) != null) { - if (_channel.Writer.TryWrite(buffer)) { - buffer.Clear(); - } + + while (_channel.Reader.TryRead(out var it)) { + (it as IDisposable)?.Dispose(); } + _fastItem?.Dispose(); + + _disposed = true; + GC.SuppressFinalize(this); } - internal readonly struct RentedBufferOwner : IDisposable { - private readonly BufferPool _pool; - public readonly List Buffer; + [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."); - public RentedBufferOwner(BufferPool pool, List buffer) { + internal PooledObjectOwner(BufferPool pool, List value) { _pool = pool; - Buffer = buffer; + _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); } - public readonly void Dispose() => _pool.Return(Buffer); + 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/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index 107148f..e5aa1db 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -125,8 +125,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d if (DisplayElapsedTime) { var elapsed = Stopwatch.GetElapsedTime(startTime); const string elapsedLabel = " [Elapsed: "; - using var bufferOwner = BufferPool.Shared.Rent(); - var buf = bufferOwner.Buffer; + using var bufferOwner = BufferPool.Shared.Rent(out var buf); CollectionsMarshal.SetCount(buf, 256); // starting size is 256 Span span = CollectionsMarshal.AsSpan(buf); elapsedLabel.CopyTo(span); diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 7dff9d8..aef0bde 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -17,8 +17,7 @@ public static string Selection(ReadOnlySpan title, TList c where TList : IList { WriteLine(title); - using var bufferOwner = BufferPool.Shared.Rent(); - var buffer = bufferOwner.Buffer; + using var bufferOwner = BufferPool.Shared.Rent(out var buffer); var width = GetWidthOrDefault(); buffer.EnsureCapacity(width); CollectionsMarshal.SetCount(buffer, width); @@ -57,8 +56,7 @@ public static string[] MultiSelection(ReadOnlySpan title, where TList : IList { WriteLine(title); - using var bufferOwner = BufferPool.Shared.Rent(); - var buffer = bufferOwner.Buffer; + using var bufferOwner = BufferPool.Shared.Rent(out var buffer); var width = GetWidthOrDefault(); buffer.EnsureCapacity(width); CollectionsMarshal.SetCount(buffer, width); @@ -117,8 +115,7 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan x.Length) + 10; // Used to make sub-tree prefix spaces uniform - using var bufferOwner = BufferPool.Shared.Rent(); - var buffer = bufferOwner.Buffer; + using var bufferOwner = BufferPool.Shared.Rent(out var buffer); var width = GetWidthOrDefault(); buffer.EnsureCapacity(width); CollectionsMarshal.SetCount(buffer, width); diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index f6f776b..6a489f6 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -201,8 +201,7 @@ public void AppendFormatted(object? value, int alignment = 0, string? format = n private void AppendSpanFormattable(T value, int alignment, string? format) where T : ISpanFormattable { - using var owner = BufferPool.Shared.Rent(); - var buffer = owner.Buffer; + using var owner = BufferPool.Shared.Rent(out var buffer); int upperBound = BufferPool.ListStartingSize; var formatSpan = format is null ? ReadOnlySpan.Empty : format.AsSpan(); diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 61f67e7..40ef244 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -77,10 +77,10 @@ public void Update(double percentage, ReadOnlySpan status) { } // Prepare the buffer exactly for the characters we will write for the bar (pLength) - using var listOwner = BufferPool.Shared.Rent(); - listOwner.Buffer.EnsureCapacity(pLength); - CollectionsMarshal.SetCount(listOwner.Buffer, pLength); - Span buf = CollectionsMarshal.AsSpan(listOwner.Buffer); + using var listOwner = BufferPool.Shared.Rent(out var list); + list.EnsureCapacity(pLength); + CollectionsMarshal.SetCount(list, pLength); + Span buf = CollectionsMarshal.AsSpan(list); _currentProgress = p; diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index d814c0d..9fa4e2c 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -67,9 +67,8 @@ 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(); + using var listOwner = BufferPool.Shared.Rent(out var lst); int upperBound = BufferPool.ListStartingSize; - var lst = listOwner.Buffer; while (true) { lst.EnsureCapacity(upperBound); CollectionsMarshal.SetCount(lst, upperBound); From 5add6ce2359dbdecaabbd1acf2f3bf66f1aa8dd0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 13:03:20 +0300 Subject: [PATCH 078/108] Remove check --- PrettyConsole.Tests/Program.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index e776b96..87cf433 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -11,8 +11,6 @@ // .Select(x => (IPrettyConsoleTest)Activator.CreateInstance(x)!) // .ToArray(); -Write(5); - var tests = new IPrettyConsoleTest[] { new ColoredOutputTest(), new SelectionTest(), From 3f1e12bf54972f6bc3742ebca52b47e6f10b0d6a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:07:44 +0300 Subject: [PATCH 079/108] Handler improvement --- .../PrettyConsoleInterpolatedStringHandler.cs | 82 +++++++++++++++---- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 6a489f6..6f781ce 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -13,6 +13,16 @@ public readonly ref struct PrettyConsoleInterpolatedStringHandler { private readonly TextWriter _writer; private readonly IFormatProvider? _provider; + /// + /// Creates a new handler that writes to . + /// + /// Estimated literal length supplied by the compiler. + /// Formatted item count supplied by the compiler. + /// Always ; reserved for future short-circuiting. + public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, out bool shouldAppend) + : this(literalLength, formattedCount, OutputPipe.Out, provider: null, out shouldAppend) { + } + /// /// Creates a new handler that writes to . /// @@ -42,7 +52,7 @@ public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCo /// /// Appends a literal segment supplied by the compiler. /// - public void AppendLiteral(string value) { + public readonly void AppendLiteral(string value) { if (!string.IsNullOrEmpty(value)) { _writer.Write(value); } @@ -54,7 +64,7 @@ public void AppendLiteral(string value) { /// Formatted string. /// Optional alignment as provided by the interpolation. /// Unused string format specifier. - public void AppendFormatted(string? value, int alignment = 0, string? format = null) { + public readonly void AppendFormatted(string? value, int alignment = 0, string? format = null) { AppendString(value, alignment); } @@ -63,7 +73,7 @@ public void AppendFormatted(string? value, int alignment = 0, string? format = n /// /// Characters to write. /// Optional alignment as provided by the interpolation. - public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { + public readonly void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { AppendSpan(value, alignment); } @@ -72,7 +82,7 @@ public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) /// /// Character to write. /// Optional alignment as provided by the interpolation. - public void AppendFormatted(char value, int alignment = 0) { + public readonly void AppendFormatted(char value, int alignment = 0) { if (alignment == 0) { _writer.Write(value); return; @@ -85,14 +95,14 @@ public void AppendFormatted(char value, int alignment = 0) { /// /// Sets the console foreground color to . /// - public void AppendFormatted(ConsoleColor color) { + public readonly void AppendFormatted(ConsoleColor color) { Console.SetColors(color, baseConsole.BackgroundColor); } /// /// Sets the console foreground color to . /// - public void AppendFormatted(Color color) { + public readonly void AppendFormatted(Color color) { Console.SetColors(color, baseConsole.BackgroundColor); } @@ -101,7 +111,7 @@ public void AppendFormatted(Color color) { /// /// /// - public void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors, int alignment = 0) { + public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor background) colors, int alignment = 0) { Console.SetColors(colors.foreground, colors.background); if (alignment != 0) { AppendSpan(ReadOnlySpan.Empty, alignment); @@ -113,7 +123,7 @@ public void AppendFormatted((ConsoleColor foreground, ConsoleColor background) c /// /// Segment to write. /// Optional alignment as provided by the interpolation. - public void AppendFormatted(ColoredOutput output, int alignment = 0) { + public readonly void AppendFormatted(ColoredOutput output, int alignment = 0) { Console.Write(output, _pipe); if (alignment != 0) { AppendSpan(ReadOnlySpan.Empty, alignment); @@ -124,38 +134,74 @@ public void AppendFormatted(ColoredOutput output, int alignment = 0) { /// Writes a buffer of items. /// /// Segments to write. - public void AppendFormatted(ReadOnlySpan outputs) { + public readonly void AppendFormatted(ReadOnlySpan outputs) { if (outputs.Length is 0) { return; } Console.Write(outputs, _pipe); } + /// + /// Append timeSpan with or without elapsed time formatting (human readable) + /// + /// + /// + public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) { + if (format != "hr") { + AppendSpanFormattable(timeSpan, 0, 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); + } + } + /// /// Appends a value type that implements without boxing. /// - public void AppendFormatted(T value) where T : ISpanFormattable { + public readonly void AppendFormatted(T value) where T : ISpanFormattable { AppendSpanFormattable(value, alignment: 0, format: null); } /// /// Appends a value type that implements without boxing while respecting alignment. /// - public void AppendFormatted(T value, int alignment) where T : ISpanFormattable { + public readonly void AppendFormatted(T value, int alignment) where T : ISpanFormattable { AppendSpanFormattable(value, alignment, format: null); } /// /// Appends a value type that implements without boxing using the provided format string. /// - public void AppendFormatted(T value, string? format) where T : ISpanFormattable { + public readonly void AppendFormatted(T value, string? format) where T : ISpanFormattable { AppendSpanFormattable(value, alignment: 0, format); } /// /// Appends a value type that implements without boxing using alignment and format string. /// - public void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { + public readonly void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { AppendSpanFormattable(value, alignment, format); } @@ -165,7 +211,7 @@ public void AppendFormatted(T value, int alignment, string? format) where T : /// Value to write. /// Optional alignment as provided by the interpolation. /// Optional format specifier. - public void AppendFormatted(object? value, int alignment = 0, string? format = null) { + public readonly void AppendFormatted(object? value, int alignment = 0, string? format = null) { if (value is null) { AppendSpan(ReadOnlySpan.Empty, alignment); return; @@ -199,7 +245,7 @@ public void AppendFormatted(object? value, int alignment = 0, string? format = n AppendString(value.ToString(), alignment); } - private void AppendSpanFormattable(T value, int alignment, string? format) + 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; @@ -218,7 +264,7 @@ private void AppendSpanFormattable(T value, int alignment, string? format) } } - private void AppendString(string? value, int alignment) { + private readonly void AppendString(string? value, int alignment) { if (string.IsNullOrEmpty(value)) { AppendSpan(ReadOnlySpan.Empty, alignment); return; @@ -227,7 +273,7 @@ private void AppendString(string? value, int alignment) { AppendSpan(value.AsSpan(), alignment); } - private void AppendSpan(scoped ReadOnlySpan span, int alignment) { + private readonly void AppendSpan(scoped ReadOnlySpan span, int alignment) { if (alignment != 0) { bool leftAlign = alignment < 0; int width = Math.Abs(alignment); @@ -251,7 +297,7 @@ private void AppendSpan(scoped ReadOnlySpan span, int alignment) { } } - private void WritePadding(int count) { + private readonly void WritePadding(int count) { if (count <= 0) { return; } From bd846ddd249ae4c679b068b55cac33db6292efc3 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:07:55 +0300 Subject: [PATCH 080/108] Remove interpolated suffix --- PrettyConsole/WriteInterpolated.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/WriteInterpolated.cs b/PrettyConsole/WriteInterpolated.cs index cd78a39..3c397a0 100644 --- a/PrettyConsole/WriteInterpolated.cs +++ b/PrettyConsole/WriteInterpolated.cs @@ -4,12 +4,30 @@ namespace PrettyConsole; public static partial class Console { #pragma warning disable IDE0060 // Remove unused parameter + /// + /// 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 WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + public static void Write(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + } + + /// + /// Writes interpolated content using to . + /// + /// Interpolated string handler that streams the content. + public static void WriteLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + NewLine(OutputPipe.Out); } /// @@ -17,10 +35,9 @@ public static void WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandler /// /// Destination pipe. Defaults to . /// Interpolated string handler that streams the content. - - public static void WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + public static void WriteLine(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); NewLine(pipe); } #pragma warning restore IDE0060 // Remove unused parameter - } From 1b6d9d778a7c58c9f1bbcb155f2d5f8f3c9a3492 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:46:12 +0300 Subject: [PATCH 081/108] Suppressed unused parameters - string handler --- PrettyConsole/GlobalSuppressions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 PrettyConsole/GlobalSuppressions.cs diff --git a/PrettyConsole/GlobalSuppressions.cs b/PrettyConsole/GlobalSuppressions.cs new file mode 100644 index 0000000..203ad04 --- /dev/null +++ b/PrettyConsole/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// +using System.Diagnostics.CodeAnalysis; + +// Mainly used for PrettyConsoleInterpolatedStringHandler (will be used at call site by the compiler) +[assembly: SuppressMessage( + "Style", + "IDE0060:Remove unused parameter", + Justification = "Parameters may be intentionally unused for API shape consistency or interface compliance.")] \ No newline at end of file From 023242bc7f1e58587ab776f7f44c68121f4bac46 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:46:24 +0300 Subject: [PATCH 082/108] Add compiler services --- PrettyConsole/GlobalUsings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PrettyConsole/GlobalUsings.cs b/PrettyConsole/GlobalUsings.cs index 28e2df2..2766f22 100755 --- a/PrettyConsole/GlobalUsings.cs +++ b/PrettyConsole/GlobalUsings.cs @@ -1 +1,2 @@ -global using baseConsole = System.Console; \ No newline at end of file +global using System.Runtime.CompilerServices; +global using baseConsole = System.Console; From 43dbf986696b777ea9a2e95e3461f963f495cf93 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:46:40 +0300 Subject: [PATCH 083/108] Implemented string handler --- PrettyConsole/Menus.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index aef0bde..2a3b404 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -17,15 +17,8 @@ public static string Selection(ReadOnlySpan title, TList c where TList : IList { WriteLine(title); - using var bufferOwner = BufferPool.Shared.Rent(out var buffer); - var width = GetWidthOrDefault(); - buffer.EnsureCapacity(width); - CollectionsMarshal.SetCount(buffer, width); - var span = CollectionsMarshal.AsSpan(buffer); - for (int i = 0; i < choices.Count; i++) { - span.TryWrite($" {i + 1}) {choices[i]}", out var written); - Out.WriteLine(span.Slice(0, written)); + WriteLine($" {i + 1}) {choices[i]}"); } NewLine(); @@ -56,15 +49,8 @@ public static string[] MultiSelection(ReadOnlySpan title, where TList : IList { WriteLine(title); - using var bufferOwner = BufferPool.Shared.Rent(out var buffer); - var width = GetWidthOrDefault(); - buffer.EnsureCapacity(width); - CollectionsMarshal.SetCount(buffer, width); - var span = CollectionsMarshal.AsSpan(buffer); - for (int i = 0; i < choices.Count; i++) { - span.TryWrite($" {i + 1}) {choices[i]}", out var written); - Out.WriteLine(span.Slice(0, written)); + WriteLine($" {i + 1}) {choices[i]}"); } NewLine(); From e45dfff6d3e99d94f59454d995514876eb2f0155 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:47:01 +0300 Subject: [PATCH 084/108] Moved string handler overloads to write and writeline respectively --- PrettyConsole/Write.cs | 17 ++++++++++++ PrettyConsole/WriteInterpolated.cs | 43 ------------------------------ PrettyConsole/WriteLine.cs | 19 +++++++++++++ 3 files changed, 36 insertions(+), 43 deletions(-) delete mode 100644 PrettyConsole/WriteInterpolated.cs diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index 9fa4e2c..d90f446 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -3,6 +3,23 @@ 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 /// diff --git a/PrettyConsole/WriteInterpolated.cs b/PrettyConsole/WriteInterpolated.cs deleted file mode 100644 index 3c397a0..0000000 --- a/PrettyConsole/WriteInterpolated.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace PrettyConsole; - -public static partial class Console { -#pragma warning disable IDE0060 // Remove unused parameter - /// - /// 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 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); - } -#pragma warning restore IDE0060 // Remove unused parameter -} diff --git a/PrettyConsole/WriteLine.cs b/PrettyConsole/WriteLine.cs index 0f4ab94..e32cd89 100755 --- a/PrettyConsole/WriteLine.cs +++ b/PrettyConsole/WriteLine.cs @@ -1,6 +1,25 @@ 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); + } + /// /// Write a to the error console /// From 5fa3238bdcebfffb55ce3e7aada7f7d893652350 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:47:12 +0300 Subject: [PATCH 085/108] Added string handler overloads --- PrettyConsole/ReadLine.cs | 98 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/PrettyConsole/ReadLine.cs b/PrettyConsole/ReadLine.cs index 7d73835..21ec3a2 100755 --- a/PrettyConsole/ReadLine.cs +++ b/PrettyConsole/ReadLine.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace PrettyConsole; public static partial class Console { @@ -11,7 +13,20 @@ public static partial class Console { public static bool TryReadLine(ReadOnlySpan message, out T? result) where T : IParsable { Write(message, OutputPipe.Out); var input = In.ReadLine(); - return T.TryParse(input, null, out result); + return T.TryParse(input, CultureInfo.CurrentCulture, out result); + } + + /// + /// 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); } /// @@ -32,6 +47,24 @@ public static bool TryReadLine(ReadOnlySpan message, T @defaul return false; } + /// + /// 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. /// @@ -44,6 +77,18 @@ public static bool TryReadLine(ReadOnlySpan message, bool return TryReadLine(message, ignoreCase, default, out result); } + /// + /// 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. /// @@ -63,6 +108,25 @@ public static bool TryReadLine(ReadOnlySpan message, bool return res; } + /// + /// 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 without any prepended message /// @@ -82,6 +146,15 @@ public static bool TryReadLine(ReadOnlySpan message, bool return ReadLine(); } + /// + /// 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. /// @@ -93,6 +166,17 @@ public static bool TryReadLine(ReadOnlySpan message, bool return result; } + /// + /// 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. /// @@ -104,4 +188,16 @@ public static T ReadLine(ReadOnlySpan message, T @default) whe _ = TryReadLine(message, @default, out T result); 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 From 5ada6cee22806cda0b1eb58f6da8c281acfabec0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 17:47:26 +0300 Subject: [PATCH 086/108] Removed unused using statement --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 6f781ce..50fff1e 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace PrettyConsole; From 923f5f0ab5305a8f0f516252b92c1d4cd3852884 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 18:02:25 +0300 Subject: [PATCH 087/108] Improved using handler --- PrettyConsole/IndeterminateProgressBar.cs | 13 +------------ PrettyConsole/ProgressBar.cs | 8 ++------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/IndeterminateProgressBar.cs index e5aa1db..ff6894a 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -1,7 +1,5 @@ using System.Collections.ObjectModel; using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace PrettyConsole; @@ -124,16 +122,7 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d if (DisplayElapsedTime) { var elapsed = Stopwatch.GetElapsedTime(startTime); - const string elapsedLabel = " [Elapsed: "; - using var bufferOwner = BufferPool.Shared.Rent(out var buf); - CollectionsMarshal.SetCount(buf, 256); // starting size is 256 - Span span = CollectionsMarshal.AsSpan(buf); - elapsedLabel.CopyTo(span); - int length = elapsedLabel.Length; - length += Utils.FormatTimeSpan(elapsed, span.Slice(length)); - span.Slice(length)[0] = ']'; - length += 1; - Error.Write(buf.Slice(0, length)); + Write(OutputPipe.Error, $" [Elapsed: {elapsed:hr}]"); } Error.WriteWhiteSpaces(PaddingLength); diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 40ef244..795819b 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace PrettyConsole; @@ -31,9 +30,6 @@ public class ProgressBar { /// public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; - // small buffer to write percentages - private readonly char[] _percentageBuffer = new char[20]; - private int _currentProgress; private readonly Lock _lock = new(); @@ -114,7 +110,7 @@ public void Update(double percentage, ReadOnlySpan status) { baseConsole.ForegroundColor = ForegroundColor; Error.Write("] "); // Write percentage - Error.Write(Utils.FormatPercentage(percentage, _percentageBuffer)); + Write(OutputPipe.Error, $"{percentage, 5:##.##}"); GoToLine(currentLine); } finally { // Ensure colors and buffer are reset even if an exception occurs mid-render From fb7f686c771e9151e0a1cffedf8ed6a4dece3f0a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 18:02:37 +0300 Subject: [PATCH 088/108] Better whitespace --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 50fff1e..2201b71 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -301,14 +301,7 @@ private readonly void WritePadding(int count) { return; } - const string paddingChunk = " "; - - ReadOnlySpan chunk = paddingChunk; - while (count > 0) { - int segmentLength = Math.Min(count, chunk.Length); - _writer.Write(chunk.Slice(0, segmentLength)); - count -= segmentLength; - } + _writer.WriteWhiteSpaces(count); } } #pragma warning restore CA1822 // Mark members as static From 3fffe3a7029c58ccf41757e40c463b0ccfebef9d Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 18:07:00 +0300 Subject: [PATCH 089/108] Added overloads for handler --- PrettyConsole/AdvancedInputs.cs | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/PrettyConsole/AdvancedInputs.cs b/PrettyConsole/AdvancedInputs.cs index 3e32462..92e288d 100755 --- a/PrettyConsole/AdvancedInputs.cs +++ b/PrettyConsole/AdvancedInputs.cs @@ -16,6 +16,15 @@ public static void RequestAnyInput(ReadOnlySpan output) { _ = baseConsole.ReadKey(); } + /// + /// 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"] /// @@ -24,6 +33,7 @@ public static void RequestAnyInput(ReadOnlySpan output) { /// /// 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 /// @@ -31,6 +41,17 @@ 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 + /// + /// 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 /// @@ -55,4 +76,29 @@ public static bool Confirm(ReadOnlySpan message, ReadOnlySpan + /// 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 From 81c975f05724c602227718da6a3bad51610e6a72 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 18:07:06 +0300 Subject: [PATCH 090/108] Removed unused methods --- PrettyConsole/Utils.cs | 74 ------------------------------------------ 1 file changed, 74 deletions(-) diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 5342b41..cefd6ab 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -1,83 +1,9 @@ -using System.Globalization; - namespace PrettyConsole; /// /// A static class containing utility methods /// internal static class Utils { - /// - /// Returns a formatted percentage string, i.e 0,5:##0.##% - /// - /// - /// - /// - internal static ReadOnlySpan FormatPercentage(double percentage, Span buffer) { - const int length = 5; - percentage = Math.Round(Math.Clamp(percentage, 0, 100), 2, MidpointRounding.AwayFromZero); - var currentCulture = CultureInfo.CurrentCulture; - - percentage.TryFormat(buffer, out int written, provider: currentCulture); - if (written == length) { - return buffer.Slice(0, written); - } - - var padding = length - written; - buffer.Slice(0, padding).Fill(' '); - percentage.TryFormat(buffer.Slice(padding), out written, provider: currentCulture); - - return buffer.Slice(0, padding + written); - } - - /// - /// Formats - /// - /// - /// - /// The number of characters written to - internal static int FormatTimeSpan(TimeSpan timeSpan, Span buffer) { - // < 1s → "500ms" - int written; - - if (timeSpan.TotalSeconds < 1) { - if (!timeSpan.Milliseconds.TryFormat(buffer, out written, provider: CultureInfo.CurrentCulture)) { - return 0; - } - "ms".CopyTo(buffer.Slice(written)); - return written + 2; - } - - // < 60s → "SS:MMMs" (zero-padded) - if (timeSpan.TotalSeconds < 60) { - if (!buffer.TryWrite($"{timeSpan.Seconds:00}:{timeSpan.Milliseconds:000}s", out written)) { - return 0; - } - return written; - } - - // < 1h → "MM:SSm" - if (timeSpan.TotalSeconds < 3600) { - if (!buffer.TryWrite($"{timeSpan.Minutes:00}:{timeSpan.Seconds:00}m", out written)) { - return 0; - } - return written; - } - - // < 1d → "HH:MMhr" - if (timeSpan.TotalSeconds < 86400) { - if (!buffer.TryWrite($"{timeSpan.Hours:00}:{timeSpan.Minutes:00}hr", out written)) { - return 0; - } - return written; - } - - // ≥ 1d → "DD:HHd" - if (!buffer.TryWrite($"{timeSpan.Days:00}:{timeSpan.Hours:00}d", out written)) { - return 0; - } - return written; - } - /// /// Constant buffer filled with whitespaces /// From 25359a77e02f2e6b4330429a5ae6ef1322c99b02 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 21:23:42 +0300 Subject: [PATCH 091/108] Moved and added tests --- PrettyConsole.Tests.Unit/AdvancedInputs.cs | 40 ++++++++++++ .../InterpolatedStringHandlerTests.cs | 61 ------------------- PrettyConsole.Tests.Unit/Write.cs | 44 +++++++++++++ PrettyConsole.Tests.Unit/WriteLine.cs | 14 +++++ 4 files changed, 98 insertions(+), 61 deletions(-) delete mode 100644 PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs diff --git a/PrettyConsole.Tests.Unit/AdvancedInputs.cs b/PrettyConsole.Tests.Unit/AdvancedInputs.cs index e1d5a24..c736997 100755 --- a/PrettyConsole.Tests.Unit/AdvancedInputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedInputs.cs @@ -40,4 +40,44 @@ public void Confirm_Case_No() { 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:"); + Assert.Contains("Enter y:", stringWriter.ToString()); + Assert.True(res); + } + + [Fact] + public void Confirm_Case_Yes_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("yes"); + In = reader; + var res = Confirm($"Enter yes:"); + Assert.Contains("Enter yes", stringWriter.ToString()); + Assert.True(res); + } + + [Fact] + public void Confirm_Case_Empty_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader(""); + In = reader; + var res = Confirm($"Enter yes:"); + Assert.Contains("Enter yes", stringWriter.ToString()); + Assert.True(res); + } + + [Fact] + public void Confirm_Case_No_Interpolated() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("no"); + In = reader; + var res = Confirm($"Enter no:"); + Assert.Contains("Enter no", stringWriter.ToString()); + Assert.False(res); + } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs b/PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs deleted file mode 100644 index 4decd5d..0000000 --- a/PrettyConsole.Tests.Unit/InterpolatedStringHandlerTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace PrettyConsole.Tests.Unit; - -public class InterpolatedStringHandlerTests { - [Fact] - public void WriteInterpolated_WritesFormattedContent_ToOutPipe() { - var originalOut = Out; - var writer = new StringWriter(); - Out = writer; - - try { - WriteInterpolated(OutputPipe.Out, $"Hello {42}"); - Assert.Equal("Hello 42", writer.ToString()); - } finally { - Out = originalOut; - } - } - - [Fact] - public void WriteInterpolated_WritesFormattedContent_ToErrorPipe() { - var originalError = Error; - var writer = new StringWriter(); - Error = writer; - - try { - WriteInterpolated(OutputPipe.Error, $"Error {123}"); - Assert.Equal("Error 123", writer.ToString()); - } finally { - Error = originalError; - } - } - - [Fact] - public void WriteLineInterpolated_AppendsNewLine() { - var originalOut = Out; - var writer = new StringWriter(); - Out = writer; - - try { - WriteLineInterpolated(OutputPipe.Out, $"Line {7}"); - Assert.Equal($"Line 7{writer.NewLine}", writer.ToString()); - } finally { - Out = originalOut; - } - } - - [Fact] - public void WriteInterpolated_IgnoresColorTokensInOutput() { - var originalOut = Out; - var writer = new StringWriter(); - Out = writer; - - try { - WriteInterpolated(OutputPipe.Out, - $"Colors {Color.Black / Color.Green}Green{Color.Default} {Color.Red}Red{Color.Default}"); - - Assert.Equal("Colors Green Red", writer.ToString()); - } finally { - Out = originalOut; - } - } -} diff --git a/PrettyConsole.Tests.Unit/Write.cs b/PrettyConsole.Tests.Unit/Write.cs index 37c9dcf..6caa23c 100755 --- a/PrettyConsole.Tests.Unit/Write.cs +++ b/PrettyConsole.Tests.Unit/Write.cs @@ -9,6 +9,50 @@ public Write() { Error = Utilities.GetWriter(out _errorWriter); } + [Fact] + public void Write_Interpolated_WritesFormattedContent_ToOutPipe() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + Write(OutputPipe.Out, $"Hello {42}"); + Assert.Equal("Hello 42", writer.ToString()); + } finally { + Out = originalOut; + } + } + + [Fact] + public void Write_Interpolated_WritesFormattedContent_ToErrorPipe() { + var originalError = Error; + var writer = new StringWriter(); + Error = writer; + + try { + Write(OutputPipe.Error, $"Error {123}"); + Assert.Equal("Error 123", writer.ToString()); + } finally { + Error = originalError; + } + } + + [Fact] + public void Write_Interpolated_IgnoresColorTokensInOutput() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + Write(OutputPipe.Out, + $"Colors {Color.Black / Color.Green}Green{Color.Default} {Color.Red}Red{Color.Default}"); + + Assert.Equal("Colors Green Red", writer.ToString()); + } finally { + Out = originalOut; + } + } + [Fact] public void Write_SpanFormattable_NoColors() { Write(3.14); diff --git a/PrettyConsole.Tests.Unit/WriteLine.cs b/PrettyConsole.Tests.Unit/WriteLine.cs index 01512b2..7ddb670 100755 --- a/PrettyConsole.Tests.Unit/WriteLine.cs +++ b/PrettyConsole.Tests.Unit/WriteLine.cs @@ -9,6 +9,20 @@ public WriteLine() { 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); From 6e6eb0ae31280f13521bc0b1a27eef642932d4b9 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 21:23:54 +0300 Subject: [PATCH 092/108] Added core write methods to slim down handler --- PrettyConsole/Write.cs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index d90f446..52abe2d 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -133,8 +133,21 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor /// 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); - GetWriter(pipe).Write(output.Value); + writer.Write(output.Value); ResetColors(); } @@ -144,11 +157,21 @@ public static void Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out) /// /// 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) { - Write(output, pipe); + WriteCore(output, writer); } } } \ No newline at end of file From cc7c849f7f1396bf1756a6d3a421cddd41c5362a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 21:24:05 +0300 Subject: [PATCH 093/108] Removed pipe field --- PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 2201b71..80cfe86 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -8,7 +8,6 @@ namespace PrettyConsole; /// [InterpolatedStringHandler] public readonly ref struct PrettyConsoleInterpolatedStringHandler { - private readonly OutputPipe _pipe; private readonly TextWriter _writer; private readonly IFormatProvider? _provider; @@ -42,7 +41,6 @@ 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) { - _pipe = pipe; _writer = Console.GetWriter(pipe); _provider = provider; shouldAppend = true; @@ -123,7 +121,7 @@ public readonly void AppendFormatted((ConsoleColor foreground, ConsoleColor back /// Segment to write. /// Optional alignment as provided by the interpolation. public readonly void AppendFormatted(ColoredOutput output, int alignment = 0) { - Console.Write(output, _pipe); + Console.WriteCore(output, _writer); if (alignment != 0) { AppendSpan(ReadOnlySpan.Empty, alignment); } @@ -137,7 +135,7 @@ public readonly void AppendFormatted(ReadOnlySpan outputs) { if (outputs.Length is 0) { return; } - Console.Write(outputs, _pipe); + Console.WriteCore(outputs, _writer); } /// From 4a4026d72ad19d77f98e3fbb83d20b033f2be516 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 18 Oct 2025 21:34:18 +0300 Subject: [PATCH 094/108] Removed tests of old utilities --- PrettyConsole.Tests.Unit/UtilityTests.cs | 56 ------------------------ 1 file changed, 56 deletions(-) diff --git a/PrettyConsole.Tests.Unit/UtilityTests.cs b/PrettyConsole.Tests.Unit/UtilityTests.cs index 61e4776..9bd12a4 100644 --- a/PrettyConsole.Tests.Unit/UtilityTests.cs +++ b/PrettyConsole.Tests.Unit/UtilityTests.cs @@ -1,62 +1,6 @@ -using System.Globalization; - namespace PrettyConsole.Tests.Unit; public class UtilityTests { - [Fact] - public void FormatPercentage_PadsAndRoundsToFiveCharacters() { - var originalCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - try { - Span buffer = stackalloc char[8]; - - var span = Utils.FormatPercentage(3, buffer); - Assert.Equal(" 3", span.ToString()); - - span = Utils.FormatPercentage(12.345, buffer); - Assert.Equal("12.35", span.ToString()); - } finally { - CultureInfo.CurrentCulture = originalCulture; - } - } - - [Fact] - public void FormatPercentage_ClampsWithinBounds() { - var originalCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - try { - Span buffer = stackalloc char[8]; - - var span = Utils.FormatPercentage(150, buffer); - Assert.Equal(" 100", span.ToString()); - - span = Utils.FormatPercentage(-10, buffer); - Assert.Equal(" 0", span.ToString()); - } finally { - CultureInfo.CurrentCulture = originalCulture; - } - } - - [Fact] - public void FormatTimeSpan_AdaptsToDuration() { - Span buffer = stackalloc char[32]; - - var written = Utils.FormatTimeSpan(TimeSpan.FromMilliseconds(500), buffer); - Assert.Equal("500ms", new string(buffer[..written])); - - written = Utils.FormatTimeSpan(TimeSpan.FromMilliseconds(12_345), buffer); - Assert.Equal("12:345s", new string(buffer[..written])); - - written = Utils.FormatTimeSpan(TimeSpan.FromMinutes(5) + TimeSpan.FromSeconds(30), buffer); - Assert.Equal("05:30m", new string(buffer[..written])); - - written = Utils.FormatTimeSpan(TimeSpan.FromHours(2) + TimeSpan.FromMinutes(15), buffer); - Assert.Equal("02:15hr", new string(buffer[..written])); - - written = Utils.FormatTimeSpan(TimeSpan.FromDays(1) + TimeSpan.FromHours(3), buffer); - Assert.Equal("01:03d", new string(buffer[..written])); - } - [Fact] public void WriteWhiteSpaces_WritesRequestedLength() { var writer = new StringWriter(); From 63495d8cd0609f62c3f51e43c8f26871f1751b7a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:21:15 +0300 Subject: [PATCH 095/108] Added overwrite method capable of zero allocation reactive components creation --- PrettyConsole/AdvancedOutputs.cs | 34 +++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs index 0487b7c..716f567 100755 --- a/PrettyConsole/AdvancedOutputs.cs +++ b/PrettyConsole/AdvancedOutputs.cs @@ -1,5 +1,3 @@ -using System.Runtime.CompilerServices; - namespace PrettyConsole; public static partial class Console { @@ -9,13 +7,43 @@ public static partial class Console { /// /// The output pipe to use [MethodImpl(MethodImplOptions.Synchronized)] - public static void OverrideCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { + 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 + /// + /// The output action. + /// The amount of lines to clear. + /// The output pipe to use. + [MethodImpl(MethodImplOptions.Synchronized)] + 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. + [MethodImpl(MethodImplOptions.Synchronized)] + 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; /// From caa425b1c0a024ef2b1012d414b02275b8ec5b82 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:23:07 +0300 Subject: [PATCH 096/108] Update release notes --- PrettyConsole/PrettyConsole.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index f5d354c..86cc71c 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -24,7 +24,7 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 3.2.0 + 4.0.0 enable MIT True @@ -38,6 +38,8 @@ - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. + - Many overloads were added that support the new PrettyConsoleStringInterpolationHandler, + enabling zero allocation formatted outputs. - Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. - IndeterminateProgressBar will no allow customization of the animated sequence via From 88fae567831985f4fe24c2fef8c7d73d1c380f20 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:30:04 +0300 Subject: [PATCH 097/108] Removed unused import --- PrettyConsole/ConsolePipes.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index d8ccf0d..dd0f522 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -1,6 +1,4 @@ -using System.Runtime.CompilerServices; - -namespace PrettyConsole; +namespace PrettyConsole; public static partial class Console { /// From 06446bb94d54c0b97d387bf32346521d746cdc1e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:30:14 +0300 Subject: [PATCH 098/108] Fixed typo in release notes --- PrettyConsole/PrettyConsole.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 86cc71c..5bb116f 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -42,7 +42,7 @@ enabling zero allocation formatted outputs. - Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. - - IndeterminateProgressBar will no allow customization of the animated sequence via + - IndeterminateProgressBar will now allow customization of the animated sequence via the property AnimationSequence, and it also includes an inner class Patterns that contains some constant sequences that could be used with it. - IndeterminateProgressBar.UpdateRate is 200 ms by default. From 9afdd4dcc5edec58fafefc516cc85c33ae0ea97c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:30:39 +0300 Subject: [PATCH 099/108] Updated versions --- Versions.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Versions.md b/Versions.md index 78047b7..a99b7d6 100755 --- a/Versions.md +++ b/Versions.md @@ -1,15 +1,27 @@ -# CHANGELOG +# Versions -## v3.2.0 +## v4.0.0 + +### Added + +- `PrettyConsoleInterpolatedStringHandler` was added to allow streaming zero allocation formatted and styled outputs to console pipes. With supported overloads for: + - `Write` and `WriteLine` + - `ReadLine` and `TryReadLine` + - `Selection` and `MultiSelection` + - `Overwrite` is a wrapper around an action of displaying outputs, with or without closures using `TState`, it enables you to use a lambda and call the `PrettyConsoleInterpolatedStringHandler` methods inside, to create zero allocation reactive and refreshable components. +- `IndeterminateProgressBar` will now allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. + +### Fixed - Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. Possibly effected: - `ProgressBar` and `IndeterminateProgressBar` - `ClearNextLines` -- `IndeterminateProgressBar` will no allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. - `IndeterminateProgressBar.UpdateRate` is 200 ms by default. - `IndeterminateProgressBar` header is now positioned right of the animation. Similar to common CLIs. - `ProgressBar` had numeral optimizations and should perform better in all scenarios. +### Also + - Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient. ## v3.1.0 From 970adebb9cd5375ebd02e1ebddabcad5149b853a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:35:27 +0300 Subject: [PATCH 100/108] Added color remarks --- Versions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Versions.md b/Versions.md index a99b7d6..def7497 100755 --- a/Versions.md +++ b/Versions.md @@ -9,6 +9,8 @@ - `ReadLine` and `TryReadLine` - `Selection` and `MultiSelection` - `Overwrite` is a wrapper around an action of displaying outputs, with or without closures using `TState`, it enables you to use a lambda and call the `PrettyConsoleInterpolatedStringHandler` methods inside, to create zero allocation reactive and refreshable components. + - To customize colors, use `Color` as an interpolation parameter at the correct place, and restore the colors with `Color.Default`. for example: `WriteLine($"This is in {Color.Green}green{Color.Default} and this is in {Color.Red}red{Color.Default}.");`, all overloads that accept the interpolation reset the color at the end, so you can omit `Color.Default` if you colored the last section of your string. + - The same conventions and syntax of setting `Foreground / Background` colors works here as well. - `IndeterminateProgressBar` will now allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. ### Fixed From 36507da83e8c6e67edd011ac5a10a3b7484b4a03 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:50:13 +0300 Subject: [PATCH 101/108] Update readme and versions --- README.md | 63 +++++++++++++++++++++++++++++++++++++++-------------- Versions.md | 1 + 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cf487b4..95c06f9 100755 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ An abstraction over `System.Console` that adds new input and output methods, col ## Features -* 🚀 High performance, Low memory usage and allocation -* 🪶 Very lightweight (No external dependencies) -* Easy to use (no need to learn a new syntax while still writing less boilerplate code) -* 💾 Supports legacy ansi terminals (like Windows 7) +* 🚀 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. +* ⛓ Uses original output pipes, so that your CLI's can be piped properly ## Installation [![NUGET DOWNLOADS](https://img.shields.io/nuget/dt/PrettyConsole?label=Downloads)](https://www.nuget.org/packages/PrettyConsole/) @@ -25,6 +25,21 @@ using static PrettyConsole.Console; // Access to all Console methods using PrettyConsole; // Access to the Color struct and OutputPipe enum ``` +### Interpolated Strings + +`PrettyConsoleInterpolatedStringHandler` lets you stream interpolated text directly to the selected pipe without allocating intermediate strings, while still using the familiar `$"..."` syntax. + +```csharp +Write($"Hello {Color.Green}world{Color.Default}!"); +Write(OutputPipe.Error, $"{Color.Yellow}Warning:{Color.Default} {message}"); + +if (!TryReadLine(out int choice, $"Pick option {Color.Cyan}1-5{Color.Default}: ")) { + WriteLine($"{Color.Red}Not a number.{Color.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. + ### ColoredOutput PrettyConsole uses an equation inspired syntax to colorize text. The syntax is as follows: @@ -40,18 +55,19 @@ same goes for the background. ### Basic Outputs -The most basic method for outputting is `Write`, which has multiple overloads: +The most basic method for outputting is `Write`, which has multiple overloads. All equivalents exist for `WriteLine`: ```csharp -Write(ColoredOutput, OutputPipe pipe = OutputPipe.Out); -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // use collections expression for the compiler to inline the array -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPip, ConsoleColor); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPipe, ConsoleColor, ConsoleColor); -Write(T , OutputPipe pipe = OutputPipe.Out); // no string allocation with T : ISpanFormattable -Write(T, OutputPipe, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor, ReadOnlySpan, IFormatProvider?); +// 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); ``` Overload for `WriteLine` are available with the same parameters @@ -61,14 +77,22 @@ Overload for `WriteLine` are available with the same parameters These are the methods for reading user input: ```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}: "); ``` I always recommend using `TryReadLine` instead of `ReadLine` as you need to maintain less null checks and the result, @@ -82,10 +106,13 @@ These are some special methods for inputs: // 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? "); ``` ### Rendering Controls @@ -108,7 +135,10 @@ Combining `ClearNextLines` with `GoToLine` will enable you to efficiently use th ```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 OverrideCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error); +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); @@ -143,6 +173,7 @@ var prg = new IndeterminateProgressBar(); // this setups the internal states 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 ``` #### ProgressBar diff --git a/Versions.md b/Versions.md index def7497..fd28867 100755 --- a/Versions.md +++ b/Versions.md @@ -21,6 +21,7 @@ - `IndeterminateProgressBar.UpdateRate` is 200 ms by default. - `IndeterminateProgressBar` header is now positioned right of the animation. Similar to common CLIs. - `ProgressBar` had numeral optimizations and should perform better in all scenarios. +- `OverrideCurrentLine` was renamed to `OverwriteCurrentLine` to be more semantically correct. ### Also From 5eb49f8fef6545f5ed28bd6be9a499f4567bdcd3 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 09:53:24 +0300 Subject: [PATCH 102/108] Add agents.md --- .gitignore | 2 +- AGENTS.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index dc788bb..62d8b6d 100755 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ *.sln.docstates PrettyConsole.Tests.Integration/ WARP.md -AGENTS.md +# AGENTS.md # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4b3f03c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# AGENTS.md + +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 and is AOT compatible. +- 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 + +Commands you’ll use often + +- Build + - Build library only: + - dotnet build PrettyConsole/PrettyConsole.csproj + - Build unit tests only: + - dotnet build PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj + - Build the whole solution: + - dotnet build PrettyConsole.sln +- Format (uses the repo’s .editorconfig conventions) + - Check and fix code style/formatting: + - dotnet format +- Run + - Never run interactive/demo tests (PrettyConsole.Tests) + - Run unit tests (xUnit v3 via Microsoft Testing Platform): + - dotnet run --project PrettyConsole.Tests.Unit + - Run a single unit test: + - dotnet run --project PrettyConsole.Tests.Unit --filter-method "*UniquePartOfMethodName*" + - Examples: + - dotnet run --project PrettyConsole.Tests.Unit --filter-method "*WritesColoredLine*" +- Pack - DOT NOT DO THIS YOURSELF! + +Repo-specific agent rules and conventions + +- Prefer dotnet CLI for making, verifying, and running changes. +- When changing a specific project, build/run just that project to validate, not the entire solution. +- For tests using Microsoft Testing Platform and/or xUnit v3, use dotnet run, never dotnet test. +- Adhere to .editorconfig in the repo for style and analyzers. +- If code needs to be “removed” as part of a change, do not delete files; comment out their contents so they won’t compile. +- Avoid reflection/dynamic assembly loading in published library code unless explicitly requested. + +High-level architecture and key concepts + +- Console wrapper + - PrettyConsole.Console is a static wrapper around System.Console, exposing In, Out, Error as OutputPipe and providing helpers like NewLine, GoToLine, ClearNextLines, SetColors, ResetColors. The wrapper keeps the API surface practical for common console tasks while preserving piping behavior. +- Coloring model + - ColoredOutput and Color provide a terse, equation-like syntax for color composition: "Text" * Color.Red / Color.Blue + - Colors and outputs are designed to be composed with minimal allocations, often using ReadOnlySpan and span-based overloads to avoid string allocations. +- Interpolated string handler + - `PrettyConsoleInterpolatedStringHandler` powers zero-allocation `$"..."` writes, reads, confirmations, and menu prompts, automatically resetting colors per call and supporting pipe selection. +- Output pipes + - OutputPipe abstracts the output stream (Out, Error). All Write/WriteLine APIs take an optional pipe so output can be routed appropriately while remaining pipe-friendly for shell usage. +- Inputs + - ReadLine helpers support typed parsing (IParsable), TryReadLine variants with defaults, enum parsing with optional case-insensitivity, and confirmation prompts with configurable true values. +- Rendering controls + - Methods like GetCurrentLine, GoToLine, ClearNextLines enable in-place updates and structured screen management without external dependencies. +- Advanced outputs + - Overwrite helpers (`OverwriteCurrentLine`, `Overwrite`, `Overwrite`) enable transient sections over error/out pipes for textual progress and reactive components. + - TypeWrite/TypeWriteLine animate character-by-character output with a configurable delay. +- Menus and tables + - Selection and MultiSelection render indexed lists and return chosen items; TreeMenu renders a two-level selection; Table prints header+columns from IList inputs. +- Progress bars + - IndeterminateProgressBar binds to a Task/Task and renders an animated pattern until completion; its AnimationSequence is customizable and common patterns are provided. + - ProgressBar tracks percentage (int/double 0–100), exposes properties like ProgressChar, ForegroundColor, ProgressColor. +- Packaging and targets + - PrettyConsole.csproj multi-targets net9.0 and net8.0, is AOT compatible, and includes SourceLink. Internals are visible to PrettyConsole.Tests.Unit for deeper validation. + +Testing structure and workflows + +- PrettyConsole.Tests (interactive) + - Program.cs constructs a list of visual tests/demos (e.g., IndeterminateProgressBarTest) and awaits each Render(). Modify the tests array for the scenarios you want to render live. +- PrettyConsole.Tests.Unit (xUnit v3) + - Uses Microsoft.NET.Test.Sdk with the Microsoft Testing Platform runner; xunit.runner.json is included. Execute with dotnet run as shown above; pass filters after to narrow to a class or method. + +Notes and gotchas + +- The library aims to minimize allocations; prefer span-based overloads (ReadOnlySpan, ReadOnlySpan) for best performance when contributing. +- 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. From b5a6417b04a5f53a6b01611971aa1a2a6054a6d7 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 10:01:30 +0300 Subject: [PATCH 103/108] Update agents.md --- AGENTS.md | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4b3f03c..d14e328 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ 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 and is AOT compatible. +- 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. - Solution layout: - PrettyConsole/ — main library - PrettyConsole.Tests/ — interactive/demo runner (manually selects visual feature demos) @@ -13,12 +13,11 @@ Summary Commands you’ll use often - Build - - Build library only: + - Build library: - dotnet build PrettyConsole/PrettyConsole.csproj - - Build unit tests only: + - Build unit tests: - dotnet build PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj - - Build the whole solution: - - dotnet build PrettyConsole.sln + - The solution using .slnx format; run it as usual but prefer to build individual projects as needed. - Format (uses the repo’s .editorconfig conventions) - Check and fix code style/formatting: - dotnet format @@ -30,7 +29,7 @@ Commands you’ll use often - dotnet run --project PrettyConsole.Tests.Unit --filter-method "*UniquePartOfMethodName*" - Examples: - dotnet run --project PrettyConsole.Tests.Unit --filter-method "*WritesColoredLine*" -- Pack - DOT NOT DO THIS YOURSELF! +- Pack - DO NOT DO THIS YOURSELF! Repo-specific agent rules and conventions @@ -43,34 +42,34 @@ Repo-specific agent rules and conventions High-level architecture and key concepts -- Console wrapper - - PrettyConsole.Console is a static wrapper around System.Console, exposing In, Out, Error as OutputPipe and providing helpers like NewLine, GoToLine, ClearNextLines, SetColors, ResetColors. The wrapper keeps the API surface practical for common console tasks while preserving piping behavior. -- Coloring model - - ColoredOutput and Color provide a terse, equation-like syntax for color composition: "Text" * Color.Red / Color.Blue - - Colors and outputs are designed to be composed with minimal allocations, often using ReadOnlySpan and span-based overloads to avoid string allocations. +- 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. +- 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. - Interpolated string handler - - `PrettyConsoleInterpolatedStringHandler` powers zero-allocation `$"..."` writes, reads, confirmations, and menu prompts, automatically resetting colors per call and supporting pipe selection. -- Output pipes - - OutputPipe abstracts the output stream (Out, Error). All Write/WriteLine APIs take an optional pipe so output can be routed appropriately while remaining pipe-friendly for shell usage. + - `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`. +- 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. +- 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. - Inputs - - ReadLine helpers support typed parsing (IParsable), TryReadLine variants with defaults, enum parsing with optional case-insensitivity, and confirmation prompts with configurable true values. + - `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 - - Methods like GetCurrentLine, GoToLine, ClearNextLines enable in-place updates and structured screen management without external dependencies. + - `ClearNextLines`, `GoToLine`, and `GetCurrentLine` coordinate bounded screen regions; `Clear` wipes the buffer when safe. These helpers underpin progress rendering and overwrite scenarios. - Advanced outputs - - Overwrite helpers (`OverwriteCurrentLine`, `Overwrite`, `Overwrite`) enable transient sections over error/out pipes for textual progress and reactive components. - - TypeWrite/TypeWriteLine animate character-by-character output with a configurable delay. + - `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. - Menus and tables - - Selection and MultiSelection render indexed lists and return chosen items; TreeMenu renders a two-level selection; Table prints header+columns from IList inputs. + - `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 a Task/Task and renders an animated pattern until completion; its AnimationSequence is customizable and common patterns are provided. - - ProgressBar tracks percentage (int/double 0–100), exposes properties like ProgressChar, ForegroundColor, ProgressColor. + - `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. - Packaging and targets - - PrettyConsole.csproj multi-targets net9.0 and net8.0, is AOT compatible, and includes SourceLink. Internals are visible to PrettyConsole.Tests.Unit for deeper validation. + - `PrettyConsole.csproj` targets net9.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project. Testing structure and workflows - PrettyConsole.Tests (interactive) - - Program.cs constructs a list of visual tests/demos (e.g., IndeterminateProgressBarTest) and awaits each Render(). Modify the tests array for the scenarios you want to render live. + - `Program.cs` allows to test things that need to be verified visually and can't be tested easily or at all using unit tests. It contains tests for various things like menues, tables, progress bar, etc... and at occations new overloads and other things. It's content doesn't need to be tracked, it is more like a playground. - PrettyConsole.Tests.Unit (xUnit v3) - Uses Microsoft.NET.Test.Sdk with the Microsoft Testing Platform runner; xunit.runner.json is included. Execute with dotnet run as shown above; pass filters after to narrow to a class or method. From 84103926fb2d17f61e5b42b7e6f6b07ad2824dba Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 10:03:01 +0300 Subject: [PATCH 104/108] format --- PrettyConsole.Tests.Unit/ColorTests.cs | 4 +- PrettyConsole.Tests.Unit/GlobalUsings.cs | 4 +- PrettyConsole.Tests.Unit/MenusTests.cs | 2 +- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 2 +- PrettyConsole.Tests.Unit/Utilities.cs | 22 +- PrettyConsole.Tests.Unit/UtilityTests.cs | 2 +- PrettyConsole.Tests/Program.cs | 30 +- PrettyConsole/Color.cs | 280 +++++++++--------- PrettyConsole/ColorDefaults.cs | 34 +-- PrettyConsole/ColoredOutput.cs | 22 +- PrettyConsole/ConsolePipes.cs | 72 ++--- PrettyConsole/GlobalUsings.cs | 3 +- PrettyConsole/OutputPipe.cs | 16 +- .../PrettyConsoleInterpolatedStringHandler.cs | 2 +- PrettyConsole/ProgressBar.cs | 232 +++++++-------- PrettyConsole/Utils.cs | 2 +- PrettyConsole/Write.cs | 2 +- 17 files changed, 367 insertions(+), 364 deletions(-) diff --git a/PrettyConsole.Tests.Unit/ColorTests.cs b/PrettyConsole.Tests.Unit/ColorTests.cs index 91ea006..1237039 100644 --- a/PrettyConsole.Tests.Unit/ColorTests.cs +++ b/PrettyConsole.Tests.Unit/ColorTests.cs @@ -58,7 +58,7 @@ 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() { @@ -116,4 +116,4 @@ public void ColoredOutput_DivideOperator() { Assert.Equal(ConsoleColor.Red, coloredOutput.ForegroundColor); Assert.Equal(ConsoleColor.Blue, coloredOutput.BackgroundColor); } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index 652c576..cdafe2e 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -1,3 +1,5 @@ -global using static PrettyConsole.Console; global using PrettyConsole; + global using Xunit; + +global using static PrettyConsole.Console; diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.Tests.Unit/MenusTests.cs index ce39dc5..1b9da16 100644 --- a/PrettyConsole.Tests.Unit/MenusTests.cs +++ b/PrettyConsole.Tests.Unit/MenusTests.cs @@ -153,4 +153,4 @@ public void Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() { Assert.Throws(() => Table(headers, [column1])); } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index fda9dcc..84b4392 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -52,4 +52,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.Unit/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index 10ed7f0..ed85740 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -4,18 +4,18 @@ namespace PrettyConsole.Tests.Unit; public static class Utilities { - public static StringReader GetReader(string str) => new(str); + public static StringReader GetReader(string str) => new(str); - public static TextWriter GetWriter(out StringWriter writer) { - writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - return writer; - } + public static TextWriter GetWriter(out StringWriter writer) { + writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); + return writer; + } - public static string ToStringAndFlush(this StringWriter writer) { - var result = writer.ToString(); - writer.GetStringBuilder().Clear(); - return result; - } + public static string ToStringAndFlush(this StringWriter writer) { + var result = writer.ToString(); + writer.GetStringBuilder().Clear(); + return result; + } - public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); + public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/UtilityTests.cs b/PrettyConsole.Tests.Unit/UtilityTests.cs index 9bd12a4..fe6458d 100644 --- a/PrettyConsole.Tests.Unit/UtilityTests.cs +++ b/PrettyConsole.Tests.Unit/UtilityTests.cs @@ -31,4 +31,4 @@ public void InColor_WithBackground_SetsBothColors() { Assert.Equal(ConsoleColor.Green, colored.ForegroundColor); Assert.Equal(ConsoleColor.Black, colored.BackgroundColor); } -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 87cf433..d5c7798 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -12,28 +12,28 @@ // .ToArray(); var tests = new IPrettyConsoleTest[] { - new ColoredOutputTest(), - new SelectionTest(), - new MultiSelectionTest(), - new TableTest(), - new TreeMenuTest(), - new IndeterminateProgressBarTest(), - new ProgressBarTest() + new ColoredOutputTest(), + new SelectionTest(), + new MultiSelectionTest(), + new TableTest(), + new TreeMenuTest(), + new IndeterminateProgressBarTest(), + new ProgressBarTest() }; foreach (var test in tests) { - await test.Render(); - NewLine(); + await test.Render(); + NewLine(); } #pragma warning disable CS8321 // Local function is declared but never used static void Measure(string label, Action action) { - long before = GC.GetAllocatedBytesForCurrentThread(); - action(); - long after = GC.GetAllocatedBytesForCurrentThread(); - NewLine(); - WriteLine($"{label} - allocated {after - before} bytes"); - NewLine(); + long before = GC.GetAllocatedBytesForCurrentThread(); + action(); + long after = GC.GetAllocatedBytesForCurrentThread(); + NewLine(); + WriteLine($"{label} - allocated {after - before} bytes"); + NewLine(); } #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 757d611..2d53649 100755 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -11,144 +11,144 @@ namespace PrettyConsole; [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); - } - - /// - /// 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. - /// - 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); + /// + /// 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); + } + + /// + /// 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. + /// + 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 index ced547e..93133f8 100755 --- a/PrettyConsole/ColorDefaults.cs +++ b/PrettyConsole/ColorDefaults.cs @@ -1,24 +1,24 @@ 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 DefaultForegroundColor; - /// - /// Represents the default color for the shell (changes based on platform) - /// - public static readonly ConsoleColor DefaultBackgroundColor; + /// + /// 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); + /// + /// 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; - } + static Color() { + baseConsole.ResetColor(); + DefaultForegroundColor = baseConsole.ForegroundColor; + DefaultBackgroundColor = baseConsole.BackgroundColor; + } } \ No newline at end of file diff --git a/PrettyConsole/ColoredOutput.cs b/PrettyConsole/ColoredOutput.cs index 8660359..43d303d 100755 --- a/PrettyConsole/ColoredOutput.cs +++ b/PrettyConsole/ColoredOutput.cs @@ -10,18 +10,18 @@ namespace PrettyConsole; [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 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) { } + /// + /// 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. diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index dd0f522..4f64541 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -1,43 +1,43 @@ namespace PrettyConsole; public static partial class Console { - /// - /// The standard input stream. - /// - public static TextWriter Out { get; internal set; } = baseConsole.Out; + /// + /// The standard input stream. + /// + public static TextWriter Out { get; internal set; } = baseConsole.Out; - /// - /// The error output stream. - /// - public static TextWriter Error { get; internal set; } = baseConsole.Error; + /// + /// The error output stream. + /// + public static TextWriter Error { get; internal set; } = baseConsole.Error; - /// - /// The standard input stream. - /// - public static TextReader In { get; internal set; } = baseConsole.In; + /// + /// The standard input stream. + /// + public static TextReader In { get; internal set; } = baseConsole.In; - /// - /// Gets the appropriate based on - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - internal static TextWriter GetWriter(OutputPipe pipe) - => pipe switch { - OutputPipe.Error => Error, - _ => Out - }; + /// + /// Gets the appropriate based on + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + internal static TextWriter GetWriter(OutputPipe pipe) + => pipe switch { + OutputPipe.Error => Error, + _ => Out + }; - /// - /// Returns the current console buffer width or if - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - internal static int GetWidthOrDefault(int defaultWidth = 120) { - if (baseConsole.IsOutputRedirected) { - return defaultWidth; - } - return baseConsole.BufferWidth; - } -} + /// + /// Returns the current console buffer width or if + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + internal static int GetWidthOrDefault(int defaultWidth = 120) { + if (baseConsole.IsOutputRedirected) { + return defaultWidth; + } + return baseConsole.BufferWidth; + } +} \ No newline at end of file diff --git a/PrettyConsole/GlobalUsings.cs b/PrettyConsole/GlobalUsings.cs index 2766f22..bbdf4b8 100755 --- a/PrettyConsole/GlobalUsings.cs +++ b/PrettyConsole/GlobalUsings.cs @@ -1,2 +1,3 @@ global using System.Runtime.CompilerServices; -global using baseConsole = System.Console; + +global using baseConsole = System.Console; \ No newline at end of file diff --git a/PrettyConsole/OutputPipe.cs b/PrettyConsole/OutputPipe.cs index b96870e..fa695f3 100755 --- a/PrettyConsole/OutputPipe.cs +++ b/PrettyConsole/OutputPipe.cs @@ -4,12 +4,12 @@ namespace PrettyConsole; /// An enum representing the output pipe to use /// public enum OutputPipe { - /// - /// Use the standard output pipe - /// - Out, - /// - /// Use the standard error pipe - /// - Error + /// + /// Use the standard output pipe + /// + Out, + /// + /// Use the standard error pipe + /// + Error } \ No newline at end of file diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 80cfe86..56fa30c 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -302,4 +302,4 @@ private readonly void WritePadding(int count) { _writer.WriteWhiteSpaces(count); } } -#pragma warning restore CA1822 // Mark members as static +#pragma warning restore CA1822 // Mark members as static \ No newline at end of file diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 795819b..96e309f 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -3,120 +3,120 @@ 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) - /// - /// - /// Updating the progress bar with decreasing percentages will cause visual bugs as it is optimized to skip rendering pre-filled characters. - /// - /// - public class ProgressBar { - /// - /// Gets or sets the character used to represent the progress. - /// - public char ProgressChar { get; set; } = '■'; - - /// - /// Gets or sets the foreground color of the progress bar. - /// - 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 int _currentProgress; - - private readonly Lock _lock = new(); - - /// - /// Updates the progress bar with the specified percentage. - /// - /// The percentage value (0-100) representing the progress. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); - - /// - /// Updates the progress bar with the specified percentage. - /// - /// The percentage value (0-100) representing the progress. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(double percentage) => Update(percentage, ReadOnlySpan.Empty); - - /// - /// 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. - public void Update(double percentage, ReadOnlySpan status) { - // Non-locking fast path: compute the desired progress and early-return if unchanged. - percentage = Math.Clamp(percentage, 0, 100); - - int bufferWidth = GetWidthOrDefault(); - // Compute pLength using exact overhead: " [" (2) + "] " (2) + percentage length (5) - int pLength = Math.Max(0, bufferWidth - status.Length - 4 - 5); - int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); - - if (p == Volatile.Read(ref _currentProgress)) { - return; - } - - lock (_lock) { - // It's possible another thread updated while we were waiting; re-check only the progress value. - if (p == _currentProgress) { - return; - } - - // Prepare the buffer exactly for the characters we will write for the bar (pLength) - using var listOwner = BufferPool.Shared.Rent(out var list); - list.EnsureCapacity(pLength); - CollectionsMarshal.SetCount(list, pLength); - Span buf = CollectionsMarshal.AsSpan(list); - - _currentProgress = p; - - var currentLine = GetCurrentLine(); - try { - ResetColors(); - baseConsole.ForegroundColor = ForegroundColor; - ClearNextLines(1, OutputPipe.Error); - if (status.Length != 0) { - Error.Write(status); - } - Error.Write(" ["); - baseConsole.ForegroundColor = ProgressColor; - - // Fill the progress portion - if (p > 0) { - Span progressSpan = buf.Slice(0, p); - progressSpan.Fill(ProgressChar); - } - - // Fill the remaining tail with spaces - int tailLength = Math.Max(0, pLength - p); - if (tailLength > 0) { - Span whiteSpaceSpan = buf.Slice(p, tailLength); - whiteSpaceSpan.Fill(' '); - } - - // Write the entire bar (progress + tail) - Error.Write(buf.Slice(0, pLength)); - - baseConsole.ForegroundColor = ForegroundColor; - Error.Write("] "); - // Write percentage - Write(OutputPipe.Error, $"{percentage, 5:##.##}"); - GoToLine(currentLine); - } finally { - // Ensure colors and buffer are reset even if an exception occurs mid-render - ResetColors(); - } - } - } - } + /// + /// 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) + /// + /// + /// Updating the progress bar with decreasing percentages will cause visual bugs as it is optimized to skip rendering pre-filled characters. + /// + /// + public class ProgressBar { + /// + /// Gets or sets the character used to represent the progress. + /// + public char ProgressChar { get; set; } = '■'; + + /// + /// Gets or sets the foreground color of the progress bar. + /// + 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 int _currentProgress; + + private readonly Lock _lock = new(); + + /// + /// Updates the progress bar with the specified percentage. + /// + /// The percentage value (0-100) representing the progress. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); + + /// + /// Updates the progress bar with the specified percentage. + /// + /// The percentage value (0-100) representing the progress. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(double percentage) => Update(percentage, ReadOnlySpan.Empty); + + /// + /// 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. + public void Update(double percentage, ReadOnlySpan status) { + // Non-locking fast path: compute the desired progress and early-return if unchanged. + percentage = Math.Clamp(percentage, 0, 100); + + int bufferWidth = GetWidthOrDefault(); + // Compute pLength using exact overhead: " [" (2) + "] " (2) + percentage length (5) + int pLength = Math.Max(0, bufferWidth - status.Length - 4 - 5); + int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); + + if (p == Volatile.Read(ref _currentProgress)) { + return; + } + + lock (_lock) { + // It's possible another thread updated while we were waiting; re-check only the progress value. + if (p == _currentProgress) { + return; + } + + // Prepare the buffer exactly for the characters we will write for the bar (pLength) + using var listOwner = BufferPool.Shared.Rent(out var list); + list.EnsureCapacity(pLength); + CollectionsMarshal.SetCount(list, pLength); + Span buf = CollectionsMarshal.AsSpan(list); + + _currentProgress = p; + + var currentLine = GetCurrentLine(); + try { + ResetColors(); + baseConsole.ForegroundColor = ForegroundColor; + ClearNextLines(1, OutputPipe.Error); + if (status.Length != 0) { + Error.Write(status); + } + Error.Write(" ["); + baseConsole.ForegroundColor = ProgressColor; + + // Fill the progress portion + if (p > 0) { + Span progressSpan = buf.Slice(0, p); + progressSpan.Fill(ProgressChar); + } + + // Fill the remaining tail with spaces + int tailLength = Math.Max(0, pLength - p); + if (tailLength > 0) { + Span whiteSpaceSpan = buf.Slice(p, tailLength); + whiteSpaceSpan.Fill(' '); + } + + // Write the entire bar (progress + tail) + Error.Write(buf.Slice(0, pLength)); + + baseConsole.ForegroundColor = ForegroundColor; + Error.Write("] "); + // Write percentage + Write(OutputPipe.Error, $"{percentage,5:##.##}"); + GoToLine(currentLine); + } finally { + // Ensure colors and buffer are reset even if an exception occurs mid-render + ResetColors(); + } + } + } + } } \ No newline at end of file diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index cefd6ab..1eaaf11 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -37,4 +37,4 @@ internal static void WriteWhiteSpaces(this TextWriter writer, int length) { writer.Write(full.AsSpan(0, length)); } } -} +} \ No newline at end of file diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index 52abe2d..160f504 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -9,7 +9,7 @@ public static partial class Console { /// Interpolated string handler that streams the content. public static void Write([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { ResetColors(); - } + } /// /// Writes interpolated content using . From b6ea8c2d6b1f965c082759ed11d616340ac89b30 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 10:16:08 +0300 Subject: [PATCH 105/108] Improve test coverage --- PrettyConsole.Tests.Unit/AdvancedInputs.cs | 12 ++++++++ PrettyConsole.Tests.Unit/ColorTests.cs | 20 ++++++------- PrettyConsole.Tests.Unit/GlobalUsings.cs | 2 +- PrettyConsole.Tests.Unit/MenusTests.cs | 7 +++-- PrettyConsole.Tests.Unit/ReadLine.cs | 35 ++++++++++++++++++++++ PrettyConsole.Tests.Unit/Write.cs | 30 +++++++++++++++++++ 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/PrettyConsole.Tests.Unit/AdvancedInputs.cs b/PrettyConsole.Tests.Unit/AdvancedInputs.cs index c736997..7b5d6ac 100755 --- a/PrettyConsole.Tests.Unit/AdvancedInputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedInputs.cs @@ -80,4 +80,16 @@ public void Confirm_Case_No_Interpolated() { Assert.Contains("Enter no", stringWriter.ToString()); Assert.False(res); } + + [Fact] + public void Confirm_CustomTrueValues_WithInterpolatedPrompt() { + Out = Utilities.GetWriter(out var stringWriter); + var reader = Utilities.GetReader("ok"); + In = reader; + + var res = Confirm(["ok", "okay"], false, $"Proceed?"); + + Assert.Equal("Proceed?", stringWriter.ToStringAndFlush()); + Assert.True(res); + } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ColorTests.cs b/PrettyConsole.Tests.Unit/ColorTests.cs index 1237039..ed7d15b 100644 --- a/PrettyConsole.Tests.Unit/ColorTests.cs +++ b/PrettyConsole.Tests.Unit/ColorTests.cs @@ -64,13 +64,13 @@ public void Color_DivideColorOperator() { public void Color_AsteriskOperator() { var coloredOutput = "Hello" * Color.Green; Assert.Equal(ConsoleColor.Green, coloredOutput.ForegroundColor); - Assert.Equal(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); } [Fact] public void Color_DivideOperator() { var coloredOutput = "Hello" / Color.Red; - Assert.Equal(System.Console.ForegroundColor, coloredOutput.ForegroundColor); + Assert.Equal(Color.DefaultForegroundColor, coloredOutput.ForegroundColor); Assert.Equal(ConsoleColor.Red, coloredOutput.BackgroundColor); } @@ -82,32 +82,32 @@ public void Color_ObjectOperator() { [Fact] public void Color_DefaultColors() { - Assert.Equal(System.Console.ForegroundColor, Color.DefaultForegroundColor); - Assert.Equal(System.Console.BackgroundColor, Color.DefaultBackgroundColor); - Assert.Equal((System.Console.ForegroundColor, System.Console.BackgroundColor), Color.Default); + 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(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); } [Fact] public void ColoredOutput_StringOperator() { ColoredOutput coloredOutput = "Hello"; Assert.Equal("Hello", coloredOutput.Value); - Assert.Equal(System.Console.ForegroundColor, coloredOutput.ForegroundColor); - Assert.Equal(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + 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(System.Console.ForegroundColor, coloredOutput.ForegroundColor); - Assert.Equal(System.Console.BackgroundColor, coloredOutput.BackgroundColor); + Assert.Equal(Color.DefaultForegroundColor, coloredOutput.ForegroundColor); + Assert.Equal(Color.DefaultBackgroundColor, coloredOutput.BackgroundColor); } [Fact] diff --git a/PrettyConsole.Tests.Unit/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index cdafe2e..fa85189 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -2,4 +2,4 @@ global using Xunit; -global using static PrettyConsole.Console; +global using static PrettyConsole.Console; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.Tests.Unit/MenusTests.cs index 1b9da16..ba08c40 100644 --- a/PrettyConsole.Tests.Unit/MenusTests.cs +++ b/PrettyConsole.Tests.Unit/MenusTests.cs @@ -12,7 +12,8 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { var output = writer.ToStringAndFlush(); - // Do not remove extra whitespace + static string Normalize(string value) => value.Replace("\r\n", "\n"); + Assert.Equal( """ Choose a fruit: @@ -21,8 +22,8 @@ public void Selection_ReturnsSelectedChoice_WhenInputValid() { 3) Cherry Enter your choice: - """ - , output); + """.Replace("\r\n", "\n"), + Normalize(output)); Assert.Equal("Banana", result); } diff --git a/PrettyConsole.Tests.Unit/ReadLine.cs b/PrettyConsole.Tests.Unit/ReadLine.cs index 22ab539..393752c 100755 --- a/PrettyConsole.Tests.Unit/ReadLine.cs +++ b/PrettyConsole.Tests.Unit/ReadLine.cs @@ -50,4 +50,39 @@ public void TryReadLine_Enum_IgnoreCase() { 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/Write.cs b/PrettyConsole.Tests.Unit/Write.cs index 6caa23c..8282cb0 100755 --- a/PrettyConsole.Tests.Unit/Write.cs +++ b/PrettyConsole.Tests.Unit/Write.cs @@ -108,6 +108,36 @@ public void WriteError_ColoredOutput_Multiple() { 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; From 9d3f6e8a6730dc0adbd5b2005a64601e2c44f288 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 10:31:39 +0300 Subject: [PATCH 106/108] Added more tests --- PrettyConsole.Tests.Unit/AdvancedOutputs.cs | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs index 1bea034..c97a229 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs @@ -1,6 +1,43 @@ namespace PrettyConsole.Tests.Unit; public class AdvancedOutputs { + [Fact] + public void OverwriteCurrentLine_WritesOutputToPipe() { + Error = Utilities.GetWriter(out var writer); + + OverwriteCurrentLine(["Updating" * Color.Green], OutputPipe.Error); + + Assert.Contains("Updating", writer.ToString()); + } + + [Fact] + public void Overwrite_ExecutesActionAndWritesOutput() { + Error = Utilities.GetWriter(out var writer); + bool executed = false; + + Overwrite(() => { + executed = true; + Write(OutputPipe.Error, $"Progress"); + }, lines: 1, pipe: OutputPipe.Error); + + Assert.True(executed); + Assert.Contains("Progress", writer.ToString()); + } + + [Fact] + public void Overwrite_WithState_ExecutesActionAndWritesOutput() { + Error = Utilities.GetWriter(out var writer); + bool executed = false; + + Overwrite("Done", status => { + executed = true; + Write(OutputPipe.Error, $"{status}"); + }, lines: 1, pipe: OutputPipe.Error); + + Assert.True(executed); + Assert.Contains("Done", writer.ToString()); + } + [Fact] public async Task TypeWrite_Regular() { Out = Utilities.GetWriter(out var stringWriter); From 0820c19dd261033c447d9e8008593831a3a1c5fe Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 10:31:51 +0300 Subject: [PATCH 107/108] Fixed typo in release notes --- PrettyConsole/PrettyConsole.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 5bb116f..705102b 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -38,7 +38,7 @@ - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. - - Many overloads were added that support the new PrettyConsoleStringInterpolationHandler, + - Many overloads were added that support the new PrettyConsoleInterpolatedStringHandler, enabling zero allocation formatted outputs. - Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. @@ -74,4 +74,4 @@ - \ No newline at end of file + From 458961e7c1e9e3c67ec96719b31df28ca04bd564 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 21 Oct 2025 10:55:25 +0300 Subject: [PATCH 108/108] Skip tests that fail due to limited environment --- PrettyConsole.Tests.Unit/AdvancedOutputs.cs | 3 +++ PrettyConsole.Tests.Unit/ProgressBarTests.cs | 3 +++ PrettyConsole.Tests.Unit/Utilities.cs | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs index c97a229..4dac2ec 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs @@ -3,6 +3,7 @@ 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); @@ -12,6 +13,7 @@ public void OverwriteCurrentLine_WritesOutputToPipe() { [Fact] public void Overwrite_ExecutesActionAndWritesOutput() { + Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var writer); bool executed = false; @@ -26,6 +28,7 @@ public void Overwrite_ExecutesActionAndWritesOutput() { [Fact] public void Overwrite_WithState_ExecutesActionAndWritesOutput() { + Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var writer); bool executed = false; diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 84b4392..3f1d83b 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -3,6 +3,7 @@ namespace PrettyConsole.Tests.Unit; public class ProgressBarTests { [Fact] public void ProgressBar_Update_WritesStatusAndPercentage() { + Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); var bar = new ProgressBar { @@ -21,6 +22,7 @@ public void ProgressBar_Update_WritesStatusAndPercentage() { [Fact] public void ProgressBar_Update_SamePercentage_NoAdditionalOutput() { + Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); var bar = new ProgressBar(); @@ -35,6 +37,7 @@ public void ProgressBar_Update_SamePercentage_NoAdditionalOutput() { [Fact] public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { + Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); var bar = new IndeterminateProgressBar { diff --git a/PrettyConsole.Tests.Unit/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index ed85740..641116a 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -1,6 +1,9 @@ +using System; using System.Globalization; using System.Text; +using Xunit; + namespace PrettyConsole.Tests.Unit; public static class Utilities { @@ -18,4 +21,20 @@ public static string ToStringAndFlush(this StringWriter writer) { } public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); + + public static void SkipIfNoInteractiveConsole() { + const string reason = "Interactive console APIs are not available in this environment."; + + if (System.Console.IsOutputRedirected) { + Assert.Skip(reason); + } + + try { + _ = System.Console.CursorTop; + } catch (System.IO.IOException) { + Assert.Skip(reason); + } catch (PlatformNotSupportedException) { + Assert.Skip(reason); + } + } } \ No newline at end of file