mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-01 01:25:27 +08:00 
			
		
		
		
	Add enhancements to progress widget
* Adds TransferSpeedColumn * Adds DownloadedColumn * Adds ElapsedTimeColumn * Minor enhancements to existing columns
This commit is contained in:
		 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						parent
						
							d87d8e4422
						
					
				
				
					commit
					07db28bb6f
				
			| @@ -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; | ||||
|             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 Spinner _spinner; | ||||
|         private int? _maxWidth; | ||||
|         private string? _completed; | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         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> | ||||
|         /// Gets or sets the style of the spinner. | ||||
|         /// </summary> | ||||
| @@ -67,7 +87,7 @@ namespace Spectre.Console | ||||
|  | ||||
|             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); | ||||
| @@ -97,7 +117,9 @@ namespace Spectre.Console | ||||
|                     var useAscii = (context.LegacyConsole || !context.Unicode) && _spinner.IsUnicode; | ||||
|                     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; | ||||
|   | ||||
| @@ -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> | ||||
|         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>(); | ||||
|             _lock = new object(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user