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/PrettyConsole.Tests.Unit/BufferPoolTests.cs b/PrettyConsole.Tests.Unit/BufferPoolTests.cs new file mode 100644 index 0000000..48034cf --- /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)!; +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs index 3f1d83b..70fb571 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] diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs new file mode 100755 index 0000000..9fbc019 --- /dev/null +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -0,0 +1,28 @@ +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.WriteProgressBar(OutputPipe.Error, p, Color.Magenta); + NewLine(OutputPipe.Error); + Write(OutputPipe.Error, $"Task {2}: "); + ProgressBar.WriteProgressBar(OutputPipe.Error, p, Color.Magenta); + NewLine(OutputPipe.Error); + }, 2); + + await Task.Delay(15); + } + ClearNextLines(2, OutputPipe.Error); + WriteLine(OutputPipe.Error, $"Done"); + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/ProgressBarTest.cs b/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs similarity index 65% rename from PrettyConsole.Tests/Features/ProgressBarTest.cs rename to PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs index 0110227..34fca71 100755 --- a/PrettyConsole.Tests/Features/ProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarDefaultTest.cs @@ -2,22 +2,23 @@ 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 { 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/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 diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index d5c7798..68ac523 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -18,7 +18,9 @@ new TableTest(), new TreeMenuTest(), new IndeterminateProgressBarTest(), - new ProgressBarTest() + new ProgressBarDefaultTest(), + new ProgressBarMultiLineTest(), + new MultiProgressBarTest(), }; foreach (var test in tests) { diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs index 716f567..770fb3a 100755 --- a/PrettyConsole/AdvancedOutputs.cs +++ b/PrettyConsole/AdvancedOutputs.cs @@ -6,7 +6,9 @@ public static partial class Console { /// /// /// The output pipe to use - [MethodImpl(MethodImplOptions.Synchronized)] + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// public static void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); ClearNextLines(1, pipe); @@ -20,7 +22,9 @@ public static void OverwriteCurrentLine(ReadOnlySpan output, Outp /// The output action. /// The amount of lines to clear. /// The output pipe to use. - [MethodImpl(MethodImplOptions.Synchronized)] + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); ClearNextLines(lines, pipe); @@ -36,7 +40,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. - [MethodImpl(MethodImplOptions.Synchronized)] + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// public static void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) where TState : allows ref struct { var currentLine = GetCurrentLine(); ClearNextLines(lines, pipe); 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() { diff --git a/PrettyConsole/Extensions.cs b/PrettyConsole/Extensions.cs new file mode 100644 index 0000000..c92ddff --- /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/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 5cd5ab2..254c423 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 @@ -38,22 +38,20 @@ - - 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 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/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 96e309f..7d1fbce 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 { /// @@ -11,17 +9,22 @@ 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 { + /// + /// 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 status (if rendered). /// public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; @@ -30,93 +33,121 @@ public class ProgressBar { /// 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. + /// + /// 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. /// /// 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); + public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty, true); /// /// Updates the progress bar with the specified percentage and header text. /// /// The percentage value (0-100) representing the progress. /// The status text to be displayed after the progress bar. - 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; - } + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) + => Update((int)percentage, status, sameLine); + /// + /// Updates the progress bar with the specified percentage and header text. + /// + /// The percentage value (0-100) representing the progress. + /// The status text to be displayed after the progress bar. + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. + /// + /// Please remember to clear the used lines after the last call to this method, you can use + /// + public void Update(int percentage, ReadOnlySpan status, bool sameLine = true) { lock (_lock) { - // 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; + if (sameLine) { 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(' '); + if (status.Length > 0) { + Write(status, OutputPipe.Error, ForegroundColor); + Write(' '); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); } + } else { + bool hasStatus = status.Length > 0; + int lines = hasStatus ? 2 : 1; + ClearNextLines(lines, OutputPipe.Error); + if (hasStatus) WriteLine(status, OutputPipe.Error, ForegroundColor); + WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar); + } + GoToLine(currentLine); + } + } - // Write the entire bar (progress + tail) - Error.Write(buf.Slice(0, pLength)); + /// + /// Writes a single progress bar segment without tracking state. + /// + /// The output pipe to write to. + /// The percentage value (0-100) representing the progress. + /// The color used for the filled segment of the bar. + /// The character used to render the filled portion of the bar. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar); - 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 + /// + /// Writes a single progress bar segment without tracking state. + /// + /// The output pipe to write to. + /// The percentage value (0-100) representing the progress. + /// The color used for the filled segment of the bar. + /// The character used to render the filled portion of the bar. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] + public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar) { + ResetColors(); + + int p = Math.Clamp(percentage, 0, 100); + int bufferWidth = GetWidthOrDefault() - baseConsole.CursorLeft; + + const int bracketsAndSpacing = 3; // '[' + ']' + ' ' + const int percentageWidth = 3; // numeric portion width + const int percentSymbolLength = 1; // '%' character + int barLength = Math.Max(0, bufferWidth - (bracketsAndSpacing + percentageWidth + percentSymbolLength)); + + var writer = GetWriter(pipe); + writer.Write('['); + + if (barLength > 0) { + int filled = Math.Min((int)(barLength * p * 0.01), barLength); + + if (filled > 0) { + SetColors(progressColor, baseConsole.BackgroundColor); + Span s = stackalloc char[filled]; + s.Fill(progressChar); + writer.Write(s); ResetColors(); } + + int remaining = barLength - filled; + if (remaining > 0) { + writer.WriteWhiteSpaces(remaining); + } } + + Write(pipe, $"] {p,3}%"); } } } \ 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 diff --git a/README.md b/README.md index 95c06f9..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: @@ -185,14 +192,39 @@ 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, '*'); ``` +##### 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, diff --git a/Versions.md b/Versions.md index fd28867..b7187c5 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,19 @@ # 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 %. +- `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 ### Added