mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-01 01:25:27 +08:00 
			
		
		
		
	Add status support
This commit is contained in:
		 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						parent
						
							cbed41e637
						
					
				
				
					commit
					501db5d287
				
			
							
								
								
									
										25
									
								
								src/Spectre.Console.Tests/Tools/DummySpinners.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/Spectre.Console.Tests/Tools/DummySpinners.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console.Tests | ||||
| { | ||||
|     public sealed class DummySpinner1 : Spinner | ||||
|     { | ||||
|         public override TimeSpan Interval => TimeSpan.FromMilliseconds(100); | ||||
|         public override bool IsUnicode => true; | ||||
|         public override IReadOnlyList<string> Frames => new List<string> | ||||
|             { | ||||
|                     "*", | ||||
|             }; | ||||
|     } | ||||
|  | ||||
|     public sealed class DummySpinner2 : Spinner | ||||
|     { | ||||
|         public override TimeSpan Interval => TimeSpan.FromMilliseconds(100); | ||||
|         public override bool IsUnicode => true; | ||||
|         public override IReadOnlyList<string> Frames => new List<string> | ||||
|             { | ||||
|                     "-", | ||||
|             }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/Spectre.Console.Tests/Unit/StatusTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/Spectre.Console.Tests/Unit/StatusTests.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| using Shouldly; | ||||
| using Xunit; | ||||
|  | ||||
| namespace Spectre.Console.Tests.Unit | ||||
| { | ||||
|     public sealed partial class StatusTests | ||||
|     { | ||||
|         [Fact] | ||||
|         public void Should_Render_Status_Correctly() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10); | ||||
|  | ||||
|             var status = new Status(console); | ||||
|             status.AutoRefresh = false; | ||||
|             status.Spinner = new DummySpinner1(); | ||||
|  | ||||
|             // When | ||||
|             status.Start("foo", ctx => | ||||
|             { | ||||
|                 ctx.Refresh(); | ||||
|                 ctx.Spinner(new DummySpinner2()); | ||||
|                 ctx.Status("bar"); | ||||
|                 ctx.Refresh(); | ||||
|                 ctx.Spinner(new DummySpinner1()); | ||||
|                 ctx.Status("baz"); | ||||
|             }); | ||||
|  | ||||
|             // Then | ||||
|             console.Output | ||||
|                 .NormalizeLineEndings() | ||||
|                 .ShouldBe( | ||||
|                     "[?25l     \n" + | ||||
|                     "[38;5;11m*[0m foo\n" + | ||||
|                     "     [1A[1A     \n" + | ||||
|                     "[38;5;11m-[0m bar\n" + | ||||
|                     "     [1A[1A     \n" + | ||||
|                     "[38;5;11m*[0m baz\n" + | ||||
|                     "     [2K[1A[2K[1A[2K[?25h"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -60,6 +60,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Progress\Progress.csproj", "{2B712A52-40F1-4C1C-833E-7C869ACA91F3}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Status", "..\examples\Status\Status.csproj", "{3716AFDF-0904-4635-8422-86E6B9356840}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| @@ -310,6 +312,18 @@ Global | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840}.Release|x86.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
| @@ -333,6 +347,7 @@ Global | ||||
| 		{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 		{5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 		{3716AFDF-0904-4635-8422-86E6B9356840} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(ExtensibilityGlobals) = postSolution | ||||
| 		SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} | ||||
|   | ||||
| @@ -13,5 +13,14 @@ namespace Spectre.Console | ||||
|         { | ||||
|             return Console.Progress(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Creates a new <see cref="Status"/> instance. | ||||
|         /// </summary> | ||||
|         /// <returns>A <see cref="Status"/> instance.</returns> | ||||
|         public static Status Status() | ||||
|         { | ||||
|             return Console.Status(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,5 +21,20 @@ namespace Spectre.Console | ||||
|  | ||||
|             return new Progress(console); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Creates a new <see cref="Status"/> instance for the console. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         /// <returns>A <see cref="Status"/> instance.</returns> | ||||
|         public static Status Status(this IAnsiConsole console) | ||||
|         { | ||||
|             if (console is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|             } | ||||
|  | ||||
|             return new Status(console); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,19 +13,18 @@ namespace Spectre.Console | ||||
|         /// <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 Style(this SpinnerColumn column, Style style) | ||||
|         public static SpinnerColumn Style(this SpinnerColumn column, Style? style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             if (style != null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|                 column.Style = style; | ||||
|             } | ||||
|  | ||||
|             column.Style = style; | ||||
|             return column; | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										61
									
								
								src/Spectre.Console/Extensions/StatusContextExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/Spectre.Console/Extensions/StatusContextExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="StatusContext"/>. | ||||
|     /// </summary> | ||||
|     public static class StatusContextExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the status message. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The status context.</param> | ||||
|         /// <param name="status">The status message.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static StatusContext Status(this StatusContext context, string status) | ||||
|         { | ||||
|             if (context is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(context)); | ||||
|             } | ||||
|  | ||||
|             context.Status = status; | ||||
|             return context; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the spinner. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The status context.</param> | ||||
|         /// <param name="spinner">The spinner.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static StatusContext Spinner(this StatusContext context, Spinner spinner) | ||||
|         { | ||||
|             if (context is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(context)); | ||||
|             } | ||||
|  | ||||
|             context.Spinner = spinner; | ||||
|             return context; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the spinner style. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The status context.</param> | ||||
|         /// <param name="style">The spinner style.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static StatusContext SpinnerStyle(this StatusContext context, Style? style) | ||||
|         { | ||||
|             if (context is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(context)); | ||||
|             } | ||||
|  | ||||
|             context.SpinnerStyle = style; | ||||
|             return context; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								src/Spectre.Console/Extensions/StatusExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/Spectre.Console/Extensions/StatusExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="Status"/>. | ||||
|     /// </summary> | ||||
|     public static class StatusExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets whether or not auto refresh is enabled. | ||||
|         /// If disabled, you will manually have to refresh the progress. | ||||
|         /// </summary> | ||||
|         /// <param name="status">The <see cref="Status"/> instance.</param> | ||||
|         /// <param name="enabled">Whether or not auto refresh is enabled.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static Status AutoRefresh(this Status status, bool enabled) | ||||
|         { | ||||
|             if (status is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(status)); | ||||
|             } | ||||
|  | ||||
|             status.AutoRefresh = enabled; | ||||
|             return status; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the spinner. | ||||
|         /// </summary> | ||||
|         /// <param name="status">The <see cref="Status"/> instance.</param> | ||||
|         /// <param name="spinner">The spinner.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static Status Spinner(this Status status, Spinner spinner) | ||||
|         { | ||||
|             if (status is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(status)); | ||||
|             } | ||||
|  | ||||
|             status.Spinner = spinner; | ||||
|             return status; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the spinner style. | ||||
|         /// </summary> | ||||
|         /// <param name="status">The <see cref="Status"/> instance.</param> | ||||
|         /// <param name="style">The spinner style.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static Status SpinnerStyle(this Status status, Style? style) | ||||
|         { | ||||
|             if (status is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(status)); | ||||
|             } | ||||
|  | ||||
|             status.SpinnerStyle = style; | ||||
|             return status; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -8,9 +8,6 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     public sealed class PercentageColumn : ProgressColumn | ||||
|     { | ||||
|         /// <inheritdoc/> | ||||
|         protected internal override int? ColumnWidth => 4; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style for a non-complete task. | ||||
|         /// </summary> | ||||
| @@ -28,5 +25,11 @@ namespace Spectre.Console | ||||
|             var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain; | ||||
|             return new Text($"{percentage}%", style).RightAligned(); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override int? GetColumnWidth(RenderContext context) | ||||
|         { | ||||
|             return 4; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,9 +8,6 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     public sealed class RemainingTimeColumn : ProgressColumn | ||||
|     { | ||||
|         /// <inheritdoc/> | ||||
|         protected internal override int? ColumnWidth => 7; | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         protected internal override bool NoWrap => true; | ||||
|  | ||||
| @@ -30,5 +27,11 @@ namespace Spectre.Console | ||||
|  | ||||
|             return new Text($"{remaining.Value:h\\:mm\\:ss}", Style ?? Style.Plain); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override int? GetColumnWidth(RenderContext context) | ||||
|         { | ||||
|             return 7; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,25 +13,39 @@ namespace Spectre.Console | ||||
|         private const string ACCUMULATED = "SPINNER_ACCUMULATED"; | ||||
|         private const string INDEX = "SPINNER_INDEX"; | ||||
|  | ||||
|         private readonly ProgressSpinner _spinner; | ||||
|         private int? _maxLength; | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         protected internal override int? ColumnWidth => 1; | ||||
|         private readonly object _lock; | ||||
|         private Spinner _spinner; | ||||
|         private int? _maxWidth; | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         protected internal override bool NoWrap => true; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the <see cref="Console.Spinner"/>. | ||||
|         /// </summary> | ||||
|         public Spinner Spinner | ||||
|         { | ||||
|             get => _spinner; | ||||
|             set | ||||
|             { | ||||
|                 lock (_lock) | ||||
|                 { | ||||
|                     _spinner = value ?? Spinner.Known.Default; | ||||
|                     _maxWidth = null; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style of the spinner. | ||||
|         /// </summary> | ||||
|         public Style Style { get; set; } = new Style(foreground: Color.Yellow); | ||||
|         public Style? Style { get; set; } = new Style(foreground: Color.Yellow); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SpinnerColumn"/> class. | ||||
|         /// </summary> | ||||
|         public SpinnerColumn() | ||||
|             : this(ProgressSpinner.Known.Default) | ||||
|             : this(Spinner.Known.Default) | ||||
|         { | ||||
|         } | ||||
|  | ||||
| @@ -39,36 +53,55 @@ namespace Spectre.Console | ||||
|         /// Initializes a new instance of the <see cref="SpinnerColumn"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="spinner">The spinner to use.</param> | ||||
|         public SpinnerColumn(ProgressSpinner spinner) | ||||
|         public SpinnerColumn(Spinner spinner) | ||||
|         { | ||||
|             _spinner = spinner ?? throw new ArgumentNullException(nameof(spinner)); | ||||
|             _lock = new object(); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) | ||||
|         { | ||||
|             var useAscii = (context.LegacyConsole || !context.Unicode) && _spinner.IsUnicode; | ||||
|             var spinner = useAscii ? ProgressSpinner.Known.Ascii : _spinner; | ||||
|             var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default; | ||||
|  | ||||
|             if (!task.IsStarted || task.IsFinished) | ||||
|             { | ||||
|                 if (_maxLength == null) | ||||
|                 { | ||||
|                     _maxLength = _spinner.Frames.Max(frame => Cell.GetCellLength(context, frame)); | ||||
|                 } | ||||
|  | ||||
|                 return new Markup(new string(' ', _maxLength.Value)); | ||||
|                 return new Markup(new string(' ', GetMaxWidth(context))); | ||||
|             } | ||||
|  | ||||
|             var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds); | ||||
|             if (accumulated >= _spinner.Interval.TotalMilliseconds) | ||||
|             if (accumulated >= spinner.Interval.TotalMilliseconds) | ||||
|             { | ||||
|                 task.State.Update<double>(ACCUMULATED, _ => 0); | ||||
|                 task.State.Update<int>(INDEX, index => index + 1); | ||||
|             } | ||||
|  | ||||
|             var index = task.State.Get<int>(INDEX); | ||||
|             return new Markup(spinner.Frames[index % spinner.Frames.Count], Style ?? Style.Plain); | ||||
|             var frame = spinner.Frames[index % spinner.Frames.Count]; | ||||
|             return new Markup(frame.EscapeMarkup(), Style ?? Style.Plain); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override int? GetColumnWidth(RenderContext context) | ||||
|         { | ||||
|             return GetMaxWidth(context); | ||||
|         } | ||||
|  | ||||
|         private int GetMaxWidth(RenderContext context) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_maxWidth == null) | ||||
|                 { | ||||
|                     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)); | ||||
|                 } | ||||
|  | ||||
|                 return _maxWidth.Value; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -34,6 +34,8 @@ namespace Spectre.Console | ||||
|  | ||||
|         internal List<ProgressColumn> Columns { get; } | ||||
|  | ||||
|         internal ProgressRenderer? FallbackRenderer { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Progress"/> class. | ||||
|         /// </summary> | ||||
| @@ -116,11 +118,11 @@ namespace Spectre.Console | ||||
|             if (interactive) | ||||
|             { | ||||
|                 var columns = new List<ProgressColumn>(Columns); | ||||
|                 return new InteractiveProgressRenderer(_console, columns, RefreshRate); | ||||
|                 return new DefaultProgressRenderer(_console, columns, RefreshRate); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return new NonInteractiveProgressRenderer(); | ||||
|                 return FallbackRenderer ?? new FallbackProgressRenderer(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -13,11 +13,6 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         protected internal virtual bool NoWrap { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the requested column width for the column. | ||||
|         /// </summary> | ||||
|         protected internal virtual int? ColumnWidth { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets a renderable representing the column. | ||||
|         /// </summary> | ||||
| @@ -26,5 +21,15 @@ namespace Spectre.Console | ||||
|         /// <param name="deltaTime">The elapsed time since last call.</param> | ||||
|         /// <returns>A renderable representing the column.</returns> | ||||
|         public abstract IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the width of the column. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The context.</param> | ||||
|         /// <returns>The width of the column, or <c>null</c> to calculate.</returns> | ||||
|         public virtual int? GetColumnWidth(RenderContext context) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Spectre.Console.Internal; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| @@ -21,6 +22,8 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         public bool IsFinished => _tasks.All(task => task.IsFinished); | ||||
|  | ||||
|         internal Encoding Encoding => _console.Encoding; | ||||
|  | ||||
|         internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer) | ||||
|         { | ||||
|             _tasks = new List<ProgressTask>(); | ||||
| @@ -56,14 +59,11 @@ namespace Spectre.Console | ||||
|             _console.Render(new ControlSequence(string.Empty)); | ||||
|         } | ||||
|  | ||||
|         internal void EnumerateTasks(Action<ProgressTask> action) | ||||
|         internal IReadOnlyList<ProgressTask> GetTasks() | ||||
|         { | ||||
|             lock (_taskLock) | ||||
|             { | ||||
|                 foreach (var task in _tasks) | ||||
|                 { | ||||
|                     action(task); | ||||
|                 } | ||||
|                 return new List<ProgressTask>(_tasks); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ using Spectre.Console.Rendering; | ||||
| 
 | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal sealed class InteractiveProgressRenderer : ProgressRenderer | ||||
|     internal sealed class DefaultProgressRenderer : ProgressRenderer | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly List<ProgressColumn> _columns; | ||||
| @@ -17,7 +17,7 @@ namespace Spectre.Console.Internal | ||||
| 
 | ||||
|         public override TimeSpan RefreshRate { get; } | ||||
| 
 | ||||
|         public InteractiveProgressRenderer(IAnsiConsole console, List<ProgressColumn> columns, TimeSpan refreshRate) | ||||
|         public DefaultProgressRenderer(IAnsiConsole console, List<ProgressColumn> columns, TimeSpan refreshRate) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _columns = columns ?? throw new ArgumentNullException(nameof(columns)); | ||||
| @@ -60,6 +60,8 @@ namespace Spectre.Console.Internal | ||||
|                     _stopwatch.Start(); | ||||
|                 } | ||||
| 
 | ||||
|                 var renderContext = new RenderContext(_console.Encoding, _console.Capabilities.LegacyConsole); | ||||
| 
 | ||||
|                 var delta = _stopwatch.Elapsed - _lastUpdate; | ||||
|                 _lastUpdate = _stopwatch.Elapsed; | ||||
| 
 | ||||
| @@ -68,9 +70,10 @@ namespace Spectre.Console.Internal | ||||
|                 { | ||||
|                     var column = new GridColumn().PadRight(1); | ||||
| 
 | ||||
|                     if (_columns[columnIndex].ColumnWidth != null) | ||||
|                     var columnWidth = _columns[columnIndex].GetColumnWidth(renderContext); | ||||
|                     if (columnWidth != null) | ||||
|                     { | ||||
|                         column.Width = _columns[columnIndex].ColumnWidth; | ||||
|                         column.Width = columnWidth; | ||||
|                     } | ||||
| 
 | ||||
|                     if (_columns[columnIndex].NoWrap) | ||||
| @@ -88,12 +91,11 @@ namespace Spectre.Console.Internal | ||||
|                 } | ||||
| 
 | ||||
|                 // Add rows | ||||
|                 var renderContext = new RenderContext(_console.Encoding, _console.Capabilities.LegacyConsole); | ||||
|                 context.EnumerateTasks(task => | ||||
|                 foreach (var task in context.GetTasks()) | ||||
|                 { | ||||
|                     var columns = _columns.Select(column => column.Render(renderContext, task, delta)); | ||||
|                     grid.AddRow(columns.ToArray()); | ||||
|                 }); | ||||
|                 } | ||||
| 
 | ||||
|                 _live.SetRenderable(new Padder(grid, new Padding(0, 1))); | ||||
|             } | ||||
| @@ -4,7 +4,7 @@ using Spectre.Console.Rendering; | ||||
| 
 | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal sealed class NonInteractiveProgressRenderer : ProgressRenderer | ||||
|     internal sealed class FallbackProgressRenderer : ProgressRenderer | ||||
|     { | ||||
|         private const double FirstMilestone = 25; | ||||
|         private static readonly double?[] _milestones = new double?[] { FirstMilestone, 50, 75, 95, 96, 97, 98, 99, 100 }; | ||||
| @@ -16,7 +16,7 @@ namespace Spectre.Console.Internal | ||||
| 
 | ||||
|         public override TimeSpan RefreshRate => TimeSpan.FromSeconds(1); | ||||
| 
 | ||||
|         public NonInteractiveProgressRenderer() | ||||
|         public FallbackProgressRenderer() | ||||
|         { | ||||
|             _taskMilestones = new Dictionary<int, double>(); | ||||
|             _lock = new object(); | ||||
| @@ -29,7 +29,7 @@ namespace Spectre.Console.Internal | ||||
|                 var hasStartedTasks = false; | ||||
|                 var updates = new List<(string, double)>(); | ||||
| 
 | ||||
|                 context.EnumerateTasks(task => | ||||
|                 foreach (var task in context.GetTasks()) | ||||
|                 { | ||||
|                     if (!task.IsStarted || task.IsFinished) | ||||
|                     { | ||||
| @@ -42,12 +42,15 @@ namespace Spectre.Console.Internal | ||||
|                     { | ||||
|                         updates.Add((task.Description, task.Percentage)); | ||||
|                     } | ||||
|                 }); | ||||
|                 } | ||||
| 
 | ||||
|                 // Got started tasks but no updates for 30 seconds? | ||||
|                 if (hasStartedTasks && updates.Count == 0 && (DateTime.Now - _lastUpdate) > TimeSpan.FromSeconds(30)) | ||||
|                 { | ||||
|                     context.EnumerateTasks(task => updates.Add((task.Description, task.Percentage))); | ||||
|                     foreach (var task in context.GetTasks()) | ||||
|                     { | ||||
|                         updates.Add((task.Description, task.Percentage)); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (updates.Count > 0) | ||||
| @@ -0,0 +1,60 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal sealed class StatusFallbackRenderer : ProgressRenderer | ||||
|     { | ||||
|         private readonly object _lock; | ||||
|         private IRenderable? _renderable; | ||||
|         private string? _lastStatus; | ||||
|  | ||||
|         public override TimeSpan RefreshRate => TimeSpan.FromSeconds(1); | ||||
|  | ||||
|         public StatusFallbackRenderer() | ||||
|         { | ||||
|             _lock = new object(); | ||||
|         } | ||||
|  | ||||
|         public override void Update(ProgressContext context) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 var task = context.GetTasks().SingleOrDefault(); | ||||
|                 if (task != null) | ||||
|                 { | ||||
|                     // Not same description? | ||||
|                     if (_lastStatus != task.Description) | ||||
|                     { | ||||
|                         _lastStatus = task.Description; | ||||
|                         _renderable = new Markup(task.Description + Environment.NewLine); | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 _renderable = null; | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 var result = new List<IRenderable>(); | ||||
|                 result.AddRange(renderables); | ||||
|  | ||||
|                 if (_renderable != null) | ||||
|                 { | ||||
|                     result.Add(_renderable); | ||||
|                 } | ||||
|  | ||||
|                 _renderable = null; | ||||
|  | ||||
|                 return result; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,7 +6,7 @@ namespace Spectre.Console | ||||
|     /// <summary> | ||||
|     /// Represents a spinner used in a <see cref="SpinnerColumn"/>. | ||||
|     /// </summary> | ||||
|     public abstract partial class ProgressSpinner | ||||
|     public abstract partial class Spinner | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets the update interval for the spinner. | ||||
							
								
								
									
										89
									
								
								src/Spectre.Console/Progress/Status.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/Spectre.Console/Progress/Status.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using Spectre.Console.Internal; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a status display. | ||||
|     /// </summary> | ||||
|     public sealed class Status | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the spinner. | ||||
|         /// </summary> | ||||
|         public Spinner? Spinner { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the spinner style. | ||||
|         /// </summary> | ||||
|         public Style? SpinnerStyle { get; set; } = new Style(foreground: Color.Yellow); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not status | ||||
|         /// should auto refresh. Defaults to <c>true</c>. | ||||
|         /// </summary> | ||||
|         public bool AutoRefresh { get; set; } = true; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Status"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         public Status(IAnsiConsole console) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts a new status display. | ||||
|         /// </summary> | ||||
|         /// <param name="status">The status to display.</param> | ||||
|         /// <param name="action">he action to execute.</param> | ||||
|         public void Start(string status, Action<StatusContext> action) | ||||
|         { | ||||
|             var task = StartAsync(status, ctx => | ||||
|             { | ||||
|                 action(ctx); | ||||
|                 return Task.CompletedTask; | ||||
|             }); | ||||
|  | ||||
|             task.GetAwaiter().GetResult(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts a new status display. | ||||
|         /// </summary> | ||||
|         /// <param name="status">The status to display.</param> | ||||
|         /// <param name="action">he action to execute.</param> | ||||
|         /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | ||||
|         public async Task StartAsync(string status, Func<StatusContext, Task> action) | ||||
|         { | ||||
|             // Set the progress columns | ||||
|             var spinnerColumn = new SpinnerColumn(Spinner ?? Spinner.Known.Default) | ||||
|             { | ||||
|                 Style = SpinnerStyle ?? Style.Plain, | ||||
|             }; | ||||
|  | ||||
|             var progress = new Progress(_console) | ||||
|             { | ||||
|                 FallbackRenderer = new StatusFallbackRenderer(), | ||||
|                 AutoClear = true, | ||||
|                 AutoRefresh = AutoRefresh, | ||||
|             }; | ||||
|  | ||||
|             progress.Columns(new ProgressColumn[] | ||||
|             { | ||||
|                 spinnerColumn, | ||||
|                 new TaskDescriptionColumn(), | ||||
|             }); | ||||
|  | ||||
|             await progress.StartAsync(async ctx => | ||||
|             { | ||||
|                 var statusContext = new StatusContext(ctx, ctx.AddTask(status), spinnerColumn); | ||||
|                 await action(statusContext).ConfigureAwait(false); | ||||
|             }).ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										76
									
								
								src/Spectre.Console/Progress/StatusContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/Spectre.Console/Progress/StatusContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a context that can be used to interact with a <see cref="Status"/>. | ||||
|     /// </summary> | ||||
|     public sealed class StatusContext | ||||
|     { | ||||
|         private readonly ProgressContext _context; | ||||
|         private readonly ProgressTask _task; | ||||
|         private readonly SpinnerColumn _spinnerColumn; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the current status. | ||||
|         /// </summary> | ||||
|         public string Status | ||||
|         { | ||||
|             get => _task.Description; | ||||
|             set => SetStatus(value); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the current spinner. | ||||
|         /// </summary> | ||||
|         public Spinner Spinner | ||||
|         { | ||||
|             get => _spinnerColumn.Spinner; | ||||
|             set => SetSpinner(value); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the current spinner style. | ||||
|         /// </summary> | ||||
|         public Style? SpinnerStyle | ||||
|         { | ||||
|             get => _spinnerColumn.Style; | ||||
|             set => _spinnerColumn.Style = value; | ||||
|         } | ||||
|  | ||||
|         internal StatusContext(ProgressContext context, ProgressTask task, SpinnerColumn spinnerColumn) | ||||
|         { | ||||
|             _context = context ?? throw new ArgumentNullException(nameof(context)); | ||||
|             _task = task ?? throw new ArgumentNullException(nameof(task)); | ||||
|             _spinnerColumn = spinnerColumn ?? throw new ArgumentNullException(nameof(spinnerColumn)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Refreshes the status. | ||||
|         /// </summary> | ||||
|         public void Refresh() | ||||
|         { | ||||
|             _context.Refresh(); | ||||
|         } | ||||
|  | ||||
|         private void SetStatus(string status) | ||||
|         { | ||||
|             if (status is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(status)); | ||||
|             } | ||||
|  | ||||
|             _task.Description = status; | ||||
|         } | ||||
|  | ||||
|         private void SetSpinner(Spinner spinner) | ||||
|         { | ||||
|             if (spinner is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(spinner)); | ||||
|             } | ||||
|  | ||||
|             _spinnerColumn.Spinner = spinner; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Internal; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| @@ -8,7 +7,7 @@ namespace Spectre.Console.Rendering | ||||
|     { | ||||
|         private readonly object _lock = new object(); | ||||
|         private IRenderable? _renderable; | ||||
|         private int? _height; | ||||
|         private SegmentShape? _shape; | ||||
|  | ||||
|         public void SetRenderable(IRenderable renderable) | ||||
|         { | ||||
| @@ -22,12 +21,12 @@ namespace Spectre.Console.Rendering | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_height == null) | ||||
|                 if (_shape == null) | ||||
|                 { | ||||
|                     return new ControlSequence(string.Empty); | ||||
|                 } | ||||
|  | ||||
|                 return new ControlSequence("\r" + "\u001b[1A".Repeat(_height.Value - 1)); | ||||
|                 return new ControlSequence("\r" + "\u001b[1A".Repeat(_shape.Value.Height - 1)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -35,12 +34,12 @@ namespace Spectre.Console.Rendering | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_height == null) | ||||
|                 if (_shape == null) | ||||
|                 { | ||||
|                     return new ControlSequence(string.Empty); | ||||
|                 } | ||||
|  | ||||
|                 return new ControlSequence("\r\u001b[2K" + "\u001b[1A\u001b[2K".Repeat(_height.Value - 1)); | ||||
|                 return new ControlSequence("\r\u001b[2K" + "\u001b[1A\u001b[2K".Repeat(_shape.Value.Height - 1)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -53,27 +52,27 @@ namespace Spectre.Console.Rendering | ||||
|                     var segments = _renderable.Render(context, maxWidth); | ||||
|                     var lines = Segment.SplitLines(context, segments); | ||||
|  | ||||
|                     _height = lines.Count; | ||||
|                     var shape = SegmentShape.Calculate(context, lines); | ||||
|                     _shape = _shape == null ? shape : _shape.Value.Inflate(shape); | ||||
|                     _shape.Value.SetShape(context, lines); | ||||
|  | ||||
|                     var result = new List<Segment>(); | ||||
|                     foreach (var (_, _, last, line) in lines.Enumerate()) | ||||
|                     { | ||||
|                         foreach (var item in line) | ||||
|                         { | ||||
|                             result.Add(item); | ||||
|                             yield return item; | ||||
|                         } | ||||
|  | ||||
|                         if (!last) | ||||
|                         { | ||||
|                             result.Add(Segment.LineBreak); | ||||
|                             yield return Segment.LineBreak; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return result; | ||||
|                     yield break; | ||||
|                 } | ||||
|  | ||||
|                 _height = 0; | ||||
|                 return Enumerable.Empty<Segment>(); | ||||
|                 _shape = null; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -599,6 +599,7 @@ namespace Spectre.Console.Rendering | ||||
|             return stack.ToList(); | ||||
|         } | ||||
|  | ||||
|         // TODO: Move this to Table | ||||
|         internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells) | ||||
|         { | ||||
|             if (cells is null) | ||||
| @@ -619,5 +620,49 @@ namespace Spectre.Console.Rendering | ||||
|  | ||||
|             return cells; | ||||
|         } | ||||
|  | ||||
|         internal static (int Width, int Height) GetShape(RenderContext context, List<SegmentLine> lines) | ||||
|         { | ||||
|             if (context is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(context)); | ||||
|             } | ||||
|  | ||||
|             if (lines is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(lines)); | ||||
|             } | ||||
|  | ||||
|             var height = lines.Count; | ||||
|             var width = lines.Max(l => CellCount(context, l)); | ||||
|  | ||||
|             return (width, height); | ||||
|         } | ||||
|  | ||||
|         internal static List<SegmentLine> SetShape(RenderContext context, List<SegmentLine> lines, (int Width, int Height) shape) | ||||
|         { | ||||
|             foreach (var line in lines) | ||||
|             { | ||||
|                 var length = CellCount(context, line); | ||||
|                 var missing = shape.Width - length; | ||||
|                 if (missing > 0) | ||||
|                 { | ||||
|                     line.Add(new Segment(new string(' ', missing))); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (lines.Count < shape.Height) | ||||
|             { | ||||
|                 var missing = shape.Height - lines.Count; | ||||
|                 for (int i = 0; i < missing; i++) | ||||
|                 { | ||||
|                     var line = new SegmentLine(); | ||||
|                     line.Add(new Segment(new string(' ', shape.Width))); | ||||
|                     lines.Add(line); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return lines; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/Spectre.Console/Rendering/SegmentShape.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/Spectre.Console/Rendering/SegmentShape.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| { | ||||
|     internal readonly struct SegmentShape | ||||
|     { | ||||
|         public int Width { get; } | ||||
|         public int Height { get; } | ||||
|  | ||||
|         public SegmentShape(int width, int height) | ||||
|         { | ||||
|             Width = width; | ||||
|             Height = height; | ||||
|         } | ||||
|  | ||||
|         public static SegmentShape Calculate(RenderContext context, List<SegmentLine> lines) | ||||
|         { | ||||
|             if (context is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(context)); | ||||
|             } | ||||
|  | ||||
|             if (lines is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(lines)); | ||||
|             } | ||||
|  | ||||
|             var height = lines.Count; | ||||
|             var width = lines.Max(l => Segment.CellCount(context, l)); | ||||
|  | ||||
|             return new SegmentShape(width, height); | ||||
|         } | ||||
|  | ||||
|         public SegmentShape Inflate(SegmentShape other) | ||||
|         { | ||||
|             return new SegmentShape( | ||||
|                 Math.Max(Width, other.Width), | ||||
|                 Math.Max(Height, other.Height)); | ||||
|         } | ||||
|  | ||||
|         public void SetShape(RenderContext context, List<SegmentLine> lines) | ||||
|         { | ||||
|             foreach (var line in lines) | ||||
|             { | ||||
|                 var length = Segment.CellCount(context, line); | ||||
|                 var missing = Width - length; | ||||
|                 if (missing > 0) | ||||
|                 { | ||||
|                     line.Add(new Segment(new string(' ', missing))); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (lines.Count < Height) | ||||
|             { | ||||
|                 var missing = Height - lines.Count; | ||||
|                 for (var i = 0; i < missing; i++) | ||||
|                 { | ||||
|                     var line = new SegmentLine(); | ||||
|                     line.Add(new Segment(new string(' ', Width))); | ||||
|                     lines.Add(line); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user