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