Part of the Kiwi Foundation
Kiwify.Kiwi.Presentation is the terminal output infrastructure layer of the Kiwi Foundation - a trio of libraries (Presentation, Renderer, CLI) that together provide the core infrastructure for building professional .NET command-line and operational tooling applications: styled output, structured data display, and argument parsing.
Presentation is the foundation of the stack. Renderer uses IOutputWriter and TextStyle to colour table cells and JSON tokens. CLI uses IOutput and StyledConsole to format help text and validation errors. All three libraries share a common output pipeline with no external package dependencies.
Source and installation
GitHub: kiwifylabs/kiwi-foundation-tooling - Kiwify.Kiwi.Presentation
NuGet: Kiwify.Kiwi.Presentation
dotnet add package Kiwify.Kiwi.Presentation
Design philosophy
Presentation is organized around two abstractions that the rest of the Kiwi Foundation builds on.
IOutputWriter is the low-level writer interface: a styled Write / WriteLine / Flush contract backed by an ANSI-aware implementation, a plain-text implementation, and an in-memory implementation for testing. All rendering in Kiwi Renderer flows through IOutputWriter. OutputWriterFactory resolves the appropriate implementation at construction time based on terminal capabilities - ANSI support, output redirection - without any application-level conditionals.
IOutput is the semantic channel abstraction: WriteInfo, WriteResult, WriteError, WriteProgress. These channels carry intent, not just text. CLI uses them to separate user-facing status from structured output, enabling reliable test assertions against specific channel content and clean separation of stdout and stderr behaviour.
On top of these two abstractions, Presentation provides four independent controls - styled text, prompts, progress bars, and choice selectors - each usable in isolation. The controls are architecturally independent and can be used individually without coupling application code to the others.
The library ships as a single assembly with no NuGet dependencies beyond the BCL. It can be adopted in any .NET application without version conflicts or transitive package concerns, and it remains available as a standalone output layer outside of Kiwi Foundation contexts.
Intent
Building a terminal tool that behaves consistently across environments involves several distinct concerns: applying colour and emphasis without hardcoding ANSI escape sequences; falling back gracefully when output is redirected or the terminal lacks ANSI support; reading validated, type-safe input from the user; reporting progress on long-running operations; and presenting navigable option lists without reimplementing keyboard handling each time.
These concerns span the full lifecycle of a terminal interaction. Presentation addresses them through a shared output pipeline and four independent controls, each with a builder interface. Application code specifies intent; the library handles terminal capabilities, encoding, and fallback behaviour.
Architectural benefits
| Benefit | Description |
|---|---|
| Shared output abstraction |
IOutputWriter and IOutput are the integration point for the entire Kiwi stack. Renderer renders through IOutputWriter. CLI channels through IOutput. All output participates in the same pipeline and can be redirected, captured, or replaced at a single boundary. |
| Runtime capability detection |
OutputWriterFactory selects between ANSI-aware and plain-text writer implementations at construction time, based on Console.IsOutputRedirected and Windows Console API availability. No application-level conditionals required. |
| Testable by design |
StringOutput captures all semantic channels (WriteResult, WriteError, etc.) in memory. CollectorWriter captures IOutputWriter writes. No mocking of Console or TextWriter required. |
| Independent controls |
TextPrompt<T>, ConsoleProgressBar, ConsoleChoiceSelector, and StyledConsole are independent. Use any subset without pulling in the rest. |
| Composable with Renderer and CLI | A writer or output instance created in Presentation can be passed directly into Renderer or CLI without adapters. The three libraries compose through shared abstractions, not shared implementation. |
| Zero external dependencies | Ships as a single assembly. No transitive NuGet packages. Compatible with any .NET Standard 2.1+ application. |
Where it fits
Presentation is the appropriate output infrastructure for any .NET application that writes to a terminal and requires consistent, testable behaviour.
-
CLI and DevOps tooling - styled output, progress reporting, and interactive prompts, with test coverage through
StringOutputandIOutputchannel assertions. -
Automated pipelines -
OutputWriterFactorystrips styling automatically when output is redirected; no application code changes when running in CI. -
Administrative and support tools - choice selectors and prompts for interactive sessions;
IOutputchannels for structured result and error separation. - Long-running background tools - dual-bar progress reporting with category labels for jobs with multiple stages.
- Any Kiwi Foundation tool - Presentation is the shared output pipeline for CLI and Renderer. Adopting it ensures all output - from prompts to table rendering - flows through a single consistent abstraction.
Comparison with alternatives
vs. Spectre.Console
Spectre.Console is a comprehensive .NET terminal library offering rich widgets - trees, tables with live rendering, exception displays, and advanced layout composition. It is a strong choice for applications where sophisticated terminal presentation is the primary concern.
Presentation focuses on output infrastructure and operational interaction patterns - semantic output channels, capability-aware writers, prompts, selectors, and progress reporting - rather than a comprehensive terminal widget model. It provides the output pipeline that Renderer and CLI build on: IOutputWriter, IOutput, ANSI-aware capability detection, and testable output capture. Its controls - styled text, prompts, progress bars, choice selectors - cover the common interactive patterns in tooling contexts without the weight of a full terminal widget framework. For teams building on the Kiwi Foundation stack, Presentation provides the shared output infrastructure across all three layers.
vs. Pastel
Pastel provides string-extension methods for ANSI colour wrapping. It is minimal and has essentially no learning curve.
Kiwi Presentation covers a wider surface: terminal capability detection, the IOutputWriter / IOutput abstractions, validated prompts, progress bars, and choice selectors. Where Pastel applies colour, Presentation manages the full output pipeline.
vs. ConsoleTables / ConsoleMenu
These libraries each solve one problem - ASCII tables and menu navigation. Kiwi Presentation covers interactive selection through ConsoleChoiceSelector and integrates with the Kiwi Renderer table engine through the shared IOutputWriter abstraction.
vs. McMaster.Extensions.CommandLineUtils
That library bundles output alongside argument parsing. Kiwi Presentation is independent of the CLI layer and can be used in any context that writes to a terminal - long-running services, test harnesses, batch jobs - without coupling to argument parsing concerns.
How it is used - detailed walkthrough
Styled output
All markup parsing happens at call time; there is no compile step or pre-processing:
StyledConsole.WriteLine("[bold cyan]Build started[/]");
StyledConsole.WriteLine("[green] ✓ Compiled[/] [grey]0 warnings, 0 errors[/]");
StyledConsole.WriteLine("[red] ✗ Tests failed[/] [yellow]3 of 47[/]");
StyledConsole.WriteLine("[white on darkblue] PUBLISHED [/] v2.3.1");
StyledConsole.WriteLine("[#ff8800]Hex colour[/] [rgb(100,200,50)]RGB colour[/]");
Tags nest. Inner tags override the colour of the outer tag but inherit modifiers (bold, underline):
StyledConsole.WriteLine("[bold yellow]WARNING[/] [bold grey](check logs)[/]");
Use StyledConsole.StripMarkup(markup) to measure display width before padding, since markup characters have no visual width.
Writing through IOutputWriter
StyledConsole writes to Console.Out by default. Inject a writer to redirect output:
IOutputWriter writer = OutputWriterFactory.CreateConsoleWriter();
StyledConsole.WriteLine(writer, "[bold red]Critical:[/] out of disk space");
OutputWriterFactory.CreatePlainWriter(TextWriter) strips all styling - useful for log files, CI pipelines, and test assertions on raw text.
IOutput semantic channels
IOutput provides named channels with different semantics:
IOutput output = new ConsoleOutput(verbose: !args.Contains("--quiet"));
output.WriteInfo("Scanning…"); // Suppressed when verbose=false
output.WriteResult("42 issues found"); // Always shown; goes to stdout
output.WriteError("Permission denied"); // Always shown; goes to stderr
output.WriteProgress(".");
FileOutput writes to a file. StringOutput captures in memory for tests:
var out = new StringOutput();
MyCommand.Execute(out);
Assert.Contains("42 issues found", out.GetResults());
Prompts
TextPrompt<T> converts and validates in one step:
string name = new TextPrompt<string>()
.WithPromptText("Your name")
.Required("Name is required.")
.Show();
int port = new TextPrompt<int>()
.WithPromptText("Port")
.WithRange(1024, 65535)
.WithDefaultValue(8080)
.Show();
Uri endpoint = new TextPrompt<Uri>()
.WithPromptText("API endpoint")
.WithConverter(raw => new Uri(raw))
.WithConversionErrorMessage("Must be a valid URL")
.Show();
Multiple validators chain in order; the first failure short-circuits:
string cs = new TextPrompt<string>()
.WithPromptText("Connection string")
.Required()
.WithRegex(@"^Server=", "Must start with 'Server='")
.WithValidation(s => s.Contains("Database="), "Must include 'Database='")
.Show();
Progress bars
// Single bar
using var bar = new ConsoleProgressBar(ProgressBarStyle.Solid);
for (int i = 0; i <= 100; i++)
{
bar.Report(i / 100.0);
await Task.Delay(20);
}
bar.Complete();
// Dual bar: overall + current category
using var bar = new MultiConsoleProgressBar("Deploying", labelWidth: 20);
bar.NextCategory("Building", overallValue: 0.0);
bar.ReportCategory(1.0);
bar.NextCategory("Uploading", overallValue: 0.4);
for (double p = 0; p <= 1.0; p += 0.05) { bar.ReportCategory(p); }
bar.NextCategory("Verifying", overallValue: 0.9);
bar.ReportCategory(1.0);
bar.Complete();
Choice selectors
string region = new ConsoleChoiceSelector()
.WithCaption("Select region:")
.AddOption("US East", "us-east-1")
.AddOption("EU West", "eu-west-1")
.AddOption("AP South", "ap-south-1")
.Show();
IReadOnlyList<string> features = new ConsoleMultiChoiceSelector()
.WithCaption("Enable features:")
.AddOption("Audit logging", "audit")
.AddOption("Two-factor auth", "2fa")
.AddOption("Rate limiting", "rate")
.Show();
Sample screens
Styled console output
Build started
✓ Compiled 0 warnings, 0 errors
✗ Tests failed 3 of 47
PUBLISHED v2.3.1
Build started is bold cyan. The success line is green with the suffix in grey. The failure line is red with the count in yellow. PUBLISHED is white text on a dark blue background.
TextPrompt - interactive session
Your name: Alice Chen
Port [1024-65535, default: 8080]: 99
✗ Value must be between 1024 and 65535.
Port [1024-65535, default: 8080]: 3000
Connection string: Server=db;Database=
✗ Must include 'Database='.
Connection string: Server=db;Database=orders;
Prompts re-display after each validation failure. The first failing validator short-circuits; subsequent rules run only once earlier ones pass.
ConsoleChoiceSelector - single select
Select region:
❯ US East
EU West
AP South
Arrow keys move the cursor. Enter confirms. The returned value is the bound string passed to AddOption, not the display label - so selecting US East returns "us-east-1".
ConsoleMultiChoiceSelector - multi-select
Enable features:
[✓] Audit logging
[ ] Two-factor auth
[✓] Rate limiting
Space to toggle · Enter to confirm
Returns the bound values of all checked items as IReadOnlyList<string>.
Progress bars
Single bar:
[████████████████████████████████████░░░░] 90%
Dual bar with phase labels (MultiConsoleProgressBar):
Deploying
Building [████████████████████████████████████████] 100%
Uploading [████████████████████████░░░░░░░░░░░░░░░░] 60%
The overall bar advances with each NextCategory call; the category bar resets and tracks the current phase independently.
Thread safety notes
StyledConsole is stateless - the markup parser creates new state per call. Multiple threads can call Write / WriteLine concurrently, but interleaved writes will produce interleaved output. Lock the writer if concurrent rendering to the same sink is required.
Progress bars and prompts are single-threaded controls: construct from one thread, report from one thread.
Top comments (0)