mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 17:02:51 +08:00
Add enhancements to progress widget
* Adds TransferSpeedColumn * Adds DownloadedColumn * Adds ElapsedTimeColumn * Minor enhancements to existing columns
This commit is contained in:
parent
d87d8e4422
commit
07db28bb6f
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Tests.Unit
|
||||||
|
{
|
||||||
|
public sealed class DownloadedColumnTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 1, "0/1 byte")]
|
||||||
|
[InlineData(37, 101, "37/101 bytes")]
|
||||||
|
[InlineData(101, 101, "101 bytes")]
|
||||||
|
[InlineData(512, 1024, "0.5/1.0 KB")]
|
||||||
|
[InlineData(1024, 1024, "1.0 KB")]
|
||||||
|
[InlineData(1024 * 512, 5 * 1024 * 1024, "0.5/5.0 MB")]
|
||||||
|
[InlineData(5 * 1024 * 1024, 5 * 1024 * 1024, "5.0 MB")]
|
||||||
|
public void Should_Return_Correct_Value(double value, double total, string expected)
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var fixture = new ProgressColumnFixture<DownloadedColumn>(value, total);
|
||||||
|
fixture.Column.Culture = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
// When
|
||||||
|
var result = fixture.Render();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
using Spectre.Console.Testing;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Tests.Unit
|
||||||
|
{
|
||||||
|
public sealed class ProgressColumnFixture<T>
|
||||||
|
where T : ProgressColumn, new()
|
||||||
|
{
|
||||||
|
public T Column { get; }
|
||||||
|
public ProgressTask Task { get; set; }
|
||||||
|
|
||||||
|
public ProgressColumnFixture(double completed, double total)
|
||||||
|
{
|
||||||
|
Column = new T();
|
||||||
|
Task = new ProgressTask(1, "Foo", total);
|
||||||
|
Task.Increment(completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Render()
|
||||||
|
{
|
||||||
|
var console = new FakeConsole();
|
||||||
|
var context = new RenderContext(Encoding.UTF8, false);
|
||||||
|
console.Render(Column.Render(context, Task, TimeSpan.Zero));
|
||||||
|
return console.Output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,5 +23,40 @@ namespace Spectre.Console
|
|||||||
column.Style = style;
|
column.Style = style;
|
||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the text that should be shown instead of the spinner
|
||||||
|
/// once a task completes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="column">The column.</param>
|
||||||
|
/// <param name="text">The text.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static SpinnerColumn CompletedText(this SpinnerColumn column, string? text)
|
||||||
|
{
|
||||||
|
if (column is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
column.CompletedText = text;
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the completed style of the spinner.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="column">The column.</param>
|
||||||
|
/// <param name="style">The style.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static SpinnerColumn CompletedStyle(this SpinnerColumn column, Style? style)
|
||||||
|
{
|
||||||
|
if (column is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
column.CompletedStyle = style;
|
||||||
|
return column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
89
src/Spectre.Console/Internal/FileSize.cs
Normal file
89
src/Spectre.Console/Internal/FileSize.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Internal
|
||||||
|
{
|
||||||
|
internal struct FileSize
|
||||||
|
{
|
||||||
|
public double Bytes { get; }
|
||||||
|
public FileSizeUnit Unit { get; }
|
||||||
|
public string Suffix => GetSuffix();
|
||||||
|
|
||||||
|
public FileSize(double bytes)
|
||||||
|
{
|
||||||
|
Bytes = bytes;
|
||||||
|
Unit = Detect(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileSize(double bytes, FileSizeUnit unit)
|
||||||
|
{
|
||||||
|
Bytes = bytes;
|
||||||
|
Unit = unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Format(CultureInfo? culture = null)
|
||||||
|
{
|
||||||
|
var @base = GetBase(Unit);
|
||||||
|
if (@base == 0)
|
||||||
|
{
|
||||||
|
@base = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Bytes / @base;
|
||||||
|
|
||||||
|
return Unit == FileSizeUnit.Bytes
|
||||||
|
? ((int)bytes).ToString(culture ?? CultureInfo.InvariantCulture)
|
||||||
|
: bytes.ToString("F1", culture ?? CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return ToString(suffix: true, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToString(bool suffix = true, CultureInfo? culture = null)
|
||||||
|
{
|
||||||
|
if (suffix)
|
||||||
|
{
|
||||||
|
return $"{Format(culture)} {Suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Format(culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSuffix()
|
||||||
|
{
|
||||||
|
return (Bytes, Unit) switch
|
||||||
|
{
|
||||||
|
(_, FileSizeUnit.KiloByte) => "KB",
|
||||||
|
(_, FileSizeUnit.MegaByte) => "MB",
|
||||||
|
(_, FileSizeUnit.GigaByte) => "GB",
|
||||||
|
(_, FileSizeUnit.TeraByte) => "TB",
|
||||||
|
(_, FileSizeUnit.PetaByte) => "PB",
|
||||||
|
(_, FileSizeUnit.ExaByte) => "EB",
|
||||||
|
(_, FileSizeUnit.ZettaByte) => "ZB",
|
||||||
|
(_, FileSizeUnit.YottaByte) => "YB",
|
||||||
|
(1, _) => "byte",
|
||||||
|
(_, _) => "bytes",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileSizeUnit Detect(double bytes)
|
||||||
|
{
|
||||||
|
foreach (var unit in (FileSizeUnit[])Enum.GetValues(typeof(FileSizeUnit)))
|
||||||
|
{
|
||||||
|
if (bytes < (GetBase(unit) * 1024))
|
||||||
|
{
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileSizeUnit.Bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetBase(FileSizeUnit unit)
|
||||||
|
{
|
||||||
|
return Math.Pow(1024, (int)unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/Spectre.Console/Internal/FileSizeUnit.cs
Normal file
17
src/Spectre.Console/Internal/FileSizeUnit.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Internal
|
||||||
|
{
|
||||||
|
internal enum FileSizeUnit
|
||||||
|
{
|
||||||
|
Bytes = 0,
|
||||||
|
KiloByte = 1,
|
||||||
|
MegaByte = 2,
|
||||||
|
GigaByte = 3,
|
||||||
|
TeraByte = 4,
|
||||||
|
PetaByte = 5,
|
||||||
|
ExaByte = 6,
|
||||||
|
ZettaByte = 7,
|
||||||
|
YottaByte = 8,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Spectre.Console.Internal;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A column showing download progress.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DownloadedColumn : ProgressColumn
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="CultureInfo"/> to use.
|
||||||
|
/// </summary>
|
||||||
|
public CultureInfo? Culture { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
var total = new FileSize(task.MaxValue);
|
||||||
|
|
||||||
|
if (task.IsFinished)
|
||||||
|
{
|
||||||
|
return new Markup(string.Format(
|
||||||
|
"[green]{0} {1}[/]",
|
||||||
|
total.Format(Culture),
|
||||||
|
total.Suffix));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var downloaded = new FileSize(task.Value, total.Unit);
|
||||||
|
|
||||||
|
return new Markup(string.Format(
|
||||||
|
"{0}[grey]/[/]{1} [grey]{2}[/]",
|
||||||
|
downloaded.Format(Culture),
|
||||||
|
total.Format(Culture),
|
||||||
|
total.Suffix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A column showing the elapsed time of a task.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ElapsedTimeColumn : ProgressColumn
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected internal override bool NoWrap => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the style of the remaining time text.
|
||||||
|
/// </summary>
|
||||||
|
public Style Style { get; set; } = new Style(foreground: Color.Blue);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
var elapsed = task.ElapsedTime;
|
||||||
|
if (elapsed == null)
|
||||||
|
{
|
||||||
|
return new Markup("-:--:--");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Text($"{elapsed.Value:h\\:mm\\:ss}", Style ?? Style.Plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int? GetColumnWidth(RenderContext context)
|
||||||
|
{
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ namespace Spectre.Console
|
|||||||
private readonly object _lock;
|
private readonly object _lock;
|
||||||
private Spinner _spinner;
|
private Spinner _spinner;
|
||||||
private int? _maxWidth;
|
private int? _maxWidth;
|
||||||
|
private string? _completed;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected internal override bool NoWrap => true;
|
protected internal override bool NoWrap => true;
|
||||||
@ -36,6 +37,25 @@ namespace Spectre.Console
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text that should be shown instead
|
||||||
|
/// of the spinner once a task completes.
|
||||||
|
/// </summary>
|
||||||
|
public string? CompletedText
|
||||||
|
{
|
||||||
|
get => _completed;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_completed = value;
|
||||||
|
_maxWidth = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the completed style.
|
||||||
|
/// </summary>
|
||||||
|
public Style? CompletedStyle { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the style of the spinner.
|
/// Gets or sets the style of the spinner.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -67,7 +87,7 @@ namespace Spectre.Console
|
|||||||
|
|
||||||
if (!task.IsStarted || task.IsFinished)
|
if (!task.IsStarted || task.IsFinished)
|
||||||
{
|
{
|
||||||
return new Markup(new string(' ', GetMaxWidth(context)));
|
return new Markup(CompletedText ?? " ", CompletedStyle ?? Style.Plain);
|
||||||
}
|
}
|
||||||
|
|
||||||
var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds);
|
var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds);
|
||||||
@ -97,7 +117,9 @@ namespace Spectre.Console
|
|||||||
var useAscii = (context.LegacyConsole || !context.Unicode) && _spinner.IsUnicode;
|
var useAscii = (context.LegacyConsole || !context.Unicode) && _spinner.IsUnicode;
|
||||||
var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default;
|
var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default;
|
||||||
|
|
||||||
_maxWidth = spinner.Frames.Max(frame => Cell.GetCellLength(context, frame));
|
_maxWidth = Math.Max(
|
||||||
|
((IRenderable)new Markup(CompletedText ?? " ")).Measure(context, int.MaxValue).Max,
|
||||||
|
spinner.Frames.Max(frame => Cell.GetCellLength(context, frame)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _maxWidth.Value;
|
return _maxWidth.Value;
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Spectre.Console.Internal;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A column showing transfer speed.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TransferSpeedColumn : ProgressColumn
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="CultureInfo"/> to use.
|
||||||
|
/// </summary>
|
||||||
|
public CultureInfo? Culture { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
if (task.Speed == null)
|
||||||
|
{
|
||||||
|
return new Text("?/s");
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = new FileSize(task.Speed.Value);
|
||||||
|
return new Markup(string.Format("{0}/s", size.ToString(suffix: true, Culture)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -89,7 +89,14 @@ namespace Spectre.Console
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? RemainingTime => GetRemainingTime();
|
public TimeSpan? RemainingTime => GetRemainingTime();
|
||||||
|
|
||||||
internal ProgressTask(int id, string description, double maxValue, bool autoStart)
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProgressTask"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The task ID.</param>
|
||||||
|
/// <param name="description">The task description.</param>
|
||||||
|
/// <param name="maxValue">The task max value.</param>
|
||||||
|
/// <param name="autoStart">Whether or not the task should start automatically.</param>
|
||||||
|
public ProgressTask(int id, string description, double maxValue, bool autoStart = true)
|
||||||
{
|
{
|
||||||
_samples = new List<ProgressSample>();
|
_samples = new List<ProgressSample>();
|
||||||
_lock = new object();
|
_lock = new object();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user