CLI приложения #14. Прогресс-бары в Spectre.Console
В прошлом посте мы написали прогресс-бар руками и уперлись в ограничение: он умеет показывать ровно одну задачу. А реальные утилиты часто делают несколько дел сразу — скачивают три файла параллельно, обрабатывают данные в несколько потоков. Каждой задаче нужна своя полоса, и все они должны обновляться независимо.
Писать такое самому — это уже задача со звёздочкой: управление позицией курсора по вертикали, подсчёт занятых строк и обработка изменения размера окна. В лучших традициях - всё придумано до нас и можно взять готовое решение.
AnsiConsole.Progress()
.Start(ctx =>
{
var download = ctx.AddTask("Скачивание");
var process = ctx.AddTask("Обработка");
while (!ctx.IsFinished)
{
download.Increment(2.5);
process.Increment(1.2);
Thread.Sleep(100);
}
});
Задачи создаются через AddTask, по умолчанию у каждой шкала от 0 до 100. Свойство ctx.IsFinished становится истинным, когда все задачи дошли до максимума.
Если общий объём работы известен заранее, шкалу удобно задать в единицах самой задачи — например, в количестве файлов:
var files = Directory.GetFiles("data");
AnsiConsole.Progress()
.Start(ctx =>
{
var task = ctx.AddTask("Обработка файлов", maxValue: files.Length);
foreach (var file in files)
{
ProcessFile(file);
task.Increment(1);
}
});
Никаких процентов считать не нужно — библиотека переведёт сама.
Внешний вид собирается из колонок, и здесь можно взять ровно то, что нужно:
AnsiConsole.Progress()
.AutoClear(false) // оставить полосы на экране после завершения
.HideCompleted(false) // не прятать завершённые задачи
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn())
.Start(ctx => { /* ... */ });
Колонка RemainingTimeColumn — это та самая оценка времени, которую мы в прошлом посте считали вручную. Здесь она просто есть.
Для параллельной работы есть асинхронный вариант:
await AnsiConsole.Progress().StartAsync(async ctx =>
{
var tasks = urls.Select(url =>
{
var task = ctx.AddTask(Markup.Escape(url));
return DownloadAsync(url, task);
});
await Task.WhenAll(tasks);
});
Объект задачи передаётся внутрь метода скачивания, и тот дёргает task.Increment(...) по мере получения данных. Полосы обновляются независимо друг от друга.
Важно!
Пока активен прогресс-бар, нельзя писать в консоль напрямую. ЛюбойConsole.WriteLineилиAnsiConsole.MarkupLineиз середины цикла разрушит картинку: библиотека перерисовывает область экрана и не знает о вашем выводе. Сообщения либо накапливайте и выводите после завершения, либо отражайте прямо в описании задачи:
task.Description = $"Обработка: {Markup.Escape(currentFile)}";
По той же причине одновременно может быть активен только один блок Progress.
По умолчанию вывод уходит в stdout, но прогресс — не результат работы, поэтому правильнее отправить его во второй поток:
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Out = new AnsiConsoleOutput(Console.Error)
});
console.Progress().Start(ctx => { /* ... */ });
Приятный бонус: при перенаправлении вывода библиотека сама переключится в спокойный режим без анимации. То, что в прошлом посте мы делали проверкой IsErrorRedirected, здесь работает из коробки.