Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
e51565b
Update to net10
dusrdev Nov 12, 2025
9032fd7
Use extension members
dusrdev Nov 12, 2025
63e217c
-
dusrdev Nov 12, 2025
8dc4907
Update workflow
dusrdev Nov 13, 2025
04b4dbf
Remove ColoredOutput 1
dusrdev Nov 13, 2025
ae0fcf3
Rework library to extend System.Console and ConsoleColor
dusrdev Nov 13, 2025
6b28423
Removed tests that tried to mock test external method
dusrdev Nov 13, 2025
d397891
Use internal apis
dusrdev Nov 13, 2025
c77daab
Fix old docs
dusrdev Nov 13, 2025
71d7fce
Fix old docs 2
dusrdev Nov 13, 2025
972252c
fix docs 3
dusrdev Nov 13, 2025
a5d0cc3
Removed newlines
dusrdev Nov 13, 2025
2d29e3a
Added SpanFormattable for object
dusrdev Nov 13, 2025
ae962bc
Updated versions
dusrdev Nov 13, 2025
0982714
Update agents.md
dusrdev Nov 13, 2025
37b6e8d
Update readme
dusrdev Nov 13, 2025
472d75f
Added size limiting to progress bar
dusrdev Nov 13, 2025
8d84562
Change "hr" format and add alignment overload
dusrdev Nov 13, 2025
48995c1
fix hr alignment
dusrdev Nov 13, 2025
64f8cb1
-
dusrdev Nov 13, 2025
66259f6
Constant output length for timeSpan hr
dusrdev Nov 13, 2025
3b3b87e
Update hr description in readme
dusrdev Nov 13, 2025
aa061d9
Simplify code and remove end padding
dusrdev Nov 13, 2025
6938025
Forward format for boxed arguments
dusrdev Nov 14, 2025
47569dd
Formatting
dusrdev Nov 14, 2025
825ad55
Added benchmarks
dusrdev Nov 14, 2025
7071703
Added benchmark results to git
dusrdev Nov 14, 2025
5a080b3
Added initial implementation of markup
dusrdev Nov 14, 2025
26b9484
Added more options
dusrdev Nov 14, 2025
fc4481d
Added full support for markup feature
dusrdev Nov 14, 2025
7d5f501
-
dusrdev Nov 14, 2025
522982a
Use ArrayPool to remove BufferPool overhead
dusrdev Nov 14, 2025
d6509e7
Allow empty span alignment
dusrdev Nov 14, 2025
7d3ccd3
Use arraypool
dusrdev Nov 14, 2025
6641c23
Faster color management
dusrdev Nov 14, 2025
e42d046
Make Handler smarter and ANSI aware
dusrdev Nov 14, 2025
5e65ab2
Clean benchmarks
dusrdev Nov 14, 2025
f0c612b
Fix invalid delegate call
dusrdev Nov 14, 2025
e7a7405
Added ansi colors internal class
dusrdev Nov 14, 2025
96ed589
Fix index out of range when ConsoleColor is -1
dusrdev Nov 14, 2025
95931d6
Skip work when disabled
dusrdev Nov 14, 2025
560efb4
Fix no default overload binding to string and creating invalid handler
dusrdev Nov 14, 2025
ecc6331
Properly test sequences
dusrdev Nov 14, 2025
21163f4
Fixed inaccurate test
dusrdev Nov 14, 2025
6f15271
Added Markup tests
dusrdev Nov 14, 2025
2374753
Increase coverage
dusrdev Nov 14, 2025
2c16e20
Extended mocking capabilities
dusrdev Nov 14, 2025
04899f2
Increase test coverage
dusrdev Nov 14, 2025
3b25bc1
Made handler struct to widen use-cases
dusrdev Nov 15, 2025
6704206
Mock TextWriter to remove IO influence on metrics
dusrdev Nov 15, 2025
818f48b
Add visibility to benchmarks to allow mocking
dusrdev Nov 15, 2025
5927cbe
Update config for more stability
dusrdev Nov 15, 2025
4dbb0ec
Expose NewLine from handler
dusrdev Nov 15, 2025
08b269c
Updated the benchmark to use spectre as baseline
dusrdev Nov 15, 2025
fae1767
Renamed hr format to duration, and added bytes for double
dusrdev Nov 15, 2025
44927d1
Update callsite with new format name
dusrdev Nov 15, 2025
199b4a9
Update output of duration in readme examples
dusrdev Nov 15, 2025
0c3170c
Added bytes formatting docs
dusrdev Nov 15, 2025
31b647c
formatting
dusrdev Nov 15, 2025
06b7788
Address larger than default file sizes by using larger buffer
dusrdev Nov 15, 2025
77cc1c1
Tweak handler custom formatting for more accurate buffer management
dusrdev Nov 15, 2025
93a7242
Added test to cover handler custom formats
dusrdev Nov 15, 2025
0411cdd
Formatting
dusrdev Nov 15, 2025
7a50e31
Added maxWidth option to progressBar class
dusrdev Nov 15, 2025
69a3c06
Updated versions
dusrdev Nov 15, 2025
ba4dfd5
Benchmark integration in readme draft
dusrdev Nov 15, 2025
1973d6b
Updated readme with results
dusrdev Nov 15, 2025
f9785fe
Better benchmark configuration
dusrdev Nov 15, 2025
13e084f
Update readme
dusrdev Nov 15, 2025
e2c00c8
Update release notes
dusrdev Nov 15, 2025
be71b4e
-
dusrdev Nov 15, 2025
3b5ea08
Formatting
dusrdev Nov 15, 2025
90f28bf
Use cleaner escape sequences
dusrdev Nov 15, 2025
ae6a42d
Cleanup
dusrdev Nov 15, 2025
8b46a76
Skip progress bar tests if CI doesn't have access to cursor handle
dusrdev Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/UnitTests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ jobs:
uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main
with:
platform: ${{ matrix.platform }}
dotnet-version: 9.0.x
dotnet-version: 10.0.x
test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj

