spectreconsole / spectre.console

A .NET library that makes it easier to create beautiful console applications.
https://spectreconsole.net
MIT License
9.08k stars 464 forks source link

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

Open rafaelsc opened 8 months ago

rafaelsc commented 8 months ago

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 :+1: this issue if you are interested in it.

rafaelsc commented 8 months ago

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

image

https://github.com/spectreconsole/spectre.console/assets/502282/1b5f5b21-037c-49f9-a363-d5c9bb2240cd

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,
    }
}