spectreconsole / spectre.console

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

Implement fixed-size log-text area for `ProgressTask` #1520

Closed flobernd closed 5 months ago

flobernd commented 5 months ago

Is your feature request related to a problem? Please describe.

I would like to display live log output individually for each progress task - similar to what Docker does when building an image.

Describe the solution you'd like

ProgressTask should provide a WriteLine method that allows to output log strings to the console. These log strings should be displayed below the progress bar. The log area for each progress task should have a (configurable) fixed height of e.g. 10 lines. After exceeding the maximum, text should start to scroll out of the screen.

As an alternative, ProgressTask could contain a Rows component which would allow the user to dynamically add- or remove lines while the task is running.


Please upvote :+1: this issue if you are interested in it.

patriksvensson commented 5 months ago

Everything written to the console using an IAnsiConsole will already automatically be put above the progress.

flobernd commented 5 months ago

Hi @patriksvensson, thanks for the reply. That's not exactly what I'm trying to achieve 🙂 I need something like this:

|= Reticulating splines ----------------------------------------  63% 00:06:181 -
52438428
1870133191
1511040860
494825954
1900287500
1576363183
934959048
1910108725
1465638547
1366240317

|=        Folding space ----------------------------------------  21% 00:06:179 -
52438428
1870133191
1511040860
494825954
1900287500
1576363183
934959048
1910108725
1465638547
1366240317

This is useful to have output of a task near (in this case below) the corresponding progress bar to allow the user to quickly determine which output belongs to which concurrently running task.

Utilizing the new RenderHook property, I managed to create a sample application that demonstrates the desired behavior. It's just hacked together, so please don't take too close of a look at the code style. The rows output is shared for both tasks in this example which of course should not be the case in production.

internal static class Program
{
    private static async Task Main()
    {
        var ri = 0;
        var rows = Enumerable.Repeat<IRenderable>(new Text(""), 11).ToArray();

        var progress = AnsiConsole.Progress()
            .AutoRefresh(true)
            .AutoClear(false)
            .HideCompleted(false)
            .Columns(
                new TaskDescriptionColumn(),
                new ProgressBarColumn(),
                new PercentageColumn(),
                new ElapsedTimeMilliColumn(),
                new SpinnerColumn()
            );
        progress.RefreshRate = TimeSpan.FromMilliseconds(50);
        progress.RenderHook = RenderHook;

        IRenderable RenderHook(IRenderable renderable, IReadOnlyList<ProgressTask> tasks)
        {
            if (renderable is not Grid layout)
            {
                return renderable;
            }

            if (layout.Rows[0][0] is not Grid grid)
            {
                return renderable;
            }

            var result = new List<IRenderable>();

            var i = 0;
            foreach (var row in grid.Rows)
            {
                var g = new Grid();
                foreach (var column in grid.Columns)
                {
                    g.AddColumn(column);
                }

                g.AddRow([.. row]);

                result.Add(g);

                if (ri != 0 && !tasks[i].IsFinished)
                {
                    result.Add(new Rows(rows[..(ri + 1)]));
                }

                ++i;
            }

            var l = new Grid { Expand = true };
            l.AddColumn();
            l.AddRow(new Rows(result));

            return l;
        }

        AnsiConsole.Clear();
        AnsiConsole.MarkupLine("[blue]Elasticsearch.NET[/] Client Generator");
        AnsiConsole.WriteLine();

        progress
            .Start(ctx =>
            {
                // Define tasks
                var task1 = ctx.AddTask("[green]|= Reticulating splines[/]");
                var task2 = ctx.AddTask("[green]|=        Folding space[/]");

                while (!ctx.IsFinished)
                {
                    // Simulate some work
                    Task.Delay(50).Wait();

                    // Increment
                    task1.Increment(1.5);
                    task2.Increment(0.5);

                    if (ri == 10)
                    {
                        for (var i = 1; i < 10; ++i)
                        {
                            rows[i - 1] = rows[i];
                        }

                        ri = 9;
                    }

                    rows[ri++] = new Text($"{new Random((int)DateTime.Now.Ticks).Next()}");
                }

                ctx.Refresh();
            });

        // Fixme: Empty lines between the last progress bar and "test"
        AnsiConsole.WriteLine("test");
    }
}
flobernd commented 5 months ago

I would as well offer to create a PR, if this functionality is something you would consider adding 🙂

patriksvensson commented 5 months ago

Since this is a very niche thing, and nothing that we're really planning the progress bars to support, I think this would make an excellent addition as a external Spectre.Console extension.

flobernd commented 5 months ago

Hi @patriksvensson, no problem. This will however result in a lot of duplicate code I'm afraid.

Closing this feature request as not planned.