dotnet-spectre-console dotnet-spectre-console

dotnet-spectre-console 是一个基于 .NET 的库,用于创建富文本控制台应用程序,提供表格、树、进度条、提示和实时显示等功能,以及 Spectre.Console.Cli 框架用于结构化命令行应用解析。支持跨平台,适用于构建交互式命令行工具和自动化脚本,关键词包括 .NET、控制台输出、Spectre.Console、CLI框架、跨平台、富文本、表格、树、进度条。

后端开发 0 次安装 0 次浏览 更新于 3/6/2026

name: dotnet-spectre-console description: “渲染丰富的控制台输出。Spectre.Console 表格、树、进度、提示、实时显示。” user-invocable: false

dotnet-spectre-console

Spectre.Console 用于构建丰富的控制台输出(表格、树、进度条、提示、标记、实时显示)和 Spectre.Console.Cli 用于结构化命令行应用程序解析。跨平台支持 Windows、macOS 和 Linux 终端。

版本假设: .NET 8.0+ 基线。Spectre.Console 0.54.0(最新稳定版)。Spectre.Console.Cli 0.53.1(最新稳定版)。两个包都支持 net8.0+ 和 netstandard2.0。

范围

  • Spectre.Console 丰富输出:标记、表格、树、进度条、提示、实时显示
  • Spectre.Console.Cli 命令行应用程序框架

超出范围

  • 完整的 TUI 应用程序(窗口、菜单、对话框、视图)—— 参见 [skill:dotnet-terminal-gui]
  • System.CommandLine 解析 —— 参见 [skill:dotnet-system-commandline]
  • CLI 应用程序架构和分发 —— 参见 [skill:dotnet-cli-architecture] 和 [skill:dotnet-cli-distribution]

交叉引用:[skill:dotnet-terminal-gui] 用于完整的 TUI 替代方案,[skill:dotnet-system-commandline] 用于 System.CommandLine 范围边界,[skill:dotnet-cli-architecture] 用于 CLI 结构,[skill:dotnet-csharp-async-patterns] 用于异步模式,[skill:dotnet-csharp-dependency-injection] 用于 DI 与 Spectre.Console.Cli,[skill:dotnet-accessibility] 用于 TUI 可访问性限制和屏幕阅读器考虑。


包引用

<ItemGroup>
  <!-- 丰富的控制台输出:标记、表格、树、进度、提示、实时显示 -->
  <PackageReference Include="Spectre.Console" Version="0.54.0" />

  <!-- CLI 命令框架(添加命令解析、设置、DI 支持) -->
  <PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
</ItemGroup>

Spectre.Console.Cli 依赖于 Spectre.Console —— 仅当需要 CLI 框架时安装两者。对于仅丰富输出,单独使用 Spectre.Console 足够。


标记和样式

Spectre.Console 使用受 BBCode 启发的标记语法用于样式化控制台输出。

基本标记

using Spectre.Console;

// 使用标记标签的样式化文本
AnsiConsole.MarkupLine("[bold red]错误:[/] 文件未找到。");
AnsiConsole.MarkupLine("[green]成功![/] 构建在 [blue]2.3 秒[/] 内完成。");
AnsiConsole.MarkupLine("[underline]https://example.com[/]");
AnsiConsole.MarkupLine("[dim italic]这是细微文本[/]");

// 嵌套样式
AnsiConsole.MarkupLine("[bold [red on white]警告:[/] 检查配置[/]");

// 使用双括号转义括号
AnsiConsole.MarkupLine("使用 [[bold]] 表示粗体文本。");

Figlet 文本

AnsiConsole.Write(
    new FigletText("Hello!")
        .Color(Color.Green)
        .Centered());

规则(水平线)

// 简单规则
AnsiConsole.Write(new Rule());

// 带标题的规则
AnsiConsole.Write(new Rule("[yellow]章节标题[/]"));

// 对齐规则
AnsiConsole.Write(new Rule("[blue]左对齐[/]").LeftJustified());

表格

var table = new Table();

// 添加列
table.AddColumn("名称");
table.AddColumn(new TableColumn("年龄").Centered());
table.AddColumn(new TableColumn("城市").RightAligned());

// 添加行
table.AddRow("Alice", "30", "西雅图");
table.AddRow("[green]Bob[/]", "25", "波特兰");
table.AddRow("Charlie", "35", "温哥华");

// 样式化
table.Border(TableBorder.Rounded);
table.BorderColor(Color.Grey);
table.Title("[underline]团队成员[/]");
table.Caption("[dim]每日更新[/]");

// 列配置
table.Columns[0].PadLeft(2);
table.Columns[0].NoWrap();

AnsiConsole.Write(table);

嵌套表格

var innerTable = new Table()
    .AddColumn("详情")
    .AddColumn("值")
    .AddRow("角色", "开发者")
    .AddRow("级别", "高级");

var outerTable = new Table()
    .AddColumn("名称")
    .AddColumn("信息")
    .AddRow("Alice", innerTable);

AnsiConsole.Write(outerTable);

var tree = new Tree("解决方案");

// 添加节点
var srcNode = tree.AddNode("[yellow]src[/]");
var apiNode = srcNode.AddNode("Api");
apiNode.AddNode("Controllers/");
apiNode.AddNode("Program.cs");

var libNode = srcNode.AddNode("库");
libNode.AddNode("服务/");

var testNode = tree.AddNode("[blue]tests[/]");
testNode.AddNode("Api.测试/");

// 样式化
tree.Style = Style.Parse("dim");

AnsiConsole.Write(tree);

面板

var panel = new Panel("这是 [green]重要[/] 内容。")
    .Header("[bold]通知[/]")
    .Border(BoxBorder.Rounded)
    .BorderColor(Color.Blue)
    .Padding(2, 1)    // 水平, 垂直
    .Expand();         // 填充可用宽度

AnsiConsole.Write(panel);

使用列组合可渲染对象

AnsiConsole.Write(new Columns(
    new Panel("左面板").Expand(),
    new Panel("右面板").Expand()));

进度显示

进度条

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]下载中[/]", maxValue: 100);
        var extractTask = ctx.AddTask("[blue]解压中[/]", maxValue: 100);

        while (!ctx.IsFinished)
        {
            await Task.Delay(50);
            downloadTask.Increment(1.5);

            if (downloadTask.Value > 50)
            {
                extractTask.Increment(0.8);
            }
        }
    });

状态旋转器

await AnsiConsole.Status()
    .Spinner(Spinner.Known.Dots)
    .SpinnerStyle(Style.Parse("green bold"))
    .StartAsync("处理中...", async ctx =>
    {
        await Task.Delay(1000);
        ctx.Status("编译中...");
        ctx.Spinner(Spinner.Known.Star);
        await Task.Delay(1000);
        ctx.Status("发布中...");
        await Task.Delay(1000);
    });

提示

文本提示

// 简单类型输入
var name = AnsiConsole.Ask<string>("你的 [green]名字[/] 是什么?");
var age = AnsiConsole.Ask<int>("你的 [green]年龄[/] 是多少?");

// 带默认值
var city = AnsiConsole.Prompt(
    new TextPrompt<string>("输入 [green]城市[/]:")
        .DefaultValue("西雅图")
        .ShowDefaultValue());

// 秘密输入(密码)
var password = AnsiConsole.Prompt(
    new TextPrompt<string>("输入 [green]密码[/]:")
        .Secret());

// 带验证
var email = AnsiConsole.Prompt(
    new TextPrompt<string>("输入 [green]电子邮件[/]:")
        .Validate(input =>
            input.Contains('@') && input.Contains('.')
                ? ValidationResult.Success()
                : ValidationResult.Error("[red]无效的电子邮件地址[/]")));

// 可选(允许空)
var nickname = AnsiConsole.Prompt(
    new TextPrompt<string>("输入 [green]昵称[/](可选):")
        .AllowEmpty());

确认提示

bool proceed = AnsiConsole.Confirm("继续部署吗?");

选择提示

var fruit = AnsiConsole.Prompt(
    new SelectionPrompt<string>()
        .Title("选择一个 [green]水果[/]:")
        .PageSize(10)
        .EnableSearch()
        .WrapAround()
        .AddChoices("苹果", "香蕉", "橙子", "芒果", "葡萄"));

// 分组选择
var country = AnsiConsole.Prompt(
    new SelectionPrompt<string>()
        .Title("选择 [green]目的地[/]:")
        .AddChoiceGroup("欧洲", "法国", "意大利", "西班牙")
        .AddChoiceGroup("亚洲", "日本", "泰国", "越南"));

多选提示

var toppings = AnsiConsole.Prompt(
    new MultiSelectionPrompt<string>()
        .Title("选择 [green]配料[/]:")
        .PageSize(10)
        .Required()
        .InstructionsText("[grey](按 [blue]<空格>[/] 切换,[green]<回车>[/] 接受)[/]")
        .AddChoices("奶酪", "意大利辣香肠", "蘑菇", "橄榄", "洋葱"));

实时显示

实时显示在原地更新动态内容。

var table = new Table()
    .AddColumn("时间")
    .AddColumn("状态");

await AnsiConsole.Live(table)
    .AutoClear(false)
    .Overflow(VerticalOverflow.Ellipsis)
    .Cropping(VerticalOverflowCropping.Bottom)
    .StartAsync(async ctx =>
    {
        table.AddRow(DateTime.Now.ToString("T"), "[yellow]启动中...[/]");
        ctx.Refresh();
        await Task.Delay(1000);

        table.AddRow(DateTime.Now.ToString("T"), "[green]处理中...[/]");
        ctx.Refresh();
        await Task.Delay(1000);

        table.AddRow(DateTime.Now.ToString("T"), "[blue]完成![/]");
        ctx.Refresh();
    });

替换目标

await AnsiConsole.Live(new Markup("[yellow]初始化中...[/]"))
    .StartAsync(async ctx =>
    {
        await Task.Delay(1000);
        ctx.UpdateTarget(new Markup("[green]准备就绪![/]"));
        await Task.Delay(1000);
        ctx.UpdateTarget(
            new Panel("最终结果: [bold]42[/]")
                .Header("完成")
                .Border(BoxBorder.Rounded));
    });

Spectre.Console.Cli 框架

Spectre.Console.Cli 提供了一个结构化的命令行解析框架,支持命令层次、类型化设置、验证和自动帮助生成。

基本命令应用

using Spectre.Console.Cli;

var app = new CommandApp<GreetCommand>();
return app.Run(args);

// 带类型化设置的命令
public sealed class GreetSettings : CommandSettings
{
    [CommandArgument(0, "<名称>")]
    [Description("要问候的人")]
    public string Name { get; init; } = string.Empty;

    [CommandOption("-c|--count")]
    [Description("问候次数")]
    [DefaultValue(1)]
    public int Count { get; init; }

    [CommandOption("--shout")]
    [Description("以大写字母问候")]
    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++)
        {
            var greeting = $"你好, {settings.Name}!";
            AnsiConsole.MarkupLine(settings.Shout
                ? $"[bold]{greeting.ToUpperInvariant()}[/]"
                : greeting);
        }
        return 0;  // 退出码
    }
}

带分支的命令层次

var app = new CommandApp();
app.Configure(config =>
{
    config.AddBranch<RemoteSettings>("remote", remote =>
    {
        remote.AddCommand<RemoteAddCommand>("add")
            .WithDescription("添加远程");
        remote.AddCommand<RemoteRemoveCommand>("remove")
            .WithDescription("移除远程");
    });

    config.AddCommand<CloneCommand>("clone")
        .WithDescription("克隆仓库");
});

return app.Run(args);

// 分支的共享设置 —— 被子命令继承
public class RemoteSettings : CommandSettings
{
    [CommandOption("-v|--verbose")]
    [Description("详细输出")]
    public bool Verbose { get; init; }
}

// 子命令设置从分支设置继承
public sealed class RemoteAddSettings : RemoteSettings
{
    [CommandArgument(0, "<名称>")]
    public string Name { get; init; } = string.Empty;

    [CommandArgument(1, "<URL>")]
    public string Url { get; init; } = string.Empty;
}

public sealed class RemoteAddCommand : Command<RemoteAddSettings>
{
    public override int Execute(CommandContext context, RemoteAddSettings settings)
    {
        if (settings.Verbose)
        {
            AnsiConsole.MarkupLine($"[dim]正在添加远程...[/]");
        }
        AnsiConsole.MarkupLine($"已添加远程 [green]{settings.Name}[/] -> {settings.Url}");
        return 0;
    }
}

设置验证

public sealed class DeploySettings : CommandSettings
{
    [CommandArgument(0, "<环境>")]
    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(
                $"环境必须是以下之一: {string.Join(", ", validEnvs)}");
        }

        if (TimeoutSeconds <= 0)
        {
            return ValidationResult.Error("超时必须为正数");
        }

        return ValidationResult.Success();
    }
}

异步命令

public sealed class FetchCommand : AsyncCommand<FetchSettings>
{
    public override async Task<int> ExecuteAsync(
        CommandContext context, FetchSettings settings)
    {
        await AnsiConsole.Status()
            .StartAsync("正在获取数据...", async ctx =>
            {
                await Task.Delay(2000); // 模拟工作
            });

        AnsiConsole.MarkupLine("[green]完成![/]");
        return 0;
    }
}

