spectreconsole / spectre.console

A .NET library that makes it easier to create beautiful console applications.

Home Page:https://spectreconsole.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Support to Terminal progress indicator to the the spectre console progress widget

rafaelsc opened this issue · comments

Spectre console is lacking integration with Operation System and Terminal progress Indication/feedback. And would be a nice to have feature, improving the feedback of the progress to the User.

Describe the solution you'd like
Any Progress or group of Progress update in Spectre console, update the Terminal Progress too.

Additional context


Please upvote 👍 this issue if you are interested in it.

Proof of concept. (Only for the progress bar, title bar update still need some work)

image

Windows.PowerShell.2023-12-06.22-49-40.mp4

Code:

using Spectre.Console;
using Spectre.Console.Rendering;

Console.CancelKeyPress += (_, _) => TerminalProgressUpdater.Clear();

var progress = AnsiConsole.Progress();
progress.RenderHook = Render;

await progress.StartAsync(async ctx =>
{
    var task1 = ctx.AddTask("[green]Reticulating splines[/]");
    var task2 = ctx.AddTask("[green]Folding space[/]");

    while (task1.Percentage < 50)
    {
        await Task.Delay(200);
        task1.Increment(1.5);
        task2.Increment(1);
    }
    
    var task3 = ctx.AddTask("[red]New Challenger[/]");

    while (!ctx.IsFinished)
    {
        await Task.Delay(100);
        task1.Increment(1.75);
        task2.Increment(1);
        task3.Increment(2);
    }
});

static IRenderable Render(IRenderable renderable, IReadOnlyList<ProgressTask> tasks)
{
    TerminalProgressUpdater.Update(tasks);
    return renderable;
}

internal static class TerminalProgressUpdater
{
    public static void Start() => AnsiConsole.Write(Ansi.Progress(State.Indeterminate, 0));
    public static void Indeterminate() => AnsiConsole.Write(Ansi.Progress(State.Indeterminate, 0));
    
    public static void Update(byte progress)
    {
        ArgumentOutOfRangeException.ThrowIfGreaterThan(progress, 100);
        AnsiConsole.Write(Ansi.Progress(State.Default, progress));
    }
    public static void Update(IReadOnlyList<ProgressTask> tasks)
    {
        ArgumentNullException.ThrowIfNull(tasks);
        if (tasks.Count == 0)
        {
            Finished();
            return;
        }
        var IsFinished = tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
        if (IsFinished)
        {
            Finished();
            return;
        }
        var isIndeterminate = tasks.Any(x => x.IsIndeterminate);
        if (isIndeterminate)
        {
            Indeterminate();
            return;
        }
        var allProgress = tasks.Sum(x => x.Percentage);
        var progress = (byte)(allProgress / tasks.Count);
        Update(progress);
    }

    public static void Finished() => AnsiConsole.Write(Ansi.Progress(State.Hidden, 0));
    public static void Clear() => AnsiConsole.Write(Ansi.Progress(State.Hidden, 0));

    private static class Ansi
    {
        public const string ESC = "\u001b";
        public const string BEL = "\u0007";
        public static string Progress(State state, byte progress) => $"{ESC}]9;4;{(byte)state};{progress}{BEL}";
    }

    private enum State
    {
        Hidden = 0,
        Default = 1,
        Error = 2,
        Indeterminate = 3,
        Warning = 4,
    }
}