mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00
Add support for indeterminate progress
This commit also changes the behavior of ProgressContext.IsFinished. Only tasks that have been started will be taken into consideration, and not indeterminate tasks. Closes #329 Closes #331
This commit is contained in:
parent
6121203fee
commit
6f16081f42
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Spectre.Console;
|
||||
|
||||
@ -25,8 +26,12 @@ namespace ProgressExample
|
||||
.Start(ctx =>
|
||||
{
|
||||
var random = new Random(DateTime.Now.Millisecond);
|
||||
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)
|
||||
{
|
||||
// Increment progress
|
||||
@ -44,13 +49,24 @@ namespace ProgressExample
|
||||
// Simulate some delay
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// Now start the "warp" task
|
||||
warpTask.StartTask();
|
||||
warpTask.IsIndeterminate(false);
|
||||
while (!ctx.IsFinished)
|
||||
{
|
||||
warpTask.Increment(12 * random.NextDouble());
|
||||
|
||||
// Simulate some delay
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
});
|
||||
|
||||
// Done
|
||||
AnsiConsole.MarkupLine("[green]Done![/]");
|
||||
}
|
||||
|
||||
private static List<(ProgressTask, int)> CreateTasks(ProgressContext progress, Random random)
|
||||
private static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext progress, Random random)
|
||||
{
|
||||
var tasks = new List<(ProgressTask, int)>();
|
||||
while (tasks.Count < 5)
|
||||
|
@ -20,7 +20,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
public string Render()
|
||||
{
|
||||
var console = new FakeConsole();
|
||||
var context = new RenderContext(console.Profile.Capabilities);
|
||||
var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities);
|
||||
console.Write(Column.Render(context, Task, TimeSpan.Zero));
|
||||
return console.Output;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
|
||||
|
||||
// When
|
||||
var result = ((IRenderable)text).Measure(new RenderContext(caps), 80);
|
||||
var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80);
|
||||
|
||||
// Then
|
||||
result.Min.ShouldBe(6);
|
||||
@ -29,7 +29,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
|
||||
|
||||
// When
|
||||
var result = ((IRenderable)text).Measure(new RenderContext(caps), 80);
|
||||
var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80);
|
||||
|
||||
// Then
|
||||
result.Max.ShouldBe(11);
|
||||
|
@ -6,6 +6,17 @@ namespace Spectre.Console
|
||||
{
|
||||
internal static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<T> Repeat<T>(this IEnumerable<T> source, int count)
|
||||
{
|
||||
while (count-- > 0)
|
||||
{
|
||||
foreach (var item in source)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int IndexOf<T>(this IEnumerable<T> source, T item)
|
||||
where T : class
|
||||
{
|
||||
|
@ -57,5 +57,22 @@ namespace Spectre.Console
|
||||
task.Value = value;
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the task is considered indeterminate or not.
|
||||
/// </summary>
|
||||
/// <param name="task">The task.</param>
|
||||
/// <param name="indeterminate">Whether the task is considered indeterminate or not.</param>
|
||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||
public static ProgressTask IsIndeterminate(this ProgressTask task, bool indeterminate = true)
|
||||
{
|
||||
if (task is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(task));
|
||||
}
|
||||
|
||||
task.IsIndeterminate = indeterminate;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace Spectre.Console
|
||||
throw new ArgumentNullException(nameof(renderable));
|
||||
}
|
||||
|
||||
var context = new RenderContext(console.Profile.Capabilities);
|
||||
var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities);
|
||||
var renderables = console.Pipeline.Process(context, new[] { renderable });
|
||||
|
||||
return GetSegments(console, context, renderables);
|
||||
|
@ -9,7 +9,7 @@ namespace Spectre.Console.Internal
|
||||
{
|
||||
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
|
||||
{
|
||||
var context = new RenderContext(EncoderCapabilities.Default);
|
||||
var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<pre style=\"font-size:90%;font-family:consolas,'Courier New',monospace\">\n");
|
||||
|
@ -20,7 +20,7 @@ namespace Spectre.Console.Internal
|
||||
{
|
||||
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
|
||||
{
|
||||
var context = new RenderContext(EncoderCapabilities.Default);
|
||||
var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
|
||||
var builder = new StringBuilder();
|
||||
|
||||
foreach (var renderable in renderables)
|
||||
|
@ -9,6 +9,11 @@ namespace Spectre.Console.Rendering
|
||||
{
|
||||
private readonly IReadOnlyCapabilities _capabilities;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current color system.
|
||||
/// </summary>
|
||||
public ColorSystem ColorSystem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not unicode is supported.
|
||||
/// </summary>
|
||||
@ -28,17 +33,19 @@ namespace Spectre.Console.Rendering
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RenderContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="colorSystem">The color system.</param>
|
||||
/// <param name="capabilities">The capabilities.</param>
|
||||
/// <param name="justification">The justification.</param>
|
||||
public RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null)
|
||||
: this(capabilities, justification, false)
|
||||
public RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null)
|
||||
: this(colorSystem, capabilities, justification, false)
|
||||
{
|
||||
}
|
||||
|
||||
private RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
|
||||
private RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
|
||||
{
|
||||
_capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
|
||||
|
||||
ColorSystem = colorSystem;
|
||||
Justification = justification;
|
||||
SingleLine = singleLine;
|
||||
}
|
||||
@ -50,7 +57,7 @@ namespace Spectre.Console.Rendering
|
||||
/// <returns>A new <see cref="RenderContext"/> instance.</returns>
|
||||
public RenderContext WithJustification(Justify? justification)
|
||||
{
|
||||
return new RenderContext(_capabilities, justification, SingleLine);
|
||||
return new RenderContext(ColorSystem, _capabilities, justification, SingleLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -65,7 +72,7 @@ namespace Spectre.Console.Rendering
|
||||
/// <returns>A new <see cref="RenderContext"/> instance.</returns>
|
||||
internal RenderContext WithSingleLine()
|
||||
{
|
||||
return new RenderContext(_capabilities, Justification, true);
|
||||
return new RenderContext(ColorSystem, _capabilities, Justification, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,11 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the style of an indeterminate progress bar.
|
||||
/// </summary>
|
||||
public Style IndeterminateStyle { get; set; } = ProgressBar.DefaultPulseStyle;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
|
||||
{
|
||||
@ -39,6 +44,8 @@ namespace Spectre.Console
|
||||
CompletedStyle = CompletedStyle,
|
||||
FinishedStyle = FinishedStyle,
|
||||
RemainingStyle = RemainingStyle,
|
||||
IndeterminateStyle = IndeterminateStyle,
|
||||
IsIndeterminate = task.IsIndeterminate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,9 @@ namespace Spectre.Console
|
||||
private int _taskId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not all tasks have completed.
|
||||
/// Gets a value indicating whether or not all started tasks have completed.
|
||||
/// </summary>
|
||||
public bool IsFinished => _tasks.All(task => task.IsFinished);
|
||||
public bool IsFinished => _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
|
||||
|
||||
internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
|
||||
{
|
||||
@ -28,20 +28,41 @@ namespace Spectre.Console
|
||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task.
|
||||
/// </summary>
|
||||
/// <param name="description">The task description.</param>
|
||||
/// <param name="autoStart">Whether or not the task should start immediately.</param>
|
||||
/// <param name="maxValue">The task's max value.</param>
|
||||
/// <returns>The newly created task.</returns>
|
||||
public ProgressTask AddTask(string description, bool autoStart = true, double maxValue = 100)
|
||||
{
|
||||
return AddTask(description, new ProgressTaskSettings
|
||||
{
|
||||
AutoStart = autoStart,
|
||||
MaxValue = maxValue,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task.
|
||||
/// </summary>
|
||||
/// <param name="description">The task description.</param>
|
||||
/// <param name="settings">The task settings.</param>
|
||||
/// <returns>The newly created task.</returns>
|
||||
public ProgressTask AddTask(string description, ProgressTaskSettings? settings = null)
|
||||
public ProgressTask AddTask(string description, ProgressTaskSettings settings)
|
||||
{
|
||||
if (settings is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
lock (_taskLock)
|
||||
{
|
||||
settings ??= new ProgressTaskSettings();
|
||||
var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
|
||||
|
||||
_tasks.Add(task);
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,12 @@ namespace Spectre.Console
|
||||
/// </summary>
|
||||
public TimeSpan? RemainingTime => GetRemainingTime();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the ProgressBar shows
|
||||
/// actual values or generic, continuous progress feedback.
|
||||
/// </summary>
|
||||
public bool IsIndeterminate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProgressTask"/> class.
|
||||
/// </summary>
|
||||
|
@ -16,5 +16,10 @@ namespace Spectre.Console
|
||||
/// will be auto started. Defaults to <c>true</c>.
|
||||
/// </summary>
|
||||
public bool AutoStart { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default progress task settings.
|
||||
/// </summary>
|
||||
internal static ProgressTaskSettings Default { get; } = new ProgressTaskSettings();
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace Spectre.Console
|
||||
_stopwatch.Start();
|
||||
}
|
||||
|
||||
var renderContext = new RenderContext(_console.Profile.Capabilities);
|
||||
var renderContext = new RenderContext(_console.Profile.ColorSystem, _console.Profile.Capabilities);
|
||||
|
||||
var delta = _stopwatch.Elapsed - _lastUpdate;
|
||||
_lastUpdate = _stopwatch.Elapsed;
|
||||
|
@ -1,12 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class ProgressBar : Renderable, IHasCulture
|
||||
{
|
||||
private const int PULSESIZE = 20;
|
||||
private const int PULSESPEED = 15;
|
||||
|
||||
public double Value { get; set; }
|
||||
public double MaxValue { get; set; } = 100;
|
||||
|
||||
@ -15,11 +19,15 @@ namespace Spectre.Console
|
||||
public char UnicodeBar { get; set; } = '━';
|
||||
public char AsciiBar { get; set; } = '-';
|
||||
public bool ShowValue { get; set; }
|
||||
public bool IsIndeterminate { get; set; }
|
||||
public CultureInfo? Culture { get; set; }
|
||||
|
||||
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
|
||||
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
|
||||
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
|
||||
public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
|
||||
|
||||
internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
|
||||
|
||||
protected override Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
@ -30,43 +38,49 @@ namespace Spectre.Console
|
||||
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
var width = Math.Min(Width ?? maxWidth, maxWidth);
|
||||
var completed = Math.Min(MaxValue, Math.Max(0, Value));
|
||||
var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
|
||||
var isCompleted = completedBarCount >= MaxValue;
|
||||
|
||||
var token = !context.Unicode ? AsciiBar : UnicodeBar;
|
||||
var style = completed >= MaxValue ? FinishedStyle : CompletedStyle;
|
||||
|
||||
var bars = Math.Max(0, (int)(width * (completed / MaxValue)));
|
||||
|
||||
var value = completed.ToString(Culture ?? CultureInfo.InvariantCulture);
|
||||
if (ShowValue)
|
||||
if (IsIndeterminate && !isCompleted)
|
||||
{
|
||||
bars = bars - value.Length - 1;
|
||||
bars = Math.Max(0, bars);
|
||||
foreach (var segment in RenderIndeterminate(context, width))
|
||||
{
|
||||
yield return segment;
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (bars < 0)
|
||||
var bar = !context.Unicode ? AsciiBar : UnicodeBar;
|
||||
var style = isCompleted ? FinishedStyle : CompletedStyle;
|
||||
var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
|
||||
|
||||
// Show value?
|
||||
var value = completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
|
||||
if (ShowValue)
|
||||
{
|
||||
barCount = barCount - value.Length - 1;
|
||||
barCount = Math.Max(0, barCount);
|
||||
}
|
||||
|
||||
if (barCount < 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new Segment(new string(token, bars), style);
|
||||
yield return new Segment(new string(bar, barCount), style);
|
||||
|
||||
if (ShowValue)
|
||||
{
|
||||
// TODO: Fix this at some point
|
||||
if (bars == 0)
|
||||
{
|
||||
yield return new Segment(value, style);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new Segment(" " + value, style);
|
||||
}
|
||||
yield return barCount == 0
|
||||
? new Segment(value, style)
|
||||
: new Segment(" " + value, style);
|
||||
}
|
||||
|
||||
if (bars < width)
|
||||
// More space available?
|
||||
if (barCount < width)
|
||||
{
|
||||
var diff = width - bars;
|
||||
var diff = width - barCount;
|
||||
if (ShowValue)
|
||||
{
|
||||
diff = diff - value.Length - 1;
|
||||
@ -76,9 +90,59 @@ namespace Spectre.Console
|
||||
}
|
||||
}
|
||||
|
||||
var remainingToken = ShowRemaining ? token : ' ';
|
||||
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
|
||||
var remainingToken = ShowRemaining && !legacy ? bar : ' ';
|
||||
yield return new Segment(new string(remainingToken, diff), RemainingStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Segment> RenderIndeterminate(RenderContext context, int width)
|
||||
{
|
||||
var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
|
||||
var style = IndeterminateStyle ?? DefaultPulseStyle;
|
||||
|
||||
IEnumerable<Segment> GetPulseSegments()
|
||||
{
|
||||
// For 1-bit and 3-bit colors, fall back to
|
||||
// a simpler versions with only two colors.
|
||||
if (context.ColorSystem == ColorSystem.NoColors ||
|
||||
context.ColorSystem == ColorSystem.Legacy)
|
||||
{
|
||||
// First half of the pulse
|
||||
var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
|
||||
|
||||
// Second half of the pulse
|
||||
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
|
||||
var bar2 = legacy ? " " : bar;
|
||||
segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
yield return segment;
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
for (var index = 0; index < PULSESIZE; index++)
|
||||
{
|
||||
var position = index / (float)PULSESIZE;
|
||||
var fade = 0.5f + ((float)Math.Cos(position * Math.PI * 2) / 2.0f);
|
||||
var color = style.Foreground.Blend(style.Background, fade);
|
||||
|
||||
yield return new Segment(bar, new Style(foreground: color));
|
||||
}
|
||||
}
|
||||
|
||||
// Get the pulse segments
|
||||
var pulseSegments = GetPulseSegments();
|
||||
pulseSegments = pulseSegments.Repeat((width / PULSESIZE) + 2);
|
||||
|
||||
// Repeat the pulse segments
|
||||
var currentTime = (DateTime.Now - DateTime.Today).TotalSeconds;
|
||||
var offset = (int)(currentTime * PULSESPEED) % PULSESIZE;
|
||||
|
||||
return pulseSegments.Skip(offset).Take(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user