CLI приложения #13. Пишем прогресс-бар
CLI приложения #13. Пишем прогресс-бар
В прошлом посте разобрали механику: \r возвращает курсор в начало строки, \u001b[K стирает остаток. Этого достаточно, чтобы собрать полноценный прогресс-бар — без единой зависимости.
Что в нём должно быть: полоса заполнения, проценты, и желательно оценка оставшегося времени. Полосу рисуем двумя символами — заполненная часть и пустая.
internal sealed class ProgressBar : IDisposable
{
private readonly int _total;
private readonly int _width;
private readonly bool _animated;
private readonly Stopwatch _watch = Stopwatch.StartNew();
public ProgressBar(int total, int width = 30)
{
_total = total;
_width = width;
_animated = !Console.IsErrorRedirected;
if (_animated)
Console.Error.Write("\u001b[?25l");
}
public void Report(int current)
{
if (!_animated)
return;
var fraction = Math.Clamp((double)current / _total, 0, 1);
var filled = (int)(fraction * _width);
var bar = new string('█', filled) + new string('░', _width - filled);
var eta = Estimate(fraction);
Console.Error.Write($"\r\u001b[K[{bar}] {fraction:P0} {eta}");
}
private string Estimate(double fraction)
{
if (fraction <= 0)
return "";
var elapsed = _watch.Elapsed;
var remaining = elapsed * (1 / fraction - 1);
return $"осталось ~{remaining:mm\\:ss}";
}
public void Dispose()
{
if (!_animated)
return;
Console.Error.Write("\u001b[?25h");
Console.Error.WriteLine();
}
}
Пользоваться удобно:
var files = Directory.GetFiles("data");
using (var bar = new ProgressBar(files.Length))
{
for (int i = 0; i < files.Length; i++)
{
ProcessFile(files[i]);
bar.Report(i + 1);
}
}
Интерфейс IDisposable здесь не просто так . Он гарантирует, что курсор вернётся на место и строка закроется переводом — даже если внутри цикла вылетит исключение. Ровно та задача, для которой using и придуман.
Про расчёт оставшегося времени. Формула простая: если за прошедшее время сделали 25% работы, значит на все 100% уйдёт вчетверо больше, а осталось три четверти от этого. Работает, пока скорость примерно постоянная. На неравномерной нагрузке оценка будет прыгать — это нормально, все так живут, включая apt и pip.
Важно!
Символы█и░требуют кодировкиUTF-8. НаWindowsеё нужно включить явно, иначе вместо полосы будут знаки вопроса:Console.OutputEncoding = Encoding.UTF8;А ещё, ранее был пост на тему замены кодировки.
Если хочется совместимости с совсем древними терминалами — можно взять # и -, выглядит скромнее, но работает везде.
Отдельно обратите внимание на поле _animated. Когда вывод перенаправлен, прогресс-бар просто молчит. Это лучше, чем засорять лог, но ещё лучше — в этом режиме печатать обычные строки с большим шагом, скажем каждые 10%. Тогда и в логе сборки будет видно, что процесс движется.
Мы написали около полусотни строк и получили то, что видели в десятках чужих утилит. Понимать эту механику полезно....
...Но в реальном проекте её писать не надо: в Spectre.Console прогресс-бары уже есть, причём с поддержкой нескольких задач одновременно. Ими и займёмся в следующем посте.