From ed9e198d600438fb0a1f710f2fea9979c6b2380f Mon Sep 17 00:00:00 2001 From: Phil Scott Date: Sat, 16 Sep 2023 16:39:43 -0400 Subject: [PATCH] Progress bar header and footer (#1262) --- examples/Console/Progress/Program.cs | 52 +++++++++++++++---- .../Extensions/Progress/ProgressExtensions.cs | 13 +++++ src/Spectre.Console/Live/Progress/Progress.cs | 7 ++- .../Renderers/DefaultProgressRenderer.cs | 15 ++++-- src/Spectre.Console/Widgets/ControlCode.cs | 11 +++- src/Spectre.Console/Widgets/Rows.cs | 2 +- 6 files changed, 84 insertions(+), 16 deletions(-) diff --git a/examples/Console/Progress/Program.cs b/examples/Console/Progress/Program.cs index ae728b0..268c826 100644 --- a/examples/Console/Progress/Program.cs +++ b/examples/Console/Progress/Program.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using Spectre.Console; +using Spectre.Console.Rendering; namespace Progress; @@ -22,35 +24,36 @@ public static class Program new RemainingTimeColumn(), // Remaining time new SpinnerColumn(), // Spinner }) + .UseRenderHook((renderable, tasks) => RenderHook(tasks, renderable)) .Start(ctx => { var random = new Random(DateTime.Now.Millisecond); - // Create some tasks - var tasks = CreateTasks(ctx, random); + // Create some tasks + var tasks = CreateTasks(ctx, random); var warpTask = ctx.AddTask("Going to warp", autoStart: false).IsIndeterminate(); // Wait for all tasks (except the indeterminate one) to complete - while (!ctx.IsFinished) + while (!ctx.IsFinished) { - // Increment progress - foreach (var (task, increment) in tasks) + // Increment progress + foreach (var (task, increment) in tasks) { task.Increment(random.NextDouble() * increment); } - // Write some random things to the terminal - if (random.NextDouble() < 0.1) + // Write some random things to the terminal + if (random.NextDouble() < 0.1) { WriteLogMessage(); } - // Simulate some delay - Thread.Sleep(100); + // Simulate some delay + Thread.Sleep(100); } // Now start the "warp" task - warpTask.StartTask(); + warpTask.StartTask(); warpTask.IsIndeterminate(false); while (!ctx.IsFinished) { @@ -65,6 +68,35 @@ public static class Program AnsiConsole.MarkupLine("[green]Done![/]"); } + private static IRenderable RenderHook(IReadOnlyList tasks, IRenderable renderable) + { + var header = new Panel("Going on a :rocket:, we're going to the :crescent_moon:").Expand().RoundedBorder(); + var footer = new Rows( + new Rule(), + new Markup( + $"[blue]{tasks.Count}[/] total tasks. [green]{tasks.Count(i => i.IsFinished)}[/] complete.") + ); + + const string ESC = "\u001b"; + string escapeSequence; + if (tasks.All(i => i.IsFinished)) + { + escapeSequence = $"{ESC}]]9;4;0;100{ESC}\\"; + } + else + { + var total = tasks.Sum(i => i.MaxValue); + var done = tasks.Sum(i => i.Value); + var percent = (int)(done / total * 100); + escapeSequence = $"{ESC}]]9;4;1;{percent}{ESC}\\"; + } + + var middleContent = new Grid().AddColumns(new GridColumn(), new GridColumn().Width(20)); + middleContent.AddRow(renderable, new FigletText(tasks.Count(i => i.IsFinished == false).ToString())); + + return new Rows(header, middleContent, footer, new ControlCode(escapeSequence)); + } + private static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext progress, Random random) { var tasks = new List<(ProgressTask, int)>(); diff --git a/src/Spectre.Console/Extensions/Progress/ProgressExtensions.cs b/src/Spectre.Console/Extensions/Progress/ProgressExtensions.cs index 58672d3..dc8cf01 100644 --- a/src/Spectre.Console/Extensions/Progress/ProgressExtensions.cs +++ b/src/Spectre.Console/Extensions/Progress/ProgressExtensions.cs @@ -34,6 +34,19 @@ public static class ProgressExtensions return progress; } + /// + /// Sets an optional hook to intercept rendering. + /// + /// The instance. + /// The custom render function. + /// The same instance so that multiple calls can be chained. + public static Progress UseRenderHook(this Progress progress, Func, IRenderable> renderHook) + { + progress.RenderHook = renderHook; + + return progress; + } + /// /// Sets whether or not auto refresh is enabled. /// If disabled, you will manually have to refresh the progress. diff --git a/src/Spectre.Console/Live/Progress/Progress.cs b/src/Spectre.Console/Live/Progress/Progress.cs index b183990..37d52f2 100644 --- a/src/Spectre.Console/Live/Progress/Progress.cs +++ b/src/Spectre.Console/Live/Progress/Progress.cs @@ -7,6 +7,11 @@ public sealed class Progress { private readonly IAnsiConsole _console; + /// + /// Gets or sets a optional custom render function. + /// + public Func, IRenderable> RenderHook { get; set; } = (renderable, _) => renderable; + /// /// Gets or sets a value indicating whether or not task list should auto refresh. /// Defaults to true. @@ -158,7 +163,7 @@ public sealed class Progress if (interactive) { var columns = new List(Columns); - return new DefaultProgressRenderer(_console, columns, RefreshRate, HideCompleted); + return new DefaultProgressRenderer(_console, columns, RefreshRate, HideCompleted, RenderHook); } else { diff --git a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs index 8155751..0607f2d 100644 --- a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs +++ b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs @@ -8,11 +8,12 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer private readonly object _lock; private readonly Stopwatch _stopwatch; private readonly bool _hideCompleted; + private readonly Func, IRenderable> _renderHook; private TimeSpan _lastUpdate; public override TimeSpan RefreshRate { get; } - public DefaultProgressRenderer(IAnsiConsole console, List columns, TimeSpan refreshRate, bool hideCompleted) + public DefaultProgressRenderer(IAnsiConsole console, List columns, TimeSpan refreshRate, bool hideCompleted, Func, IRenderable> renderHook) { _console = console ?? throw new ArgumentNullException(nameof(console)); _columns = columns ?? throw new ArgumentNullException(nameof(columns)); @@ -21,6 +22,7 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer _stopwatch = new Stopwatch(); _lastUpdate = TimeSpan.Zero; _hideCompleted = hideCompleted; + _renderHook = renderHook; RefreshRate = refreshRate; } @@ -95,13 +97,20 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer } // Add rows - foreach (var task in context.GetTasks().Where(tsk => !(_hideCompleted && tsk.IsFinished))) + var tasks = context.GetTasks(); + + var layout = new Grid(); + layout.AddColumn(); + + foreach (var task in tasks.Where(tsk => !(_hideCompleted && tsk.IsFinished))) { var columns = _columns.Select(column => column.Render(renderContext, task, delta)); grid.AddRow(columns.ToArray()); } - _live.SetRenderable(new Padder(grid, new Padding(0, 1))); + layout.AddRow(grid); + + _live.SetRenderable(new Padder(_renderHook(layout, tasks), new Padding(0, 1))); } } diff --git a/src/Spectre.Console/Widgets/ControlCode.cs b/src/Spectre.Console/Widgets/ControlCode.cs index b83d833..69efd62 100644 --- a/src/Spectre.Console/Widgets/ControlCode.cs +++ b/src/Spectre.Console/Widgets/ControlCode.cs @@ -1,19 +1,28 @@ namespace Spectre.Console; -internal sealed class ControlCode : Renderable +/// +/// A control code. +/// +public sealed class ControlCode : Renderable { private readonly Segment _segment; + /// + /// Initializes a new instance of the class. + /// + /// The control code. public ControlCode(string control) { _segment = Segment.Control(control); } + /// protected override Measurement Measure(RenderOptions options, int maxWidth) { return new Measurement(0, 0); } + /// protected override IEnumerable Render(RenderOptions options, int maxWidth) { if (options.Ansi) diff --git a/src/Spectre.Console/Widgets/Rows.cs b/src/Spectre.Console/Widgets/Rows.cs index 2ee2723..8e3e595 100644 --- a/src/Spectre.Console/Widgets/Rows.cs +++ b/src/Spectre.Console/Widgets/Rows.cs @@ -63,7 +63,7 @@ public sealed class Rows : Renderable, IExpandable if (last) { - if (!segment.IsLineBreak) + if (!segment.IsLineBreak && child is not ControlCode) { result.Add(Segment.LineBreak); }