diff --git a/AGENTS.md b/AGENTS.md index 76926b7..d09488a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,8 @@ Summary - PrettyConsole/ — main library - PrettyConsole.Tests/ — interactive/demo runner (manually selects visual feature demos) - PrettyConsole.Tests.Unit/ — xUnit v3 unit tests using Microsoft Testing Platform -- v5.3.0 (current) makes more `PrettyConsoleInterpolatedStringHandler` members public, adds `AppendInline` for nesting handlers, introduces a ctor that takes only `OutputPipe` + optional `IFormatProvider`, and passes handlers by `ref` to callers. It adds `SkipLines` to advance the cursor while keeping overwritten UIs, reorders `Confirm(trueValues, ref handler, bool emptyIsTrue = true)` arguments (the boolean is now last), switches `IndeterminateProgressBar` header factories to `PrettyConsoleInterpolatedStringHandlerFactory` with the singleton `PrettyConsoleInterpolatedStringHandlerBuilder`, and makes `AnsiColors` public. v5.2.0 rewrote the handler to buffer before writing and added `WhiteSpace`; v5.1.0 renamed `PrettyConsoleExtensions` to `ConsoleContext`, added `Console.WriteWhiteSpaces(length, pipe)`, and made `Out`/`Error`/`In` settable; v5.0.0 removed the legacy `ColoredOutput`/`Color` types in favor of `ConsoleColor` helpers and tuples. + - Examples/ — standalone `.cs` sample apps plus `assets/` previews; documented in `Examples/README.md` and excluded from automated builds/tests +- v5.3.1 (current) renames `IndeterminateProgressBar` to `Spinner` (and `AnimationSequence` to `Pattern`), triggers the line reset at the start of each spinner frame, gives all `RunAsync` overloads default cancellation tokens, and renames `ProgressBar.WriteProgressBar` to `Render` while adding handler-factory overloads and switching header parameters to `string`. It still passes handlers by `ref`, adds `AppendInline`, and introduces the ctor that takes only `OutputPipe` + optional `IFormatProvider`; `SkipLines` advances the cursor while keeping overwritten UIs; `Confirm(trueValues, ref handler, bool emptyIsTrue = true)` has the boolean last; spinner header factories use `PrettyConsoleInterpolatedStringHandlerFactory` with the singleton builder; `AnsiColors` is public. v5.2.0 rewrote the handler to buffer before writing and added `WhiteSpace`; v5.1.0 renamed `PrettyConsoleExtensions` to `ConsoleContext`, added `Console.WriteWhiteSpaces(length, pipe)`, and made `Out`/`Error`/`In` settable; v5.0.0 removed the legacy `ColoredOutput`/`Color` types in favor of `ConsoleColor` helpers and tuples. Commands you’ll use often @@ -67,8 +68,8 @@ High-level architecture and key concepts - Menus and tables - `Selection` returns a single choice or empty string on invalid input; `MultiSelection` parses space-separated indices into string arrays; `TreeMenu` renders two-level hierarchies and validates input (throwing `ArgumentException` when selections are invalid); `Table` renders headers + columns with width calculations. - Progress bars - - `IndeterminateProgressBar` binds to running `Task` instances, optionally starts tasks, supports cancellable `RunAsync` overloads, exposes `AnimationSequence`, `Patterns`, `ForegroundColor`, `DisplayElapsedTime`, and `UpdateRate`. Frames render on the error pipe and auto-clear. v5.3.0 switches header factories to `PrettyConsoleInterpolatedStringHandlerFactory`; use `(builder, out handler) => handler = builder.Build(OutputPipe.Error, $"status")` with the singleton `PrettyConsoleInterpolatedStringHandlerBuilder`. - - `ProgressBar` maintains a single-line bar on the error pipe. `Update` accepts `int`/`double` percentages plus optional status spans, and exposes `ProgressChar`, `ForegroundColor`, and `ProgressColor` for customization. The static `ProgressBar.WriteProgressBar` helper renders one-off segments without moving the cursor (so you can stack multiple bars within an `Overwrite` block). + - `Spinner` (formerly `IndeterminateProgressBar`) binds to running `Task` instances, optionally starts tasks, supports cancellable `RunAsync` overloads with default tokens, exposes `Pattern` (formerly `AnimationSequence`), `Patterns`, `ForegroundColor`, `DisplayElapsedTime`, and `UpdateRate`. Frames render on the error pipe and auto-clear. Header factories use `PrettyConsoleInterpolatedStringHandlerFactory`; call `(builder, out handler) => handler = builder.Build(OutputPipe.Error, $"status")` with the singleton `PrettyConsoleInterpolatedStringHandlerBuilder`. + - `ProgressBar` maintains a single-line bar on the error pipe. `Update` accepts `int`/`double` percentages plus optional status spans, and exposes `ProgressChar`, `ForegroundColor`, and `ProgressColor` for customization. The static `ProgressBar.Render` helper (renamed from `WriteProgressBar`) renders one-off segments without moving the cursor, so you can stack multiple bars within an `Overwrite` block. It now also has overloads that accept `PrettyConsoleInterpolatedStringHandlerFactory` for low-allocation headers. - Packaging and targets - `PrettyConsole.csproj` targets net10.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project. @@ -78,12 +79,12 @@ 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. + - Progress bar coverage now includes multi-line rendering (`sameLine: false`), repeat renders at the same percentage, and the static `ProgressBar.Render` helper. Keep these behaviours in sync with docs. Notes and gotchas - The library aims to minimize allocations; prefer span-based overloads (`ReadOnlySpan`, `ISpanFormattable`) plus the inline `ConsoleColor` tuples instead of recreating strings or structs. - When authoring new features, pick the appropriate OutputPipe to keep CLI piping behavior intact. - On macOS terminals, ANSI is supported; Windows legacy terminals are handled via ANSI-compatible rendering in the library. -- `ProgressBar.Update` re-renders on every call (even when the percentage is unchanged) and accepts `sameLine` to place the status above the bar; the static `ProgressBar.WriteProgressBar` renders one-off bars without writing a trailing newline, so rely on `Console.Overwrite`/`lines` to stack multiple bars cleanly. +- `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.Render` renders one-off bars without writing a trailing newline, so rely on `Console.Overwrite`/`lines` to stack multiple bars cleanly. - After the final `Overwrite`/`Overwrite` call in a rendering loop, call `Console.ClearNextLines(totalLines, pipe)` once more to clear the region and prevent ghost text. diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 0000000..2f24121 --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,29 @@ +# Examples Gallery + +These are simple examples of UIs that can be achieved by properly using the APIs that `PrettyConsole` provides. The examples are written using .NET 10 file-based apps, and can be run with `dotnet run example.cs` (change to the specific "app" name that you want). + +- Please keep in mind that colors in the recordings might not appear exactly like their name in code (i.e `ConsoleColor.Green` may be a different color in the recordings), this is not an issue with the package, it is the because themes are used in the environment of the recordings, and they override how colors are rendered. + +## [Interactive menu](interactive-menu.cs) + +Three-step wizard that reuses the same screen area via `Console.Overwrite` instead of scrolling, keeping prompts tidy. + +![Interactive menu demo](assets/interactive-menu.gif) + +## [Progress with status](progress-with-status.cs) + +Simple progress bar paired with a live status line underneath—good for downloads or migrations. + +![Progress with status demo](assets/progress-with-status.gif) + +## [Compilation spinner](compilation-spinner.cs) + +Single-line braille spinner with step labels and elapsed time for a “building…” style readout. + +![Compilation spinner demo](assets/compilation-spinner.gif) + +## [Brew‑style downloads](brew-style-downloads.cs) + +Shows multiple concurrent package downloads with a braille spinner, aligned byte counters, and overwrite-based repainting on the error pipe. + +![Brew-style downloads demo](assets/brew-style-downloads.gif) diff --git a/Examples/assets/brew-style-downloads.gif b/Examples/assets/brew-style-downloads.gif new file mode 100644 index 0000000..6d7662b Binary files /dev/null and b/Examples/assets/brew-style-downloads.gif differ diff --git a/Examples/assets/compilation-spinner.gif b/Examples/assets/compilation-spinner.gif new file mode 100644 index 0000000..18d5385 Binary files /dev/null and b/Examples/assets/compilation-spinner.gif differ diff --git a/Examples/assets/interactive-menu.gif b/Examples/assets/interactive-menu.gif new file mode 100644 index 0000000..22c8eee Binary files /dev/null and b/Examples/assets/interactive-menu.gif differ diff --git a/Examples/assets/progress-with-status.gif b/Examples/assets/progress-with-status.gif new file mode 100644 index 0000000..649a7d3 Binary files /dev/null and b/Examples/assets/progress-with-status.gif differ diff --git a/Examples/brew-style-downloads.cs b/Examples/brew-style-downloads.cs new file mode 100644 index 0000000..188283c --- /dev/null +++ b/Examples/brew-style-downloads.cs @@ -0,0 +1,117 @@ +#:package PrettyConsole@5.4.0 + +using PrettyConsole; + +bool keepProgressOutput = true; + +var downloads = BrewStyleDownloads.CreateDownloadTasks(); +var count = downloads.Count; + +var spinner = Spinner.Patterns.Braille; +var spinnerLength = spinner.Count; +int spinnerIndex = 0; + +var bufferWidth = Console.BufferWidth; + +var task = Task.WhenAll(downloads.Select(BrewStyleDownloads.AdvanceDownload)); + +Console.CursorVisible = false; // Hide cursor to see progress better +while (!task.IsCompleted) { + spinnerIndex = (spinnerIndex + 1) % spinnerLength; + + Console.Overwrite(() => { + foreach (var download in downloads) { + int written = 0; + if (download.IsComplete) { + written += Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.Green}✔︎{ConsoleColor.DefaultForeground} {download.Name}") - 1; + // I remove 1 from written here because "✔︎" is 2 characters long but renders a single block in the terminal + } else { + written += Console.WriteInterpolated(OutputPipe.Error, $"{ConsoleColor.Green}{spinner[spinnerIndex]}{ConsoleColor.DefaultForeground} {download.Name}"); + } + + var current = (double)download.BytesDownloaded; + var total = (double)download.FileSize; + + var padding = bufferWidth - 34 - written; + + // progress section takes 34 characters - found by measuring in test run + // var progressLength = Console.WriteLineInterpolated(... without WhiteSpace) + Console.WriteLineInterpolated(OutputPipe.Error, $"{new WhiteSpace(padding)}[Downloaded {current,10:bytes}/{total,10:bytes}]"); + } + }, count); + + await Task.Delay(25); +} +Console.CursorVisible = true; // restore cursor visibility + +if (keepProgressOutput) { + Console.SkipLines(count); + Console.WriteLine(); // Add a newline to separate progress from future outputs +} else { + Console.ClearNextLines(count, OutputPipe.Error); +} + +Console.WriteLineInterpolated($"{ConsoleColor.Green}Done!{ConsoleColor.DefaultForeground}"); + +/// +/// Represents a single download in a "brew" style feed with progress tracking. +/// +sealed class DownloadStyleTask { + public DownloadStyleTask(string name, long fileSize) { + Name = name; + FileSize = fileSize; + } + + public string Name { get; } + + public long BytesDownloaded { get; private set; } + + public bool IsComplete { get; private set; } + + public long FileSize { get; } + + /// + /// Advance the download and notify any listeners. + /// + public void Advance(long bytes) { + if (bytes <= 0 || IsComplete) { + return; + } + + var updated = BytesDownloaded + bytes; + BytesDownloaded = updated >= FileSize ? FileSize : updated; + if (BytesDownloaded == FileSize) IsComplete = true; + } +} + +static class BrewStyleDownloads +{ + /// + /// Creates sample download tasks you can feed into a renderer or progress loop. + /// Each task exposes an event so a UI can react when progress changes. + /// + public static List CreateDownloadTasks() { + var tasks = new List + { + new("git", 48_000_000), + new("curl", 27_500_000), + new("openssl", 96_000_000), + new("python@3.13", 121_000_000) + }; + + return tasks; + } + + public static async Task AdvanceDownload(DownloadStyleTask task) { + const long minChunkSize = 1_000_000; + + while (task.BytesDownloaded < task.FileSize) { + var chunk = Random.Shared.NextInt64(minChunkSize, 2 * minChunkSize); + var remaining = task.FileSize - task.BytesDownloaded; + var current = Math.Min(chunk, remaining); + task.Advance(current); + var delay = Random.Shared.Next(150, 250); + await Task.Delay(delay); + } + } +} diff --git a/Examples/compilation-spinner.cs b/Examples/compilation-spinner.cs new file mode 100644 index 0000000..b4ae9ed --- /dev/null +++ b/Examples/compilation-spinner.cs @@ -0,0 +1,36 @@ +#:package PrettyConsole@5.4.0 + +using System.Diagnostics; +using PrettyConsole; + +string[] steps = [ + "Restore", + "Compile", + "Link native shims", + "Run analyzers", + "Pack artifacts", +]; + +var step = 0; +var start = Stopwatch.GetTimestamp(); + +var build = Task.Run(async () => { + for (; step < steps.Length; Interlocked.Increment(ref step)) { + await Task.Delay(800); + } +}); + +var spinner = new Spinner { + Pattern = Spinner.Patterns.Braille, + ForegroundColor = ConsoleColor.Green, + DisplayElapsedTime = true, + UpdateRate = 100, +}; + +await spinner.RunAsync(build, (builder, out handler) => { + var current = Volatile.Read(ref step); + handler = builder.Build(OutputPipe.Error, $"Current step: {ConsoleColor.Green}{steps[current]}"); +}, CancellationToken.None); + +var elapsed = Stopwatch.GetElapsedTime(start); +Console.WriteLineInterpolated($"Build complete in {ConsoleColor.Green}{elapsed:duration}"); diff --git a/Examples/interactive-menu.cs b/Examples/interactive-menu.cs new file mode 100644 index 0000000..fc2e364 --- /dev/null +++ b/Examples/interactive-menu.cs @@ -0,0 +1,53 @@ +#:package PrettyConsole@5.4.0 + +using PrettyConsole; + +var environment = PromptSelection( + title: "Select environment", + options: ["Development", "Staging", "Production"]); + +var features = PromptMultiSelection( + title: "Choose features (space to pick multiple)", + options: ["Core", "Metrics", "Tracing"]); + +var region = PromptSelection( + title: "Pick region", + options: ["us-east", "us-west", "eu-central"]); + +Console.WriteLineInterpolated($"{ConsoleColor.Green}Ready to deploy!"); +Console.WriteLineInterpolated($"Environment: {Markup.Underline}{environment}{Markup.ResetUnderline}"); +Console.WriteLineInterpolated($"Features: {Markup.Underline}{string.Join(", ", features)}{Markup.ResetUnderline}"); +Console.WriteLineInterpolated($"Region: {Markup.Underline}{region}{Markup.ResetUnderline}"); + +// The following helper methods wrap the selection in Overwrite to resemble page routes +// If you want simple scrolling you can use Console.Selection or Console.MultiSelection directly. + +static string PromptSelection(string title, string[] options) { + string selection = string.Empty; + + while (selection.Length == 0) { + Console.Overwrite(() => { + selection = Console.Selection(options, $"{ConsoleColor.Cyan}{title}{ConsoleColor.DefaultForeground}:"); + if (selection.Length == 0) { + Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Red}Invalid choice. Try again."); + } + }, lines: options.Length + 3, pipe: OutputPipe.Out); + } + + return selection; +} + +static string[] PromptMultiSelection(string title, string[] options) { + string[] selection = Array.Empty(); + + while (selection.Length == 0) { + Console.Overwrite(() => { + selection = Console.MultiSelection(options, $"{ConsoleColor.Cyan}{title}{ConsoleColor.DefaultForeground}:"); + if (selection.Length == 0) { + Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Red}Please pick at least one option."); + } + }, lines: options.Length + 3, pipe: OutputPipe.Out); + } + + return selection; +} diff --git a/Examples/progress-with-status.cs b/Examples/progress-with-status.cs new file mode 100644 index 0000000..e89b8c4 --- /dev/null +++ b/Examples/progress-with-status.cs @@ -0,0 +1,18 @@ +#:package PrettyConsole@5.4.0 + +using PrettyConsole; + +Console.CursorVisible = false; +for (int i = 0; i <= 100; i += 4) { + Console.Overwrite(i, static ii => { + ProgressBar.Render(OutputPipe.Error, ii, ConsoleColor.Cyan, maxLineWidth: 40); + Console.NewLine(OutputPipe.Error); + Console.WriteInterpolated(OutputPipe.Error, $"Downloading assets... {ConsoleColor.Cyan}{ii}"); + }, lines: 2, pipe: OutputPipe.Error); + + await Task.Delay(70); +} +Console.CursorVisible = true; + +Console.ClearNextLines(2, OutputPipe.Error); +Console.WriteLineInterpolated($"{ConsoleColor.Green}Download complete!"); diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs deleted file mode 100755 index bd013c3..0000000 --- a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace PrettyConsole.Tests.Features; - -public sealed class IndeterminateProgressBarTest : IPrettyConsoleTest { - public string FeatureName => "IndeterminateProgressBar"; - - public async ValueTask Implementation() { - var prg = new IndeterminateProgressBar { - AnimationSequence = IndeterminateProgressBar.Patterns.Braille, - ForegroundColor = ConsoleColor.Magenta, - // UpdateRate = 120, - DisplayElapsedTime = true - }; - await prg.RunAsync(Task.Delay(5_000), (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}..."), CancellationToken.None); - } -} \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs index 053aa78..46f3499 100755 --- a/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs +++ b/PrettyConsole.Tests/Features/MultiProgressBarLeftAlignedTest.cs @@ -10,9 +10,9 @@ public async ValueTask Implementation() { double percentage = 100 * (double)i / count; Console.Overwrite((int)percentage, p => { - ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta, maxLineWidth: 50); + ProgressBar.Render(OutputPipe.Error, p, ConsoleColor.Magenta, maxLineWidth: 50); Console.WriteLineInterpolated(OutputPipe.Error, $" - Task {1}"); - ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta, maxLineWidth: 50); + ProgressBar.Render(OutputPipe.Error, p, ConsoleColor.Magenta, maxLineWidth: 50); Console.WriteInterpolated(OutputPipe.Error, $" - Task {2}"); }, 2); diff --git a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs index 31fe0d1..2865f5d 100755 --- a/PrettyConsole.Tests/Features/MultiProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/MultiProgressBarTest.cs @@ -11,9 +11,9 @@ public async ValueTask Implementation() { Console.Overwrite((int)percentage, p => { Console.WriteInterpolated(OutputPipe.Error, $"Task {1}: "); - ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta); + ProgressBar.Render(OutputPipe.Error, p, ConsoleColor.Magenta); Console.WriteInterpolated(OutputPipe.Error, $"Task {2}: "); - ProgressBar.WriteProgressBar(OutputPipe.Error, p, ConsoleColor.Magenta); + ProgressBar.Render(OutputPipe.Error, p, ConsoleColor.Magenta); }, 2); await Task.Delay(15); diff --git a/PrettyConsole.Tests/Features/SpinnerTest.cs b/PrettyConsole.Tests/Features/SpinnerTest.cs new file mode 100755 index 0000000..7838723 --- /dev/null +++ b/PrettyConsole.Tests/Features/SpinnerTest.cs @@ -0,0 +1,14 @@ +namespace PrettyConsole.Tests.Features; + +public sealed class SpinnerTest : IPrettyConsoleTest { + public string FeatureName => "Spinner"; + + public async ValueTask Implementation() { + var spinner = new Spinner { + Pattern = Spinner.Patterns.Braille, + ForegroundColor = ConsoleColor.Magenta, + DisplayElapsedTime = true + }; + await spinner.RunAsync(Task.Delay(5_000), (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"...{ConsoleColor.Green}Running{ConsoleColor.DefaultForeground}...")); + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index d729b3d..fe18520 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -15,7 +15,7 @@ new MultiSelectionTest(), new TableTest(), new TreeMenuTest(), - new IndeterminateProgressBarTest(), + new SpinnerTest(), new ProgressBarDefaultTest(), new ProgressBarMultiLineTest(), new MultiProgressBarTest(), diff --git a/PrettyConsole.UnitTests/AssemblyInfo.cs b/PrettyConsole.UnitTests/AssemblyInfo.cs index 30c3a8c..8412006 100644 --- a/PrettyConsole.UnitTests/AssemblyInfo.cs +++ b/PrettyConsole.UnitTests/AssemblyInfo.cs @@ -1,3 +1 @@ -using TUnit.Core; - [assembly: NotInParallel] \ No newline at end of file diff --git a/PrettyConsole.UnitTests/ProgressBarTests.cs b/PrettyConsole.UnitTests/ProgressBarTests.cs index b45bb90..8561b7c 100644 --- a/PrettyConsole.UnitTests/ProgressBarTests.cs +++ b/PrettyConsole.UnitTests/ProgressBarTests.cs @@ -75,12 +75,56 @@ public async Task ProgressBar_Update_SameLineFalse_WritesStatusOnSeparateLine() } [Test] - public async Task ProgressBar_WriteProgressBar_WritesFormattedOutput() { + public async Task ProgressBar_Update_WithFactory_SameLine_WritesHeaderAndBar() { + var originalError = Error; + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar { ProgressColor = Cyan }; + + bar.Update(40, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"hdr"), sameLine: true); + + var output = Utilities.StripAnsiSequences(errorWriter.ToString()); + await Assert.That(output).Contains("hdr"); + await Assert.That(output).Contains("["); + await Assert.That(output).Contains("40%"); + } finally { + Error = originalError; + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } + + [Test] + public async Task ProgressBar_Update_WithFactory_TwoLines_AppendsNewLine() { + var originalError = Error; + int cursorLine = 0; + RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); + try { + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar { ProgressColor = Cyan }; + + bar.Update(55, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"status"), sameLine: false); + + var output = Utilities.StripAnsiSequences(errorWriter.ToString()); + await Assert.That(output).Contains("status"); + await Assert.That(output).Contains(Environment.NewLine); + await Assert.That(output).Contains("55%"); + } finally { + Error = originalError; + RenderingExtensions.ConfigureCursorAccessors(null, null); + } + } + + [Test] + public async Task ProgressBar_Render_WritesFormattedOutput() { var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); - ProgressBar.WriteProgressBar(OutputPipe.Out, 75, Cyan, '*'); + ProgressBar.Render(OutputPipe.Out, 75, Cyan, '*'); var output = outWriter.ToString(); await Assert.That(output).Contains("["); @@ -92,12 +136,12 @@ public async Task ProgressBar_WriteProgressBar_WritesFormattedOutput() { } [Test] - public async Task ProgressBar_WriteProgressBar_RespectsMaxLineWidth() { + public async Task ProgressBar_Render_RespectsMaxLineWidth() { var originalOut = Out; try { Out = Utilities.GetWriter(out var outWriter); - ProgressBar.WriteProgressBar(OutputPipe.Out, 50, Cyan, '*', maxLineWidth: 24); + ProgressBar.Render(OutputPipe.Out, 50, Cyan, '*', maxLineWidth: 24); var output = outWriter.ToString(); await Assert.That(output.Length).IsEqualTo(24); @@ -155,12 +199,12 @@ public async Task ProgressBar_Update_Overloads_WriteOutput() { } [Test] - public async Task ProgressBar_WriteProgressBar_DoubleOverload_WritesOutput() { + public async Task ProgressBar_Render_DoubleOverload_WritesOutput() { var originalOut = Out; try { Out = Utilities.GetWriter(out var writer); - ProgressBar.WriteProgressBar(OutputPipe.Out, 33.3, Blue, '*'); + ProgressBar.Render(OutputPipe.Out, 33.3, Blue, '*'); await Assert.That(writer.ToString()).Contains("33%"); } finally { @@ -190,9 +234,8 @@ public async Task ProgressBar_Update_StatusSpan_SameLineFalse() { RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); try { var bar = new ProgressBar { ProgressColor = Green }; - ReadOnlySpan status = "span-status".AsSpan(); - bar.Update(30, status, sameLine: false); + bar.Update(30, "span-status", sameLine: false); } finally { RenderingExtensions.ConfigureCursorAccessors(null, null); } @@ -201,19 +244,19 @@ public async Task ProgressBar_Update_StatusSpan_SameLineFalse() { } [Test] - public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { + public async Task Spinner_RunAsync_CompletesAndReturnsResult() { Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); try { - var bar = new IndeterminateProgressBar { - AnimationSequence = new(["|", "/"]), + var spinner = new Spinner { + Pattern = new(["|", "/"]), DisplayElapsedTime = false, UpdateRate = 5 }; var cancellation = CancellationToken.None; - int result = await bar.RunAsync(Task.Run(async () => { + int result = await spinner.RunAsync(Task.Run(async () => { await Task.Delay(20, cancellation); return 42; }, cancellation), (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"Working"), cancellation); @@ -226,20 +269,20 @@ public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() } [Test] - public async Task IndeterminateProgressBar_RunAsync_OverloadsAndForegroundSetter() { + public async Task Spinner_RunAsync_OverloadsAndForegroundSetter() { var originalError = Error; Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); try { - var bar = new IndeterminateProgressBar { + var spinner = new Spinner { DisplayElapsedTime = false, - UpdateRate = 5 + UpdateRate = 5, + ForegroundColor = Cyan }; - bar.ForegroundColor = Cyan; - var genericResult = await bar.RunAsync(Task.Run(async () => { await Task.Delay(10); return 7; })); - await bar.RunAsync(Task.Run(async () => await Task.Delay(10))); + var genericResult = await spinner.RunAsync(Task.Run(async () => { await Task.Delay(10); return 7; })); + await spinner.RunAsync(Task.Run(async () => await Task.Delay(10))); await Assert.That(genericResult).IsEqualTo(7); await Assert.That(errorWriter.ToString()).IsNotEqualTo(string.Empty); @@ -250,18 +293,18 @@ public async Task IndeterminateProgressBar_RunAsync_OverloadsAndForegroundSetter } [Test] - public async Task IndeterminateProgressBar_RunAsync_Generic_TaskAlreadyCompleted() { + public async Task Spinner_RunAsync_Generic_TaskAlreadyCompleted() { Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); try { - var bar = new IndeterminateProgressBar { + var spinner = new Spinner { DisplayElapsedTime = false, UpdateRate = 5 }; var completed = Task.FromResult(5); - var result = await bar.RunAsync(completed, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"done")); + var result = await spinner.RunAsync(completed, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"done")); await Assert.That(result).IsEqualTo(5); } finally { @@ -270,13 +313,13 @@ public async Task IndeterminateProgressBar_RunAsync_Generic_TaskAlreadyCompleted } [Test] - public async Task IndeterminateProgressBar_RunAsync_CancelsQuickly() { + public async Task Spinner_RunAsync_CancelsQuickly() { Error = Utilities.GetWriter(out var errorWriter); int cursorLine = 0; RenderingExtensions.ConfigureCursorAccessors(() => cursorLine, (_, line) => cursorLine = line); using var cts = new CancellationTokenSource(); try { - var bar = new IndeterminateProgressBar { + var spinner = new Spinner { DisplayElapsedTime = false, UpdateRate = 100 }; @@ -286,7 +329,7 @@ public async Task IndeterminateProgressBar_RunAsync_CancelsQuickly() { }, cts.Token); cts.CancelAfter(200); - await bar.RunAsync(task, "cancelled", cts.Token); + await spinner.RunAsync(task, "cancelled", cts.Token); } catch (OperationCanceledException) { // expected in this path } finally { diff --git a/PrettyConsole/AdvancedOutputExtensions.cs b/PrettyConsole/AdvancedOutputExtensions.cs index c2e486f..306418d 100755 --- a/PrettyConsole/AdvancedOutputExtensions.cs +++ b/PrettyConsole/AdvancedOutputExtensions.cs @@ -8,13 +8,13 @@ public static class AdvancedOutputExtensions { extension(Console) { /// - /// Runs that should involve some form of outputting to the console. Set according to the outputs you use in and configure the appropriate + /// Runs that should involve some form of outputting to the console. Set to the number of lines your output consumes and choose the appropriate . /// /// The output action. - /// The amount of lines to clear. + /// The number of lines to clear. /// The output pipe to use. /// - /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines + /// Remember to clear the used lines after the last call to this method (for example with Console.ClearNextLines). /// public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { var currentLine = Console.GetCurrentLine(); @@ -24,15 +24,15 @@ public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = Out } /// - /// 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 + /// Runs that should involve some form of outputting to the console and uses to prevent closure allocation. Set to the number of lines your output consumes and choose the appropriate . /// /// /// The parameters that needs to use. /// The output action. - /// The amount of lines to clear. + /// The number of lines to clear. /// The output pipe to use. /// - /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines + /// Remember to clear the used lines after the last call to this method (for example with Console.ClearNextLines). /// public static void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) where TState : allows ref struct { var currentLine = Console.GetCurrentLine(); diff --git a/PrettyConsole/ConsoleColorExtensions.cs b/PrettyConsole/ConsoleColorExtensions.cs index 4da91fc..c4762e7 100644 --- a/PrettyConsole/ConsoleColorExtensions.cs +++ b/PrettyConsole/ConsoleColorExtensions.cs @@ -1,7 +1,7 @@ namespace PrettyConsole; /// -/// Provides methods extending ; +/// Provides methods extending . /// public static class ConsoleColorExtensions { /// diff --git a/PrettyConsole/ConsoleContext.cs b/PrettyConsole/ConsoleContext.cs index 3415cc9..1cb81b0 100755 --- a/PrettyConsole/ConsoleContext.cs +++ b/PrettyConsole/ConsoleContext.cs @@ -3,7 +3,7 @@ namespace PrettyConsole; /// -/// The static class the provides the abstraction over and other extensions. +/// The static class that provides the abstraction over and other extensions. /// [UnsupportedOSPlatform("android")] [UnsupportedOSPlatform("browser")] @@ -61,7 +61,7 @@ internal static int GetWidthOrDefault(int defaultWidth = 120) { extension(TextWriter @this) { /// - /// Writes whitespace to this up to length by chucks + /// Writes whitespace to this in chunks up to the requested length. /// /// public void WriteWhiteSpaces(int length) { diff --git a/PrettyConsole/MenuExtensions.cs b/PrettyConsole/MenuExtensions.cs index 0f74388..d4ec0cb 100755 --- a/PrettyConsole/MenuExtensions.cs +++ b/PrettyConsole/MenuExtensions.cs @@ -41,7 +41,7 @@ public static string Selection(TList choices, [InterpolatedStringHandlerA } /// - /// Enumerates a list of strings and allows the user to select multiple strings by any order, and uses the default index color (White) + /// Enumerates a list of strings and allows the user to select multiple strings in any order, using the default index color (White). /// /// Any collection of strings /// title diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 008dbde..e5b135f 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -26,7 +26,7 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 5.3.0 + 5.4.0 enable MIT True @@ -43,10 +43,12 @@ - - Exposed more of `PrettyConsoleInterpolatedStringHandler`, including a ctor that takes only `OutputPipe` with optional `IFormatProvider`, public helpers, and `AppendInline`; handlers are now passed by `ref`. + - **BREAKING**: `IndeterminateProgressBar` was renamed to `Spinner`; `AnimationSequence` was renamed to `Pattern`; all `RunAsync` overloads now default the `CancellationToken`; the line reset happens at the start of each frame so the last render stays visible unless cleared. + - **BREAKING**: `ProgressBar.WriteProgressBar` was renamed to `Render`; added `PrettyConsoleInterpolatedStringHandlerFactory` overloads for dynamic headers and switched header parameters to `string` to avoid incorrect escape analysis. + - Exposed more of `PrettyConsoleInterpolatedStringHandler`, including a ctor that takes only `OutputPipe` with optional `IFormatProvider`, public helpers, and `AppendInline`; handlers are passed by `ref`. - Added `Console.SkipLines(int)` to advance the cursor while preserving overwritten UIs such as progress bars and spinners. - Reordered `Confirm(ReadOnlySpan<string> trueValues, ref PrettyConsoleInterpolatedStringHandler handler, bool emptyIsTrue = true)` so `emptyIsTrue` is the last parameter. - - `IndeterminateProgressBar` header factories now use `PrettyConsoleInterpolatedStringHandlerFactory` with the `PrettyConsoleInterpolatedStringHandlerBuilder` singleton to avoid extra struct copies; the old `Build` helper was removed. + - Spinner header factories use `PrettyConsoleInterpolatedStringHandlerFactory` with the `PrettyConsoleInterpolatedStringHandlerBuilder` singleton to avoid extra struct copies; the old `Build` helper was removed. - `AnsiColors` is now public for converting `ConsoleColor` to ANSI sequences. diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs index 19e9fe2..cd59cb4 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -490,7 +490,7 @@ private void Grow(int targetCapacity) { private readonly void ThrowIfFlushed() { if (!_flushed) return; - throw new InvalidOperationException("The handler was consumed and its buffer have been freed."); + throw new InvalidOperationException("The handler was consumed and its buffer has been freed."); } /// @@ -502,7 +502,7 @@ public void ResetColors() { } /// - /// Clears the internal buffer and returns it to the underlying array pool without writing to the to the held . + /// Clears the internal buffer and returns it to the underlying array pool without writing to the held . /// /// This overload does not reset colors. public void FlushWithoutWrite() { @@ -514,7 +514,7 @@ public void FlushWithoutWrite() { } /// - /// Writes the underline buffer to the held and clears and returns the underlying buffer to the underlying array pool. + /// Writes the underlying buffer to the held , then clears it and returns it to the underlying array pool. /// /// Whether to reset colors before flushing public void Flush(bool resetColors = true) { diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs index 4483809..fc51a5f 100644 --- a/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandlerBuilder.cs @@ -2,13 +2,13 @@ namespace PrettyConsole; #pragma warning disable CA1822 // Mark members as static /// -/// Provides an api to build a string handler. +/// Provides an API to build a string handler. /// public sealed class PrettyConsoleInterpolatedStringHandlerBuilder { /// /// A singleton instance of . /// - /// This instance is stateless and thread-safe + /// This instance is stateless and thread-safe. public static readonly PrettyConsoleInterpolatedStringHandlerBuilder Singleton = new(); /// diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 4ec3ffa..af20bb0 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -46,7 +46,7 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); + public void Update(int percentage) => Update(percentage, string.Empty); /// /// Updates the progress bar with the specified percentage. @@ -55,7 +55,17 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public void Update(double percentage) => Update((int)percentage, ReadOnlySpan.Empty); + public void Update(double percentage) => Update((int)percentage, string.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. + /// 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. + /// Remember to clear the used lines after the last call (e.g., with Console.ClearNextLines). + public void Update(double percentage, string status, bool sameLine = true) + => Update((int)percentage, status, sameLine); /// /// Updates the progress bar with the specified percentage and header text. @@ -66,34 +76,55 @@ public class ProgressBar { /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public void Update(double percentage, ReadOnlySpan status, bool sameLine = true) - => Update((int)percentage, status, sameLine); + public void Update(int percentage, string status, bool sameLine = true) { + if (status.Length == 0) Update(percentage, null, sameLine); + else Update(percentage, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"{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. + /// Optional header factory invoked on each render; use it to emit dynamic status text with (same pattern as ). /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. /// /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. /// - public void Update(int percentage, ReadOnlySpan status, bool sameLine = true) { + public void Update(double percentage, PrettyConsoleInterpolatedStringHandlerFactory? factory = null, bool sameLine = true) + => Update((int)percentage, factory, sameLine); + + /// + /// Updates the progress bar with the specified percentage and header text. + /// + /// The percentage value (0-100) representing the progress. + /// Optional header factory invoked on each render; use it to emit dynamic status text with (same pattern as ). + /// Whether to display the status before the progress bar on the same line. If not it will be displayed above the progress bar, if set to false, the progress bar will use 2 lines. + /// + /// Please remember to clear the used lines after the last call to this method, you can use Console.ClearNextLines. + /// + [OverloadResolutionPriority(int.MaxValue)] + public void Update(int percentage, PrettyConsoleInterpolatedStringHandlerFactory? factory = null, bool sameLine = true) { lock (_lock) { var currentLine = Console.GetCurrentLine(); if (sameLine) { Console.ClearNextLines(1); - if (status.Length > 0) { - Console.Write(status, OutputPipe.Error, ForegroundColor); + if (factory is not null) { + factory(PrettyConsoleInterpolatedStringHandlerBuilder.Singleton, out var handler); + handler.Flush(); ConsoleContext.GetPipeTarget(OutputPipe.Error).WriteWhiteSpaces(1); - WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); + Render(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); } } else { - bool hasStatus = status.Length > 0; - int lines = hasStatus ? 2 : 1; - Console.ClearNextLines(lines); - if (hasStatus) Console.WriteLine(status, OutputPipe.Error, ForegroundColor); - WriteProgressBar(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); + if (factory is not null) { + Console.ClearNextLines(2); + factory(PrettyConsoleInterpolatedStringHandlerBuilder.Singleton, out var handler); + handler.AppendNewLine(); + handler.Flush(); + Render(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); + } else { + Console.ClearNextLines(1); + Render(OutputPipe.Error, percentage, ProgressColor, ProgressChar, MaxLineWidth); + } } Console.GoToLine(currentLine); } @@ -107,8 +138,8 @@ 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. /// Optional total line length (including brackets and percentage). When provided, the rendered output will not exceed this width unless the decorations already require more characters. - public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) - => WriteProgressBar(pipe, (int)percentage, progressColor, progressChar, maxLineWidth); + public static void Render(OutputPipe pipe, double percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) + => Render(pipe, (int)percentage, progressColor, progressChar, maxLineWidth); /// /// Writes a single progress bar segment without tracking state. @@ -118,7 +149,7 @@ public static void WriteProgressBar(OutputPipe pipe, double percentage, ConsoleC /// The color used for the filled segment of the bar. /// The character used to render the filled portion of the bar. /// Optional total line length (including brackets and percentage). When provided, the rendered output will not exceed this width unless the decorations already require more characters. - public static void WriteProgressBar(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) { + public static void Render(OutputPipe pipe, int percentage, ConsoleColor progressColor, char progressChar = DefaultProgressChar, int? maxLineWidth = null) { Console.ResetColor(); int p = Math.Clamp(percentage, 0, 100); diff --git a/PrettyConsole/RenderingExtensions.cs b/PrettyConsole/RenderingExtensions.cs index ec45b4c..1decd84 100755 --- a/PrettyConsole/RenderingExtensions.cs +++ b/PrettyConsole/RenderingExtensions.cs @@ -33,10 +33,10 @@ public static void WriteWhiteSpaces(int length, OutputPipe pipe = OutputPipe.Out /// /// Clears the next . /// - /// Amount of lines to clear + /// Number of lines to clear. /// The output pipe to use /// - /// Useful for clearing output of overriding functions, like the ProgressBar + /// Useful for clearing output from overwriting functions like the progress bar. /// public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { var textWriter = ConsoleContext.GetPipeTarget(pipe); @@ -52,7 +52,7 @@ public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) /// /// Moves the cursor forward by . /// - /// Amount of lines to skip + /// Number of lines to skip. /// /// Useful for keeping overwritten lines, like progress bar or dashboards after the task is done. /// diff --git a/PrettyConsole/IndeterminateProgressBar.cs b/PrettyConsole/Spinner.cs similarity index 79% rename from PrettyConsole/IndeterminateProgressBar.cs rename to PrettyConsole/Spinner.cs index a24a78e..36518ce 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/Spinner.cs @@ -4,43 +4,41 @@ namespace PrettyConsole; /// -/// Represents an indeterminate progress bar that visually indicates the progress of a time-consuming task. +/// Represents a spinner that visually indicates the progress of a time-consuming task. /// /// /// -/// After the time-consuming task is completed, the progress bar is removed from the console. and the next output will take its place. +/// After the time-consuming task is completed, the spinner output is not cleared, use or to handle the final output. /// /// -/// The cancellation token parameter on the RunAsync methods is to cancel the progress bar (not necessarily the task) and end it any time. +/// The cancellation token parameter on the RunAsync methods cancels the spinner itself (not necessarily the task) and ends it at any time. /// /// -public class IndeterminateProgressBar { +public class Spinner { /// - /// Contains the characters that will be iterated through while running + /// Contains the characters that will be iterated through while running. /// - /// - /// You can also choose from some defaults in - /// - public ReadOnlyCollection AnimationSequence { get; init; } = Patterns.Twirl; + /// Choose from the defaults in + public ReadOnlyCollection Pattern { get; init; } = Patterns.Twirl; /// - /// Gets or sets the foreground color of the progress bar. + /// Gets or sets the foreground color of the spinner. /// public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.DefaultForeground; /// - /// Gets or sets a value indicating whether to display the elapsed time in the progress bar. + /// Gets or sets a value indicating whether to display the elapsed time next to the spinner. /// public bool DisplayElapsedTime { get; init; } = true; /// - /// Gets or sets the update rate (in ms) of the indeterminate progress bar. + /// Gets or sets the update rate (in ms) of the spinner frames. /// /// Default = 200 public int UpdateRate { get; init; } = 200; /// - /// Runs the indeterminate progress bar while the specified task is running. + /// Runs the spinner while the specified task is running. /// /// /// @@ -52,19 +50,19 @@ public async Task RunAsync(Task task, CancellationToken token = default } /// - /// Runs the indeterminate progress bar while the specified task is running. + /// Runs the spinner while the specified task is running. /// /// /// /// - public async Task RunAsync(Task task, string header, CancellationToken token) { + public async Task RunAsync(Task task, string header, CancellationToken token = default) { await RunAsyncNonGeneric(task, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"{header}"), token); return task.IsCompleted ? task.Result : await task; } /// - /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. + /// Runs the spinner while the specified task is running, using a dynamic header factory. /// /// /// Factory invoked every frame to render a header with . @@ -77,7 +75,7 @@ public async Task RunAsync(Task task, PrettyConsoleInterpolatedStringHa } /// - /// Runs the indeterminate progress bar while the specified task is running. + /// Runs the spinner while the specified task is running. /// /// /// @@ -85,25 +83,25 @@ public async Task RunAsync(Task task, PrettyConsoleInterpolatedStringHa public Task RunAsync(Task task, CancellationToken token = default) => RunAsyncNonGeneric(task, null, token); /// - /// Runs the indeterminate progress bar while the specified task is running. + /// Runs the spinner while the specified task is running. /// /// /// /// - public Task RunAsync(Task task, string header, CancellationToken token) { + public Task RunAsync(Task task, string header, CancellationToken token = default) { return RunAsyncNonGeneric(task, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"{header}"), token); } /// - /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. + /// Runs the spinner while the specified task is running, using a dynamic header factory. /// /// /// Factory invoked every frame to render a header with . /// - public Task RunAsync(Task task, PrettyConsoleInterpolatedStringHandlerFactory? headerFactory, CancellationToken token) => RunAsyncNonGeneric(task, headerFactory, token); + public Task RunAsync(Task task, PrettyConsoleInterpolatedStringHandlerFactory? headerFactory, CancellationToken token = default) => RunAsyncNonGeneric(task, headerFactory, token); /// - /// Runs the indeterminate progress bar while the specified task is running, using a dynamic header factory. + /// Runs the spinner while the specified task is running, using a dynamic header factory. /// /// /// Factory invoked every frame to render a header with . @@ -131,7 +129,9 @@ private async Task RunAsyncNonGeneric(Task task, PrettyConsoleInterpolatedString CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); while (!task.IsCompleted && !token.IsCancellationRequested) { - Console.WriteInterpolated(OutputPipe.Error, $"{ForegroundColor}{AnimationSequence[seqIndex]}{ConsoleColor.DefaultForeground}"); + Console.ClearNextLines(1, OutputPipe.Error); // Clear at start to prevent auto-delete after last write + + Console.WriteInterpolated(OutputPipe.Error, $"{ForegroundColor}{Pattern[seqIndex]}{ConsoleColor.DefaultForeground}"); if (headerFactory is not null) { ConsoleContext.Error.WriteWhiteSpaces(1); @@ -171,23 +171,20 @@ private async Task RunAsyncNonGeneric(Task task, PrettyConsoleInterpolatedString } } - // Always clear once per frame - Console.ClearNextLines(1); - if (token.IsCancellationRequested || task.IsCompleted) { break; } // Advance animation sequence index without allocations seqIndex++; - if (seqIndex == AnimationSequence.Count) { + if (seqIndex == Pattern.Count) { seqIndex = 0; } } } /// - /// Provides constant animation sequences that can be used for + /// Provides constant animation sequences that can be used for /// public static class Patterns { /// @@ -226,13 +223,13 @@ public static readonly ReadOnlyCollection LoadingBar public static readonly ReadOnlyCollection PingPong = new([ "|• |", - "| • |", - "| • |", - "| • |", - "| •|", - "| • |", - "| • |", - "| • |", + "| • |", + "| • |", + "| • |", + "| •|", + "| • |", + "| • |", + "| • |", ]); } } \ No newline at end of file diff --git a/PrettyConsole/WhiteSpace.cs b/PrettyConsole/WhiteSpace.cs index 0314681..d1ed654 100644 --- a/PrettyConsole/WhiteSpace.cs +++ b/PrettyConsole/WhiteSpace.cs @@ -1,7 +1,7 @@ namespace PrettyConsole; /// -/// Declares a of whitespaces +/// Declares a count of whitespace characters to emit. /// /// public readonly record struct WhiteSpace(int Length); \ No newline at end of file diff --git a/PrettyConsole/WriteExtensions.cs b/PrettyConsole/WriteExtensions.cs index e3293d2..29e6d9f 100755 --- a/PrettyConsole/WriteExtensions.cs +++ b/PrettyConsole/WriteExtensions.cs @@ -35,7 +35,7 @@ public static int WriteInterpolated(OutputPipe pipe, [InterpolatedStringHandlerA /// The output pipe to use /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void Write(T item, OutputPipe pipe = OutputPipe.Out) where T : ISpanFormattable, allows ref struct { @@ -50,7 +50,7 @@ public static void Write(T item, OutputPipe pipe = OutputPipe.Out) /// foreground color /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) where T : ISpanFormattable, allows ref struct { @@ -66,7 +66,7 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) /// background color /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) where T : ISpanFormattable, allows ref struct { @@ -84,7 +84,7 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, Co /// format provider /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) diff --git a/PrettyConsole/WriteLineExtensions.cs b/PrettyConsole/WriteLineExtensions.cs index 9e90890..db491b2 100755 --- a/PrettyConsole/WriteLineExtensions.cs +++ b/PrettyConsole/WriteLineExtensions.cs @@ -36,7 +36,7 @@ public static int WriteLineInterpolated(OutputPipe pipe, [InterpolatedStringHand /// The output pipe to use /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) where T : ISpanFormattable, allows ref struct { @@ -51,7 +51,7 @@ public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) /// foreground color /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground) where T : ISpanFormattable, allows ref struct { @@ -67,7 +67,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// background color /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) where T : ISpanFormattable, allows ref struct { @@ -85,7 +85,7 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// format provider /// /// - /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 4096. For best performance, it is recommended that formats to fewer characters than this initial size. /// public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) diff --git a/README.md b/README.md index 14bef75..44a5909 100755 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ PrettyConsole is **the go-to choice for ultra-low-latency, allocation-free conso dotnet add package PrettyConsole ``` +## Examples + +Standalone samples made with .NET 10 file-based apps with preview clips are available in [Examples](Examples/README.md). + ## Usage ### Bring PrettyConsole APIs into scope @@ -235,22 +239,22 @@ for (int i = 0; i <= 100; i += 5) { progress.Update(42.5, "Syncing", sameLine: false); // One-off render without state -ProgressBar.WriteProgressBar(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', maxLineWidth: 32); +ProgressBar.Render(OutputPipe.Error, 75, ConsoleColor.Magenta, '*', maxLineWidth: 32); ``` -`ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. You can also set `ProgressBar.MaxLineWidth` on the instance to limit the rendered `[=====] 42%` line width before each update, mirroring the `maxLineWidth` option on `ProgressBar.WriteProgressBar`. The helper `ProgressBar.WriteProgressBar` keeps the cursor on the same line, which is ideal inside `Console.Overwrite`, and accepts an optional `maxLineWidth` so the entire `[=====] 42%` line can be constrained for left-column layouts. +`ProgressBar.Update` always re-renders (even if the percentage didn't change) so you can refresh status text. You can also set `ProgressBar.MaxLineWidth` on the instance to limit the rendered `[=====] 42%` line width before each update, mirroring the `maxLineWidth` option on `ProgressBar.Render`. The helper `ProgressBar.Render` keeps the cursor on the same line, which is ideal inside `Console.Overwrite`, and accepts an optional `maxLineWidth` so the entire `[=====] 42%` line can be constrained for left-column layouts. For dynamic headers, use the overload that accepts a `PrettyConsoleInterpolatedStringHandlerFactory`, mirroring the spinner pattern. -#### Indeterminate progress +#### Spinner (indeterminate progress) -`IndeterminateProgressBar` renders animated frames on the error pipe. `PrettyConsoleInterpolatedStringHandlerFactory` overloads all use of a lambda to create a `PrettyConsoleInterpolatedStringHandler` with a builder for a header that will be created when the spinner is re-rendered.: +`Spinner` renders animated frames on the error pipe. `PrettyConsoleInterpolatedStringHandlerFactory` overloads take a lambda that creates a `PrettyConsoleInterpolatedStringHandler` via the builder for per-frame headers: ```csharp -var spinner = new IndeterminateProgressBar(); +var spinner = new Spinner(); await spinner.RunAsync(workTask, (builder, out handler) => handler = builder.Build(OutputPipe.Error, $"Syncing {DateTime.Now:T}")); ``` -The factory runs each frame, letting you inject dynamic status text without allocations while avoiding extra struct copies. +The factory runs each frame so you can inject dynamic status text without allocations while avoiding extra struct copies. #### Multiple progress bars with tasks + channels @@ -280,7 +284,7 @@ var consumer = Task.Run(async () => { Console.Overwrite(progress, state => { for (int i = 0; i < state.Length; i++) { Console.WriteInterpolated(OutputPipe.Error, $"Task {i + 1} ({downloads[i]}): "); - ProgressBar.WriteProgressBar(OutputPipe.Error, state[i], ConsoleColor.Cyan); + ProgressBar.Render(OutputPipe.Error, state[i], ConsoleColor.Cyan); } }, lines: downloads.Length, pipe: OutputPipe.Error); } diff --git a/Versions.md b/Versions.md index 951e3f7..8f9b754 100755 --- a/Versions.md +++ b/Versions.md @@ -1,5 +1,16 @@ # Versions +## v5.4.0 + +- **BREAKING**: `IndeterminateProgressBar` was renamed to `Spinner` + - Internal line reset is now triggered at the start of each frame render, this keeps the output after the last render. You can choose whether to override or keep the output by using `Console.ClearNextLines` or `Console.SkipLines`. + - All overloads of `RunAsync` now have a default value for the `CancellationToken`. + - `AnimationSequence` was renamed to `Pattern` to fit `Spinner.Patterns`. +- Changes in `ProgressBar` + - **BREAKING**: the static `WriteProgressBar` methods have been renamed to `Render`. + - Added overloads that accept a `PrettyConsoleInterpolatedStringHandlerFactory` like the overloads in `Spinner` to allow more complex outputs at lower costs. + - **BREAKING**: `ReadOnlySpan` parameters of header, now use `string` instead due to language limitation that caused incorrect escape analysis with the `PrettyConsoleInterpolatedStringHandlerFactory` + ## v5.3.0 - `PrettyConsoleInterpolatedStringHandler`: