В нашем последнем разговоре о System.CommandLine мы узнали, как легко создавать приложения командной строки с помощью .NET и C#, следуя большинству советов из Рекомендаций по интерфейсу командной строки. Мы также ввели внедрение зависимостей, чтобы сделать наш код более гибким, удобным для чтения и тестирования.

Теперь, когда у нас есть готовые базовые настройки, пришло время улучшить взаимодействие с пользователем. До сих пор пользователи взаимодействовали с нашим приложением, вводя каждый аргумент при каждом вызове приложения. В следующей части моего путешествия по System.CommandLine мы собираемся сделать наши CLI-приложения красивыми, но в то же время интерактивными благодаря библиотеке Spectre.Console .NET.

Spectre.Console помогает создавать красивые интерактивные консольные приложения.

Spectre.Console — популярная библиотека .NET с открытым исходным кодом, поддерживаемая .NET Foundation, с более чем 7 тысячами звезд GitHub ⭐. Ключевые особенности:

  • Улучшенные подсказки и интерактивность: попрощайтесь с Console.ReadLine и Console.WriteLine. Теперь мы можем использовать сложные подсказки, такие как интерактивный выбор одного элемента, выбор множественного выбора, секретная подсказка, подсказки с проверкой и многое другое.
  • Расширенные возможности рендеринга: Spectre.Console предлагает 24-битные цвета, стили текста (например, полужирный, курсив и т. д.), различные виджеты (такие как таблицы, деревья и даже изображения ASCII), отображение прогресса. для длительных задач, контроля состояния и многого другого.
  • Совместимость терминала. Не все терминалы поддерживают эти расширенные функции. Но не волнуйтесь, Spectre.Console ловко определяет возможности терминала для настройки отображаемого вывода.

Интеграция Spectre.Console в консольное приложение System.CommandLine

Начните с установки NuGet-пакета Spectre.Console. В дальнейшем мы будем использовать интерфейс IAnsiConsole, так что давайте отложим в сторону статический класс System.Console и абстракцию IConsole из System.CommandLine. Далее мы добавим реализацию IAnsiConsole по умолчанию в наши службы внедрения зависимостей. Если вы не знакомы с методом UseDependencyInjection(...), вы можете взять код из моей предыдущей записи в блоге.

// [...]
var builder = new CommandLineBuilder(rootCommand);

builder.UseDefaults();
builder.UseDependencyInjection(services =>
{
    services.AddSingleton(AnsiConsole.Console); // <-- HERE!
});

return builder.Build().Invoke(args);

Создание красивой интерактивной команды

В демонстрационных целях давайте создадим команду, предназначенную для создания проекта .NET. Пользователю будет предложено выбрать тип проекта, а затем мы отобразим выбранный тип и нет операции фактический процесс создания проекта (поскольку это не является нашей целью здесь). Мы будем использовать класс SelectionPrompt<string> для представления доступных типов проектов, позволяя пользователю перемещаться с помощью клавиш со стрелками вверх и вниз. Мы также добавим несколько цветов, чтобы выделить части вывода нашей консоли.

Вот как выглядит код:

using System.CommandLine;
using Spectre.Console;

public class CreateProjectCommand : Command<CreateProjectCommandOptions, CreateProjectCommandOptionsHandler>
{
    public CreateProjectCommand()
        : base("create-project", "Create a .NET project")
    {
    }
}

public class CreateProjectCommandOptions : ICommandOptions
{
}

public class CreateProjectCommandOptionsHandler : ICommandOptionsHandler<CreateProjectCommandOptions>
{
    private readonly IAnsiConsole _console;
    private readonly IProjectManager _projectManager;

    public CreateProjectCommandOptionsHandler(IAnsiConsole console, IProjectManager projectManager)
    {
        this._console = console;
        this._projectManager = projectManager;
    }

    public async Task<int> HandleAsync(CreateProjectCommandOptions options, CancellationToken cancellationToken)
    {
        var prompt = new SelectionPrompt<string>()
            .Title("What [green]type of project[/] would you like to create?")
            .AddChoices(this._projectManager.ProjectTypes);

        var projectType = await prompt.ShowAsync(this._console, cancellationToken);
        this._console.MarkupLineInterpolated($"You selected [green]{projectType}[/].");

        await this._projectManager.CreateProjectAsync(projectType, cancellationToken);

        return 0;
    }
}

// Also add "services.AddSingleton<IProjectManager, NoopProjectManager>()" in the dependency injection setup
public interface IProjectManager
{
    string[] ProjectTypes { get; }

    Task CreateProjectAsync(string projectType, CancellationToken cancellationToken);
}

public sealed class NoopProjectManager : IProjectManager
{
    // For demonstrations purposes only
    public string[] ProjectTypes => new[]
    {
        "ASP.NET Core Web API",
        "ASP.NET Core Web App",
        "Class Library",
        "WPF Application",
    };

    public Task CreateProjectAsync(string projectType, CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

Вы заметили, как мы поддерживаем отмену пользователем (Ctrl+C) с помощью CancellationToken, предоставленного System.CommandLine? Эта функция невероятно полезна. Вы можете поймать OperationCanceledException, отобразить сообщение о выходе на выходе, очистить некоторые ресурсы и многое другое. Всегда старайтесь выполнить запрос пользователя на отмену, когда это возможно.

Модульное тестирование интерактивных команд

Поскольку мы используем абстракцию IAnsiConsole вместо статического класса AnsiConsole, у нас есть возможность модульного тестирования нашего обработчика команд. Этому способствует дополнительный NuGet-пакет Spectre.Console.Testing. Он обеспечивает TestConsole реализацию, в которой мы можем предустановить клавиши ввода. В приведенном ниже модульном тесте мы гарантируем, что при заданном списке типов проектов дважды нажав клавишу со стрелкой вниз, а затем клавишу ввода, выбирается третий тип проекта. Затем этот выбор передается методу зависимости IProjectManager.CreateProjectAsync.

using FakeItEasy;
using Spectre.Console.Testing;
using Xunit;

namespace HelloCommandLine.Tests;

public class CreateProjectCommandTests
{
    [Fact]
    public async Task Test1()
    {
        // I use FakeItEasy to mock the project manager: https://fakeiteasy.github.io/
        var projectManager = A.Fake<IProjectManager>();
        A.CallTo(() => projectManager.ProjectTypes).Returns(new[] { "a", "b", "c", "d" });

        var console = new TestConsole();
        console.Profile.Capabilities.Interactive = true;
        console.Input.PushKey(ConsoleKey.DownArrow);
        console.Input.PushKey(ConsoleKey.DownArrow);
        console.Input.PushKey(ConsoleKey.Enter);

        var handler = new CreateProjectCommandOptionsHandler(console, projectManager);

        var result = await handler.HandleAsync(new CreateProjectCommandOptions(), CancellationToken.None);

        Assert.Equal(0, result);

        A.CallTo(() => projectManager.CreateProjectAsync("c", CancellationToken.None))
            .MustHaveHappenedOnceExactly();
    }
}

Включение всех цветов, смайликов и анимированных счетчиков

Чтобы получить доступ к полному набору цветов, эмодзи и анимированных счетчиков в Spectre.Console, нам нужно изменить кодировку ввода и вывода консоли на UTF-8. Это не включено по умолчанию, но это легко сделать:

Console.InputEncoding = System.Text.Encoding.UTF8;
Console.OutputEncoding = System.Text.Encoding.UTF8;

Вы также можете рассмотреть возможность применения этого изменения только тогда, когда консольный ввод и вывод не перенаправляется.

Подведение итогов

Создавать удобные и привлекательные CLI-приложения в .NET намного проще благодаря объединенным возможностям System.CommandLine и Spectre.Console. Эти библиотеки — от улучшенных подсказок и интерактивности до расширенных возможностей рендеринга — позволяют разработчикам создавать надежные и привлекательные интерфейсы командной строки.

Вы пробовали использовать эти библиотеки раньше? Мне интересен ваш опыт!