diff --git a/.editorconfig b/.editorconfig index 1b7d88b..30024f0 100755 --- a/.editorconfig +++ b/.editorconfig @@ -77,12 +77,17 @@ dotnet_remove_unnecessary_suppression_exclusions = none #### C# Coding Conventions #### [*.cs] # analyzers -dotnet_diagnostic.IDE0290.severity = none # use primary constuctor +dotnet_diagnostic.IDE0290.severity = none # use primary constructor dotnet_diagnostic.IDE0028.severity = none # use collection expression dotnet_diagnostic.IDE0056.severity = none # simplify index operator dotnet_diagnostic.IDE0057.severity = none # use range operator +dotnet_diagnostic.IDE0301.severity = none # simplify collection initialization +dotnet_diagnostic.IDE0053.severity = none # expression body lambda +dotnet_diagnostic.IDE0046.severity = none # simplify if(s) - conditional operator +dotnet_diagnostic.IDE0305.severity = none # [, ...] instead of .ToArray() -# namespace decleration + +# namespace declaration csharp_style_namespace_declarations = file_scoped:warning # var preferences diff --git a/.gitattributes b/.gitattributes deleted file mode 100755 index 1ff0c42..0000000 --- a/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/.github/workflows/UnitTests.yaml b/.github/workflows/UnitTests.yaml index 9cb4634..0ff99eb 100755 --- a/.github/workflows/UnitTests.yaml +++ b/.github/workflows/UnitTests.yaml @@ -2,56 +2,24 @@ name: Unit Tests on: pull_request: - workflow_dispatch: # Allows the workflow to be triggered manually + workflow_dispatch: jobs: - - run-tests: - - runs-on: ${{ matrix.os }} + unit-tests-matrix: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - configuration: [Debug, Release] - - env: - # Define the path to project and test project - PROJECT_PATH: PrettyConsole/PrettyConsole.csproj - TEST_PROJECT_PATH: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj - - steps: - # 1. Checkout the repository code - - name: Checkout Repository - uses: actions/checkout@v4 - - # 2. Cache NuGet packages - - name: Cache NuGet Packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-nuget- - - # 3. Setup .NET - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - # 4. Clean - - name: Clean - run: | - dotnet clean ${{ env.PROJECT_PATH }} -c ${{ matrix.configuration }} - dotnet clean ${{ env.TEST_PROJECT_PATH }} -c ${{ matrix.configuration }} - - # 5. Restore dependencies - - name: Restore Dependencies - run: | - dotnet restore ${{ env.PROJECT_PATH }} - dotnet restore ${{ env.TEST_PROJECT_PATH }} - - # 6. Run Unit Tests - - name: Run Unit Tests - run: dotnet test ${{ env.TEST_PROJECT_PATH }} -c ${{ matrix.configuration }} \ No newline at end of file + platform: [ubuntu-latest, windows-latest, macos-latest] + uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main + with: + platform: ${{ matrix.platform }} + dotnet-version: 9.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 + 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 1253c88..62d8b6d 100755 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ *.userosscache *.sln.docstates PrettyConsole.Tests.Integration/ +WARP.md +# AGENTS.md # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d14e328 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,80 @@ +# AGENTS.md + +Repository: PrettyConsole + +Summary + +- PrettyConsole is a high-performance, allocation-conscious wrapper over System.Console that provides structured colored output, input helpers, rendering controls, menus, and progress bars. It targets net9.0, 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 + +Commands you’ll use often + +- Build + - Build library: + - dotnet build PrettyConsole/PrettyConsole.csproj + - Build unit tests: + - dotnet build PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj + - The solution using .slnx format; run it as usual but prefer to build individual projects as needed. +- Format (uses the repo’s .editorconfig conventions) + - Check and fix code style/formatting: + - dotnet format +- Run + - Never run interactive/demo tests (PrettyConsole.Tests) + - Run unit tests (xUnit v3 via Microsoft Testing Platform): + - dotnet run --project PrettyConsole.Tests.Unit + - Run a single unit test: + - dotnet run --project PrettyConsole.Tests.Unit --filter-method "*UniquePartOfMethodName*" + - Examples: + - dotnet run --project PrettyConsole.Tests.Unit --filter-method "*WritesColoredLine*" +- Pack - DO NOT DO THIS YOURSELF! + +Repo-specific agent rules and conventions + +- Prefer dotnet CLI for making, verifying, and running changes. +- When changing a specific project, build/run just that project to validate, not the entire solution. +- For tests using Microsoft Testing Platform and/or xUnit v3, use dotnet run, never dotnet test. +- Adhere to .editorconfig in the repo for style and analyzers. +- If code needs to be “removed” as part of a change, do not delete files; comment out their contents so they won’t compile. +- Avoid reflection/dynamic assembly loading in published library code unless explicitly requested. + +High-level architecture and key concepts + +- Console facade + - `PrettyConsole.Console` is a static, partial wrapper over `System.Console`. It exposes the live `In`, `Out`, and `Error` streams, and adds helpers like `NewLine`, `Clear`, `ClearNextLines`, `GetCurrentLine`, `GoToLine`, `SetColors`, and `ResetColors` for structured rendering. +- Output routing + - `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `Console.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly. +- Interpolated string handler + - `PrettyConsoleInterpolatedStringHandler` enables zero-allocation `$"..."` calls for `Write`, `WriteLine`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors automatically reset after each invocation, and handlers respect the selected pipe and optional `IFormatProvider`. +- Coloring model + - `ColoredOutput` and the `Color` record provide terse composition via `"Text" * Color.Red / Color.Blue` and implicit conversions. Default foreground/background values are stored on `Color` so spans render without string allocations. +- Write APIs + - `Write`/`WriteLine` cover interpolated strings, `ColoredOutput` spans, raw `ReadOnlySpan`, and generic `ISpanFormattable` values (including `ref struct`s) with optional foreground/background colors and format providers. Internally they rely on `BufferPool` to avoid allocation spikes. +- Inputs + - `ReadLine`/`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. +- 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. +- Packaging and targets + - `PrettyConsole.csproj` targets net9.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project. + +Testing structure and workflows + +- PrettyConsole.Tests (interactive) + - `Program.cs` allows to test things that need to be verified visually and can't be tested easily or at all using unit tests. It contains tests for various things like menues, tables, progress bar, etc... and at occations new overloads and other things. It's content doesn't need to be tracked, it is more like a playground. +- PrettyConsole.Tests.Unit (xUnit v3) + - Uses Microsoft.NET.Test.Sdk with the Microsoft Testing Platform runner; xunit.runner.json is included. Execute with dotnet run as shown above; pass filters after to narrow to a class or method. + +Notes and gotchas + +- The library aims to minimize allocations; prefer span-based overloads (ReadOnlySpan, ReadOnlySpan) for best performance when contributing. +- When authoring new features, pick the appropriate OutputPipe to keep CLI piping behavior intact. +- On macOS terminals, ANSI is supported; Windows legacy terminals are handled via ANSI-compatible rendering in the library. diff --git a/PrettyConsole.Tests.Unit/AdvancedInputs.cs b/PrettyConsole.Tests.Unit/AdvancedInputs.cs index 074fdf3..7b5d6ac 100755 --- a/PrettyConsole.Tests.Unit/AdvancedInputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedInputs.cs @@ -7,8 +7,8 @@ public void Confirm_Case_Y() { var reader = Utilities.GetReader("y"); In = reader; var res = Confirm(["Enter y" * Color.White]); - stringWriter.ToString().Should().Contain("Enter y"); - res.Should().BeTrue(); + Assert.Contains("Enter y", stringWriter.ToString()); + Assert.True(res); } [Fact] @@ -17,8 +17,8 @@ public void Confirm_Case_Yes() { var reader = Utilities.GetReader("yes"); In = reader; var res = Confirm(["Enter yes" * Color.White]); - stringWriter.ToString().Should().Contain("Enter yes"); - res.Should().BeTrue(); + Assert.Contains("Enter yes", stringWriter.ToString()); + Assert.True(res); } [Fact] @@ -27,8 +27,8 @@ public void Confirm_Case_Empty() { var reader = Utilities.GetReader(""); In = reader; var res = Confirm(["Enter yes" * Color.White]); - stringWriter.ToString().Should().Contain("Enter yes"); - res.Should().BeTrue(); + Assert.Contains("Enter yes", stringWriter.ToString()); + Assert.True(res); } [Fact] @@ -37,7 +37,59 @@ public void Confirm_Case_No() { var reader = Utilities.GetReader("no"); In = reader; var res = Confirm(["Enter no" * Color.White]); - stringWriter.ToString().Should().Contain("Enter no"); - res.Should().BeFalse(); + Assert.Contains("Enter no", stringWriter.ToString()); + Assert.False(res); + } + + [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/AdvancedOutputs.cs b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs index a85c0e4..4dac2ec 100755 --- a/PrettyConsole.Tests.Unit/AdvancedOutputs.cs +++ b/PrettyConsole.Tests.Unit/AdvancedOutputs.cs @@ -1,17 +1,57 @@ 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); - stringWriter.ToString().Should().Contain("Hello world!"); + Assert.Contains("Hello world!", stringWriter.ToString()); } [Fact] public async Task TypeWriteLine_Regular() { Out = Utilities.GetWriter(out var stringWriter); await TypeWriteLine("Hello world!" * Color.Green, 10); - stringWriter.ToString().Should().Contain("Hello world!" + Environment.NewLine); + Assert.Contains("Hello world!" + Environment.NewLine, stringWriter.ToString()); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/AssemblyInfo.cs b/PrettyConsole.Tests.Unit/AssemblyInfo.cs deleted file mode 100755 index b0b47aa..0000000 --- a/PrettyConsole.Tests.Unit/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ColorTests.cs b/PrettyConsole.Tests.Unit/ColorTests.cs new file mode 100644 index 0000000..ed7d15b --- /dev/null +++ b/PrettyConsole.Tests.Unit/ColorTests.cs @@ -0,0 +1,119 @@ +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/GlobalUsings.cs b/PrettyConsole.Tests.Unit/GlobalUsings.cs index cee7dfb..fa85189 100755 --- a/PrettyConsole.Tests.Unit/GlobalUsings.cs +++ b/PrettyConsole.Tests.Unit/GlobalUsings.cs @@ -1,4 +1,5 @@ -global using static PrettyConsole.Console; global using PrettyConsole; + global using Xunit; -global using FluentAssertions; + +global using static PrettyConsole.Console; \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/MenusTests.cs b/PrettyConsole.Tests.Unit/MenusTests.cs new file mode 100644 index 0000000..ba08c40 --- /dev/null +++ b/PrettyConsole.Tests.Unit/MenusTests.cs @@ -0,0 +1,157 @@ +namespace PrettyConsole.Tests.Unit; + +public class MenusTests { + [Fact] + public void Selection_ReturnsSelectedChoice_WhenInputValid() { + Out = Utilities.GetWriter(out var writer); + In = Utilities.GetReader("2"); + + var choices = new List { "Apple", "Banana", "Cherry" }; + + var result = Selection(["Choose a fruit:"], choices); + + var output = writer.ToStringAndFlush(); + + static string Normalize(string value) => value.Replace("\r\n", "\n"); + + Assert.Equal( + """ + Choose a fruit: + 1) Apple + 2) Banana + 3) Cherry + + Enter your choice: + """.Replace("\r\n", "\n"), + Normalize(output)); + Assert.Equal("Banana", result); + } + + [Fact] + public void Selection_InvalidNumber_ReturnsEmptyString() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("5"); + + var choices = new List { "One", "Two" }; + + var result = Selection(["Pick a number:"], choices); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public void Selection_NonNumericInput_ReturnsEmptyString() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("abc"); + + var choices = new List { "First", "Second" }; + + var result = Selection(["Pick a number:"], choices); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public void MultiSelection_ReturnsSelectedChoices_InOrder() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("3 1"); + + var choices = new List { "Mercury", "Venus", "Earth" }; + + var result = MultiSelection(["Planets:"], choices); + + Assert.Equal(["Earth", "Mercury"], result); + } + + [Fact] + public void MultiSelection_InvalidEntry_ReturnsEmptyArray() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("2 x"); + + var choices = new List { "Alpha", "Beta", "Gamma" }; + + var result = MultiSelection(["Letters:"], choices); + + Assert.Empty(result); + } + + [Fact] + public void MultiSelection_EmptyInput_ReturnsEmptyArray() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader(string.Empty); + + var choices = new List { "Alpha", "Beta" }; + + var result = MultiSelection(["Letters:"], choices); + + Assert.Empty(result); + } + + [Fact] + public void TreeMenu_ValidSelection_ReturnsTuple() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("2 1"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open", "Save" }, + ["Edit"] = new List { "Undo", "Redo" } + }; + + var (option, subOption) = TreeMenu(["Menu:"], menu); + + Assert.Equal("Edit", option); + Assert.Equal("Undo", subOption); + } + + [Fact] + public void TreeMenu_MissingSelectionParts_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("1"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open" } + }; + + Assert.Throws(() => TreeMenu(["Menu:"], menu)); + } + + [Fact] + public void TreeMenu_InvalidIndexes_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + In = Utilities.GetReader("3 1"); + + var menu = new Dictionary> { + ["Files"] = new List { "Open" }, + ["Edit"] = new List { "Undo" } + }; + + Assert.Throws(() => TreeMenu(["Menu:"], menu)); + } + + [Fact] + public void Table_WritesHeaderAndRows() { + Out = Utilities.GetWriter(out var writer); + + var headers = new List { "Name", "Age" }; + var column1 = new List { "Alice", "Bob" }; + var column2 = new List { "30", "25" }; + + Table(headers, [column1, column2]); + + var output = writer.ToString(); + Assert.Contains("Name", output); + Assert.Contains("Age", output); + Assert.Contains("Alice", output); + Assert.Contains("Bob", output); + } + + [Fact] + public void Table_DifferentHeaderAndColumnCounts_ThrowsArgumentException() { + Out = Utilities.GetWriter(out _); + + var headers = new List { "Name", "Age" }; + var column1 = new List { "Alice", "Bob" }; + + Assert.Throws(() => Table(headers, [column1])); + } +} \ 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 26e5df1..79e6075 100755 --- a/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj +++ b/PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj @@ -1,28 +1,26 @@ - net9.0;net8.0 enable enable - false - true - - true + Exe + net9.0 + true + true - - Windows - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -36,4 +34,4 @@ - \ No newline at end of file + diff --git a/PrettyConsole.Tests.Unit/ProgressBarTests.cs b/PrettyConsole.Tests.Unit/ProgressBarTests.cs new file mode 100644 index 0000000..3f1d83b --- /dev/null +++ b/PrettyConsole.Tests.Unit/ProgressBarTests.cs @@ -0,0 +1,58 @@ +namespace PrettyConsole.Tests.Unit; + +public class ProgressBarTests { + [Fact] + public void ProgressBar_Update_WritesStatusAndPercentage() { + Utilities.SkipIfNoInteractiveConsole(); + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar { + ProgressChar = '#', + ForegroundColor = ConsoleColor.White, + ProgressColor = ConsoleColor.Green + }; + + bar.Update(50, "Loading"); + + var output = errorWriter.ToString(); + Assert.Contains("Loading", output); + Assert.Contains("#", output); + Assert.Contains("50", output); + } + + [Fact] + public void ProgressBar_Update_SamePercentage_NoAdditionalOutput() { + Utilities.SkipIfNoInteractiveConsole(); + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new ProgressBar(); + + bar.Update(25); + errorWriter.ToStringAndFlush(); + + bar.Update(25); + + Assert.Equal(string.Empty, errorWriter.ToString()); + } + + [Fact] + public async Task IndeterminateProgressBar_RunAsync_CompletesAndReturnsResult() { + Utilities.SkipIfNoInteractiveConsole(); + Error = Utilities.GetWriter(out var errorWriter); + + var bar = new IndeterminateProgressBar { + AnimationSequence = new(["|", "/"]), + DisplayElapsedTime = false, + UpdateRate = 5 + }; + + var cancellation = TestContext.Current.CancellationToken; + var result = await bar.RunAsync(Task.Run(async () => { + await Task.Delay(20, cancellation); + return 42; + }, cancellation), "Working", cancellation); + + Assert.Equal(42, result); + Assert.NotEqual(string.Empty, errorWriter.ToString()); + } +} \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/ReadLine.cs b/PrettyConsole.Tests.Unit/ReadLine.cs index 40fef46..393752c 100755 --- a/PrettyConsole.Tests.Unit/ReadLine.cs +++ b/PrettyConsole.Tests.Unit/ReadLine.cs @@ -6,7 +6,7 @@ public void ReadLine_String_NoOutput() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello world!"); In = reader; - ReadLine().Should().Be("Hello world!"); + Assert.Equal("Hello world!", ReadLine()); } [Fact] @@ -14,7 +14,7 @@ public void ReadLine_String_WithOutput() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello world!"); In = reader; - ReadLine(["Enter something:"]).Should().Be("Hello world!"); + Assert.Equal("Hello world!", ReadLine(["Enter something:"])); } [Fact] @@ -22,7 +22,7 @@ public void ReadLine_Int() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("5"); In = reader; - ReadLine(["Enter num:"]).Should().Be(5); + Assert.Equal(5, ReadLine(["Enter num:"])); } [Fact] @@ -30,7 +30,7 @@ public void ReadLine_Int_InvalidWithDefault() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello"); In = reader; - ReadLine(["Enter num:"], 5).Should().Be(5); + Assert.Equal(5, ReadLine(["Enter num:"], 5)); } [Fact] @@ -38,8 +38,8 @@ public void TryReadLine_Int_InvalidWithDefault() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("Hello"); In = reader; - TryReadLine(["Enter num:"], 5, out int num).Should().BeFalse(); - num.Should().Be(5); + Assert.False(TryReadLine(["Enter num:"], 5, out int num)); + Assert.Equal(5, num); } [Fact] @@ -47,7 +47,42 @@ public void TryReadLine_Enum_IgnoreCase() { Out = Utilities.GetWriter(out var _); var reader = Utilities.GetReader("bLack"); In = reader; - TryReadLine(["Enter color:"], true, out ConsoleColor color).Should().BeTrue(); - color.Should().Be(ConsoleColor.Black); + Assert.True(TryReadLine(["Enter color:"], true, out ConsoleColor color)); + Assert.Equal(ConsoleColor.Black, color); + } + + [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/Utilities.cs b/PrettyConsole.Tests.Unit/Utilities.cs index 10ed7f0..641116a 100755 --- a/PrettyConsole.Tests.Unit/Utilities.cs +++ b/PrettyConsole.Tests.Unit/Utilities.cs @@ -1,21 +1,40 @@ +using System; using System.Globalization; using System.Text; +using Xunit; + namespace PrettyConsole.Tests.Unit; public static class Utilities { - public static StringReader GetReader(string str) => new(str); + public static StringReader GetReader(string str) => new(str); + + public static TextWriter GetWriter(out StringWriter writer) { + writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); + return writer; + } + + public static string ToStringAndFlush(this StringWriter writer) { + var result = writer.ToString(); + writer.GetStringBuilder().Clear(); + return result; + } + + public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); - public static TextWriter GetWriter(out StringWriter writer) { - writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - return writer; - } + public static void SkipIfNoInteractiveConsole() { + const string reason = "Interactive console APIs are not available in this environment."; - public static string ToStringAndFlush(this StringWriter writer) { - var result = writer.ToString(); - writer.GetStringBuilder().Clear(); - return result; - } + if (System.Console.IsOutputRedirected) { + Assert.Skip(reason); + } - public static string WithNewLine(this string str) => string.Concat(str, Environment.NewLine); + try { + _ = System.Console.CursorTop; + } catch (System.IO.IOException) { + Assert.Skip(reason); + } catch (PlatformNotSupportedException) { + Assert.Skip(reason); + } + } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/UtilityTests.cs b/PrettyConsole.Tests.Unit/UtilityTests.cs new file mode 100644 index 0000000..fe6458d --- /dev/null +++ b/PrettyConsole.Tests.Unit/UtilityTests.cs @@ -0,0 +1,34 @@ +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/Write.cs index aad3ef8..8282cb0 100755 --- a/PrettyConsole.Tests.Unit/Write.cs +++ b/PrettyConsole.Tests.Unit/Write.cs @@ -9,51 +9,151 @@ public Write() { Error = Utilities.GetWriter(out _errorWriter); } + [Fact] + public void Write_Interpolated_WritesFormattedContent_ToOutPipe() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + Write(OutputPipe.Out, $"Hello {42}"); + Assert.Equal("Hello 42", writer.ToString()); + } finally { + Out = originalOut; + } + } + + [Fact] + public void Write_Interpolated_WritesFormattedContent_ToErrorPipe() { + var originalError = Error; + var writer = new StringWriter(); + Error = writer; + + try { + Write(OutputPipe.Error, $"Error {123}"); + Assert.Equal("Error 123", writer.ToString()); + } finally { + Error = originalError; + } + } + + [Fact] + public void Write_Interpolated_IgnoresColorTokensInOutput() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + Write(OutputPipe.Out, + $"Colors {Color.Black / Color.Green}Green{Color.Default} {Color.Red}Red{Color.Default}"); + + Assert.Equal("Colors Green Red", writer.ToString()); + } finally { + Out = originalOut; + } + } + [Fact] public void Write_SpanFormattable_NoColors() { Write(3.14); - _writer.ToStringAndFlush().Should().Be("3.14"); + Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_ForegroundColor() { Write(3.14, OutputPipe.Out, Color.White); - _writer.ToStringAndFlush().Should().Be("3.14"); + Assert.Equal("3.14", _writer.ToStringAndFlush()); } [Fact] public void Write_SpanFormattable_ForegroundAndBackgroundColor() { Write(3.14, OutputPipe.Out, Color.White, Color.Black); - _writer.ToStringAndFlush().Should().Be("3.14"); + Assert.Equal("3.14", _writer.ToStringAndFlush()); + } + + [Fact] + public void Write_SpanFormattable_VeryLongObjectFormat() { + var obj = new LongFormatStud(); + Write(obj); + Assert.Equal(new string('X', LongFormatStud.Length), _writer.ToStringAndFlush()); } [Fact] public void Write_ColoredOutput_Single() { Write("Hello world!" * Color.Green); - _writer.ToStringAndFlush().Should().Be("Hello world!"); + Assert.Equal("Hello world!", _writer.ToStringAndFlush()); } [Fact] public void Write_ColoredOutput_Multiple() { Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"]); - _writer.ToStringAndFlush().Should().Be("Hello David!"); + Assert.Equal("Hello David!", _writer.ToStringAndFlush()); } [Fact] public void WriteError_ColoredOutput_Single() { Write("Hello world!" * Color.Yellow, OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello world!"); + Assert.Equal("Hello world!", _errorWriter.ToStringAndFlush()); } [Fact] public void WriteError_ColoredOutput_Single2() { Write(["Hello world!" * Color.Green], OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello world!"); + Assert.Equal("Hello world!", _errorWriter.ToStringAndFlush()); } [Fact] public void WriteError_ColoredOutput_Multiple() { Write(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello David!"); + Assert.Equal("Hello David!", _errorWriter.ToStringAndFlush()); + } + + [Fact] + public void Write_Interpolated_RightAlignmentPadsWithSpaces() { + Write($"Value {42,5}"); + Assert.Equal("Value 42", _writer.ToStringAndFlush()); + } + + [Fact] + public void Write_Interpolated_LeftAlignmentPadsWithSpaces() { + Write($"Value {42,-5}"); + Assert.Equal("Value 42 ", _writer.ToStringAndFlush()); + } + + [Fact] + public void Write_Interpolated_TimeSpanHumanReadableFormat() { + Write($"Elapsed {TimeSpan.FromSeconds(75):hr}"); + Assert.Equal("Elapsed 01:15m", _writer.ToStringAndFlush()); + } + + [Fact] + public void Write_Interpolated_ColoredOutputSpan_WritesValues() { + ReadOnlySpan outputs = [ + "Hi " * Color.Green, + "There" * Color.Yellow + ]; + + Write($"Span {outputs}"); + + Assert.Equal("Span Hi There", _writer.ToStringAndFlush()); + } + + private readonly ref struct LongFormatStud : ISpanFormattable { + public const int Length = 1024; + + public string ToString(string? format, IFormatProvider? formatProvider) { + return new string('X', Length); + } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) { + if (destination.Length < Length) { + charsWritten = 0; + return false; + } + var slice = destination.Slice(0, Length); + slice.Fill('X'); + charsWritten = Length; + return true; + } } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/WriteLine.cs b/PrettyConsole.Tests.Unit/WriteLine.cs index 3b2a648..7ddb670 100755 --- a/PrettyConsole.Tests.Unit/WriteLine.cs +++ b/PrettyConsole.Tests.Unit/WriteLine.cs @@ -9,27 +9,41 @@ public WriteLine() { Error = Utilities.GetWriter(out _errorWriter); } + [Fact] + public void WriteLine_Interpolated_AppendsNewLine() { + var originalOut = Out; + var writer = new StringWriter(); + Out = writer; + + try { + WriteLine(OutputPipe.Out, $"Line {7}"); + Assert.Equal($"Line 7{writer.NewLine}", writer.ToString()); + } finally { + Out = originalOut; + } + } + [Fact] public void WriteLine_ColoredOutput_Single() { WriteLine("Hello world!" * Color.Green); - _writer.ToStringAndFlush().Should().Be("Hello world!".WithNewLine()); + Assert.Equal("Hello world!".WithNewLine(), _writer.ToStringAndFlush()); } [Fact] public void WriteLine_ColoredOutput_Multiple() { WriteLine(["Hello " * Color.Green, "David" * Color.Yellow, "!"]); - _writer.ToStringAndFlush().Should().Be("Hello David!".WithNewLine()); + Assert.Equal("Hello David!".WithNewLine(), _writer.ToStringAndFlush()); } [Fact] public void WriteLineError_ColoredOutput_Single() { WriteLine("Hello world!" * Color.Green, OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello world!".WithNewLine()); + Assert.Equal("Hello world!".WithNewLine(), _errorWriter.ToStringAndFlush()); } [Fact] public void WriteLineError_ColoredOutput_Multiple() { WriteLine(["Hello " * Color.Green, "David" * Color.Yellow, "!"], OutputPipe.Error); - _errorWriter.ToStringAndFlush().Should().Be("Hello David!".WithNewLine()); + Assert.Equal("Hello David!".WithNewLine(), _errorWriter.ToStringAndFlush()); } } \ No newline at end of file diff --git a/PrettyConsole.Tests.Unit/xunit.runner.json b/PrettyConsole.Tests.Unit/xunit.runner.json new file mode 100644 index 0000000..9acde45 --- /dev/null +++ b/PrettyConsole.Tests.Unit/xunit.runner.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true, + "parallelizeAssembly": false, + "parallelizeTestCollections": false, + "showLiveOutput": true +} \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs index 453c0cb..6017716 100755 --- a/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/IndeterminateProgressBarTest.cs @@ -7,9 +7,11 @@ public sealed class IndeterminateProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new IndeterminateProgressBar { - ForegroundColor = Color.Red, + AnimationSequence = IndeterminateProgressBar.Patterns.Braille, + ForegroundColor = Color.Magenta, + // UpdateRate = 120, DisplayElapsedTime = true }; - await prg.RunAsync(Task.Delay(1_000), "running..."); + await prg.RunAsync(Task.Delay(5_000), "running..."); } } \ No newline at end of file diff --git a/PrettyConsole.Tests/Features/ProgressBarTest.cs b/PrettyConsole.Tests/Features/ProgressBarTest.cs index f84989e..0110227 100755 --- a/PrettyConsole.Tests/Features/ProgressBarTest.cs +++ b/PrettyConsole.Tests/Features/ProgressBarTest.cs @@ -7,15 +7,15 @@ public sealed class ProgressBarTest : IPrettyConsoleTest { public async ValueTask Implementation() { var prg = new ProgressBar { - ProgressColor = Color.Yellow, - ProgressChar = '»' + ProgressColor = Color.Magenta, + // ProgressChar = '🧎‍♂️‍➡️' }; - const int count = 1_000; + const int count = 333; var currentLine = GetCurrentLine(); for (int i = 1; i <= count; i++) { double percentage = 100 * (double)i / count; - prg.Update(percentage); - await Task.Delay(1); + prg.Update(percentage, "TESTING"); + await Task.Delay(15); } ClearNextLines(1, OutputPipe.Error); GoToLine(currentLine); diff --git a/PrettyConsole.Tests/Program.cs b/PrettyConsole.Tests/Program.cs index 8dd4c7c..d5c7798 100755 --- a/PrettyConsole.Tests/Program.cs +++ b/PrettyConsole.Tests/Program.cs @@ -1,5 +1,4 @@ -// See https://aka.ms/new-console-template for more information - +using PrettyConsole; using PrettyConsole.Tests; using PrettyConsole.Tests.Features; @@ -13,16 +12,28 @@ // .ToArray(); var tests = new IPrettyConsoleTest[] { - new ColoredOutputTest(), - new SelectionTest(), - new MultiSelectionTest(), - new TableTest(), - new TreeMenuTest(), - new IndeterminateProgressBarTest(), - new ProgressBarTest() + new ColoredOutputTest(), + new SelectionTest(), + new MultiSelectionTest(), + new TableTest(), + new TreeMenuTest(), + new IndeterminateProgressBarTest(), + new ProgressBarTest() }; foreach (var test in tests) { - await test.Render(); - NewLine(); -} \ No newline at end of file + await test.Render(); + NewLine(); +} + +#pragma warning disable CS8321 // Local function is declared but never used + +static void Measure(string label, Action action) { + long before = GC.GetAllocatedBytesForCurrentThread(); + action(); + long after = GC.GetAllocatedBytesForCurrentThread(); + NewLine(); + WriteLine($"{label} - allocated {after - before} bytes"); + NewLine(); +} +#pragma warning restore CS8321 // Local function is declared but never used \ No newline at end of file diff --git a/PrettyConsole.sln b/PrettyConsole.sln deleted file mode 100755 index e535e98..0000000 --- a/PrettyConsole.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31717.71 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PrettyConsole", "PrettyConsole\PrettyConsole.csproj", "{03ADF094-C7A1-4771-A4F1-4A8498D87B34}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrettyConsole.Tests", "PrettyConsole.Tests\PrettyConsole.Tests.csproj", "{128CE278-869A-4896-8977-1A588FFD6A15}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrettyConsole.Tests.Unit", "PrettyConsole.Tests.Unit\PrettyConsole.Tests.Unit.csproj", "{202848E2-F4F8-4DEE-904D-AF610CF30765}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03ADF094-C7A1-4771-A4F1-4A8498D87B34}.Release|Any CPU.Build.0 = Release|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {128CE278-869A-4896-8977-1A588FFD6A15}.Release|Any CPU.Build.0 = Release|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Debug|Any CPU.Build.0 = Debug|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Release|Any CPU.ActiveCfg = Release|Any CPU - {202848E2-F4F8-4DEE-904D-AF610CF30765}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {457DC559-ED77-4D40-AC98-77E3339516C9} - EndGlobalSection -EndGlobal diff --git a/PrettyConsole.slnx b/PrettyConsole.slnx new file mode 100644 index 0000000..61c5abd --- /dev/null +++ b/PrettyConsole.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/PrettyConsole/AdvancedInputs.cs b/PrettyConsole/AdvancedInputs.cs index 5526efd..92e288d 100755 --- a/PrettyConsole/AdvancedInputs.cs +++ b/PrettyConsole/AdvancedInputs.cs @@ -16,6 +16,15 @@ public static void RequestAnyInput(ReadOnlySpan output) { _ = baseConsole.ReadKey(); } + /// + /// Used to wait for user input + /// + /// Interpolated string handler that streams the content. + public static void RequestAnyInput([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + _ = baseConsole.ReadKey(); + } + /// /// Used to get user confirmation with the default values ["y", "yes"] /// @@ -24,6 +33,7 @@ public static void RequestAnyInput(ReadOnlySpan output) { /// /// Used to get user confirmation with the default values ["y", "yes"] or just pressing enter /// + /// /// /// It does not display a question mark or any other prompt, only the message /// @@ -31,6 +41,17 @@ public static bool Confirm(ReadOnlySpan message) { return Confirm(message, DefaultConfirmValues); } + /// + /// Used to get user confirmation with the default values ["y", "yes"] or just pressing enter + /// + /// Interpolated string handler that streams the content. + /// + /// It does not display a question mark or any other prompt, only the message + /// + public static bool Confirm([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + return Confirm(DefaultConfirmValues, true, handler); + } + /// /// Used to get user confirmation /// @@ -48,7 +69,32 @@ public static bool Confirm(ReadOnlySpan message, ReadOnlySpan + /// Used to get user confirmation + /// + /// a collection of values that indicate positive confirmation + /// if simply pressing enter is considered positive or not + /// Interpolated string handler that streams the content. + /// + /// It does not display a question mark or any other prompt, only the message + /// + public static bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue = true, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + var input = In.ReadLine(); + if (input is null or { Length: 0 }) { + return emptyIsTrue; + } + + foreach (var value in trueValues) { + if (input.Equals(value, StringComparison.OrdinalIgnoreCase)) { return true; } } diff --git a/PrettyConsole/AdvancedOutputs.cs b/PrettyConsole/AdvancedOutputs.cs index 0487b7c..716f567 100755 --- a/PrettyConsole/AdvancedOutputs.cs +++ b/PrettyConsole/AdvancedOutputs.cs @@ -1,5 +1,3 @@ -using System.Runtime.CompilerServices; - namespace PrettyConsole; public static partial class Console { @@ -9,13 +7,43 @@ public static partial class Console { /// /// The output pipe to use [MethodImpl(MethodImplOptions.Synchronized)] - public static void OverrideCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { + public static void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error) { var currentLine = GetCurrentLine(); ClearNextLines(1, pipe); Write(output, pipe); GoToLine(currentLine); } + /// + /// Runs that should involve some form of outputting to the console. Set according to the outputs you use in and configure the appropriate + /// + /// The output action. + /// The amount of lines to clear. + /// The output pipe to use. + [MethodImpl(MethodImplOptions.Synchronized)] + public static void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) { + var currentLine = GetCurrentLine(); + ClearNextLines(lines, pipe); + action(); + GoToLine(currentLine); + } + + /// + /// Runs that should involve some form of outputting to the console and use to prevent closure allocation. Set according to the outputs you use in and configure the appropriate + /// + /// + /// The parameters that needs to use. + /// The output action. + /// The amount of lines to clear. + /// The output pipe to use. + [MethodImpl(MethodImplOptions.Synchronized)] + public static void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) where TState : allows ref struct { + var currentLine = GetCurrentLine(); + ClearNextLines(lines, pipe); + action(state); + GoToLine(currentLine); + } + private const int TypeWriteDefaultDelay = 200; /// diff --git a/PrettyConsole/BufferPool.cs b/PrettyConsole/BufferPool.cs new file mode 100644 index 0000000..7c33ce2 --- /dev/null +++ b/PrettyConsole/BufferPool.cs @@ -0,0 +1,115 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +namespace PrettyConsole; + +internal sealed class BufferPool : IDisposable { + private bool _disposed; + private readonly Channel> _channel; + private readonly Func> _createPolicy; + private readonly Func, bool> _returnPolicy; + private readonly ThreadLocal?> _fastItem; + + internal const int ListStartingSize = 256; + internal const int ListMaxSize = 4096; + + public static readonly BufferPool Shared + = new(() => new(ListStartingSize), + item => { + if (item.Count > ListMaxSize) { + return false; + } + item.Clear(); + return true; + }); + + private BufferPool(Func> createPolicy, Func, bool> returnPolicy) { + _channel = Channel.CreateBounded>(new BoundedChannelOptions(Environment.ProcessorCount * 2) { + SingleWriter = false, + SingleReader = false, + FullMode = BoundedChannelFullMode.DropWrite + }); + _createPolicy = createPolicy ?? throw new ArgumentNullException(nameof(createPolicy)); + _returnPolicy = returnPolicy ?? throw new ArgumentNullException(nameof(returnPolicy)); + _fastItem = new(() => null, trackAllValues: false); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public PooledObjectOwner Rent(out List value) { + ObjectDisposedException.ThrowIf(_disposed, this); + var item = _fastItem.Value; + if (item is not null) { + _fastItem.Value = null; + value = item; + return new(this, value); + } + if (_channel.Reader.TryRead(out item)) { + value = item; + return new(this, value); + } + value = _createPolicy(); + return new(this, value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Return(List item) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!_returnPolicy(item)) { + (item as IDisposable)?.Dispose(); + return; + } + if (_fastItem.Value is null) { + _fastItem.Value = item; + return; + } + if (!_channel.Writer.TryWrite(item)) { + (item as IDisposable)?.Dispose(); + } + } + + 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/CHANGELOG.md b/PrettyConsole/CHANGELOG.md deleted file mode 100755 index 0670b87..0000000 --- a/PrettyConsole/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# Changelog - -## v3.1.0 - -* Updated to support .NET 9.0 -* Updated to use `Sharpify 2.5.0` -* `ProgressBar` was redesigned since it randomly produced artifacts such as printing the header multiple times. - * Now the buffer area of `ProgressBar` is only 1 line, the (now "status") is printed on the same line, after the bar - * Only if the header is empty, the percentage is printed instead. - * An internal lock is now also used to prevent race conditions. -* `ClearNextLines` show now work properly, previously it could actually clear 1 line too many. - -### Breaking changes - -* `OutputPipe` enum was added to unify APIs -* `ClearNextLinesError` was removed, use `ClearNextLines` with `OutputPipe.Error` instead -* `NewLineError` was removed, use `NewLine` with `OutputPipe.Error` instead -* `WriteError` was removed, use `Write` with `OutputPipe.Error` instead -* `WriteLineError` was removed, use `WriteLine` with `OutputPipe.Error` instead -* `OverrideCurrentLine` now has an option to choose the output pipe, use `OutputPipe.Error` by default -* `Write` had the same treatment, now it has an option to choose the output pipe, use `OutputPipe.Out` by default, so the overload of `WriteError` were removed -* `WriteLine` and `WriteLine(ReadOnlySpan)` were introduced thanks to this change diff --git a/PrettyConsole/Color.cs b/PrettyConsole/Color.cs index fcce47f..2d53649 100755 --- a/PrettyConsole/Color.cs +++ b/PrettyConsole/Color.cs @@ -20,6 +20,16 @@ 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. /// @@ -30,6 +40,17 @@ public static implicit operator ConsoleColor(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. /// @@ -40,83 +61,94 @@ public static implicit operator ConsoleColor(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); + /// + /// Gets a object representing the color dark blue. + /// + public static readonly Color DarkBlue = new(ConsoleColor.DarkBlue); + + /// + /// Gets a object representing the color dark green. + /// + public static readonly Color DarkGreen = new(ConsoleColor.DarkGreen); + + /// + /// Gets a object representing the color dark cyan. + /// + public static readonly Color DarkCyan = new(ConsoleColor.DarkCyan); + + /// + /// Gets a object representing the color dark red. + /// + public static readonly Color DarkRed = new(ConsoleColor.DarkRed); + + /// + /// Gets a object representing the color dark magenta. + /// + public static readonly Color DarkMagenta = new(ConsoleColor.DarkMagenta); + + /// + /// Gets a object representing the color dark yellow. + /// + public static readonly Color DarkYellow = new(ConsoleColor.DarkYellow); + + /// + /// Gets a object representing the color gray. + /// + public static readonly Color Gray = new(ConsoleColor.Gray); + + /// + /// Gets a object representing the color dark gray. + /// + public static readonly Color DarkGray = new(ConsoleColor.DarkGray); + + /// + /// Gets a object representing the color blue. + /// + public static readonly Color Blue = new(ConsoleColor.Blue); + + /// + /// Gets a object representing the color green. + /// + public static readonly Color Green = new(ConsoleColor.Green); + + /// + /// Gets a object representing the color cyan. + /// + public static readonly Color Cyan = new(ConsoleColor.Cyan); + + /// + /// Gets a object representing the color red. + /// + public static readonly Color Red = new(ConsoleColor.Red); + + /// + /// Gets a object representing the color magenta. + /// + public static readonly Color Magenta = new(ConsoleColor.Magenta); + + /// + /// Gets a object representing the color yellow. + /// + public static readonly Color Yellow = new(ConsoleColor.Yellow); + + /// + /// Gets a object representing the color white. + /// + public static readonly Color White = new(ConsoleColor.White); } \ No newline at end of file diff --git a/PrettyConsole/ColorDefaults.cs b/PrettyConsole/ColorDefaults.cs index be6b5ff..93133f8 100755 --- a/PrettyConsole/ColorDefaults.cs +++ b/PrettyConsole/ColorDefaults.cs @@ -1,19 +1,24 @@ namespace PrettyConsole; public readonly partial record struct Color { - /// - /// Represents the default color for the shell (changes based on platform) - /// - public static readonly ConsoleColor DefaultForegroundColor; + /// + /// Represents the default color for the shell (changes based on platform) + /// + public static readonly ConsoleColor DefaultForegroundColor; - /// - /// Represents the default color for the shell (changes based on platform) - /// - public static readonly ConsoleColor DefaultBackgroundColor; + /// + /// Represents the default color for the shell (changes based on platform) + /// + public static readonly ConsoleColor DefaultBackgroundColor; - static Color() { - baseConsole.ResetColor(); - DefaultForegroundColor = baseConsole.ForegroundColor; - DefaultBackgroundColor = baseConsole.BackgroundColor; - } + /// + /// 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 index 8660359..43d303d 100755 --- a/PrettyConsole/ColoredOutput.cs +++ b/PrettyConsole/ColoredOutput.cs @@ -10,18 +10,18 @@ namespace PrettyConsole; [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] public readonly record struct ColoredOutput(string Value, ConsoleColor ForegroundColor, ConsoleColor BackgroundColor) { - /// - /// Creates a new instance of with default colors - /// - /// - public ColoredOutput(string value) : this(value, Color.DefaultForegroundColor, Color.DefaultBackgroundColor) { } + /// + /// Creates a new instance of with default colors + /// + /// + public ColoredOutput(string value) : this(value, Color.DefaultForegroundColor, Color.DefaultBackgroundColor) { } - /// - /// Creates a new instance of with default background color - /// - /// - /// - public ColoredOutput(string value, ConsoleColor foregroundColor) : this(value, foregroundColor, Color.DefaultBackgroundColor) { } + /// + /// Creates a new instance of with default background color + /// + /// + /// + public ColoredOutput(string value, ConsoleColor foregroundColor) : this(value, foregroundColor, Color.DefaultBackgroundColor) { } /// /// Implicitly converts a string to a with default colors. diff --git a/PrettyConsole/Console.cs b/PrettyConsole/Console.cs index 495994b..799d075 100755 --- a/PrettyConsole/Console.cs +++ b/PrettyConsole/Console.cs @@ -9,10 +9,4 @@ namespace PrettyConsole; [UnsupportedOSPlatform("browser")] [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] -public static partial class Console { - internal static readonly char[] WhiteSpace = new char[256]; - - static Console() { - WhiteSpace.AsSpan().Fill(' '); - } -} +public static partial class Console; \ No newline at end of file diff --git a/PrettyConsole/ConsolePipes.cs b/PrettyConsole/ConsolePipes.cs index 01f47c6..4f64541 100755 --- a/PrettyConsole/ConsolePipes.cs +++ b/PrettyConsole/ConsolePipes.cs @@ -1,18 +1,43 @@ namespace PrettyConsole; public static partial class Console { - /// - /// The standard input stream. - /// - public static TextWriter Out { get; internal set; } = baseConsole.Out; + /// + /// The standard input stream. + /// + public static TextWriter Out { get; internal set; } = baseConsole.Out; - /// - /// The error output stream. - /// - public static TextWriter Error { get; internal set; } = baseConsole.Error; + /// + /// The error output stream. + /// + public static TextWriter Error { get; internal set; } = baseConsole.Error; - /// - /// The standard input stream. - /// - public static TextReader In { get; internal set; } = baseConsole.In; -} + /// + /// The standard input stream. + /// + public static TextReader In { get; internal set; } = baseConsole.In; + + /// + /// Gets the appropriate based on + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + internal static TextWriter GetWriter(OutputPipe pipe) + => pipe switch { + OutputPipe.Error => Error, + _ => Out + }; + + /// + /// Returns the current console buffer width or if + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + internal static int GetWidthOrDefault(int defaultWidth = 120) { + if (baseConsole.IsOutputRedirected) { + return defaultWidth; + } + return baseConsole.BufferWidth; + } +} \ No newline at end of file diff --git a/PrettyConsole/GlobalSuppressions.cs b/PrettyConsole/GlobalSuppressions.cs new file mode 100644 index 0000000..203ad04 --- /dev/null +++ b/PrettyConsole/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// +using System.Diagnostics.CodeAnalysis; + +// Mainly used for PrettyConsoleInterpolatedStringHandler (will be used at call site by the compiler) +[assembly: SuppressMessage( + "Style", + "IDE0060:Remove unused parameter", + Justification = "Parameters may be intentionally unused for API shape consistency or interface compliance.")] \ No newline at end of file diff --git a/PrettyConsole/GlobalUsings.cs b/PrettyConsole/GlobalUsings.cs index 28e2df2..bbdf4b8 100755 --- a/PrettyConsole/GlobalUsings.cs +++ b/PrettyConsole/GlobalUsings.cs @@ -1 +1,3 @@ +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 fc77ec5..ff6894a 100755 --- a/PrettyConsole/IndeterminateProgressBar.cs +++ b/PrettyConsole/IndeterminateProgressBar.cs @@ -1,5 +1,5 @@ +using System.Collections.ObjectModel; using System.Diagnostics; -using System.Runtime.CompilerServices; namespace PrettyConsole; @@ -16,11 +16,16 @@ public static partial class Console { /// /// public class IndeterminateProgressBar { - // Constant pattern containing the characters needed for the indeterminate progress bar - private const string Twirl = "-\\|/"; + /// + /// Contains the characters that will be iterated through while running + /// + /// + /// You can also choose from some defaults in + /// + public ReadOnlyCollection AnimationSequence { get; set; } = Patterns.Twirl; - // A whitespace the length of 10 spaces - private const string ExtraBuffer = " "; + // A length of whitespace padding to the end + private const int PaddingLength = 10; /// /// Gets or sets the foreground color of the progress bar. @@ -35,15 +40,8 @@ public class IndeterminateProgressBar { /// /// Gets or sets the update rate (in ms) of the indeterminate progress bar. /// - public int UpdateRate { get; set; } = 50; - private readonly char[] _buffer; - - /// - /// Represents an indeterminate progress bar that continuously animates without a specific progress value. - /// - public IndeterminateProgressBar() { - _buffer = new char[20]; - } + /// Default = 200 + public int UpdateRate { get; set; } = 200; /// /// Runs the indeterminate progress bar while the specified task is running. @@ -95,37 +93,78 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d } ResetColors(); - var originalColor = baseConsole.ForegroundColor; - var startTime = Stopwatch.GetTimestamp(); - var lineNum = GetCurrentLine(); + ConsoleColor originalColor = baseConsole.ForegroundColor; + long startTime = Stopwatch.GetTimestamp(); + long updateRateAsTicks = TimeSpan.FromMilliseconds(UpdateRate).Ticks; - while (!task.IsCompleted && !token.IsCancellationRequested) { - // Await until the TaskAwaiter informs of completion - foreach (var c in Twirl) { - if (header.Length > 0) { - Error.Write(header); - Error.Write(' '); - } + // Maintain a stable cadence that accounts for render time + long nextTick = startTime; + int seqIndex = 0; - // Cycle through the characters of twirl + 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 { baseConsole.ForegroundColor = ForegroundColor; - Error.Write(c); + Error.Write(AnimationSequence[seqIndex]); + } finally { baseConsole.ForegroundColor = originalColor; - if (DisplayElapsedTime) { - var elapsed = Stopwatch.GetElapsedTime(startTime); - Error.Write(" [Elapsed: "); - Error.Write(Sharpify.Utils.DateAndTime.FormatTimeSpan(elapsed, _buffer)); - Error.Write(']'); - } + } + + if (header.Length > 0) { + Error.Write(' '); + Error.Write(header); + } - Error.Write(ExtraBuffer); - GoToLine(lineNum); - await Task.Delay(UpdateRate, token); // The update rate - Error.Write(WhiteSpace.AsSpan(0, baseConsole.BufferWidth)); - GoToLine(lineNum); - if (token.IsCancellationRequested) { - return; + if (DisplayElapsedTime) { + var elapsed = Stopwatch.GetElapsedTime(startTime); + Write(OutputPipe.Error, $" [Elapsed: {elapsed:hr}]"); + } + + Error.WriteWhiteSpaces(PaddingLength); + + // Compute sleep to maintain UpdateRate between frame starts + var now = Stopwatch.GetTimestamp(); + nextTick += updateRateAsTicks; + var remaining = nextTick - now; + + 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 + } + } + + // Always clear once per frame + ClearNextLines(1, OutputPipe.Error); + + if (token.IsCancellationRequested || task.IsCompleted) { + break; + } + + // Advance animation sequence index without allocations + seqIndex++; + if (seqIndex == AnimationSequence.Count) { + seqIndex = 0; } } @@ -134,5 +173,55 @@ public async Task RunAsync(Task task, string header, CancellationToken token = d [MethodImpl(MethodImplOptions.NoInlining)] private Task RunAsyncNonGeneric(Task task, string header, CancellationToken token) => RunAsync(task, header, token); + + /// + /// Provides constant animation sequences that can be used for + /// + public static class Patterns { + /// + /// A twirl animation sequence + /// + public 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([ + "|• |", + "| • |", + "| • |", + "| • |", + "| •|", + "| • |", + "| • |", + "| • |", + ]); + } } } \ No newline at end of file diff --git a/PrettyConsole/Menus.cs b/PrettyConsole/Menus.cs index 033a22f..2a3b404 100755 --- a/PrettyConsole/Menus.cs +++ b/PrettyConsole/Menus.cs @@ -1,8 +1,6 @@ using System.Buffers; using System.Runtime.InteropServices; -using Sharpify.Collections; - namespace PrettyConsole; public static partial class Console { @@ -19,15 +17,8 @@ public static string Selection(ReadOnlySpan title, TList c where TList : IList { WriteLine(title); - Span buffer = stackalloc char[baseConsole.BufferWidth]; - for (int i = 0; i < choices.Count; i++) { - var builder = StringBuffer.Create(buffer); - builder.Append(" "); - builder.Append(i + 1); - builder.Append(") "); - builder.Append(choices[i]); - Out.WriteLine(builder.WrittenSpan); + WriteLine($" {i + 1}) {choices[i]}"); } NewLine(); @@ -58,15 +49,8 @@ public static string[] MultiSelection(ReadOnlySpan title, where TList : IList { WriteLine(title); - Span buffer = stackalloc char[baseConsole.BufferWidth]; - for (int i = 0; i < choices.Count; i++) { - var builder = StringBuffer.Create(buffer); - builder.Append(" "); - builder.Append(i + 1); - builder.Append(") "); - builder.Append(choices[i]); - Out.WriteLine(builder.WrittenSpan); + WriteLine($" {i + 1}) {choices[i]}"); } NewLine(); @@ -117,33 +101,32 @@ public static (string option, string subOption) TreeMenu(ReadOnlySpan x.Length) + 10; // Used to make sub-tree prefix spaces uniform - Span buffer = stackalloc char[baseConsole.BufferWidth]; - Span emptySpaces = stackalloc char[maxMainOption]; - emptySpaces.Fill(' '); + using var 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]; - var builder = StringBuffer.Create(buffer); - builder.Append(" "); - builder.Append(i + 1); - builder.Append(") "); - builder.Append(mainEntry); - var remainingLength = maxMainOption - builder.Position; - builder.Append(emptySpaces.Slice(0, remainingLength)); - Out.Write(builder.WrittenSpan); + + 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.Write(emptySpaces); + Out.WriteWhiteSpaces(maxMainOption); } - builder.Reset(); - builder.Append(" "); - builder.Append(j + 1); - builder.Append(") "); - builder.Append(subChoices[j]); - Out.WriteLine(builder.WrittenSpan); + span.TryWrite($" {j + 1}) {subChoices[j]}", out written); + Out.WriteLine(span.Slice(0, written)); } NewLine(); @@ -201,15 +184,14 @@ public static void Table(TList headers, ReadOnlySpan columns) wher var columnsLength = columns.Length; - using var memoryOwner = MemoryPool.Shared.Rent(columnsLength); + List buffer = new(columnsLength); for (int i = 0; i < columnsLength; i++) { - memoryOwner.Memory.Span[i] = headers[i].PadRight(lengths[i]); + buffer.Add(headers[i].PadRight(lengths[i])); } - ReadOnlyMemory slice = memoryOwner.Memory.Slice(0, columnsLength); - var enumerable = MemoryMarshal.ToEnumerable(slice); - var header = string.Join(columnSeparator, enumerable); + var header = string.Join(columnSeparator, buffer); + buffer.Clear(); Span rowSeparation = stackalloc char[header.Length]; rowSeparation.Fill(rowSeparator); @@ -218,12 +200,11 @@ public static void Table(TList headers, ReadOnlySpan columns) wher Out.WriteLine(rowSeparation); for (int row = 0; row < height; row++) { for (int i = 0; i < columnsLength; i++) { - memoryOwner.Memory.Span[i] = columns[i][row].PadRight(lengths[i]); + buffer.Add(columns[i][row].PadRight(lengths[i])); } - slice = memoryOwner.Memory.Slice(0, columnsLength); - enumerable = MemoryMarshal.ToEnumerable(slice); - var line = string.Join(columnSeparator, enumerable); + var line = string.Join(columnSeparator, buffer); + buffer.Clear(); Out.WriteLine(line); } diff --git a/PrettyConsole/OutputPipe.cs b/PrettyConsole/OutputPipe.cs index b96870e..fa695f3 100755 --- a/PrettyConsole/OutputPipe.cs +++ b/PrettyConsole/OutputPipe.cs @@ -4,12 +4,12 @@ namespace PrettyConsole; /// An enum representing the output pipe to use /// public enum OutputPipe { - /// - /// Use the standard output pipe - /// - Out, - /// - /// Use the standard error pipe - /// - Error + /// + /// Use the standard output pipe + /// + Out, + /// + /// Use the standard error pipe + /// + Error } \ No newline at end of file diff --git a/PrettyConsole/PrettyConsole.csproj b/PrettyConsole/PrettyConsole.csproj index 2bfc794..705102b 100755 --- a/PrettyConsole/PrettyConsole.csproj +++ b/PrettyConsole/PrettyConsole.csproj @@ -1,7 +1,20 @@  - - net9.0;net8.0 + net9.0 + latest + + true + true + true + true + latest-recommended + true + true + true + true + true + true + David Shnayder PrettyConsole High performance, feature rich and easy to use wrap over System.Console @@ -11,30 +24,54 @@ https://github.com/dusrdev/PrettyConsole/ https://github.com/dusrdev/PrettyConsole/ git - 3.1.0 + 4.0.0 enable - true MIT True - CHANGELOG.md - true + README.md - - - + + + + - Dropped Sharpify as a dependency - PrettyConsole is now self-sufficient. + - Many overloads were added that support the new PrettyConsoleInterpolatedStringHandler, + enabling zero allocation formatted outputs. + - Fixed issue that could sometimes cause writing into buffers beyond their bounds - + throwing an exception. + - IndeterminateProgressBar will now allow customization of the animated sequence via + the property AnimationSequence, and it also includes an inner class Patterns that + contains some constant sequences that could be used with it. + - IndeterminateProgressBar.UpdateRate is 200 ms by default. + - IndeterminateProgressBar header is now positioned right of the animation. Similar to + common CLIs. + - ProgressBar had numeral optimizations and should perform better in all scenarios. + - Color received 2 new implicit operators, enabling it to convert a combination with any object + to ColoredOutput, which means you can write much less verbose statements with .ToString() calls. + - Internal buffer pooling system was upgraded to further reduce allocations and relief more pressure from the GC. + - Write/WriteLine overload for T, will no longer throw an exception when a capacity above 256 characters is required to format the type, and also support ref structs. + + + - + + portable + true + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + <_Parameter1>PrettyConsole.Tests.Unit - \ No newline at end of file + diff --git a/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs new file mode 100644 index 0000000..56fa30c --- /dev/null +++ b/PrettyConsole/PrettyConsoleInterpolatedStringHandler.cs @@ -0,0 +1,305 @@ +using System.Runtime.InteropServices; + +namespace PrettyConsole; + +#pragma warning disable CA1822 // Mark members as static +/// +/// Interpolated string handler that streams segments directly to an while allowing inline color changes. +/// +[InterpolatedStringHandler] +public readonly ref struct PrettyConsoleInterpolatedStringHandler { + private readonly TextWriter _writer; + private readonly IFormatProvider? _provider; + + /// + /// Creates a new handler that writes to . + /// + /// Estimated literal length supplied by the compiler. + /// Formatted item count supplied by the compiler. + /// Always ; reserved for future short-circuiting. + public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, out bool shouldAppend) + : this(literalLength, formattedCount, OutputPipe.Out, provider: null, out shouldAppend) { + } + + /// + /// Creates a new handler that writes to . + /// + /// Estimated literal length supplied by the compiler. + /// Formatted item count supplied by the compiler. + /// The pipe to stream the output to. + /// Always ; reserved for future short-circuiting. + public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, OutputPipe pipe, out bool shouldAppend) + : this(literalLength, formattedCount, pipe, provider: null, out shouldAppend) { + } + + /// + /// Creates a new handler that writes to using for formatting. + /// + /// Estimated literal length supplied by the compiler. + /// Formatted item count supplied by the compiler. + /// The pipe to stream the output to. + /// Optional format provider used when formatting values. + /// Always ; reserved for future short-circuiting. + public PrettyConsoleInterpolatedStringHandler(int literalLength, int formattedCount, OutputPipe pipe, IFormatProvider? provider, out bool shouldAppend) { + _writer = Console.GetWriter(pipe); + _provider = provider; + shouldAppend = true; + } + + /// + /// Appends a literal segment supplied by the compiler. + /// + public readonly void AppendLiteral(string value) { + if (!string.IsNullOrEmpty(value)) { + _writer.Write(value); + } + } + + /// + /// Appends a formatted string value. + /// + /// Formatted string. + /// Optional alignment as provided by the interpolation. + /// Unused string format specifier. + public readonly void AppendFormatted(string? value, int alignment = 0, string? format = null) { + AppendString(value, alignment); + } + + /// + /// Appends a span segment without allocations. + /// + /// Characters to write. + /// Optional alignment as provided by the interpolation. + public readonly void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0) { + AppendSpan(value, alignment); + } + + /// + /// Appends a single character. + /// + /// Character to write. + /// Optional alignment as provided by the interpolation. + public readonly void AppendFormatted(char value, int alignment = 0) { + if (alignment == 0) { + _writer.Write(value); + return; + } + + Span buffer = [value]; + AppendSpan(buffer, alignment); + } + + /// + /// Sets the console foreground color to . + /// + public readonly void AppendFormatted(ConsoleColor color) { + Console.SetColors(color, baseConsole.BackgroundColor); + } + + /// + /// Sets the console foreground color to . + /// + public readonly void AppendFormatted(Color color) { + Console.SetColors(color, baseConsole.BackgroundColor); + } + + /// + /// Sets the foreground and background colors of the console + /// + /// + /// + 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); + } + } + + /// + /// Writes a segment and applies its colors for the duration of the write. + /// + /// 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); + } + } + + /// + /// Writes a buffer of items. + /// + /// Segments to write. + public readonly void AppendFormatted(ReadOnlySpan outputs) { + if (outputs.Length is 0) { + return; + } + Console.WriteCore(outputs, _writer); + } + + /// + /// Append timeSpan with or without elapsed time formatting (human readable) + /// + /// + /// + public readonly void AppendFormatted(TimeSpan timeSpan, string? format = null) { + if (format != "hr") { + AppendSpanFormattable(timeSpan, 0, format); + return; + } + if (timeSpan.TotalSeconds < 1) { + AppendSpanFormattable(timeSpan.Milliseconds, 0, null); + AppendSpan("ms", 0); + } else if (timeSpan.TotalSeconds < 60) { + AppendSpanFormattable(timeSpan.Seconds, 0, "00"); + AppendFormatted(':'); + AppendSpanFormattable(timeSpan.Milliseconds, 0, "00"); + AppendFormatted('s'); + } else if (timeSpan.TotalSeconds < 3600) { + AppendSpanFormattable(timeSpan.Minutes, 0, "00"); + AppendFormatted(':'); + AppendSpanFormattable(timeSpan.Seconds, 0, "00"); + AppendFormatted('m'); + } else if (timeSpan.TotalSeconds < 86400) { + AppendSpanFormattable(timeSpan.Hours, 0, "00"); + AppendFormatted(':'); + AppendSpanFormattable(timeSpan.Minutes, 0, "00"); + AppendSpan("hr", 0); + } else { + AppendSpanFormattable(timeSpan.Days, 0, "00"); + AppendFormatted(':'); + AppendSpanFormattable(timeSpan.Hours, 0, "00"); + AppendSpan("d", 0); + } + } + + /// + /// Appends a value type that implements without boxing. + /// + public readonly void AppendFormatted(T value) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment: 0, format: null); + } + + /// + /// Appends a value type that implements without boxing while respecting alignment. + /// + public readonly void AppendFormatted(T value, int alignment) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment, format: null); + } + + /// + /// Appends a value type that implements without boxing using the provided format string. + /// + public readonly void AppendFormatted(T value, string? format) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment: 0, format); + } + + /// + /// Appends a value type that implements without boxing using alignment and format string. + /// + public readonly void AppendFormatted(T value, int alignment, string? format) where T : ISpanFormattable { + AppendSpanFormattable(value, alignment, format); + } + + /// + /// Appends an object value when the compiler cannot resolve a more specific overload. + /// + /// Value to write. + /// Optional alignment as provided by the interpolation. + /// Optional format specifier. + public 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; + } + + 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(); + + 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; + } + + upperBound *= 2; + } + } + + private readonly void AppendString(string? value, int alignment) { + if (string.IsNullOrEmpty(value)) { + AppendSpan(ReadOnlySpan.Empty, alignment); + return; + } + + 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 (!span.IsEmpty) { + _writer.Write(span); + } + + if (padding > 0 && leftAlign) { + WritePadding(padding); + } + return; + } + + if (!span.IsEmpty) { + _writer.Write(span); + } + } + + private readonly void WritePadding(int count) { + if (count <= 0) { + return; + } + + _writer.WriteWhiteSpaces(count); + } +} +#pragma warning restore CA1822 // Mark members as static \ No newline at end of file diff --git a/PrettyConsole/ProgressBar.cs b/PrettyConsole/ProgressBar.cs index 6c51a4e..96e309f 100755 --- a/PrettyConsole/ProgressBar.cs +++ b/PrettyConsole/ProgressBar.cs @@ -1,102 +1,122 @@ -using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace PrettyConsole; public static partial class Console { - /// - /// Represents a progress bar that can be displayed in the console. - /// - /// - /// - /// The progress bar update isn't tied to unit of time, it's up to the user to update it as needed. By managing the when the Update method is called, the user can have a more precise control over the progress bar. More calls, means more frequent rendering but at the cost of performance (very frequent updates may cause the terminal to lose sync with the method and will produce visual bugs, such as items not rendering in the right place) - /// - /// - /// Updating the progress bar with decreasing percentages will cause visual bugs as it is optimized to skip rendering pre-filled characters. - /// - /// - public class ProgressBar { - /// - /// Gets or sets the character used to represent the progress. - /// - public char ProgressChar { get; set; } = '■'; - - /// - /// Gets or sets the foreground color of the progress bar. - /// - public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; - - /// - /// Gets or sets the color of the progress portion of the bar. - /// - public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; - - private readonly char[] _percentageBuffer; - - private int _currentProgress = 0; - - private readonly char[] _pBuffer; - -#if NET9_0_OR_GREATER - private readonly Lock _lock = new(); -#else - private readonly object _lock = new(); -#endif - - /// - /// Represents a progress bar that can be displayed in the console. - /// - public ProgressBar() { - _pBuffer = new char[baseConsole.BufferWidth]; - _percentageBuffer = new char[20]; - } - - /// - /// Updates the progress bar with the specified percentage. - /// - /// The percentage value (0-100) representing the progress. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); - - /// - /// Updates the progress bar with the specified percentage. - /// - /// The percentage value (0-100) representing the progress. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(double percentage) => Update(percentage, ReadOnlySpan.Empty); - - /// - /// Updates the progress bar with the specified percentage and header text. - /// - /// The percentage value (0-100) representing the progress. - /// The status text to be displayed after the progress bar. - public void Update(double percentage, ReadOnlySpan status) { - lock (_lock) { - if (status.Length is 0) { - status = Utils.FormatPercentage(percentage, _percentageBuffer); - } - int pLength = baseConsole.BufferWidth - status.Length - 5; - var p = (int)(pLength * percentage * 0.01); - if (p == _currentProgress) { - return; - } - _currentProgress = p; - ResetColors(); - baseConsole.ForegroundColor = ForegroundColor; - var currentLine = GetCurrentLine(); - ClearNextLines(1, OutputPipe.Error); - Error.Write('['); - baseConsole.ForegroundColor = ProgressColor; - Span span = _pBuffer.AsSpan(0, p); - span.Fill(ProgressChar); - Span end = WhiteSpace.AsSpan(0, pLength - p); - Error.Write(span); - Error.Write(end); - baseConsole.ForegroundColor = ForegroundColor; - Error.Write("] "); - Error.Write(status); - ResetColors(); - GoToLine(currentLine); - } - } - } + /// + /// Represents a progress bar that can be displayed in the console. + /// + /// + /// + /// The progress bar update isn't tied to unit of time, it's up to the user to update it as needed. By managing the when the Update method is called, the user can have a more precise control over the progress bar. More calls, means more frequent rendering but at the cost of performance (very frequent updates may cause the terminal to lose sync with the method and will produce visual bugs, such as items not rendering in the right place) + /// + /// + /// Updating the progress bar with decreasing percentages will cause visual bugs as it is optimized to skip rendering pre-filled characters. + /// + /// + public class ProgressBar { + /// + /// Gets or sets the character used to represent the progress. + /// + public char ProgressChar { get; set; } = '■'; + + /// + /// Gets or sets the foreground color of the progress bar. + /// + public ConsoleColor ForegroundColor { get; set; } = Color.DefaultForegroundColor; + + /// + /// Gets or sets the color of the progress portion of the bar. + /// + public ConsoleColor ProgressColor { get; set; } = Color.DefaultForegroundColor; + + private int _currentProgress; + + private readonly Lock _lock = new(); + + /// + /// Updates the progress bar with the specified percentage. + /// + /// The percentage value (0-100) representing the progress. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(int percentage) => Update(percentage, ReadOnlySpan.Empty); + + /// + /// Updates the progress bar with the specified percentage. + /// + /// The percentage value (0-100) representing the progress. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(double percentage) => Update(percentage, ReadOnlySpan.Empty); + + /// + /// Updates the progress bar with the specified percentage and header text. + /// + /// The percentage value (0-100) representing the progress. + /// The status text to be displayed after the progress bar. + public void Update(double percentage, ReadOnlySpan status) { + // Non-locking fast path: compute the desired progress and early-return if unchanged. + percentage = Math.Clamp(percentage, 0, 100); + + int bufferWidth = GetWidthOrDefault(); + // Compute pLength using exact overhead: " [" (2) + "] " (2) + percentage length (5) + int pLength = Math.Max(0, bufferWidth - status.Length - 4 - 5); + int p = Math.Clamp((int)(pLength * percentage * 0.01), 0, pLength); + + if (p == Volatile.Read(ref _currentProgress)) { + return; + } + + lock (_lock) { + // It's possible another thread updated while we were waiting; re-check only the progress value. + if (p == _currentProgress) { + return; + } + + // Prepare the buffer exactly for the characters we will write for the bar (pLength) + using var listOwner = BufferPool.Shared.Rent(out var list); + list.EnsureCapacity(pLength); + CollectionsMarshal.SetCount(list, pLength); + Span buf = CollectionsMarshal.AsSpan(list); + + _currentProgress = p; + + var currentLine = GetCurrentLine(); + try { + ResetColors(); + baseConsole.ForegroundColor = ForegroundColor; + ClearNextLines(1, OutputPipe.Error); + if (status.Length != 0) { + Error.Write(status); + } + Error.Write(" ["); + baseConsole.ForegroundColor = ProgressColor; + + // Fill the progress portion + if (p > 0) { + Span progressSpan = buf.Slice(0, p); + progressSpan.Fill(ProgressChar); + } + + // Fill the remaining tail with spaces + int tailLength = Math.Max(0, pLength - p); + if (tailLength > 0) { + Span whiteSpaceSpan = buf.Slice(p, tailLength); + whiteSpaceSpan.Fill(' '); + } + + // Write the entire bar (progress + tail) + Error.Write(buf.Slice(0, pLength)); + + baseConsole.ForegroundColor = ForegroundColor; + Error.Write("] "); + // Write percentage + Write(OutputPipe.Error, $"{percentage,5:##.##}"); + GoToLine(currentLine); + } finally { + // Ensure colors and buffer are reset even if an exception occurs mid-render + ResetColors(); + } + } + } + } } \ No newline at end of file diff --git a/PrettyConsole/README.md b/PrettyConsole/README.md deleted file mode 100755 index cf487b4..0000000 --- a/PrettyConsole/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# PrettyConsole - -An abstraction over `System.Console` that adds new input and output methods, colors and advanced outputs like progress bars and menus. And everything is ansi supported so it works on legacy systems and terminals. - -## Features - -* 🚀 High performance, Low memory usage and allocation -* 🪶 Very lightweight (No external dependencies) -* Easy to use (no need to learn a new syntax while still writing less boilerplate code) -* 💾 Supports legacy ansi terminals (like Windows 7) -* 🔥 Complete NativeAOT compatibility -* Supports all major platforms (Windows, Linux, Mac) -* ⛓ Uses original output pipes, so that your cli's can be piped properly. - -## Installation [![NUGET DOWNLOADS](https://img.shields.io/nuget/dt/PrettyConsole?label=Downloads)](https://www.nuget.org/packages/PrettyConsole/) - -> dotnet add package PrettyConsole - -## Usage - -Everything starts off with the using statements, I recommend using the `Console` statically - -```csharp -using static PrettyConsole.Console; // Access to all Console methods -using PrettyConsole; // Access to the Color struct and OutputPipe enum -``` - -### ColoredOutput - -PrettyConsole uses an equation inspired syntax to colorize text. The syntax is as follows: - -```csharp -WriteLine("Test" * Color.Red / Color.Blue); -``` - -i.e `TEXT * FOREGROUND / BACKGROUND` - -Any the 2 colors can be played with just like a real equation, omit the foreground and the default will be used, -same goes for the background. - -### Basic Outputs - -The most basic method for outputting is `Write`, which has multiple overloads: - -```csharp -Write(ColoredOutput, OutputPipe pipe = OutputPipe.Out); -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // use collections expression for the compiler to inline the array -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPip, ConsoleColor); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPipe, ConsoleColor, ConsoleColor); -Write(T , OutputPipe pipe = OutputPipe.Out); // no string allocation with T : ISpanFormattable -Write(T, OutputPipe, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor, ReadOnlySpan, IFormatProvider?); -``` - -Overload for `WriteLine` are available with the same parameters - -### Basic Inputs - -These are the methods for reading user input: - -```csharp -string? ReadLine(); // ReadLine -string? ReadLine(ReadOnlySpan); -T? ReadLine(ReadOnlySpan); // T : IParsable -T ReadLine(ReadOnlySpan, T @default); // @default will be returned if parsing fails -bool TryReadLine(ReadOnlySpan, out T?); // T : IParsable -bool TryReadLine(ReadOnlySpan, T @default, out T); // @default will be returned if parsing fails -bool TryReadLine(ReadOnlySpan, bool ignoreCase, out TEnum?); // TEnum : struct, Enum -bool TryReadLine(ReadOnlySpan, bool ignoreCase, TEnum @default, out TEnum); // @default will be returned if parsing fails -``` - -I always recommend using `TryReadLine` instead of `ReadLine` as you need to maintain less null checks and the result, -especially with `@default` is much more concise. - -### Advanced Inputs - -These are some special methods for inputs: - -```csharp -// These will wait for the user to press any key -void RequestAnyInput(string message = "Press any key to continue..."); -void RequestAnyInput(ReadOnlySpan output); -// These request confirmation by special input from user -bool Confirm(ReadOnlySpan message); // uses the default values ["y", "yes"] -// the default values can also be used by you at Console.DefaultConfirmValues -bool Confirm(ReadOnlySpan message, ReadOnlySpan trueValues, bool emptyIsTrue = true); -``` - -### Rendering Controls - -To aid in rendering and building your own complex outputs, there are many methods that simplify some processes. - -```csharp -ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error); // clears the next lines -NewLine(OutputPipe pipe = OutputPipe.Out); // outputs a new line -SetColors(ConsoleColor foreground, ConsoleColor background); // sets the colors of the console output -ResetColors(); // resets the colors of the console output -int GetCurrentLine(); // returns the current line number -GoToLine(int line); // moves the cursor to the specified line -``` - -Combining `ClearNextLines` with `GoToLine` will enable you to efficiently use the same space in the console for continuous output, such as progress outputting, for some cases there are also built-in methods for this, more on that later. - -### Advanced Outputs - -```csharp -// This method will essentially write a line, clear it, go back to same position -// This allows a form of text-only progress bar -void OverrideCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error); -// This methods will write a character at a time, with a delay between each character -async Task TypeWrite(ColoredOutput output, int delay = TypeWriteDefaultDelay); -async Task TypeWriteLine(ColoredOutput output, int delay = TypeWriteDefaultDelay); -``` - -### Menus - -```csharp -// This prints an index view of the list, allows the user to select by index -// returns the actual choice that corresponds to the index -string Selection(ReadOnlySpan title, TList choices) where TList : IList {} -// Same as selection but allows the user to select multiple indexes -// Separated by spaces, and returns an array of the actual choices that correspond to the indexes -string[] MultiSelection(ReadOnlySpan title, TList choices) where TList : IList {} -// This prints a tree menu of 2 levels, allows the user to select the index -// Of the first and second level and returns the corresponding choices -(string option, string subOption) TreeMenu(ReadOnlySpan title, - Dictionary menu) where TList : IList {} -// This prints a table with headers, and columns for each list -void Table(TList headers, ReadOnlySpan columns) where TList : IList {} -``` - -### Progress Bars - -There are two types of progress bars here, they both are implemented using a class to maintain states. - -#### IndeterminateProgressBar - -```csharp -var prg = new IndeterminateProgressBar(); // this setups the internal states -// Then you need to provide either a Task or Task, the progress bar binds to it and runs until the task completes -await prg.RunAsync(task, "Running...", cancellationToken); // There are also overloads without header -// if the task is not started before being passed to the progress bar, it will be started automatically -// It is even better this way to synchronize the runtime of the progress bar with the task -``` - -#### ProgressBar - -`ProgressBar` is a more powerful version, but requires a percentage of progress. - -```csharp -// ProgressBar implements IDisposable -var prg = new ProgressBar(); -// then on each time the progress percentage is actually changed, you call Update -Update(percentage, ReadOnlySpan status); -// There are also overloads without header, and percentage can be either int or double (0-100) -// Also, you can change some of the visual properties of the progress bar after initialization -// by using the properties of the ProgressBar class -prg.ProgressChar = '■'; // Character to fill the progress bar -prg.ForegroundColor = Color.Red; // Color of the empty part -prg.ProgressColor = Color.Blue; // The color of the filled part -``` - -### Pipes - -`Console` wraps over `System.Console` and uses its `In`, `Out`, and `Error` streams. Since the names of the classes are identical, combining them in usage is somewhat painful as the compiler doesn't know which overloads to use. To aid in most cases, -`Console` exposes those streams as static properties, and you can use them directly. - -In rare cases, you will need something that there is in `System.Console` but not in `Console`, such as `ReadKey`, or `SetCursorPosition`, some events or otherwise, then you can simply call `System.Console`, this added verbosity is a worthy trade-off. - -## Contributing - -This project uses an MIT license, if you want to contribute, you can do so by forking the repository and creating a pull request. - -If you have feature requests or bug reports, please create an issue. - -## Contact - -For bug reports, feature requests or offers of support/sponsorship contact - -> This project is proudly made in Israel 🇮🇱 for the benefit of mankind. diff --git a/PrettyConsole/ReadLine.cs b/PrettyConsole/ReadLine.cs index 7d73835..21ec3a2 100755 --- a/PrettyConsole/ReadLine.cs +++ b/PrettyConsole/ReadLine.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace PrettyConsole; public static partial class Console { @@ -11,7 +13,20 @@ public static partial class Console { public static bool TryReadLine(ReadOnlySpan message, out T? result) where T : IParsable { Write(message, OutputPipe.Out); var input = In.ReadLine(); - return T.TryParse(input, null, out result); + return T.TryParse(input, CultureInfo.CurrentCulture, out result); + } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// The result of the parsing + /// Interpolated string handler that streams the content. + /// True if the parsing was successful, false otherwise + public static bool TryReadLine(out T? result, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + ResetColors(); + var input = In.ReadLine(); + return T.TryParse(input, CultureInfo.CurrentCulture, out result); } /// @@ -32,6 +47,24 @@ public static bool TryReadLine(ReadOnlySpan message, T @defaul return false; } + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// The result of the parsing + /// The default value to return if parsing fails + /// Interpolated string handler that streams the content. + /// True if the parsing was successful, false otherwise + public static bool TryReadLine(out T result, T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + var couldParse = TryReadLine(out T? innerResult, handler); + if (couldParse) { + result = innerResult!; + return true; + } + result = @default; + return false; + } + /// /// Used to request user input, validates and converts common types. /// @@ -44,6 +77,18 @@ public static bool TryReadLine(ReadOnlySpan message, bool return TryReadLine(message, ignoreCase, default, out result); } + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// The result of the parsing + /// Whether to ignore case when parsing + /// Interpolated string handler that streams the content. + /// True if the parsing was successful, false otherwise + public static bool TryReadLine(out TEnum result, bool ignoreCase, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TEnum : struct, Enum { + return TryReadLine(out result, ignoreCase, default, handler); + } + /// /// Used to request user input, validates and converts common types. /// @@ -63,6 +108,25 @@ public static bool TryReadLine(ReadOnlySpan message, bool return res; } + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// The result of the parsing + /// Whether to ignore case when parsing + /// The default value to return if parsing fails + /// Interpolated string handler that streams the content. + /// True if the parsing was successful, false otherwise + public static bool TryReadLine(out TEnum result, bool ignoreCase, TEnum @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where TEnum : struct, Enum { + ResetColors(); + var input = In.ReadLine(); + var res = Enum.TryParse(input, ignoreCase, out result); + if (!res) { + result = @default; + } + return res; + } + /// /// Used to request user input without any prepended message /// @@ -82,6 +146,15 @@ public static bool TryReadLine(ReadOnlySpan message, bool return ReadLine(); } + /// + /// Used to request user input + /// + /// Interpolated string handler that streams the content. + public static string? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + return ReadLine(); + } + /// /// Used to request user input, validates and converts common types. /// @@ -93,6 +166,17 @@ public static bool TryReadLine(ReadOnlySpan message, bool return result; } + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// Interpolated string handler that streams the content. + /// The result of the parsing + public static T? ReadLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + _ = TryReadLine(out T? result, handler); + return result; + } + /// /// Used to request user input, validates and converts common types. /// @@ -104,4 +188,16 @@ public static T ReadLine(ReadOnlySpan message, T @default) whe _ = TryReadLine(message, @default, out T result); return result; } + + /// + /// Used to request user input, validates and converts common types. + /// + /// + /// The default value to return if parsing fails + /// Interpolated string handler that streams the content. + /// The result of the parsing + public static T ReadLine(T @default, [InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) where T : IParsable { + _ = TryReadLine(out T result, @default, handler); + return result; + } } \ No newline at end of file diff --git a/PrettyConsole/RenderingControls.cs b/PrettyConsole/RenderingControls.cs index eb40eac..da27206 100755 --- a/PrettyConsole/RenderingControls.cs +++ b/PrettyConsole/RenderingControls.cs @@ -10,39 +10,26 @@ public static partial class Console { /// Useful for clearing output of overriding functions, like the ProgressBar /// public static void ClearNextLines(int lines, OutputPipe pipe = OutputPipe.Error) { - if (pipe == OutputPipe.Error) { - InternalClearNextLines(lines, Error); - return; - } - InternalClearNextLines(lines, Out); - return; - - static void InternalClearNextLines(int lines, TextWriter writer) { - ReadOnlySpan emptyLine = WhiteSpace.AsSpan(0, baseConsole.BufferWidth); - var currentLine = GetCurrentLine(); - for (int i = 0; i < lines; i++) { - writer.Write(emptyLine); - } - GoToLine(currentLine); + var textWriter = GetWriter(pipe); + var lineLength = GetWidthOrDefault(); + var currentLine = GetCurrentLine(); + GoToLine(currentLine); + for (int i = 0; i < lines; i++) { + textWriter.WriteWhiteSpaces(lineLength); } + GoToLine(currentLine); } /// /// Used to clear all previous outputs to the console /// - public static void Clear() { - baseConsole.Clear(); - } + public static void Clear() => baseConsole.Clear(); /// /// Used to end current line or write an empty one, depends whether the current line has any text /// public static void NewLine(OutputPipe pipe = OutputPipe.Out) { - if (pipe == OutputPipe.Out) { - Out.WriteLine(); - return; - } - Error.WriteLine(); + GetWriter(pipe).WriteLine(); } /// diff --git a/PrettyConsole/Utils.cs b/PrettyConsole/Utils.cs index 69df861..1eaaf11 100755 --- a/PrettyConsole/Utils.cs +++ b/PrettyConsole/Utils.cs @@ -1,7 +1,3 @@ -using System.Buffers; - -using Sharpify.Collections; - namespace PrettyConsole; /// @@ -9,31 +5,36 @@ namespace PrettyConsole; /// internal static class Utils { /// - /// Returns a formatted percentage string, i.e 0,5:##0.##% + /// Constant buffer filled with whitespaces /// - /// - /// - /// - internal static ReadOnlySpan FormatPercentage(double percentage, Span buffer) { - const int length = 5; - var rounded = Math.Round(percentage, 2); - var builder = StringBuffer.Create(buffer); - builder.Append(rounded); - if (builder.Position is length) { - return buffer.Slice(0, length); - } - var padding = length - builder.Position; - builder.Reset(); - while (padding-- > 0) { - builder.Append(' '); - } - builder.Append(rounded); - return builder.WrittenSpan; - } + private static readonly string WhiteSpaces = new(' ', 256); /// - /// Rents a memory owner from the shared memory pool + /// Writes whitespace to a up to length by chucks /// - /// The minimum length - internal static IMemoryOwner ObtainMemory(int length) => MemoryPool.Shared.Rent(length); -} + /// + /// + internal static void WriteWhiteSpaces(this TextWriter writer, int length) { + if (length <= 0) { + return; + } + + // Fast path: single call when length fits in the buffer + if (length <= WhiteSpaces.Length) { + writer.Write(WhiteSpaces.AsSpan(0, length)); + return; + } + + // Write full chunks + var full = WhiteSpaces; + while (length >= full.Length) { + writer.Write(full); // writes all 256 in one call + length -= full.Length; + } + + // Write the remainder + if (length > 0) { + writer.Write(full.AsSpan(0, length)); + } + } +} \ No newline at end of file diff --git a/PrettyConsole/Write.cs b/PrettyConsole/Write.cs index e938e84..160f504 100755 --- a/PrettyConsole/Write.cs +++ b/PrettyConsole/Write.cs @@ -1,10 +1,24 @@ +using System.Runtime.InteropServices; + namespace PrettyConsole; public static partial class Console { /// - /// The size of the buffer used for items + /// Writes interpolated content using to . /// - private const int SpanFormattableBufferSize = 256; + /// 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 @@ -12,8 +26,11 @@ public static partial class Console { /// /// The output pipe to use /// - /// If the result of formatted item length is > 256 characters - public static void Write(T item, OutputPipe pipe = OutputPipe.Out) where T : ISpanFormattable { + /// + /// 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); } @@ -25,8 +42,11 @@ public static void Write(T item, OutputPipe pipe = OutputPipe.Out) where T : /// The output pipe to use /// foreground color /// - /// If the result of formatted item length is > 256 characters - public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) where T : ISpanFormattable { + /// + /// 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); } @@ -39,9 +59,11 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground) wh /// foreground color /// background color /// - /// If the result of formatted item length is > 256 characters - public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, - ConsoleColor background) where T : ISpanFormattable { + /// + /// 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); } @@ -56,16 +78,25 @@ public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, /// item format /// format provider /// - /// If the result of formatted item length is > 256 characters + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// public static void Write(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) - where T : ISpanFormattable { - using var memoryOwner = Utils.ObtainMemory(SpanFormattableBufferSize); - var span = memoryOwner.Memory.Span; - if (!item.TryFormat(span, out int charsWritten, format, formatProvider)) { - throw new ArgumentException($"Formatted item length > {SpanFormattableBufferSize}, please use a different overload", nameof(item)); + 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; + } } - Write(span.Slice(0, charsWritten), pipe, foreground, background); } /// @@ -89,11 +120,7 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor /// background color public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) { SetColors(foreground, background); - if (pipe == OutputPipe.Out) { - Out.Write(span); - } else { - Error.Write(span); - } + GetWriter(pipe).Write(span); ResetColors(); } @@ -106,12 +133,21 @@ public static void Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor /// To end line, use /// public static void Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out) { + WriteCore(output, GetWriter(pipe)); + } + + /// + /// Write a to the error console + /// + /// + /// The writer to use + /// + /// To end line, use + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteCore(ColoredOutput output, TextWriter writer) { SetColors(output.ForegroundColor, output.BackgroundColor); - if (pipe == OutputPipe.Out) { - Out.Write(output.Value); - } else { - Error.Write(output.Value); - } + writer.Write(output.Value); ResetColors(); } @@ -121,11 +157,21 @@ public static void Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out) /// /// The output pipe to use public static void Write(ReadOnlySpan outputs, OutputPipe pipe = OutputPipe.Out) { + WriteCore(outputs, GetWriter(pipe)); + } + + /// + /// Write a number of to the console + /// + /// + /// The writer to use + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteCore(ReadOnlySpan outputs, TextWriter writer) { if (outputs.Length is 0) { return; } foreach (var output in outputs) { - Write(output, pipe); + WriteCore(output, writer); } } } \ No newline at end of file diff --git a/PrettyConsole/WriteLine.cs b/PrettyConsole/WriteLine.cs index fe0db64..e32cd89 100755 --- a/PrettyConsole/WriteLine.cs +++ b/PrettyConsole/WriteLine.cs @@ -1,6 +1,25 @@ namespace PrettyConsole; public static partial class Console { + /// + /// Writes interpolated content using to . + /// + /// Interpolated string handler that streams the content. + public static void WriteLine([InterpolatedStringHandlerArgument] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + NewLine(OutputPipe.Out); + } + + /// + /// Writes interpolated content using . + /// + /// Destination pipe. Defaults to . + /// Interpolated string handler that streams the content. + public static void WriteLine(OutputPipe pipe, [InterpolatedStringHandlerArgument(nameof(pipe))] PrettyConsoleInterpolatedStringHandler handler = default) { + ResetColors(); + NewLine(pipe); + } + /// /// Write a to the error console /// @@ -37,8 +56,11 @@ public static void WriteLine(ReadOnlySpan outputs, OutputPipe pip /// /// The output pipe to use /// - /// If the result of formatted item length is > 256 characters - public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) where T : ISpanFormattable { + /// + /// 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); } @@ -50,8 +72,11 @@ public static void WriteLine(T item, OutputPipe pipe = OutputPipe.Out) where /// The output pipe to use /// foreground color /// - /// If the result of formatted item length is > 256 characters - public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground) where T : ISpanFormattable { + /// + /// 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); } @@ -64,9 +89,11 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// foreground color /// background color /// - /// If the result of formatted item length is > 256 characters + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, - ConsoleColor background) where T : ISpanFormattable { + ConsoleColor background) where T : ISpanFormattable, allows ref struct { WriteLine(item, pipe, foreground, background, ReadOnlySpan.Empty, null); } @@ -81,10 +108,12 @@ public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground /// item format /// format provider /// - /// If the result of formatted item length is > 256 characters + /// + /// This function iteratively grows a rented span until formatting is successful, starting at capacity = 256, to ensure the fastest execution speed, it is recommend that would be able to format to a smaller length string than that. + /// public static void WriteLine(T item, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, ReadOnlySpan format, IFormatProvider? formatProvider) - where T : ISpanFormattable { + where T : ISpanFormattable, allows ref struct { Write(item, pipe, foreground, background, format, formatProvider); NewLine(pipe); } diff --git a/README.md b/README.md index cf487b4..95c06f9 100755 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ An abstraction over `System.Console` that adds new input and output methods, col ## Features -* 🚀 High performance, Low memory usage and allocation -* 🪶 Very lightweight (No external dependencies) -* Easy to use (no need to learn a new syntax while still writing less boilerplate code) -* 💾 Supports legacy ansi terminals (like Windows 7) +* 🚀 High performance, low allocations and span-first APIs +* 🪶 Very lightweight (no external dependencies) +* ✨ Zero-allocation interpolated string handler for inline colors and formatting +* 💾 Supports legacy ANSI terminals (like Windows 7) * 🔥 Complete NativeAOT compatibility * Supports all major platforms (Windows, Linux, Mac) -* ⛓ Uses original output pipes, so that your cli's can be piped properly. +* ⛓ Uses original output pipes, so that your CLI's can be piped properly ## Installation [![NUGET DOWNLOADS](https://img.shields.io/nuget/dt/PrettyConsole?label=Downloads)](https://www.nuget.org/packages/PrettyConsole/) @@ -25,6 +25,21 @@ using static PrettyConsole.Console; // Access to all Console methods using PrettyConsole; // Access to the Color struct and OutputPipe enum ``` +### Interpolated Strings + +`PrettyConsoleInterpolatedStringHandler` lets you stream interpolated text directly to the selected pipe without allocating intermediate strings, while still using the familiar `$"..."` syntax. + +```csharp +Write($"Hello {Color.Green}world{Color.Default}!"); +Write(OutputPipe.Error, $"{Color.Yellow}Warning:{Color.Default} {message}"); + +if (!TryReadLine(out int choice, $"Pick option {Color.Cyan}1-5{Color.Default}: ")) { + WriteLine($"{Color.Red}Not a number.{Color.Default}"); +} +``` + +Colors reset automatically at the end of each call. Use `Color.Default` (or explicit background tuples) when you need to restore colors mid-string. + ### ColoredOutput PrettyConsole uses an equation inspired syntax to colorize text. The syntax is as follows: @@ -40,18 +55,19 @@ same goes for the background. ### Basic Outputs -The most basic method for outputting is `Write`, which has multiple overloads: +The most basic method for outputting is `Write`, which has multiple overloads. All equivalents exist for `WriteLine`: ```csharp -Write(ColoredOutput, OutputPipe pipe = OutputPipe.Out); -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // use collections expression for the compiler to inline the array -Write(ReadOnlySpan, OutputPipe pipe = OutputPipe.Out); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPip, ConsoleColor); // no string allocation with ReadOnlySpan -Write(ReadOnlySpan, OutputPipe, ConsoleColor, ConsoleColor); -Write(T , OutputPipe pipe = OutputPipe.Out); // no string allocation with T : ISpanFormattable -Write(T, OutputPipe, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor); -Write(T, OutputPipe, ConsoleColor, ConsoleColor, ReadOnlySpan, IFormatProvider?); +// Usage + overload highlights: +Write($"Interpolated {Color.Blue}string{Color.Default}"); +Write(OutputPipe.Error, $"..."); +Write(ColoredOutput output, OutputPipe pipe = OutputPipe.Out); +Write(ReadOnlySpan outputs, OutputPipe pipe = OutputPipe.Out); +Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground); +Write(ReadOnlySpan span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background); +Write(T value, OutputPipe pipe = OutputPipe.Out) where T : ISpanFormattable; +Write(T value, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background, + ReadOnlySpan format, IFormatProvider? provider); ``` Overload for `WriteLine` are available with the same parameters @@ -61,14 +77,22 @@ Overload for `WriteLine` are available with the same parameters These are the methods for reading user input: ```csharp +// Examples: string? ReadLine(); // ReadLine string? ReadLine(ReadOnlySpan); +string? ReadLine($"Prompt {Color.Green}text{Color.Default}: "); T? ReadLine(ReadOnlySpan); // T : IParsable +T? ReadLine($"Prompt {Color.Cyan}text{Color.Default}: "); T ReadLine(ReadOnlySpan, T @default); // @default will be returned if parsing fails +T ReadLine(T @default, $"Prompt {Color.Cyan}text{Color.Default}: "); bool TryReadLine(ReadOnlySpan, out T?); // T : IParsable +bool TryReadLine(out T?, $"Prompt {Color.Cyan}text{Color.Default}: "); bool TryReadLine(ReadOnlySpan, T @default, out T); // @default will be returned if parsing fails +bool TryReadLine(out T, T @default, $"Prompt {Color.Cyan}text{Color.Default}: "); bool TryReadLine(ReadOnlySpan, bool ignoreCase, out TEnum?); // TEnum : struct, Enum +bool TryReadLine(out TEnum, bool ignoreCase, $"Prompt {Color.Cyan}text{Color.Default}: "); // TEnum : struct, Enum bool TryReadLine(ReadOnlySpan, bool ignoreCase, TEnum @default, out TEnum); // @default will be returned if parsing fails +bool TryReadLine(out TEnum, bool ignoreCase, TEnum @default, $"Prompt {Color.Cyan}text{Color.Default}: "); ``` I always recommend using `TryReadLine` instead of `ReadLine` as you need to maintain less null checks and the result, @@ -82,10 +106,13 @@ These are some special methods for inputs: // These will wait for the user to press any key void RequestAnyInput(string message = "Press any key to continue..."); void RequestAnyInput(ReadOnlySpan output); +RequestAnyInput($"Press {Color.Yellow}any key{Color.Default} to continue..."); // These request confirmation by special input from user bool Confirm(ReadOnlySpan message); // uses the default values ["y", "yes"] // the default values can also be used by you at Console.DefaultConfirmValues bool Confirm(ReadOnlySpan message, ReadOnlySpan trueValues, bool emptyIsTrue = true); +bool Confirm($"Deploy to production? ({Color.Green}y{Color.Default}/{Color.Red}n{Color.Default}) "); +bool Confirm(ReadOnlySpan trueValues, bool emptyIsTrue, $"Overwrite existing files? "); ``` ### Rendering Controls @@ -108,7 +135,10 @@ Combining `ClearNextLines` with `GoToLine` will enable you to efficiently use th ```csharp // This method will essentially write a line, clear it, go back to same position // This allows a form of text-only progress bar -void OverrideCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error); +void OverwriteCurrentLine(ReadOnlySpan output, OutputPipe pipe = OutputPipe.Error); +void Overwrite(Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error); +void Overwrite(TState state, Action action, int lines = 1, OutputPipe pipe = OutputPipe.Error) + where TState : allows ref struct; // This methods will write a character at a time, with a delay between each character async Task TypeWrite(ColoredOutput output, int delay = TypeWriteDefaultDelay); async Task TypeWriteLine(ColoredOutput output, int delay = TypeWriteDefaultDelay); @@ -143,6 +173,7 @@ var prg = new IndeterminateProgressBar(); // this setups the internal states await prg.RunAsync(task, "Running...", cancellationToken); // There are also overloads without header // if the task is not started before being passed to the progress bar, it will be started automatically // It is even better this way to synchronize the runtime of the progress bar with the task +prg.AnimationSequence = IndeterminateProgressBar.Patterns.CarriageReturn; // customize the animation ``` #### ProgressBar diff --git a/PrettyConsole/Versions.md b/Versions.md similarity index 50% rename from PrettyConsole/Versions.md rename to Versions.md index 5aa7f5d..fd28867 100755 --- a/PrettyConsole/Versions.md +++ b/Versions.md @@ -1,107 +1,134 @@ -# CHANGELOG +# Versions + +## v4.0.0 + +### Added + +- `PrettyConsoleInterpolatedStringHandler` was added to allow streaming zero allocation formatted and styled outputs to console pipes. With supported overloads for: + - `Write` and `WriteLine` + - `ReadLine` and `TryReadLine` + - `Selection` and `MultiSelection` + - `Overwrite` is a wrapper around an action of displaying outputs, with or without closures using `TState`, it enables you to use a lambda and call the `PrettyConsoleInterpolatedStringHandler` methods inside, to create zero allocation reactive and refreshable components. + - To customize colors, use `Color` as an interpolation parameter at the correct place, and restore the colors with `Color.Default`. for example: `WriteLine($"This is in {Color.Green}green{Color.Default} and this is in {Color.Red}red{Color.Default}.");`, all overloads that accept the interpolation reset the color at the end, so you can omit `Color.Default` if you colored the last section of your string. + - The same conventions and syntax of setting `Foreground / Background` colors works here as well. +- `IndeterminateProgressBar` will now allow customization of the animated sequence via the property `AnimationSequence`, and it also includes an inner class `Patterns` that contains some constant sequences that could be used with it. + +### Fixed + +- Fixed issue that could sometimes cause writing into buffers beyond their bounds - throwing an exception. Possibly effected: + - `ProgressBar` and `IndeterminateProgressBar` + - `ClearNextLines` +- `IndeterminateProgressBar.UpdateRate` is 200 ms by default. +- `IndeterminateProgressBar` header is now positioned right of the animation. Similar to common CLIs. +- `ProgressBar` had numeral optimizations and should perform better in all scenarios. +- `OverrideCurrentLine` was renamed to `OverwriteCurrentLine` to be more semantically correct. + +### Also + +- Dropped `Sharpify` as a dependency - `PrettyConsole` is now self-sufficient. ## v3.1.0 -* Updated to support .NET 9.0 -* Updated to use `Sharpify 2.5.0` -* `ProgressBar` was redesigned since it randomly produced artifacts such as printing the header multiple times. - * Now the buffer area of `ProgressBar` is only 1 line, the (now "status") is printed on the same line, after the bar - * Only if the header is empty, the percentage is printed instead. - * An internal lock is now also used to prevent race conditions. -* `ClearNextLines` show now work properly, previously it could actually clear 1 line too many. +- Updated to support .NET 9.0 +- Updated to use `Sharpify 2.5.0` +- `ProgressBar` was redesigned since it randomly produced artifacts such as printing the header multiple times. + - Now the buffer area of `ProgressBar` is only 1 line, the (now "status") is printed on the same line, after the bar + - Only if the header is empty, the percentage is printed instead. + - An internal lock is now also used to prevent race conditions. +- `ClearNextLines` show now work properly, previously it could actually clear 1 line too many. ### Breaking changes -* `OutputPipe` enum was added to unify APIs -* `ClearNextLinesError` was removed, use `ClearNextLines` with `OutputPipe.Error` instead -* `NewLineError` was removed, use `NewLine` with `OutputPipe.Error` instead -* `WriteError` was removed, use `Write` with `OutputPipe.Error` instead -* `WriteLineError` was removed, use `WriteLine` with `OutputPipe.Error` instead -* `OverrideCurrentLine` now has an option to choose the output pipe, use `OutputPipe.Error` by default -* `Write` had the same treatment, now it has an option to choose the output pipe, use `OutputPipe.Out` by default, so the overload of `WriteError` were removed -* `WriteLine` and `WriteLine(ReadOnlySpan)` were introduced thanks to this change +- `OutputPipe` enum was added to unify APIs +- `ClearNextLinesError` was removed, use `ClearNextLines` with `OutputPipe.Error` instead +- `NewLineError` was removed, use `NewLine` with `OutputPipe.Error` instead +- `WriteError` was removed, use `Write` with `OutputPipe.Error` instead +- `WriteLineError` was removed, use `WriteLine` with `OutputPipe.Error` instead +- `OverrideCurrentLine` now has an option to choose the output pipe, use `OutputPipe.Error` by default +- `Write` had the same treatment, now it has an option to choose the output pipe, use `OutputPipe.Out` by default, so the overload of `WriteError` were removed +- `WriteLine` and `WriteLine(ReadOnlySpan)` were introduced thanks to this change ## v3.0.0 -* Removed `Write` and `WriteLine` overloads that contains multiple `ColoredOutput`s, when using the overload with `ReadOnlySpan` and `CollectionExpression`, the compiler generates an inline array to hold the elements, this is very efficient, and the reduced complexity allows usage of multiple `ColoredOutput`s in more places. -* All overloads of all functions that previously took only one `ColoredOutput` now take a `ReadOnlySpan` instead, as noted it will use an inline array, and the internal implementation also has fast paths for size 1. -* All fast changing outputs (`ProgressBar`, `IndeterminateProgressBar`, `OverrideCurrentLine`) now uses the error output pipe by default, this means that if the consumer will pipe the output to another cli, it won't be filled with garbage and retain only the valuable stuff. -* Added `ReadOnlySpan` overloads to `WriteError` and `WriteLineError`. -* Added overloads for all variants of `ReadLine` and `TryReadLine` that support `T @default` that will be returned if parsing fails. -* `Write`, `WriteLine`, `WriteError` and `WriteLineError` no longer have `params ColoredOutput[]` overloads, they instead have a `ReadOnlySpan` overload. This performs even better as it uses an inline array, and the removal of the restrictions on where `params` can be used, allowed `ReadOnlySpan` to replace virtually allow single `ColoredOutput` methods as well. Allowing greater customization in any function. -* You can now access the `In`, `Out`, and `Error` streams of `System.Console` directly from `PrettyConsole.Console` by using the properties `Out`, `Error`, and `In`. This reduces verbosity since the names of the classes have collisions. -* Added `SetColors` to allow setting the colors of the console output. -* `ClearNextLines` now also has a `ClearNextLinesError` brother which does the same for the `Error` stream. -* `NewLineError` was also added for this. -* To enhance customization in extreme high perf scenarios where you write a `ReadOnlySpan` directly to the output stream, the `Write` and `WriteError` methods now have overloads that take a `ReadOnlySpan` instead of a `ReadOnlySpan`, along with foreground and background colors. -* Added `Write` and `WriteError` variants for `T : ISpanFormattable` as well. -* `ProgressBar` should now be even more performant as internal progress tracking allowed cutting the required operations by 20%. -* `Color` will now contain `static readonly ConsoleColor` fields for both `Foreground` and `Background` colors, they will be initialized during runtime to support all platforms (fixing the instant crashes on Windows). - * You can also refer them when you want to use the defaults for any reason. -* The base methods which are used for outputting `ReadOnlySpan` have been re-written to reduce assembly instructions, leading to about 15-20% runtime improvements across the board, and reducing by the binary size by a few bytes too lol. -* `Console` and `Color` now have the correct attributes to disallow compilation on unsupported platforms, if anyone tries to use them now (even thought it should've been obvious that they shouldn't be used on unsupported platforms), it should display the right message at build time. -* Removed all overloads that have options to change the input colors, it invites non-streamlined usage, and just increases the code complexity to maintain. Without them the code is simpler and more performant. -* A lot of methods that used white-spaces in any shape or form were now optimized using pre-initialized static buffer. -* `IndeterminateProgressBar` now has overloads that accept a `string header` that can be displayed before the progress char. There also was a change involving a secondary addition of catching the `CancellationToken`, removing up to 5 internal iterations. -* Added `GetCurrentLine` and `GoToLine` methods to allow efficiently using the same space in the console for continuous output, such as progress outputting, and general control flow. +- Removed `Write` and `WriteLine` overloads that contains multiple `ColoredOutput`s, when using the overload with `ReadOnlySpan` and `CollectionExpression`, the compiler generates an inline array to hold the elements, this is very efficient, and the reduced complexity allows usage of multiple `ColoredOutput`s in more places. +- All overloads of all functions that previously took only one `ColoredOutput` now take a `ReadOnlySpan` instead, as noted it will use an inline array, and the internal implementation also has fast paths for size 1. +- All fast changing outputs (`ProgressBar`, `IndeterminateProgressBar`, `OverrideCurrentLine`) now uses the error output pipe by default, this means that if the consumer will pipe the output to another cli, it won't be filled with garbage and retain only the valuable stuff. +- Added `ReadOnlySpan` overloads to `WriteError` and `WriteLineError`. +- Added overloads for all variants of `ReadLine` and `TryReadLine` that support `T @default` that will be returned if parsing fails. +- `Write`, `WriteLine`, `WriteError` and `WriteLineError` no longer have `params ColoredOutput[]` overloads, they instead have a `ReadOnlySpan` overload. This performs even better as it uses an inline array, and the removal of the restrictions on where `params` can be used, allowed `ReadOnlySpan` to replace virtually allow single `ColoredOutput` methods as well. Allowing greater customization in any function. +- You can now access the `In`, `Out`, and `Error` streams of `System.Console` directly from `PrettyConsole.Console` by using the properties `Out`, `Error`, and `In`. This reduces verbosity since the names of the classes have collisions. +- Added `SetColors` to allow setting the colors of the console output. +- `ClearNextLines` now also has a `ClearNextLinesError` brother which does the same for the `Error` stream. +- `NewLineError` was also added for this. +- To enhance customization in extreme high perf scenarios where you write a `ReadOnlySpan` directly to the output stream, the `Write` and `WriteError` methods now have overloads that take a `ReadOnlySpan` instead of a `ReadOnlySpan`, along with foreground and background colors. +- Added `Write` and `WriteError` variants for `T : ISpanFormattable` as well. +- `ProgressBar` should now be even more performant as internal progress tracking allowed cutting the required operations by 20%. +- `Color` will now contain `static readonly ConsoleColor` fields for both `Foreground` and `Background` colors, they will be initialized during runtime to support all platforms (fixing the instant crashes on Windows). + - You can also refer them when you want to use the defaults for any reason. +- The base methods which are used for outputting `ReadOnlySpan` have been re-written to reduce assembly instructions, leading to about 15-20% runtime improvements across the board, and reducing by the binary size by a few bytes too lol. +- `Console` and `Color` now have the correct attributes to disallow compilation on unsupported platforms, if anyone tries to use them now (even thought it should've been obvious that they shouldn't be used on unsupported platforms), it should display the right message at build time. +- Removed all overloads that have options to change the input colors, it invites non-streamlined usage, and just increases the code complexity to maintain. Without them the code is simpler and more performant. +- A lot of methods that used white-spaces in any shape or form were now optimized using pre-initialized static buffer. +- `IndeterminateProgressBar` now has overloads that accept a `string header` that can be displayed before the progress char. There also was a change involving a secondary addition of catching the `CancellationToken`, removing up to 5 internal iterations. +- Added `GetCurrentLine` and `GoToLine` methods to allow efficiently using the same space in the console for continuous output, such as progress outputting, and general control flow. ## v2.1.1 -* Removed redirection of `ReadOnlySpan` print overload that degraded performance. -* Improved internal handling of array and memory pooling. -* Decreased formatted length limit for `ISpanFormattable` to 50 characters, as it was too long and frequent calls could cause the os to lag. Also changed to use rented buffer instead of stack allocation. +- Removed redirection of `ReadOnlySpan` print overload that degraded performance. +- Improved internal handling of array and memory pooling. +- Decreased formatted length limit for `ISpanFormattable` to 50 characters, as it was too long and frequent calls could cause the os to lag. Also changed to use rented buffer instead of stack allocation. ## v2.1.0 -* Fixed default colors, previously colors where defaulted to `Color.Gray` for foreground and `Color.Black` for background, however many shells have custom colors that they render by default, which means the default colors seemed to render in a non ordinary fashion. The default colors now are `Color.Unknown` which will actually use the colors of the shell. -* `ProgressBar` now implements `IDisposable` since the implementation has been optimized, make sure to dispose of it, lest you induce a penalty on other such optimization which are now spread across the library. -* Introduced an overload to `Write` that accepts any `T : ISpanFormattable`. In many cases users might want to print structs or other types that implement this interface such as `int`, `double`, `DateTime` and more... splitting the output into a few lines and using this overload will enable you completely avoid the string allocation for this object, the overload is very optimized, writes it directly to the stack and prints it using a span. However, this limitation means that if the formatted item is longer than 256 characters, an exception will be thrown indicating that you should use a different overload. +- Fixed default colors, previously colors where defaulted to `Color.Gray` for foreground and `Color.Black` for background, however many shells have custom colors that they render by default, which means the default colors seemed to render in a non ordinary fashion. The default colors now are `Color.Unknown` which will actually use the colors of the shell. +- `ProgressBar` now implements `IDisposable` since the implementation has been optimized, make sure to dispose of it, lest you induce a penalty on other such optimization which are now spread across the library. +- Introduced an overload to `Write` that accepts any `T : ISpanFormattable`. In many cases users might want to print structs or other types that implement this interface such as `int`, `double`, `DateTime` and more... splitting the output into a few lines and using this overload will enable you completely avoid the string allocation for this object, the overload is very optimized, writes it directly to the stack and prints it using a span. However, this limitation means that if the formatted item is longer than 256 characters, an exception will be thrown indicating that you should use a different overload. ## v2.0.0 -* All previous variants of output types were removed, now the main output type is `ColoredOutput` (which includes a string, foreground color and background color). +- All previous variants of output types were removed, now the main output type is `ColoredOutput` (which includes a string, foreground color and background color). Thanks to implicit and other converters and the added supporting `Color` class, usage is even simpler, and much more performant. -* Overloads of many functions were changed, many were deleted. -* `ReadLine` and new overloads `TryReadLine` now support reflection free parsing for any `IParsable` +- Overloads of many functions were changed, many were deleted. +- `ReadLine` and new overloads `TryReadLine` now support reflection free parsing for any `IParsable` implementing types (which are most of the base types, and you can create any custom implementations if you choose so). -* `ReadLine` now also has an `Enum` overload that can parse for enums, with configurable case sensitivity. -* Many functions, especially more advance outputs such as selections, menus and progress bar, had undergone tremendous performance optimizations, and show now perform extremely efficiently. -* A new table view implementation was added. -* The progress bar types `ProgressBar` and `IndeterminateProgressBar` are now classes, and also have been substantially optimized. +- `ReadLine` now also has an `Enum` overload that can parse for enums, with configurable case sensitivity. +- Many functions, especially more advance outputs such as selections, menus and progress bar, had undergone tremendous performance optimizations, and show now perform extremely efficiently. +- A new table view implementation was added. +- The progress bar types `ProgressBar` and `IndeterminateProgressBar` are now classes, and also have been substantially optimized. ## v1.6.1 -* `TextRenderingScheme` type was added for better handling of outputs consisting of mixed colors -* `TypeWrite` now has a `TextRenderingScheme` overload and the `delay` in all overloads was increased to 200 (ms) to look more natural -* `Write`, `WriteLine`, `ReadLine` and `OverrideCurrentLine` now also have overloads with `TextRenderingScheme`, and they are the recommended ones for mixed color use, using them instead of the `params array` overload may allow further optimization during JIT compilation -* `Write` and `WriteLine` now have overloads for `ReadOnlySpan` for even better performance -* More `try-finally` blocks have been implemented to further reduce the possibility of render failure upon exceptions -* More methods have been re-implemented to call their overloads for better maintainability and consistency -* More methods were marked as `Pure` to provide more information to the end users -* After testing it became rather clear, the best way to avoid glitches in frequently updated outputs inside event handlers, such as `ProgressBar`, `OverrideCurrentLine` and the likes is to firstly create an early return based on elapsed time after previous render, secondly, Use the `[MethodImpl(MethodImplOptions.Synchronized)]` On the event, and if the output is a Task, use `.Wait` instead of making the event async and awaiting it -* `ProgressBarDisplay` was modified to be a `record struct` instead of `ref struct`, This is aimed to increase usage flexibility which is limited with `ref struct`s, Also it may increase performance in edge cases due to higher potential for compiler optimization +- `TextRenderingScheme` type was added for better handling of outputs consisting of mixed colors +- `TypeWrite` now has a `TextRenderingScheme` overload and the `delay` in all overloads was increased to 200 (ms) to look more natural +- `Write`, `WriteLine`, `ReadLine` and `OverrideCurrentLine` now also have overloads with `TextRenderingScheme`, and they are the recommended ones for mixed color use, using them instead of the `params array` overload may allow further optimization during JIT compilation +- `Write` and `WriteLine` now have overloads for `ReadOnlySpan` for even better performance +- More `try-finally` blocks have been implemented to further reduce the possibility of render failure upon exceptions +- More methods have been re-implemented to call their overloads for better maintainability and consistency +- More methods were marked as `Pure` to provide more information to the end users +- After testing it became rather clear, the best way to avoid glitches in frequently updated outputs inside event handlers, such as `ProgressBar`, `OverrideCurrentLine` and the likes is to firstly create an early return based on elapsed time after previous render, secondly, Use the `[MethodImpl(MethodImplOptions.Synchronized)]` On the event, and if the output is a Task, use `.Wait` instead of making the event async and awaiting it +- `ProgressBarDisplay` was modified to be a `record struct` instead of `ref struct`, This is aimed to increase usage flexibility which is limited with `ref struct`s, Also it may increase performance in edge cases due to higher potential for compiler optimization ## v1.6.0 -* Re-structured `Console` as a `static partial class` into many files to separate the code into categorized section for better maintainability and improved workflow for adding new features -* Removed `synchronized method` compiler enforcements that could in some case limit usage flexibility when intentionally using `Task`s, if that feature is needed, simply create your own wrapper method with the attribute -* Update all places where the color of the output is changed to use `try-finally` block to ensure color reset even if an exception was thrown, which before could cause bugged colors -* Added more safeguards in key places -* Improved performance of various methods -* Merged closely related method implementations to reduce possibility of future errors -* **[POSSIBLE BREAKING CHANGE]** `ProgressBarDisplay` has been restructured to safeguard against improper initialization. Also a `ProgressChar` property was added to allow further customization of the progress bar look -* Added `TypeWrite` and `TypeWriteLine` methods that provide a simple type-writer effect for text. -* Fixed issue in `RequestAnyInput` that could read a pre-existing character from the input stream and accept it, basically skipping the entire request. +- Re-structured `Console` as a `static partial class` into many files to separate the code into categorized section for better maintainability and improved workflow for adding new features +- Removed `synchronized method` compiler enforcements that could in some case limit usage flexibility when intentionally using `Task`s, if that feature is needed, simply create your own wrapper method with the attribute +- Update all places where the color of the output is changed to use `try-finally` block to ensure color reset even if an exception was thrown, which before could cause bugged colors +- Added more safeguards in key places +- Improved performance of various methods +- Merged closely related method implementations to reduce possibility of future errors +- **[POSSIBLE BREAKING CHANGE]** `ProgressBarDisplay` has been restructured to safeguard against improper initialization. Also a `ProgressChar` property was added to allow further customization of the progress bar look +- Added `TypeWrite` and `TypeWriteLine` methods that provide a simple type-writer effect for text. +- Fixed issue in `RequestAnyInput` that could read a pre-existing character from the input stream and accept it, basically skipping the entire request. ## v1.5.2 -* **FROM THIS VERSION ON, THE PACKAGE WILL BE SIGNED** -* Updated many string interpolation instances to use `string.Concat` instead to improve performance -* Modified calculations in progress bars to use functions which are more hardware optimized -* Modified evaluated strings in progress bars to only be printed when done -* Added direction to synchronize progress bars to reduce buggy outputs -* Updated csproj file for better compatibility with the nuget website +- **FROM THIS VERSION ON, THE PACKAGE WILL BE SIGNED** +- Updated many string interpolation instances to use `string.Concat` instead to improve performance +- Modified calculations in progress bars to use functions which are more hardware optimized +- Modified evaluated strings in progress bars to only be printed when done +- Added direction to synchronize progress bars to reduce buggy outputs +- Updated csproj file for better compatibility with the nuget website This is a quality of life update, in most use cases the performance benefit, whether time wise or memory allocation will be visible and sometimes very significant. @@ -109,54 +136,54 @@ This update doesn't introduce any breaking changes so updating is highly encoura ## v1.5.1 -* Fixed issues with progress bar where the next override could keep artifacts of previous render. -* Added `OverrideCurrentLine()` which allows showing of progress with string only. -* Added `ClearNextLines(num)` which allows to remove next `num` lines, this is useful when something overriding was used, such as progress bar, but after parts of are left because the new output is shorter. +- Fixed issues with progress bar where the next override could keep artifacts of previous render. +- Added `OverrideCurrentLine()` which allows showing of progress with string only. +- Added `ClearNextLines(num)` which allows to remove next `num` lines, this is useful when something overriding was used, such as progress bar, but after parts of are left because the new output is shorter. ## v1.5.0 -* Optimized code across the board for better performance and less memory allocation -* Improved organization and documentation -* Added more method overloads -* Added options to output errors to error pipeline to improve stds down the line -* Added trimming safe overloads for generic methods -* Added trim warnings for generic and reflection methods -* Added optional header to the progress bar +- Optimized code across the board for better performance and less memory allocation +- Improved organization and documentation +- Added more method overloads +- Added options to output errors to error pipeline to improve stds down the line +- Added trimming safe overloads for generic methods +- Added trim warnings for generic and reflection methods +- Added optional header to the progress bar ## v1.4.0 -* Greatly improved performance by taking generics or strings into output methods instead of objects to avoid boxing. +- Greatly improved performance by taking generics or strings into output methods instead of objects to avoid boxing. ### Upgrade notes -* Most methods, including `Write` and `WriteLine` with single parameter should not require any code modification as they will use generics -* Outputs methods which use `tuples` will require modification to enjoy the performance upgrade, with that said, existing code will be break because legacy `object` implementation was not removed but rather just marked as obsolete for now. +- Most methods, including `Write` and `WriteLine` with single parameter should not require any code modification as they will use generics +- Outputs methods which use `tuples` will require modification to enjoy the performance upgrade, with that said, existing code will be break because legacy `object` implementation was not removed but rather just marked as obsolete for now. ## v1.3.0 -* Fixed memory leak in `TreeMenu` -* Improved performance and reduced complexity -* Removed `Colors.Primary` - it reduced uniformity as default was very close in color. Now default will be used in place of both, if you like primary more, consider overriding the `Colors.Default` to `ConsoleColor.White` -* Added Labels - outputs with configurable background color. +- Fixed memory leak in `TreeMenu` +- Improved performance and reduced complexity +- Removed `Colors.Primary` - it reduced uniformity as default was very close in color. Now default will be used in place of both, if you like primary more, consider overriding the `Colors.Default` to `ConsoleColor.White` +- Added Labels - outputs with configurable background color. ## v1.2.0 -* Removed `Color` enum, use the `Colors` property instead -* Added progress bars, both regular and indeterminate -* Added documentation file so summaries will be visible +- Removed `Color` enum, use the `Colors` property instead +- Added progress bars, both regular and indeterminate +- Added documentation file so summaries will be visible ## v1.1.0 -* Added indeterminate progress-bar -* Added regular progress-bar -* Changed parameter-less `Write` and `WriteLine` to use color version with default values to trim unnecessary code. +- Added indeterminate progress-bar +- Added regular progress-bar +- Changed parameter-less `Write` and `WriteLine` to use color version with default values to trim unnecessary code. ## v1.0.2 -* Changed secondary color to be called the default color and also act as such, meaning by default `Write` and `WriteLine` will use it -* Internal outputs such as ReadLine or others will still use primary color +- Changed secondary color to be called the default color and also act as such, meaning by default `Write` and `WriteLine` will use it +- Internal outputs such as ReadLine or others will still use primary color ## v1.0.1 -* Moved `Color` enum into Console to reduce using statements -* Changed accessibility of extensions, they were meant to be used internally +- Moved `Color` enum into Console to reduce using statements +- Changed accessibility of extensions, they were meant to be used internally