From 3ff382a0f734858af48bf72738634972066bdd89 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 13:07:30 +0300 Subject: [PATCH 01/23] Added WriteBar static method to ProgressBar --- PrettyConsole/ProgressBar.cs | 65 +++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 96e309f..d22f622 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -15,13 +15,18 @@ public static partial class Console { /// /// public class ProgressBar { + /// + /// The default characters used for the progress bar filled portion. + /// + public const char DefaultProgressChar = '■'; + /// /// Gets or sets the character used to represent the progress. /// - public char ProgressChar { get; set; } = '■'; + public char ProgressChar { get; set; } = DefaultProgressChar; /// - /// Gets or sets the foreground color of the progress bar. + /// Gets or sets the foreground color of the header display. /// public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; @@ -110,7 +115,7 @@ public void Update(double percentage, ReadOnlySpan status) { baseConsole.ForegroundColor = ForegroundColor; Error.Write("] "); // Write percentage - Write(OutputPipe.Error, $"{percentage,5:##.##}"); + Write(OutputPipe.Error, $"{percentage,5:##.##}%"); GoToLine(currentLine); } finally { // Ensure colors and buffer are reset even if an exception occurs mid-render @@ -118,5 +123,57 @@ public void Update(double percentage, ReadOnlySpan status) { } } } + + /// + /// Writes a single progress bar segment without tracking state. + /// + /// The output pipe to write to. + /// The percentage value (0-100) representing the progress. + /// The color used for the filled segment of the bar. + /// The character used to render the filled portion of the bar. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteBar(pipe, (int)percentage, progressColor, progressChar); + + /// + /// Writes a single progress bar segment without tracking state. + /// + /// The output pipe to write to. + /// The percentage value (0-100) representing the progress. + /// The color used for the filled segment of the bar. + /// The character used to render the filled portion of the bar. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] + public static void WriteBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { + ResetColors(); + + int p = Math.Clamp(percentage, 0, 100); + int bufferWidth = GetWidthOrDefault() - baseConsole.CursorLeft; + + const int bracketsAndSpacing = 3; // '[' + ']' + ' ' + const int percentageWidth = 3; // numeric portion width + const int percentSymbolLength = 1; // '%' character + int barLength = Math.Max(0, bufferWidth - (bracketsAndSpacing + percentageWidth + percentSymbolLength)); + + var writer = GetWriter(pipe); + writer.Write('['); + + if (barLength > 0) { + int filled = Math.Min((int)(barLength * p * 0.01), barLength); + + if (filled > 0) { + SetColors(progressColor, baseConsole.BackgroundColor); + Span s = stackalloc char[filled]; + s.Fill(progressChar); + writer.Write(s); + ResetColors(); + } + + int remaining = barLength - filled; + if (remaining > 0) { + writer.WriteWhiteSpaces(remaining); + } + } + + Write(pipe, $"] {p,3}%"); + } } -} \ No newline at end of file +} From 89a551dbe148c0e65bd206b00cfcd5c006269f2a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 13:59:31 +0300 Subject: [PATCH 02/23] Clean unused code --- PrettyConsole/ProgressBar.cs | 81 +++++++----------------------------- 1 file changed, 16 insertions(+), 65 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index d22f622..00eee6b 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,6 +1,4 @@ -using System.Runtime.InteropServices; - -namespace PrettyConsole; +namespace PrettyConsole; public static partial class Console { /// @@ -26,7 +24,7 @@ public class ProgressBar { public char ProgressChar { get; set; } = DefaultProgressChar; /// - /// Gets or sets the foreground color of the header display. + /// Gets or sets the foreground color of the status (if rendered). /// public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; @@ -35,8 +33,6 @@ public class ProgressBar { /// public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; - private int _currentProgress; - private readonly Lock _lock = new(); /// @@ -58,69 +54,24 @@ 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 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; - } + public void Update(double percentage, ReadOnlySpan status) + => Update((int)percentage, status); + /// + /// 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(int percentage, ReadOnlySpan status) { 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(); + ClearNextLines(1, OutputPipe.Error); + if (status.Length > 0) { + Write(status, OutputPipe.Error, ForegroundColor); + Write(' '); + WriteBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } + GoToLine(currentLine); } } From 98d7dbaa4af6bdddfd334e7e40d5861eb1348fe7 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:00:22 +0300 Subject: [PATCH 03/23] Increase version --- PrettyConsole/PrettyConsole.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 5cd5ab2..b51d61f 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -25,7 +25,7 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 4.0.0 + 4.1.0 enable MIT True From 8a5cbd7048e76b43bc4b1312a0413b0b30c58bb1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:00:32 +0300 Subject: [PATCH 04/23] Added multi progress bar --- .../Features/MultiProgressBarTest.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100755 PrettyConsole.Tests/Features/MultiProgressBarTest.cs diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs new file mode 100755 index 0000000..6ea15f5 --- /dev/null +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -0,0 +1,30 @@ +using static PrettyConsole.Console; + +namespace PrettyConsole.Tests.Features; + +public sealed class MultiProgressBarTest : IPrettyConsoleTest { + public string FeatureName => "MultiProgressBar"; + + public async ValueTask Implementation() { + const int count = 333; + var currentLine = GetCurrentLine(); + for (int i = 1; i <= count; i++) { + double percentage = 100 * (double)i / count; + + Overwrite((int)percentage, p => { + Write(OutputPipe.Error, $"Task {1}: "); + ProgressBar.WriteBar(OutputPipe.Error, p, Color.Magenta); + NewLine(); + Write(OutputPipe.Error, $"Task {2}: "); + ProgressBar.WriteBar(OutputPipe.Error, p, Color.Magenta); + NewLine(); + }, 2); + + // prg.Update(percentage, "TESTING"); + await Task.Delay(15); + } + ClearNextLines(2, OutputPipe.Error); + // ClearNextLines(1, OutputPipe.Error); + // GoToLine(currentLine); + } +} \ No newline at end of file From b485b5c6defd4a7de17bfe672269acd64cb0f34e Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:21:57 +0300 Subject: [PATCH 05/23] Better tests --- PrettyConsole.Tests/Features/MultiProgressBarTest.cs | 8 +++----- PrettyConsole.Tests/Features/ProgressBarTest.cs | 4 +--- PrettyConsole.Tests/Program.cs | 3 ++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs index 6ea15f5..7495816 100755 --- a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -14,17 +14,15 @@ public async ValueTask Implementation() { Overwrite((int)percentage, p => { Write(OutputPipe.Error, $"Task {1}: "); ProgressBar.WriteBar(OutputPipe.Error, p, Color.Magenta); - NewLine(); + NewLine(OutputPipe.Error); Write(OutputPipe.Error, $"Task {2}: "); ProgressBar.WriteBar(OutputPipe.Error, p, Color.Magenta); - NewLine(); + NewLine(OutputPipe.Error); }, 2); - // prg.Update(percentage, "TESTING"); await Task.Delay(15); } ClearNextLines(2, OutputPipe.Error); - // ClearNextLines(1, OutputPipe.Error); - // GoToLine(currentLine); + WriteLine(OutputPipe.Error, $"Done"); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/ProgressBarTest.cs b/PrettyConsole.Tests/Features/ProgressBarTest.cs index 0110227..e69a964 100755 --- a/PrettyConsole.Tests/Features/ProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarTest.cs @@ -8,16 +8,14 @@ public sealed class ProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new ProgressBar { ProgressColor = Color.Magenta, - // ProgressChar = '🧎‍♂️‍➡️' }; const int count = 333; - var currentLine = GetCurrentLine(); for (int i = 1; i <= count; i++) { double percentage = 100 * (double)i / count; prg.Update(percentage, "TESTING"); await Task.Delay(15); } ClearNextLines(1, OutputPipe.Error); - GoToLine(currentLine); + WriteLine(OutputPipe.Error, $"Done"); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index d5c7798..ebe2794 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -18,7 +18,8 @@ new TableTest(), new TreeMenuTest(), new IndeterminateProgressBarTest(), - new ProgressBarTest() + new ProgressBarTest(), + new MultiProgressBarTest(), }; foreach (var test in tests) { From 4395e80f98ed34957087c32004ec029c64a4cdb1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:38:28 +0300 Subject: [PATCH 06/23] Added remarks --- PrettyConsole/AdvancedOutputs.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs index 716f567..b98c097 100755 --- a/PrettyConsole/AdvancedOutputs.cs +++ b/PrettyConsole/AdvancedOutputs.cs @@ -6,6 +6,9 @@ public static partial class Console { /// /// /// The output pipe to use + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// [MethodImpl(MethodImplOptions.Synchronized)] public static void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); @@ -20,6 +23,9 @@ public static void OverwriteCurrentLine(ReadOnlySpan output, Outp /// The output action. /// The amount of lines to clear. /// The output pipe to use. + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// [MethodImpl(MethodImplOptions.Synchronized)] public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); @@ -36,6 +42,9 @@ public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = Out /// The output action. /// The amount of lines to clear. /// The output pipe to use. + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// [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(); From f3c9c79ebe22f0e924314f07f70fbf193162a581 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:38:39 +0300 Subject: [PATCH 07/23] Added remarks and 2 line display option --- PrettyConsole/ProgressBar.cs | 38 +++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 00eee6b..286903c 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -9,7 +9,7 @@ public static partial class 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. + /// Please remember to clear the used lines after the last call to this method, you can use /// /// public class ProgressBar { @@ -39,6 +39,9 @@ public class ProgressBar { /// Updates the progress bar with the specified percentage. /// /// The percentage value (0-100) representing the progress. + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); @@ -46,6 +49,9 @@ public class ProgressBar { /// Updates the progress bar with the specified percentage. /// /// The percentage value (0-100) representing the progress. + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(double percentage) => Update(percentage, ReadOnlySpan.Empty); @@ -54,7 +60,12 @@ 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) + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) => Update((int)percentage, status); /// @@ -62,14 +73,27 @@ public void Update(double percentage, ReadOnlySpan status) /// /// The percentage value (0-100) representing the progress. /// The status text to be displayed after the progress bar. - public void Update(int percentage, ReadOnlySpan status) { + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// + public void Update(int percentage, ReadOnlySpan status, bool sameLine = true) { lock (_lock) { var currentLine = GetCurrentLine(); - ClearNextLines(1, OutputPipe.Error); - if (status.Length > 0) { - Write(status, OutputPipe.Error, ForegroundColor); - Write(' '); + if (sameLine) { + ClearNextLines(1, OutputPipe.Error); + if (status.Length > 0) { + Write(status, OutputPipe.Error, ForegroundColor); + Write(' '); + WriteBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + } + } else { + bool hasStatus = status.Length > 0; + int lines = hasStatus ? 2 : 1; + ClearNextLines(lines, OutputPipe.Error); + if (hasStatus) WriteLine(status, OutputPipe.Error, ForegroundColor); WriteBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + NewLine(OutputPipe.Error); } GoToLine(currentLine); } From b7d934939f06c75f34a12f671bbc06ecec660cce Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:38:53 +0300 Subject: [PATCH 08/23] Split progress bar tests based on UI choice --- ...ssBarTest.cs => ProgressBarDefaultTest.cs} | 7 ++++-- .../Features/ProgressBarMultiLineTest.cs | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) rename PrettyConsole.Tests/Features/{ProgressBarTest.cs => ProgressBarDefaultTest.cs} (72%) create mode 100755 PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs diff --git a/PrettyConsole.Tests/Features/ProgressBarTest.cs b/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs similarity index 72% rename from PrettyConsole.Tests/Features/ProgressBarTest.cs rename to PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs index e69a964..34fca71 100755 --- a/PrettyConsole.Tests/Features/ProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs @@ -2,8 +2,11 @@ namespace PrettyConsole.Tests.Features; -public sealed class ProgressBarTest : IPrettyConsoleTest { - public string FeatureName => "ProgressBar"; +/// +/// Uses the default update method parameter (same line) +/// +public sealed class ProgressBarDefaultTest : IPrettyConsoleTest { + public string FeatureName => "ProgressBarDefault"; public async ValueTask Implementation() { var prg = new ProgressBar { diff --git a/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs b/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs new file mode 100755 index 0000000..25a0e6b --- /dev/null +++ b/PrettyConsole.Tests/Features/ProgressBarMultiLineTest.cs @@ -0,0 +1,24 @@ +using static PrettyConsole.Console; + +namespace PrettyConsole.Tests.Features; + +/// +/// Configures sameLine = false +/// +public sealed class ProgressBarMultiLineTest : IPrettyConsoleTest { + public string FeatureName => "ProgressBarMultiLine"; + + public async ValueTask Implementation() { + var prg = new ProgressBar { + ProgressColor = Color.Magenta, + }; + const int count = 333; + for (int i = 1; i <= count; i++) { + double percentage = 100 * (double)i / count; + prg.Update(percentage, "TESTING", false); + await Task.Delay(15); + } + ClearNextLines(2, OutputPipe.Error); + WriteLine(OutputPipe.Error, $"Done"); + } +} \ No newline at end of file From 33abb95809ed19ef5f430c55626a63eab21eda18 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:42:59 +0300 Subject: [PATCH 09/23] Fixed sameLine option --- PrettyConsole/ProgressBar.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 286903c..1e2f3ed 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -43,7 +43,7 @@ public class ProgressBar { /// Please remember to clear the used lines after the last call to this method, you can use /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); + public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty, true); /// /// Updates the progress bar with the specified percentage. @@ -53,7 +53,7 @@ public class ProgressBar { /// Please remember to clear the used lines after the last call to this method, you can use /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(double percentage) => Update(percentage, ReadOnlySpan.Empty); + public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty, true); /// /// Updates the progress bar with the specified percentage and header text. @@ -66,7 +66,7 @@ public class ProgressBar { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) - => Update((int)percentage, status); + => Update((int)percentage, status, sameLine); /// /// Updates the progress bar with the specified percentage and header text. @@ -93,7 +93,6 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr ClearNextLines(lines, OutputPipe.Error); if (hasStatus) WriteLine(status, OutputPipe.Error, ForegroundColor); WriteBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); - NewLine(OutputPipe.Error); } GoToLine(currentLine); } From 1ae4f1daa1c78cca5b33daadfd36664a77861f52 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:43:29 +0300 Subject: [PATCH 10/23] Added updated tests --- PrettyConsole.Tests/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index ebe2794..68ac523 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -18,7 +18,8 @@ new TableTest(), new TreeMenuTest(), new IndeterminateProgressBarTest(), - new ProgressBarTest(), + new ProgressBarDefaultTest(), + new ProgressBarMultiLineTest(), new MultiProgressBarTest(), }; From 30186f952e157fca9aa140cd4fce596dae96f76f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:45:11 +0300 Subject: [PATCH 11/23] Update docs to reflect line count when sameLine=false --- PrettyConsole/ProgressBar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 1e2f3ed..2e4769c 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -60,7 +60,7 @@ public class ProgressBar { /// /// The percentage value (0-100) representing the progress. /// The status text to be displayed after the progress bar. - /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. /// /// Please remember to clear the used lines after the last call to this method, you can use /// @@ -73,7 +73,7 @@ public void Update(double percentage, ReadOnlySpan status, bool sameLine = /// /// The percentage value (0-100) representing the progress. /// The status text to be displayed after the progress bar. - /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. /// /// Please remember to clear the used lines after the last call to this method, you can use /// From 31b5cc708016c58bc89387db7aa11863309d78ce Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:55:51 +0300 Subject: [PATCH 12/23] Update method name --- PrettyConsole.Tests/Features/MultiProgressBarTest.cs | 4 ++-- PrettyConsole/ProgressBar.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs index 7495816..9fbc019 100755 --- a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -13,10 +13,10 @@ public async ValueTask Implementation() { Overwrite((int)percentage, p => { Write(OutputPipe.Error, $"Task {1}: "); - ProgressBar.WriteBar(OutputPipe.Error, p, Color.Magenta); + ProgressBar.WriteProgressBar(OutputPipe.Error, p, Color.Magenta); NewLine(OutputPipe.Error); Write(OutputPipe.Error, $"Task {2}: "); - ProgressBar.WriteBar(OutputPipe.Error, p, Color.Magenta); + ProgressBar.WriteProgressBar(OutputPipe.Error, p, Color.Magenta); NewLine(OutputPipe.Error); }, 2); diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 2e4769c..16df170 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -85,14 +85,14 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr if (status.Length > 0) { Write(status, OutputPipe.Error, ForegroundColor); Write(' '); - WriteBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } } else { bool hasStatus = status.Length > 0; int lines = hasStatus ? 2 : 1; ClearNextLines(lines, OutputPipe.Error); if (hasStatus) WriteLine(status, OutputPipe.Error, ForegroundColor); - WriteBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } GoToLine(currentLine); } @@ -106,7 +106,7 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteBar(pipe, (int)percentage, progressColor, progressChar); + public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar); /// /// Writes a single progress bar segment without tracking state. @@ -116,7 +116,7 @@ public void Update(int percentage, ReadOnlySpan status, bool sameLine = tr /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] - public static void WriteBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { + public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { ResetColors(); int p = Math.Clamp(percentage, 0, 100); From 6c6a69dbd5d527eca4b5fe0f1ecbdefe0acfc5ac Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 14:55:59 +0300 Subject: [PATCH 13/23] Update changelog --- PrettyConsole/PrettyConsole.csproj | 21 +++++---------------- Versions.md | 13 +++++++++++++ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index b51d61f..6e42cba 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -38,22 +38,11 @@ - - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. - - 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. - - 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. - - IndeterminateProgressBar header is now positioned right of the animation. Similar to - common CLIs. - - ProgressBar had numeral optimizations and should perform better in all scenarios. - - 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. + - ProgressBar.ForegroundColor docs were fixed (they previously were the same as ProgressColor) which is invalid. + - ProgressBar in all variations now shows progress as a round number suffixed by %. + - ProgressBar.Update overloads now include an optional parameter "sameLine" which configures whether to render the progress bar at the same of the status. It is set to "true" by default to keep current behavior. + - ProgressBar now includes a static method "WriteProgressBar" which renders a static progress bar with the set parameters, it can be used in conjunction with "Overwrite" to create multi-progress-bars UI. + - Methods that overwrite lines, now have a note in the remarks to clear the used lines after the last call, to prevent artifacts. diff --git a/Versions.md b/Versions.md index fd28867..f246f70 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,18 @@ # Versions +## v4.1.0 + +### Added + +- `ProgressBar` now includes a static method `WriteProgressBar` which renders a static progress bar with the set parameters, it can be used in conjunction with `Overwrite` to create multi-progress-bars UI. +- `ProgressBar.Update` overloads now include an optional parameter `sameLine` which configures whether to render the progress bar at the same of the status. It is set to `true` by default to keep current behavior. + +### Fixed + +- `ProgressBar.ForegroundColor` docs were fixed (they previously were the same as `ProgressColor`) which is invalid. +- `ProgressBar` in all variations now shows progress as a round number suffixed by %. +- Methods that overwrite lines, now have a note in the remarks to clear the used lines after the last call, to prevent artifacts. + ## v4.0.0 ### Added From e700a1fd503c71ad3ea786fe5c97934a59813c0c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 15:03:42 +0300 Subject: [PATCH 14/23] Added missing change to changelog --- PrettyConsole/PrettyConsole.csproj | 17 +++++++++++++---- Versions.md | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 6e42cba..254c423 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -38,11 +38,20 @@ - - ProgressBar.ForegroundColor docs were fixed (they previously were the same as ProgressColor) which is invalid. + - ProgressBar.ForegroundColor docs were fixed (they previously were the same as + ProgressColor) which is invalid. - ProgressBar in all variations now shows progress as a round number suffixed by %. - - ProgressBar.Update overloads now include an optional parameter "sameLine" which configures whether to render the progress bar at the same of the status. It is set to "true" by default to keep current behavior. - - ProgressBar now includes a static method "WriteProgressBar" which renders a static progress bar with the set parameters, it can be used in conjunction with "Overwrite" to create multi-progress-bars UI. - - Methods that overwrite lines, now have a note in the remarks to clear the used lines after the last call, to prevent artifacts. + - ProgressBar no longer tracks if the percentage is changed, being that the numbers + are round, percentage could progress or status needs to be re-written while it stays the + same when rounded. + - ProgressBar.Update overloads now include an optional parameter "sameLine" which + configures whether to render the progress bar at the same of the status. It is set to + "true" by default to keep current behavior. + - ProgressBar now includes a static method "WriteProgressBar" which renders a static + progress bar with the set parameters, it can be used in conjunction with "Overwrite" to + create multi-progress-bars UI. + - Methods that overwrite lines, now have a note in the remarks to clear the used lines + after the last call, to prevent artifacts. diff --git a/Versions.md b/Versions.md index f246f70..b7187c5 100755 --- a/Versions.md +++ b/Versions.md @@ -11,6 +11,7 @@ - `ProgressBar.ForegroundColor` docs were fixed (they previously were the same as `ProgressColor`) which is invalid. - `ProgressBar` in all variations now shows progress as a round number suffixed by %. +- `ProgressBar` no longer tracks if the percentage is changed, being that the numbers are round, percentage could progress or status needs to be re-written while it stays the same when rounded. - Methods that overwrite lines, now have a note in the remarks to clear the used lines after the last call, to prevent artifacts. ## v4.0.0 From f77cf19de4761b8c6b8f58d533f5563bad488607 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 20:04:26 +0300 Subject: [PATCH 15/23] Added test for new features --- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 60 ++++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 3f1d83b..bb575c2 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -21,18 +21,66 @@ public void ProgressBar_Update_WritesStatusAndPercentage() { } [Fact] - public void ProgressBar_Update_SamePercentage_NoAdditionalOutput() { + public void ProgressBar_Update_SamePercentage_RerendersOutput() { Utilities.SkipIfNoInteractiveConsole(); Error = Utilities.GetWriter(out var errorWriter); - var bar = new ProgressBar(); + var bar = new ProgressBar { + ProgressChar = '#', + ForegroundColor = ConsoleColor.White, + ProgressColor = ConsoleColor.Green + }; - bar.Update(25); + bar.Update(25, "Loading"); errorWriter.ToStringAndFlush(); - bar.Update(25); + bar.Update(25, "Loading"); + + var output = errorWriter.ToString(); + Assert.NotEqual(string.Empty, output); + Assert.Contains("Loading", output); + Assert.Contains("25", output); + } + + [Fact] + public void ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() { + Utilities.SkipIfNoInteractiveConsole(); + + var originalError = Error; + try { + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar { + ProgressChar = '#' + }; + + bar.Update(75, "Working", sameLine: false); + + var output = errorWriter.ToString(); + Assert.Contains("Working", output); + Assert.Contains(Environment.NewLine + "[", output); + } finally { + Error = originalError; + } + } + + [Fact] + public void ProgressBar_WriteProgressBar_WritesFormattedOutput() { + Utilities.SkipIfNoInteractiveConsole(); + + var originalOut = Out; + try { + Out = Utilities.GetWriter(out var outWriter); + + ProgressBar.WriteProgressBar(OutputPipe.Out, 75, ConsoleColor.Cyan, '*'); - Assert.Equal(string.Empty, errorWriter.ToString()); + var output = outWriter.ToString(); + Assert.Contains("[", output); + Assert.Contains("75%", output); + Assert.Contains("*", output); + } finally { + Out = originalOut; + } } [Fact] @@ -55,4 +103,4 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() Assert.Equal(42, result); Assert.NotEqual(string.Empty, errorWriter.ToString()); } -} \ No newline at end of file +} From 9963c3366c3ee21854ad9d37c1e9cce84f7a0662 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 20:07:15 +0300 Subject: [PATCH 16/23] Update agents and readme --- AGENTS.md | 2 ++ README.md | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d14e328..51caff3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,9 +72,11 @@ Testing structure and workflows - `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. + - Progress bar coverage now includes multi-line rendering (`sameLine: false`), repeat renders at the same percentage, and the static `ProgressBar.WriteProgressBar` helper. Keep these behaviours in sync with docs. 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. +- `ProgressBar.Update` re-renders on every call (even when the percentage is unchanged) and accepts `sameLine` to place the status above the bar; the static `ProgressBar.WriteProgressBar` renders one-off bars for multi-progress scenarios. diff --git a/README.md b/README.md index 95c06f9..128b11d 100755 --- a/README.md +++ b/README.md @@ -185,12 +185,18 @@ prg.AnimationSequence = IndeterminateProgressBar.Patterns.CarriageReturn; // cus 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) +// There are also overloads without header, and percentage can be either int or double (0-100). +// Update re-renders on every call, even if the percentage hasn't changed, so you can refresh the status text. // Also, you can change some of the visual properties of the progress bar after initialization // by using the properties of the ProgressBar class prg.ProgressChar = '■'; // Character to fill the progress bar prg.ForegroundColor = Color.Red; // Color of the empty part prg.ProgressColor = Color.Blue; // The color of the filled part +// Pass sameLine: false to render the status on a separate line above the bar. +prg.Update(percentage, "Downloading", sameLine: false); + +// Need a static, one-off render? Use the helper: +ProgressBar.WriteProgressBar(OutputPipe.Error, percentage, Color.Green, '*'); ``` ### Pipes From 9d0a4bd9ceb9c5563e1ed08b2f278bd0f93cf887 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Thu, 23 Oct 2025 20:11:55 +0300 Subject: [PATCH 17/23] Added multi-progress bars feature to readme --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 128b11d..9acbf48 100755 --- a/README.md +++ b/README.md @@ -199,6 +199,25 @@ prg.Update(percentage, "Downloading", sameLine: false); ProgressBar.WriteProgressBar(OutputPipe.Error, percentage, Color.Green, '*'); ``` +##### Multiple Progress Bars with `Overwrite` + +You can combine the static helper with `Overwrite` to redraw several progress bars inside the same console window—perfect for tracking multiple downloads or tasks: + +```csharp +var downloads = new[] { "Video.mp4", "Archive.zip" }; +var progress = new double[downloads.Length]; + +Overwrite(progress, state => { + for (int i = 0; i < downloads.Length; i++) { + Write(OutputPipe.Error, $"Task {i + 1} ({downloads[i]}): "); + ProgressBar.WriteProgressBar(OutputPipe.Error, state[i], Color.Cyan); + NewLine(OutputPipe.Error); + } +}, lines: downloads.Length, pipe: OutputPipe.Error); +``` + +Update the `progress` array elsewhere and call `Overwrite` again to refresh the stacked bars without leaving artifacts. + ### 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, From fcdb93271bc50d0466a86a0faed05fa7b7e6f38f Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 25 Oct 2025 19:29:07 +0300 Subject: [PATCH 18/23] Improved writewhitespaces and moved to extensions --- PrettyConsole/Extensions.cs | 20 +++++++++++++++++++ PrettyConsole/Utils.cs | 40 ------------------------------------- 2 files changed, 20 insertions(+), 40 deletions(-) create mode 100644 PrettyConsole/Extensions.cs delete mode 100755 PrettyConsole/Utils.cs diff --git a/PrettyConsole/Extensions.cs b/PrettyConsole/Extensions.cs new file mode 100644 index 0000000..5f89b8c --- /dev/null +++ b/PrettyConsole/Extensions.cs @@ -0,0 +1,20 @@ +namespace PrettyConsole; + +internal static class Extensions { + private static readonly string WhiteSpaces = new(' ', 256); + + /// + /// Writes whitespace to a up to length by chucks + /// + /// + /// + internal static void WriteWhiteSpaces(this TextWriter writer, int length) { + ReadOnlySpan whiteSpaces = WhiteSpaces; + + while (length > 0) { + int cur_length = Math.Min(length, 256); + writer.Write(whiteSpaces.Slice(0, cur_length)); + length -= cur_length; + } + } +} \ No newline at end of file diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs deleted file mode 100755 index 1eaaf11..0000000 --- a/PrettyConsole/Utils.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace PrettyConsole; - -/// -/// A static class containing utility methods -/// -internal static class Utils { - /// - /// Constant buffer filled with whitespaces - /// - private static readonly string WhiteSpaces = new(' ', 256); - - /// - /// Writes whitespace to a up to length by chucks - /// - /// - /// - internal static void WriteWhiteSpaces(this TextWriter writer, int length) { - 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)); - } - } -} \ No newline at end of file From 4ed0d5fe8501089c75a2278fbe325c73ba4495eb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 25 Oct 2025 19:29:20 +0300 Subject: [PATCH 19/23] Removed synchronization --- PrettyConsole/AdvancedOutputs.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs index b98c097..770fb3a 100755 --- a/PrettyConsole/AdvancedOutputs.cs +++ b/PrettyConsole/AdvancedOutputs.cs @@ -9,7 +9,6 @@ public static partial class Console { /// /// Please remember to clear the used lines after the last call to this method, you can use /// - [MethodImpl(MethodImplOptions.Synchronized)] public static void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); ClearNextLines(1, pipe); @@ -26,7 +25,6 @@ public static void OverwriteCurrentLine(ReadOnlySpan output, Outp /// /// Please remember to clear the used lines after the last call to this method, you can use /// - [MethodImpl(MethodImplOptions.Synchronized)] public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); ClearNextLines(lines, pipe); @@ -45,7 +43,6 @@ public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = Out /// /// Please remember to clear the used lines after the last call to this method, you can 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); From 58ef136f108caf2dd42cc57d5d809ca2725e47eb Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 25 Oct 2025 19:29:34 +0300 Subject: [PATCH 20/23] Simplified buffer pool according to usage --- PrettyConsole/BufferPool.cs | 39 +++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/PrettyConsole/BufferPool.cs b/PrettyConsole/BufferPool.cs index 7c33ce2..e5c954a 100644 --- a/PrettyConsole/BufferPool.cs +++ b/PrettyConsole/BufferPool.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Threading.Channels; namespace PrettyConsole; @@ -7,31 +6,19 @@ namespace PrettyConsole; internal sealed class BufferPool : IDisposable { private bool _disposed; private readonly Channel> _channel; - private readonly Func> _createPolicy; - private readonly Func, bool> _returnPolicy; private readonly ThreadLocal?> _fastItem; internal const int ListStartingSize = 256; internal const int ListMaxSize = 4096; - public static readonly BufferPool Shared - = new(() => new(ListStartingSize), - item => { - if (item.Count > ListMaxSize) { - return false; - } - item.Clear(); - return true; - }); - - private BufferPool(Func> createPolicy, Func, bool> returnPolicy) { + public static readonly BufferPool Shared = new(); + + private BufferPool() { _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); } @@ -48,24 +35,34 @@ public PooledObjectOwner Rent(out List value) { value = item; return new(this, value); } - value = _createPolicy(); + value = new List(ListStartingSize); return new(this, value); } [MethodImpl(MethodImplOptions.NoInlining)] private void Return(List item) { ObjectDisposedException.ThrowIf(_disposed, this); - if (!_returnPolicy(item)) { - (item as IDisposable)?.Dispose(); + if (!AcceptAndClear(item)) { return; } if (_fastItem.Value is null) { _fastItem.Value = item; return; } - if (!_channel.Writer.TryWrite(item)) { - (item as IDisposable)?.Dispose(); + _channel.Writer.TryWrite(item); + } + + /// + /// Checks if should be accepted back to the pool, and clears it if it should. + /// + /// + /// + private static bool AcceptAndClear(List item) { + if (item.Count > ListMaxSize) { + return false; } + item.Clear(); + return true; } public void Dispose() { From 8d04ab823e31fe266c0023b20f4dea9de386b4a1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 25 Oct 2025 19:34:45 +0300 Subject: [PATCH 21/23] Add tests for buffer pool --- PrettyConsole.Tests.Unit/BufferPoolTests.cs | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 PrettyConsole.Tests.Unit/BufferPoolTests.cs diff --git a/PrettyConsole.Tests.Unit/BufferPoolTests.cs b/PrettyConsole.Tests.Unit/BufferPoolTests.cs new file mode 100644 index 0000000..2fb006b --- /dev/null +++ b/PrettyConsole.Tests.Unit/BufferPoolTests.cs @@ -0,0 +1,46 @@ +namespace PrettyConsole.Tests.Unit; + +public class BufferPoolTests { + [Fact] + public void Rent_ReturnsFastItemAfterReturn() { + using var pool = CreatePool(); + + var owner = pool.Rent(out var firstBuffer); + firstBuffer.Add('x'); + owner.Dispose(); + + using var secondOwner = pool.Rent(out var reusedBuffer); + + Assert.Same(firstBuffer, reusedBuffer); + Assert.Empty(reusedBuffer); + } + + [Fact] + public void Return_DropsOversizedLists() { + using var pool = CreatePool(); + + List oversized; + using (var owner = pool.Rent(out var buffer)) { + oversized = buffer; + buffer.AddRange(new string('x', BufferPool.ListMaxSize + 1)); + } + + using var nextOwner = pool.Rent(out var nextBuffer); + + Assert.NotSame(oversized, nextBuffer); + Assert.Equal(BufferPool.ListStartingSize, nextBuffer.Capacity); + } + + [Fact] + public void Value_ThrowsAfterDispose() { + using var pool = CreatePool(); + + var owner = pool.Rent(out _); + owner.Dispose(); + + Assert.Throws(() => _ = owner.Value); + } + + private static BufferPool CreatePool() + => (BufferPool)Activator.CreateInstance(typeof(BufferPool), nonPublic: true)!; +} From 83580c799c0a1f6e485a653d67b66c192ba14e89 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 25 Oct 2025 19:35:24 +0300 Subject: [PATCH 22/23] Formatting --- PrettyConsole.Tests.Unit/BufferPoolTests.cs | 2 +- PrettyConsole.Tests.Unit/ProgressBarTests.cs | 2 +- PrettyConsole/Extensions.cs | 2 +- PrettyConsole/ProgressBar.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PrettyConsole.Tests.Unit/BufferPoolTests.cs b/PrettyConsole.Tests.Unit/BufferPoolTests.cs index 2fb006b..48034cf 100644 --- a/PrettyConsole.Tests.Unit/BufferPoolTests.cs +++ b/PrettyConsole.Tests.Unit/BufferPoolTests.cs @@ -43,4 +43,4 @@ public void Value_ThrowsAfterDispose() { private static BufferPool CreatePool() => (BufferPool)Activator.CreateInstance(typeof(BufferPool), nonPublic: true)!; -} +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index bb575c2..70fb571 100644 --- a/PrettyConsole.Tests.Unit/ProgressBarTests.cs +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -103,4 +103,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/Extensions.cs b/PrettyConsole/Extensions.cs index 5f89b8c..c92ddff 100644 --- a/PrettyConsole/Extensions.cs +++ b/PrettyConsole/Extensions.cs @@ -15,6 +15,6 @@ internal static void WriteWhiteSpaces(this TextWriter writer, int length) { int cur_length = Math.Min(length, 256); writer.Write(whiteSpaces.Slice(0, cur_length)); length -= cur_length; - } + } } } \ No newline at end of file diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 16df170..7d1fbce 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -150,4 +150,4 @@ public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColo Write(pipe, $"] {p,3}%"); } } -} +} \ No newline at end of file From 9c17f5b3a001b50de03fe66fd62c6d2636a7d8ef Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Sat, 25 Oct 2025 19:38:25 +0300 Subject: [PATCH 23/23] Added hr format to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 9acbf48..3aa1fda 100755 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ if (!TryReadLine(out int choice, $"Pick option {Color.Cyan}1-5{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. +When interpolating `TimeSpan` values you can also apply the special `:hr` format specifier to get compact, human-readable output (`ms`, `ss`, `mm`, `hh`, or `dd` depending on the magnitude): + +```csharp +var elapsed = stopwatch.Elapsed; +WriteLine($"Completed in {elapsed:hr}"); +``` + ### ColoredOutput PrettyConsole uses an equation inspired syntax to colorize text. The syntax is as follows: