| name | dotnet-spectre-console |
| description | Renders rich console output. Spectre.Console tables, trees, progress, prompts, live displays. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
dotnet-spectre-console
Spectre.Console for building rich console output (tables, trees, progress bars, prompts, markup, live displays) and Spectre.Console.Cli for structured command-line application parsing. Cross-platform across Windows, macOS, and Linux terminals.
Version assumptions: .NET 8.0+ baseline. Spectre.Console 0.54.0 (latest stable). Spectre.Console.Cli 0.53.1 (latest stable). Both packages target net8.0+ and netstandard2.0.
Scope
- Spectre.Console rich output: markup, tables, trees, progress bars, prompts, live displays
- Spectre.Console.Cli command-line application framework
Out of scope
- Full TUI applications (windows, menus, dialogs, views) -- see [skill:dotnet-terminal-gui]
- System.CommandLine parsing -- see [skill:dotnet-system-commandline]
- CLI application architecture and distribution -- see [skill:dotnet-cli-architecture] and [skill:dotnet-cli-distribution]
Cross-references: [skill:dotnet-terminal-gui] for full TUI alternative, [skill:dotnet-system-commandline] for System.CommandLine scope boundary, [skill:dotnet-cli-architecture] for CLI structure, [skill:dotnet-csharp-async-patterns] for async patterns, [skill:dotnet-csharp-dependency-injection] for DI with Spectre.Console.Cli, [skill:dotnet-accessibility] for TUI accessibility limitations and screen reader considerations.
Package References
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.54.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
</ItemGroup>
Spectre.Console.Cli has a dependency on Spectre.Console -- install both only when you need the CLI framework. For rich output only, Spectre.Console alone is sufficient.
Markup and Styling
Spectre.Console uses a BBCode-inspired markup syntax for styled console output.
Basic Markup
using Spectre.Console;
AnsiConsole.MarkupLine("[bold red]Error:[/] File not found.");
AnsiConsole.MarkupLine("[green]Success![/] Build completed in [blue]2.3s[/].");
AnsiConsole.MarkupLine("[underline]https://example.com[/]");
AnsiConsole.MarkupLine("[dim italic]This is subtle text[/]");
AnsiConsole.MarkupLine("[bold [red on white]Warning:[/] check config[/]");
AnsiConsole.MarkupLine("Use [[bold]] for bold text.");
Figlet Text
AnsiConsole.Write(
new FigletText("Hello!")
.Color(Color.Green)
.Centered());
Rule (Horizontal Line)
AnsiConsole.Write(new Rule());
AnsiConsole.Write(new Rule("[yellow]Section Title[/]"));
AnsiConsole.Write(new Rule("[blue]Left Aligned[/]").LeftJustified());
Tables
var table = new Table();
table.AddColumn("Name");
table.AddColumn(new TableColumn("Age").Centered());
table.AddColumn(new TableColumn("City").RightAligned());
table.AddRow("Alice", "30", "Seattle");
table.AddRow("[green]Bob[/]", "25", "Portland");
table.AddRow("Charlie", "35", "Vancouver");
table.Border(TableBorder.Rounded);
table.BorderColor(Color.Grey);
table.Title("[underline]Team Members[/]");
table.Caption("[dim]Updated daily[/]");
table.Columns[0].PadLeft(2);
table.Columns[0].NoWrap();
AnsiConsole.Write(table);
Nested Tables
var innerTable = new Table()
.AddColumn("Detail")
.AddColumn("Value")
.AddRow("Role", "Developer")
.AddRow("Level", "Senior");
var outerTable = new Table()
.AddColumn("Name")
.AddColumn("Info")
.AddRow("Alice", innerTable);
AnsiConsole.Write(outerTable);
Trees
var tree = new Tree("Solution");
var srcNode = tree.AddNode("[yellow]src[/]");
var apiNode = srcNode.AddNode("Api");
apiNode.AddNode("Controllers/");
apiNode.AddNode("Program.cs");
var libNode = srcNode.AddNode("Library");
libNode.AddNode("Services/");
var testNode = tree.AddNode("[blue]tests[/]");
testNode.AddNode("Api.Tests/");
tree.Style = Style.Parse("dim");
AnsiConsole.Write(tree);
Panels
var panel = new Panel("This is [green]important[/] content.")
.Header("[bold]Notice[/]")
.Border(BoxBorder.Rounded)
.BorderColor(Color.Blue)
.Padding(2, 1)
.Expand();
AnsiConsole.Write(panel);
Composing Renderables with Columns
AnsiConsole.Write(new Columns(
new Panel("Left panel").Expand(),
new Panel("Right panel").Expand()));
Progress Displays
Progress Bars
await AnsiConsole.Progress()
.AutoClear(false)
.HideCompleted(false)
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn())
.StartAsync(async ctx =>
{
var downloadTask = ctx.AddTask("[green]Downloading[/]", maxValue: 100);
var extractTask = ctx.AddTask("[blue]Extracting[/]", maxValue: 100);
while (!ctx.IsFinished)
{
await Task.Delay(50);
downloadTask.Increment(1.5);
if (downloadTask.Value > 50)
{
extractTask.Increment(0.8);
}
}
});
Status Spinners
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green bold"))
.StartAsync("Processing...", async ctx =>
{
await Task.Delay(1000);
ctx.Status("Compiling...");
ctx.Spinner(Spinner.Known.Star);
await Task.Delay(1000);
ctx.Status("Publishing...");
await Task.Delay(1000);
});
Prompts
Text Prompt
var name = AnsiConsole.Ask<string>("What's your [green]name[/]?");
var age = AnsiConsole.Ask<int>("What's your [green]age[/]?");
var city = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]city[/]:")
.DefaultValue("Seattle")
.ShowDefaultValue());
var password = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]password[/]:")
.Secret());
var email = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]email[/]:")
.Validate(input =>
input.Contains('@') && input.Contains('.')
? ValidationResult.Success()
: ValidationResult.Error("[red]Invalid email address[/]")));
var nickname = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]nickname[/] (optional):")
.AllowEmpty());
Confirmation Prompt
bool proceed = AnsiConsole.Confirm("Continue with deployment?");
Selection Prompt
var fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Pick a [green]fruit[/]:")
.PageSize(10)
.EnableSearch()
.WrapAround()
.AddChoices("Apple", "Banana", "Orange", "Mango", "Grape"));
var country = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Select [green]destination[/]:")
.AddChoiceGroup("Europe", "France", "Italy", "Spain")
.AddChoiceGroup("Asia", "Japan", "Thailand", "Vietnam"));
Multi-Selection Prompt
var toppings = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
.Title("Choose [green]toppings[/]:")
.PageSize(10)
.Required()
.InstructionsText("[grey](Press [blue]<space>[/] to toggle, [green]<enter>[/] to accept)[/]")
.AddChoices("Cheese", "Pepperoni", "Mushrooms", "Olives", "Onions"));
Live Displays
Live displays update in-place for dynamic content that changes over time.
var table = new Table()
.AddColumn("Time")
.AddColumn("Status");
await AnsiConsole.Live(table)
.AutoClear(false)
.Overflow(VerticalOverflow.Ellipsis)
.Cropping(VerticalOverflowCropping.Bottom)
.StartAsync(async ctx =>
{
table.AddRow(DateTime.Now.ToString("T"), "[yellow]Starting...[/]");
ctx.Refresh();
await Task.Delay(1000);
table.AddRow(DateTime.Now.ToString("T"), "[green]Processing...[/]");
ctx.Refresh();
await Task.Delay(1000);
table.AddRow(DateTime.Now.ToString("T"), "[blue]Complete![/]");
ctx.Refresh();
});
Replacing the Target
await AnsiConsole.Live(new Markup("[yellow]Initializing...[/]"))
.StartAsync(async ctx =>
{
await Task.Delay(1000);
ctx.UpdateTarget(new Markup("[green]Ready![/]"));
await Task.Delay(1000);
ctx.UpdateTarget(
new Panel("Final result: [bold]42[/]")
.Header("Done")
.Border(BoxBorder.Rounded));
});
Spectre.Console.Cli Framework
Spectre.Console.Cli provides a structured command-line parsing framework with command hierarchies, typed settings, validation, and automatic help generation.
Basic Command App
using Spectre.Console.Cli;
var app = new CommandApp<GreetCommand>();
return app.Run(args);
public sealed class GreetSettings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("The person to greet")]
public string Name { get; init; } = string.Empty;
[CommandOption("-c|--count")]
[Description("Number of times to greet")]
[DefaultValue(1)]
public int Count { get; init; }
[CommandOption("--shout")]
[Description("Greet in uppercase")]
public bool Shout { get; init; }
}
public sealed class GreetCommand : Command<GreetSettings>
{
public override int Execute(CommandContext context, GreetSettings settings)
{
for (int i = 0; i < settings.Count; i++)
{
greeting = ;
AnsiConsole.MarkupLine(settings.Shout
?
: greeting);
}
;
}
}
Command Hierarchy with Branches
var app = new CommandApp();
app.Configure(config =>
{
config.AddBranch<RemoteSettings>("remote", remote =>
{
remote.AddCommand<RemoteAddCommand>("add")
.WithDescription("Add a remote");
remote.AddCommand<RemoteRemoveCommand>("remove")
.WithDescription("Remove a remote");
});
config.AddCommand<CloneCommand>("clone")
.WithDescription("Clone a repository");
});
return app.Run(args);
public class RemoteSettings : CommandSettings
{
[CommandOption("-v|--verbose")]
[Description("Verbose output")]
public bool Verbose { get; init; }
}
public sealed class RemoteAddSettings : RemoteSettings
{
[CommandArgument(0, "<name>")]
public string Name { get; init; } = string.Empty;
[CommandArgument(1, "<url>")]
public string Url { get; init; } = string.Empty;
}
public sealed : <>
{
{
(settings.Verbose)
{
AnsiConsole.MarkupLine();
}
AnsiConsole.MarkupLine();
;
}
}
Settings Validation
public sealed class DeploySettings : CommandSettings
{
[CommandArgument(0, "<environment>")]
public string Environment { get; init; } = string.Empty;
[CommandOption("--timeout")]
[DefaultValue(30)]
public int TimeoutSeconds { get; init; }
public override ValidationResult Validate()
{
var validEnvs = new[] { "dev", "staging", "prod" };
if (!validEnvs.Contains(Environment, StringComparer.OrdinalIgnoreCase))
{
return ValidationResult.Error(
$"Environment must be one of: {string.Join(", ", validEnvs)}");
}
if (TimeoutSeconds <= 0)
{
return ValidationResult.Error("Timeout must be positive");
}
return ValidationResult.Success();
}
}
Async Commands
public sealed class FetchCommand : AsyncCommand<FetchSettings>
{
public override async Task<int> ExecuteAsync(
CommandContext context, FetchSettings settings)
{
await AnsiConsole.Status()
.StartAsync("Fetching data...", async ctx =>
{
await Task.Delay(2000);
});
AnsiConsole.MarkupLine("[green]Done![/]");
return 0;
}
}
Dependency Injection with ITypeRegistrar
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;
var services = new ServiceCollection();
services.AddSingleton<IGreetingService, GreetingService>();
services.AddSingleton<IAnsiConsole>(AnsiConsole.Console);
var registrar = new TypeRegistrar(services);
var app = new CommandApp<GreetCommand>(registrar);
return app.Run(args);
public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar
{
public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider());
public void Register(Type service, Type implementation)
=> services.AddSingleton(service, implementation);
public void RegisterInstance(Type service, object implementation)
=> services.AddSingleton(service, implementation);
public void RegisterLazy(Type service, Func<object> factory)
=> services.AddSingleton(service, _ => factory());
}
public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver
{
public ? Resolve(Type? type)
=> type ? : provider.GetService(type);
}
{
{
message = greetingService.GetGreeting(settings.Name);
AnsiConsole.MarkupLine(message);
;
}
}
Testable Console Output
Spectre.Console provides IAnsiConsole for testable output instead of writing directly to the real console.
IAnsiConsole console = AnsiConsole.Console;
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Out = new AnsiConsoleOutput(new StringWriter())
});
console.MarkupLine("[green]Testable output[/]");
console.Write(new Table().AddColumn("Col").AddRow("Val"));
Agent Gotchas
- Do not use
AnsiConsole.Markup* in redirected output. When stdout is redirected (piped to a file or another process), ANSI escape codes corrupt the output. Check AnsiConsole.Profile.Capabilities.Ansi before using markup, or use IAnsiConsole with appropriate settings. See [skill:dotnet-csharp-async-patterns] for async pipeline patterns.
- Do not assume ANSI support in CI environments. CI runners (GitHub Actions, Azure Pipelines) may not support ANSI escape codes. Set
TERM=dumb or use AnsiConsole.Create() with ColorSystemSupport.NoColors for CI-safe output. Spectre.Console auto-detects capabilities, but explicit configuration prevents flaky rendering.
- Do not mix
AnsiConsole static calls with IAnsiConsole instance calls. Static AnsiConsole.Write() always targets the real console. When using DI with IAnsiConsole, consistently use the injected instance. Mixing the two produces duplicated or interleaved output.
- Do not modify a renderable from a background thread during
Live(). Live displays are not thread-safe. Mutate the target renderable only inside the Start/StartAsync callback, then call ctx.Refresh(). Concurrent mutations cause corrupted terminal output.
- Do not use prompts in non-interactive contexts.
TextPrompt, SelectionPrompt, and ConfirmationPrompt block waiting for user input. In CI or automated scripts, use environment variables or command-line arguments for input instead of prompts. Check AnsiConsole.Profile.Capabilities.Interactive before prompting.
- Do not confuse Spectre.Console.Cli with System.CommandLine. They are independent frameworks with different APIs. Spectre.Console.Cli uses
CommandSettings classes with [CommandArgument]/[CommandOption] attributes, while System.CommandLine uses Option<T> and Argument<T> builder pattern. Do not mix APIs. For System.CommandLine, see [skill:dotnet-system-commandline].
- Do not forget
ctx.Refresh() after modifying live display content. Changes to tables, trees, or panels inside a Live() callback are not rendered until ctx.Refresh() is called. Omitting it produces stale displays.
Prerequisites
- NuGet packages:
Spectre.Console 0.54.0 for rich output; add Spectre.Console.Cli 0.53.1 for CLI framework
- Target framework: net8.0 or later (also supports netstandard2.0)
- Terminal: Any terminal emulator supporting ANSI escape sequences. Windows Terminal, iTerm2, or modern Linux terminal recommended for best experience (TrueColor, Unicode). Console output degrades gracefully on limited terminals.
- For DI with Spectre.Console.Cli:
Microsoft.Extensions.DependencyInjection package for the ITypeRegistrar/ITypeResolver bridge
References