unit-tests-debug:
uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main
with:
platform: ubuntu-latest
dotnet-version: 9.0.x
dotnet-version: 10.0.x
test-project-path: PrettyConsole.Tests.Unit/PrettyConsole.Tests.Unit.csproj
use-debug: true
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ nunit-*.xml
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/
# BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
Expand Down
28 changes: 17 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ Repository: PrettyConsole

Summary

- PrettyConsole is a high-performance, allocation-conscious wrapper over System.Console that provides structured colored output, input helpers, rendering controls, menus, and progress bars. It targets net9.0, is trimming/AOT ready, and ships SourceLink metadata for debugging.
- PrettyConsole is a high-performance, allocation-conscious extension layer over System.Console (implemented via C# extension members) that provides structured colored output, input helpers, rendering controls, menus, and progress bars. It targets net10.0, is trimming/AOT ready, and ships SourceLink metadata for debugging.
- Solution layout:
- PrettyConsole/ — main library
- PrettyConsole.Tests/ — interactive/demo runner (manually selects visual feature demos)
- PrettyConsole.Tests.Unit/ — xUnit v3 unit tests using Microsoft Testing Platform
- v5.0.0 (November 2025) removed the legacy `ColoredOutput`/`Color` types; color composition now flows through `ConsoleColor` helpers and tuples exposed by the library.

Commands you’ll use often

Expand Down Expand Up @@ -43,28 +44,32 @@ Repo-specific agent rules and conventions
High-level architecture and key concepts

- Console facade
- `PrettyConsole.Console` is a static, partial wrapper over `System.Console`. It exposes the live `In`, `Out`, and `Error` streams, and adds helpers like `NewLine`, `Clear`, `ClearNextLines`, `GetCurrentLine`, `GoToLine`, `SetColors`, and `ResetColors` for structured rendering.
- Extension members declared via `extension(Console)` attach directly to `System.Console`, so APIs such as `Console.WriteInterpolated`, `Console.TryReadLine`, `Console.Overwrite`, etc. light up once `using PrettyConsole;` (optionally with `using static System.Console;`) is in scope. `PrettyConsoleExtensions` still exposes the live `In`, `Out`, and `Error` streams plus helpers like `GetWidthOrDefault`.
- Output routing
- `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `Console.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly.
- `OutputPipe` is a two-value enum (`Out`, `Error`). Most write APIs accept an optional pipe; internally `PrettyConsoleExtensions.GetWriter` resolves the correct `TextWriter` so sequences remain redirect-friendly.
- Interpolated string handler
- `PrettyConsoleInterpolatedStringHandler` enables zero-allocation `$"..."` calls for `Write`, `WriteLine`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors automatically reset after each invocation, and handlers respect the selected pipe and optional `IFormatProvider`.
- `PrettyConsoleInterpolatedStringHandler` enables zero-allocation `$"..."` calls for `WriteInterpolated`, `WriteLineInterpolated`, `ReadLine`, `TryReadLine`, `Confirm`, and `RequestAnyInput`. Colors automatically reset after each invocation, and handlers respect the selected pipe and optional `IFormatProvider`. Recent changes ensure any `object` argument that implements `ISpanFormattable` is emitted through the span-based path before falling back to `IFormattable`/string allocations.
- Coloring model
- `ColoredOutput` and the `Color` record provide terse composition via `"Text" * Color.Red / Color.Blue` and implicit conversions. Default foreground/background values are stored on `Color` so spans render without string allocations.
- `ConsoleColor` now exposes `DefaultForeground`, `DefaultBackground`, and `Default` tuple properties plus `/` operator overloads so you can inline foreground/background tuples (`$"{ConsoleColor.Red / ConsoleColor.White}Error"`). These tuples play nicely with the interpolated string handler and keep color resets allocation-free.
- Markup decorations
- The `Markup` static class exposes ANSI sequences for underline, bold, italic, and strikethrough. Fields expand to escape codes only when output/error aren’t redirected; otherwise they collapse to empty strings so callers can safely interpolate them without extra checks.
- Write APIs
- `Write`/`WriteLine` cover interpolated strings, `ColoredOutput` spans, raw `ReadOnlySpan<char>`, and generic `ISpanFormattable` values (including `ref struct`s) with optional foreground/background colors and format providers. Internally they rely on `BufferPool` to avoid allocation spikes.
- `WriteInterpolated`/`WriteLineInterpolated` host the interpolated-string handler; `Write`/`WriteLine` overloads target `ISpanFormattable` values (including `ref struct`s) and raw `ReadOnlySpan<char>` spans with optional foreground/background overrides. Implementations rent buffers via `BufferPool` to avoid allocation spikes and always reset colors.
- TextWriter helpers
- `PrettyConsoleExtensions` surfaces a `TextWriter.WriteWhiteSpaces(int)` extension (available via `PrettyConsoleExtensions.Out/Error`) for allocation-free padding. Use it instead of creating temporary `string`s when building menus, tables, or progress output.
- Inputs
- `ReadLine`/`TryReadLine` support `IParsable<T>` 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<TState>` run user actions while clearing a configurable number of lines, enabling reactive text dashboards without leaving artifacts. `TypeWrite`/`TypeWriteLine` animate character-by-character output with adjustable delays.
- `OverwriteCurrentLine`, `Overwrite`, and `Overwrite<TState>` run user actions while clearing a configurable number of lines. Set the `lines` argument to however many rows you emit during the action (e.g., the multi-progress sample uses `lines: 2`) and call `Console.ClearNextLines` once after the last overwrite to remove residual UI. `TypeWrite`/`TypeWriteLine` animate character-by-character output with adjustable delays.
- Menus and tables
- `Selection` returns a single choice or empty string on invalid input; `MultiSelection` parses space-separated indices into string arrays; `TreeMenu` renders two-level hierarchies and validates input (throwing `ArgumentException` when selections are invalid); `Table` renders headers + columns with width calculations.
- Progress bars
- `IndeterminateProgressBar` binds to running `Task` instances, optionally starts tasks, supports cancellable `RunAsync` overloads, exposes `AnimationSequence`, `Patterns`, `ForegroundColor`, `DisplayElapsedTime`, and `UpdateRate`. Frames render on the error pipe and auto-clear.
- `ProgressBar` maintains a single-line bar on the error pipe. `Update` accepts `int`/`double` percentages plus optional status spans, and exposes `ProgressChar`, `ForegroundColor`, and `ProgressColor` for customization.
- `ProgressBar` maintains a single-line bar on the error pipe. `Update` accepts `int`/`double` percentages plus optional status spans, and exposes `ProgressChar`, `ForegroundColor`, and `ProgressColor` for customization. The static `ProgressBar.WriteProgressBar` helper renders one-off segments without moving the cursor (so you can stack multiple bars within an `Overwrite` block).
- Packaging and targets
- `PrettyConsole.csproj` targets net9.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project.
- `PrettyConsole.csproj` targets net10.0, enables trimming/AOT (`IsTrimmable`, `IsAotCompatible`), embeds SourceLink, and grants `InternalsVisibleTo` the unit-test project.

Testing structure and workflows

Expand All @@ -76,7 +81,8 @@ Testing structure and workflows

Notes and gotchas

- The library aims to minimize allocations; prefer span-based overloads (ReadOnlySpan<char>, ReadOnlySpan<ColoredOutput>) for best performance when contributing.
- The library aims to minimize allocations; prefer span-based overloads (`ReadOnlySpan<char>`, `ISpanFormattable`) plus the inline `ConsoleColor` tuples instead of recreating strings or structs.
- When authoring new features, pick the appropriate OutputPipe to keep CLI piping behavior intact.
- On macOS terminals, ANSI is supported; Windows legacy terminals are handled via ANSI-compatible rendering in the library.
- `ProgressBar.Update` re-renders on every call (even when the percentage is unchanged) and accepts `sameLine` to place the status above the bar; the static `ProgressBar.WriteProgressBar` renders one-off bars for multi-progress scenarios.
- `ProgressBar.Update` re-renders on every call (even when the percentage is unchanged) and accepts `sameLine` to place the status above the bar; the static `ProgressBar.WriteProgressBar` renders one-off bars without writing a trailing newline, so rely on `Console.Overwrite`/`lines` to stack multiple bars cleanly.
- After the final `Overwrite`/`Overwrite<TState>` call in a rendering loop, call `Console.ClearNextLines(totalLines, pipe)` once more to clear the region and prevent ghost text.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
```

BenchmarkDotNet v0.15.6, macOS 26.1 (25B78) [Darwin 25.1.0]
Apple M2 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK 10.0.100
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a
Job-NEXDCO : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a

OutlierMode=RemoveAll IterationCount=30 IterationTime=100ms
LaunchCount=3 WarmupCount=5

```
| Method | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|--------------- |------------:|--------------:|-------:|----------:|--------------:|
| PrettyConsole | 95.02 ns | 49.73x faster | - | - | NA |
| SpectreConsole | 4,725.48 ns | baseline | 2.0902 | 17840 B | |
| SystemConsole | 68.67 ns | 68.81x faster | 0.0028 | 24 B | 743.333x less |
18 changes: 18 additions & 0 deletions Benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.6" />
<PackageReference Include="Spectre.Console" Version="0.54.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../PrettyConsole/PrettyConsole.csproj" />
</ItemGroup>
</Project>
69 changes: 69 additions & 0 deletions Benchmarks/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Immutable;

using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;

using Perfolizer.Horology;

using Perfolizer.Mathematics.OutlierDetection;

namespace Benchmarks;

public class Config : ManualConfig {
public Config() {
UnionRule = ConfigUnionRule.AlwaysUseLocal;
SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
AddDiagnoser(MemoryDiagnoser.Default);
AddJob(Job.Default
.WithOutlierMode(OutlierMode.RemoveAll)
.WithLaunchCount(3)
.WithWarmupCount(5)
.WithIterationCount(30)
.WithIterationTime(TimeInterval.FromMilliseconds(100)));
AddColumnProvider(DefaultColumnProviders.Instance);
HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD);
WithOrderer(new GroupByTypeOrderer());
WithOptions(ConfigOptions.JoinSummary);
WithOptions(ConfigOptions.StopOnFirstError);
WithOptions(ConfigOptions.DisableLogFile);
AddExporter(MarkdownExporter.GitHub);
AddLogger(ConsoleLogger.Default);
}
}

internal sealed class GroupByTypeOrderer : IOrderer {
// Keep execution order as-declared (you can customize if you want)
public IEnumerable<BenchmarkCase> GetExecutionOrder(
ImmutableArray<BenchmarkCase> benchmarksCase,
IEnumerable<BenchmarkLogicalGroupRule>? order = null)
=> benchmarksCase;

// Sort rows in the summary: first by Type, then Method, then Params
public IEnumerable<BenchmarkCase> GetSummaryOrder(
ImmutableArray<BenchmarkCase> cases, Summary summary) =>
cases.OrderBy(c => c.Descriptor.Type.FullName)
.ThenBy(c => c.Parameters.DisplayInfo);

// We don’t use highlight groups
public string? GetHighlightGroupKey(BenchmarkCase benchmarkCase) => null;

// Tell BDN how to “group” rows in a joined summary (the section separator)
public string GetLogicalGroupKey(
ImmutableArray<BenchmarkCase> all, BenchmarkCase benchmarkCase)
=> benchmarkCase.Descriptor.Type.FullName!;

// Order the groups themselves (by class name)
public IEnumerable<IGrouping<string, BenchmarkCase>> GetLogicalGroupOrder(
IEnumerable<IGrouping<string, BenchmarkCase>> logicalGroups,
IEnumerable<BenchmarkLogicalGroupRule>? order = null)
=> logicalGroups.OrderBy(g => g.Key);

public bool SeparateLogicalGroups => true;
}
5 changes: 5 additions & 0 deletions Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using BenchmarkDotNet.Running;

using Benchmarks;

BenchmarkRunner.Run<StyledOutputBenchmarks>();
68 changes: 68 additions & 0 deletions Benchmarks/StyledOutputBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using BenchmarkDotNet.Attributes;

using PrettyConsole;

using Spectre.Console;

using static System.ConsoleColor;

namespace Benchmarks;

/// <summary>
/// Runs a benchmark printing the following output:
/// Hello {Green}John{ResetColor}, status = {Cyan}{Percentage}{Reset}%, Elapsed = {Yellow}{Elapsed:c}{Reset}
/// </summary>
[Config(typeof(Config))]
public class StyledOutputBenchmarks {
private static readonly TimeSpan Elapsed = new(1, 25, 31);
private const double Percentage = 57.91;

private TextWriter _outputWriter = default!;
private IAnsiConsole _ansiConsole = default!;

[GlobalSetup]
public void GlobalSetup() {
_outputWriter = Console.Out;
PrettyConsoleExtensions.Out = TextWriter.Null;
_ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings {
Out = new AnsiConsoleOutput(TextWriter.Null)
});
Console.SetOut(TextWriter.Null);
}

[GlobalCleanup]
public void GlobalCleanup() {
PrettyConsoleExtensions.Out = _outputWriter;
_ansiConsole = AnsiConsole.Console;
Console.SetOut(_outputWriter);
}

[Benchmark]
public int PrettyConsole() {
Console.WriteLineInterpolated($"Hello {Green}John{ConsoleColor.DefaultForeground}, status = {Cyan}{Percentage}{ConsoleColor.DefaultForeground}%, elapsed = {Yellow}{Elapsed:c}");
return int.MaxValue;
}

[Benchmark(Baseline = true)]
public int SpectreConsole() {
_ansiConsole.MarkupLineInterpolated($"Hello [green]John[/], status = [cyan]{Percentage}[/]%, elapsed = [yellow]{Elapsed:c}[/]");
return int.MaxValue;
}

[Benchmark]
public int SystemConsole() {
Console.Write("Hello ");
Console.ForegroundColor = Green;
Console.Write("John");
Console.ResetColor();
Console.Write(", status = ");
Console.ForegroundColor = Cyan;
Console.Write(Percentage);
Console.ResetColor();
Console.Write("%, elapsed = ");
Console.ForegroundColor = Yellow;
Console.WriteLine("{0:c}", Elapsed);
Console.ResetColor();
return int.MaxValue;
}
}
95 changes: 0 additions & 95 deletions PrettyConsole.Tests.Unit/AdvancedInputs.cs

This file was deleted.

Loading