使用 ITypeRegistrar 的依赖注入

using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;

// 设置 DI 容器
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);

// TypeRegistrar 桥接 Microsoft DI 到 Spectre.Console.Cli
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 object? Resolve(Type? type)
        => type is null ? null : provider.GetService(type);
}

// 命令通过构造函数注入接收服务
public sealed class GreetCommand(IGreetingService greetingService) : Command<GreetSettings>
{
    public override int Execute(CommandContext context, GreetSettings settings)
    {
        var message = greetingService.GetGreeting(settings.Name);
        AnsiConsole.MarkupLine(message);
        return 0;
    }
}

可测试的控制台输出

Spectre.Console 提供 IAnsiConsole 用于可测试的输出,而不是直接写入真实控制台。

// 生产环境:使用 AnsiConsole.Console(真实控制台)
IAnsiConsole console = AnsiConsole.Console;

// 测试环境:使用录制控制台
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
    Out = new AnsiConsoleOutput(new StringWriter())
});

// 使用抽象而不是静态 AnsiConsole 方法
console.MarkupLine("[green]可测试输出[/]");
console.Write(new Table().AddColumn("列").AddRow("值"));

代理注意事项

  1. 不要在重定向输出中使用 AnsiConsole.Markup* 当 stdout 被重定向时(管道到文件或其他进程),ANSI 转义码会破坏输出。在使用标记前检查 AnsiConsole.Profile.Capabilities.Ansi,或使用具有适当设置的 IAnsiConsole。参见 [skill:dotnet-csharp-async-patterns] 获取异步管道模式。
  2. 不要假设 CI 环境支持 ANSI。 CI 运行器(GitHub Actions、Azure Pipelines)可能不支持 ANSI 转义码。设置 TERM=dumb 或使用带 ColorSystemSupport.NoColorsAnsiConsole.Create() 以确保 CI 安全输出。Spectre.Console 自动检测能力,但显式配置可防止渲染问题。
  3. 不要混合 AnsiConsole 静态调用和 IAnsiConsole 实例调用。 静态 AnsiConsole.Write() 始终针对真实控制台。当使用带有 IAnsiConsole 的 DI 时,应一致使用注入的实例。混合两者会导致重复或交错输出。
  4. 不要在后台线程中从 Live() 修改可渲染对象。 实时显示不是线程安全的。仅在 Start/StartAsync 回调内修改目标可渲染对象,然后调用 ctx.Refresh()。并发修改会导致终端输出损坏。
  5. 不要在非交互式上下文中使用提示。 TextPromptSelectionPromptConfirmationPrompt 会阻塞等待用户输入。在 CI 或自动化脚本中,使用环境变量或命令行参数作为输入,而不是提示。在使用提示前检查 AnsiConsole.Profile.Capabilities.Interactive
  6. 不要混淆 Spectre.Console.Cli 和 System.CommandLine。 它们是具有不同 API 的独立框架。Spectre.Console.Cli 使用带 [CommandArgument]/[CommandOption] 属性的 CommandSettings 类,而 System.CommandLine 使用 Option<T>Argument<T> 构建器模式。不要混合 API。对于 System.CommandLine,参见 [skill:dotnet-system-commandline]。
  7. 不要忘记在修改实时显示内容后调用 ctx.Refresh()Live() 回调中对表格、树或面板的更改不会渲染,直到调用 ctx.Refresh()。省略它会导致显示内容过时。
  8. 不要硬编码颜色值而不提供回退。 支持有限颜色的终端会静默降级 TrueColor 值。尽可能使用命名颜色(Color.Green),并使用 NO_COLOR=1 环境变量测试以验证优雅降级。

先决条件

  • NuGet 包: Spectre.Console 0.54.0 用于丰富输出;添加 Spectre.Console.Cli 0.53.1 用于 CLI 框架
  • 目标框架: net8.0 或更高版本(也支持 netstandard2.0)
  • 终端: 任何支持 ANSI 转义序列的终端模拟器。推荐 Windows Terminal、iTerm2 或现代 Linux 终端以获得最佳体验(TrueColor、Unicode)。在有限终端上控制台输出优雅降级。
  • 对于带 Spectre.Console.Cli 的 DI: Microsoft.Extensions.DependencyInjection 包用于 ITypeRegistrar/ITypeResolver 桥接

参考