mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-01 01:25:27 +08:00 
			
		
		
		
	Add progress task list support
This commit is contained in:
		 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						parent
						
							c61e386440
						
					
				
				
					commit
					ae32785f21
				
			| @@ -89,4 +89,7 @@ dotnet_diagnostic.RCS1227.severity = none | ||||
| dotnet_diagnostic.IDE0004.severity = warning | ||||
|  | ||||
| # CA1810: Initialize reference type static fields inline | ||||
| dotnet_diagnostic.CA1810.severity = none | ||||
| dotnet_diagnostic.CA1810.severity = none | ||||
|  | ||||
| # IDE0044: Add readonly modifier | ||||
| dotnet_diagnostic.IDE0044.severity = warning | ||||
| @@ -0,0 +1 @@ | ||||
| ━━━━━━━━━━━━━━━━━━━━                                                             | ||||
| @@ -18,6 +18,7 @@ namespace Spectre.Console.Tests | ||||
|         public int Height { get; } | ||||
|  | ||||
|         IAnsiConsoleInput IAnsiConsole.Input => Input; | ||||
|         public RenderPipeline Pipeline { get; } | ||||
|  | ||||
|         public Decoration Decoration { get; set; } | ||||
|         public Color Foreground { get; set; } | ||||
| @@ -31,14 +32,15 @@ namespace Spectre.Console.Tests | ||||
|         public PlainConsole( | ||||
|             int width = 80, int height = 9000, Encoding encoding = null, | ||||
|             bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard, | ||||
|             bool legacyConsole = false) | ||||
|             bool legacyConsole = false, bool interactive = true) | ||||
|         { | ||||
|             Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole); | ||||
|             Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, interactive); | ||||
|             Encoding = encoding ?? Encoding.UTF8; | ||||
|             Width = width; | ||||
|             Height = height; | ||||
|             Writer = new StringWriter(); | ||||
|             Input = new TestableConsoleInput(); | ||||
|             Pipeline = new RenderPipeline(); | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
| @@ -50,14 +52,17 @@ namespace Spectre.Console.Tests | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public void Write(Segment segment) | ||||
|         public void Write(IEnumerable<Segment> segments) | ||||
|         { | ||||
|             if (segment is null) | ||||
|             if (segments is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(segment)); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Writer.Write(segment.Text); | ||||
|             foreach (var segment in segments) | ||||
|             { | ||||
|                 Writer.Write(segment.Text); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public string WriteNormalizedException(Exception ex, ExceptionFormats formats = ExceptionFormats.Default) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Text; | ||||
| using Spectre.Console.Rendering; | ||||
| @@ -19,16 +20,21 @@ namespace Spectre.Console.Tests | ||||
|         public int Height => _console.Height; | ||||
|         public IAnsiConsoleCursor Cursor => _console.Cursor; | ||||
|         public TestableConsoleInput Input { get; } | ||||
|         public RenderPipeline Pipeline => _console.Pipeline; | ||||
|  | ||||
|         IAnsiConsoleInput IAnsiConsole.Input => Input; | ||||
|  | ||||
|         public TestableAnsiConsole(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80) | ||||
|         public TestableAnsiConsole( | ||||
|             ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, | ||||
|             InteractionSupport interaction = InteractionSupport.Yes, | ||||
|             int width = 80) | ||||
|         { | ||||
|             _writer = new StringWriter(); | ||||
|             _console = AnsiConsole.Create(new AnsiConsoleSettings | ||||
|             { | ||||
|                 Ansi = ansi, | ||||
|                 ColorSystem = (ColorSystemSupport)system, | ||||
|                 Interactive = interaction, | ||||
|                 Out = _writer, | ||||
|                 LinkIdentityGenerator = new TestLinkIdentityGenerator(), | ||||
|             }); | ||||
| @@ -47,9 +53,17 @@ namespace Spectre.Console.Tests | ||||
|             _console.Clear(home); | ||||
|         } | ||||
|  | ||||
|         public void Write(Segment segment) | ||||
|         public void Write(IEnumerable<Segment> segments) | ||||
|         { | ||||
|             _console.Write(segment); | ||||
|             if (segments is null) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             foreach (var segment in segments) | ||||
|             { | ||||
|                 _console.Write(segment); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										58
									
								
								src/Spectre.Console.Tests/Unit/ProgressTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/Spectre.Console.Tests/Unit/ProgressTests.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| using Shouldly; | ||||
| using Xunit; | ||||
|  | ||||
| namespace Spectre.Console.Tests.Unit | ||||
| { | ||||
|     public sealed class ProgressTests | ||||
|     { | ||||
|         [Fact] | ||||
|         public void Should_Render_Task_Correctly() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10); | ||||
|  | ||||
|             var progress = new Progress(console) | ||||
|                 .Columns(new[] { new ProgressBarColumn() }) | ||||
|                 .AutoRefresh(false) | ||||
|                 .AutoClear(true); | ||||
|  | ||||
|             // When | ||||
|             progress.Start(ctx => ctx.AddTask("foo")); | ||||
|  | ||||
|             // Then | ||||
|             console.Output | ||||
|                 .NormalizeLineEndings() | ||||
|                 .ShouldBe( | ||||
|                     "[?25l" + // Hide cursor | ||||
|                     "          \n" + // Top padding | ||||
|                     "[38;5;8m━━━━━━━━━━[0m\n" + // Task | ||||
|                     "          " + // Bottom padding | ||||
|                     "[2K[1A[2K[1A[2K[?25h"); // Clear + show cursor | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public void Should_Not_Auto_Clear_If_Specified() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10); | ||||
|  | ||||
|             var progress = new Progress(console) | ||||
|                 .Columns(new[] { new ProgressBarColumn() }) | ||||
|                 .AutoRefresh(false) | ||||
|                 .AutoClear(false); | ||||
|  | ||||
|             // When | ||||
|             progress.Start(ctx => ctx.AddTask("foo")); | ||||
|  | ||||
|             // Then | ||||
|             console.Output | ||||
|                 .NormalizeLineEndings() | ||||
|                 .ShouldBe( | ||||
|                     "[?25l" + // Hide cursor | ||||
|                     "          \n" + // Top padding | ||||
|                     "[38;5;8m━━━━━━━━━━[0m\n" + // Task | ||||
|                     "          \n" + // Bottom padding | ||||
|                     "[?25h"); // show cursor | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/Spectre.Console.Tests/Unit/RenderHookTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/Spectre.Console.Tests/Unit/RenderHookTests.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Shouldly; | ||||
| using Spectre.Console.Rendering; | ||||
| using Xunit; | ||||
|  | ||||
| namespace Spectre.Console.Tests.Unit | ||||
| { | ||||
|     public sealed class RenderHookTests | ||||
|     { | ||||
|         private sealed class HelloRenderHook : IRenderHook | ||||
|         { | ||||
|             public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|             { | ||||
|                 return new IRenderable[] { new Text("Hello\n") }.Concat(renderables); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public void Should_Inject_Renderable_Before_Writing_To_Console() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Pipeline.Attach(new HelloRenderHook()); | ||||
|  | ||||
|             // When | ||||
|             console.Render(new Text("World")); | ||||
|  | ||||
|             // Then | ||||
|             console.Lines[0].ShouldBe("Hello"); | ||||
|             console.Lines[1].ShouldBe("World"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -58,6 +58,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Canva | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Progress\Progress.csproj", "{2B712A52-40F1-4C1C-833E-7C869ACA91F3}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| @@ -296,6 +298,18 @@ Global | ||||
| 		{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{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 | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
| @@ -318,6 +332,7 @@ Global | ||||
| 		{6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 		{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} | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(ExtensibilityGlobals) = postSolution | ||||
| 		SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/Spectre.Console/AnsiConsole.Progress.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/Spectre.Console/AnsiConsole.Progress.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A console capable of writing ANSI escape sequences. | ||||
|     /// </summary> | ||||
|     public static partial class AnsiConsole | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Creates a new <see cref="Progress"/> instance. | ||||
|         /// </summary> | ||||
|         /// <returns>A <see cref="Progress"/> instance.</returns> | ||||
|         public static Progress Progress() | ||||
|         { | ||||
|             return Console.Progress(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -18,6 +18,12 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         public ColorSystemSupport ColorSystem { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or | ||||
|         /// not the console is interactive. | ||||
|         /// </summary> | ||||
|         public InteractionSupport Interactive { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the link identity generator. | ||||
|         /// </summary> | ||||
|   | ||||
| @@ -36,17 +36,24 @@ namespace Spectre.Console | ||||
|         /// </remarks> | ||||
|         public bool LegacyConsole { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not the console supports interaction. | ||||
|         /// </summary> | ||||
|         public bool SupportsInteraction { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Capabilities"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="supportsAnsi">Whether or not ANSI escape sequences are supported.</param> | ||||
|         /// <param name="colorSystem">The color system that is supported.</param> | ||||
|         /// <param name="legacyConsole">Whether or not this is a legacy console.</param> | ||||
|         public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole) | ||||
|         /// <param name="supportsInteraction">Whether or not the console supports interaction.</param> | ||||
|         public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole, bool supportsInteraction) | ||||
|         { | ||||
|             SupportsAnsi = supportsAnsi; | ||||
|             ColorSystem = colorSystem; | ||||
|             LegacyConsole = legacyConsole; | ||||
|             SupportsInteraction = supportsInteraction; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|   | ||||
| @@ -8,31 +8,31 @@ namespace Spectre.Console | ||||
|         /// <summary> | ||||
|         /// Try to detect the color system. | ||||
|         /// </summary> | ||||
|         Detect = -1, | ||||
|         Detect = 0, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// No colors. | ||||
|         /// </summary> | ||||
|         NoColors = 0, | ||||
|         NoColors = 1, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Legacy, 3-bit mode. | ||||
|         /// </summary> | ||||
|         Legacy = 1, | ||||
|         Legacy = 2, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Standard, 4-bit mode. | ||||
|         /// </summary> | ||||
|         Standard = 2, | ||||
|         Standard = 3, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 8-bit mode. | ||||
|         /// </summary> | ||||
|         EightBit = 3, | ||||
|         EightBit = 4, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 24-bit mode. | ||||
|         /// </summary> | ||||
|         TrueColor = 4, | ||||
|         TrueColor = 5, | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,8 +52,7 @@ namespace Spectre.Console | ||||
|         /// <param name="args">An array of objects to write.</param> | ||||
|         public static void MarkupLine(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args) | ||||
|         { | ||||
|             Markup(console, provider, format, args); | ||||
|             console.WriteLine(); | ||||
|             Markup(console, provider, format + Environment.NewLine, args); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,25 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="IAnsiConsole"/>. | ||||
|     /// </summary> | ||||
|     public static partial class AnsiConsoleExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Creates a new <see cref="Progress"/> instance for the console. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         /// <returns>A <see cref="Progress"/> instance.</returns> | ||||
|         public static Progress Progress(this IAnsiConsole console) | ||||
|         { | ||||
|             if (console is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|             } | ||||
|  | ||||
|             return new Progress(console); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| @@ -26,19 +26,26 @@ namespace Spectre.Console | ||||
|                 throw new ArgumentNullException(nameof(renderable)); | ||||
|             } | ||||
|  | ||||
|             var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole); | ||||
|             var segments = renderable.Render(options, console.Width).ToArray(); | ||||
|             segments = Segment.Merge(segments).ToArray(); | ||||
|             var context = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole); | ||||
|             var renderables = console.Pipeline.Process(context, new[] { renderable }); | ||||
|  | ||||
|             foreach (var segment in segments) | ||||
|             Render(console, context, renderables); | ||||
|         } | ||||
|  | ||||
|         private static void Render(IAnsiConsole console, RenderContext options, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             if (renderables is null) | ||||
|             { | ||||
|                 if (string.IsNullOrEmpty(segment.Text)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 console.Write(segment.Text, segment.Style); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var result = new List<Segment>(); | ||||
|             foreach (var renderable in renderables) | ||||
|             { | ||||
|                 result.AddRange(renderable.Render(options, console.Width)); | ||||
|             } | ||||
|  | ||||
|             console.Write(Segment.Merge(result)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,26 @@ namespace Spectre.Console | ||||
|             return new Recorder(console); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Writes the specified string value to the console. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console to write to.</param> | ||||
|         /// <param name="segment">The segment to write.</param> | ||||
|         public static void Write(this IAnsiConsole console, Segment segment) | ||||
|         { | ||||
|             if (console is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|             } | ||||
|  | ||||
|             if (segment is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(segment)); | ||||
|             } | ||||
|  | ||||
|             console.Write(new[] { segment }); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Writes the specified string value to the console. | ||||
|         /// </summary> | ||||
| @@ -25,7 +45,7 @@ namespace Spectre.Console | ||||
|         /// <param name="text">The text to write.</param> | ||||
|         public static void Write(this IAnsiConsole console, string text) | ||||
|         { | ||||
|             Write(console, text, Style.Plain); | ||||
|             Render(console, new Text(text, Style.Plain)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
| @@ -36,17 +56,7 @@ namespace Spectre.Console | ||||
|         /// <param name="style">The text style.</param> | ||||
|         public static void Write(this IAnsiConsole console, string text, Style style) | ||||
|         { | ||||
|             if (console is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|             } | ||||
|  | ||||
|             if (text is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(text)); | ||||
|             } | ||||
|  | ||||
|             console.Write(new Segment(text, style)); | ||||
|             Render(console, new Text(text, style)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
| @@ -60,7 +70,7 @@ namespace Spectre.Console | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|             } | ||||
|  | ||||
|             console.Write(Environment.NewLine, Style.Plain); | ||||
|             Render(console, new Text(Environment.NewLine, Style.Plain)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
| @@ -91,8 +101,7 @@ namespace Spectre.Console | ||||
|                 throw new ArgumentNullException(nameof(text)); | ||||
|             } | ||||
|  | ||||
|             console.Write(new Segment(text, style)); | ||||
|             console.WriteLine(); | ||||
|             console.Write(text + Environment.NewLine, style); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="PercentageColumn"/>. | ||||
|     /// </summary> | ||||
|     public static class PercentageColumnExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the style for a non-complete task. | ||||
|         /// </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 PercentageColumn Style(this PercentageColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.Style = style; | ||||
|             return column; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the style for a completed task. | ||||
|         /// </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 PercentageColumn CompletedStyle(this PercentageColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.CompletedStyle = style; | ||||
|             return column; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,76 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="ProgressBarColumn"/>. | ||||
|     /// </summary> | ||||
|     public static class ProgressBarColumnExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the style of completed portions of the progress bar. | ||||
|         /// </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 ProgressBarColumn CompletedStyle(this ProgressBarColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.CompletedStyle = style; | ||||
|             return column; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the style of a finished progress bar. | ||||
|         /// </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 ProgressBarColumn FinishedStyle(this ProgressBarColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.FinishedStyle = style; | ||||
|             return column; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the style of remaining portions of the progress bar. | ||||
|         /// </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 ProgressBarColumn RemainingStyle(this ProgressBarColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.RemainingStyle = style; | ||||
|             return column; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="RemainingTimeColumn"/>. | ||||
|     /// </summary> | ||||
|     public static class RemainingTimeColumnExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the style of the remaining time text. | ||||
|         /// </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 RemainingTimeColumn Style(this RemainingTimeColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.Style = style; | ||||
|             return column; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="SpinnerColumn"/>. | ||||
|     /// </summary> | ||||
|     public static class SpinnerColumnExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the 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 Style(this SpinnerColumn column, Style style) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (style is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(style)); | ||||
|             } | ||||
|  | ||||
|             column.Style = style; | ||||
|             return column; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -27,7 +27,7 @@ namespace Spectre.Console | ||||
|             } | ||||
|  | ||||
|             alignment ??= panel.Header?.Alignment; | ||||
|             return Header(panel, new PanelHeader(text,  alignment)); | ||||
|             return Header(panel, new PanelHeader(text, alignment)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|   | ||||
							
								
								
									
										79
									
								
								src/Spectre.Console/Extensions/ProgressExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/Spectre.Console/Extensions/ProgressExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="Progress"/>. | ||||
|     /// </summary> | ||||
|     public static class ProgressExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the columns to be used for an <see cref="Progress"/> instance. | ||||
|         /// </summary> | ||||
|         /// <param name="progress">The <see cref="Progress"/> instance.</param> | ||||
|         /// <param name="columns">The columns to use.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static Progress Columns(this Progress progress, ProgressColumn[] columns) | ||||
|         { | ||||
|             if (progress is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(progress)); | ||||
|             } | ||||
|  | ||||
|             if (columns is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(columns)); | ||||
|             } | ||||
|  | ||||
|             if (!columns.Any()) | ||||
|             { | ||||
|                 throw new InvalidOperationException("At least one column must be specified."); | ||||
|             } | ||||
|  | ||||
|             progress.Columns.Clear(); | ||||
|             progress.Columns.AddRange(columns); | ||||
|  | ||||
|             return progress; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets whether or not auto refresh is enabled. | ||||
|         /// If disabled, you will manually have to refresh the progress. | ||||
|         /// </summary> | ||||
|         /// <param name="progress">The <see cref="Progress"/> 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 Progress AutoRefresh(this Progress progress, bool enabled) | ||||
|         { | ||||
|             if (progress is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(progress)); | ||||
|             } | ||||
|  | ||||
|             progress.AutoRefresh = enabled; | ||||
|  | ||||
|             return progress; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets whether or not auto clear is enabled. | ||||
|         /// If enabled, the task tabled will be removed once | ||||
|         /// all tasks have completed. | ||||
|         /// </summary> | ||||
|         /// <param name="progress">The <see cref="Progress"/> instance.</param> | ||||
|         /// <param name="enabled">Whether or not auto clear is enabled.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static Progress AutoClear(this Progress progress, bool enabled) | ||||
|         { | ||||
|             if (progress is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(progress)); | ||||
|             } | ||||
|  | ||||
|             progress.AutoClear = enabled; | ||||
|  | ||||
|             return progress; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/Spectre.Console/Extensions/ProgressTaskExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/Spectre.Console/Extensions/ProgressTaskExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="ProgressTask"/>. | ||||
|     /// </summary> | ||||
|     public static class ProgressTaskExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the task description. | ||||
|         /// </summary> | ||||
|         /// <param name="task">The task.</param> | ||||
|         /// <param name="description">The description.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static ProgressTask Description(this ProgressTask task, string description) | ||||
|         { | ||||
|             if (task is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(task)); | ||||
|             } | ||||
|  | ||||
|             task.Description = description; | ||||
|             return task; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the max value of the task. | ||||
|         /// </summary> | ||||
|         /// <param name="task">The task.</param> | ||||
|         /// <param name="value">The max value.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static ProgressTask MaxValue(this ProgressTask task, double value) | ||||
|         { | ||||
|             if (task is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(task)); | ||||
|             } | ||||
|  | ||||
|             task.MaxValue = value; | ||||
|             return task; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Globalization; | ||||
| using System.Text; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal static class StringBuilderExtensions | ||||
|     { | ||||
|   | ||||
| @@ -62,10 +62,15 @@ namespace Spectre.Console | ||||
|             return text; | ||||
|         } | ||||
|  | ||||
|         internal static string NormalizeLineEndings(this string? text, bool native = false) | ||||
|         internal static string? RemoveNewLines(this string? text) | ||||
|         { | ||||
|             return text?.ReplaceExact("\r\n", string.Empty) | ||||
|                 ?.ReplaceExact("\n", string.Empty); | ||||
|         } | ||||
|  | ||||
|         internal static string NormalizeNewLines(this string? text, bool native = false) | ||||
|         { | ||||
|             text = text?.ReplaceExact("\r\n", "\n"); | ||||
|             text = text?.ReplaceExact("\r", string.Empty); | ||||
|             text ??= string.Empty; | ||||
|  | ||||
|             if (native && !_alreadyNormalized) | ||||
| @@ -78,7 +83,7 @@ namespace Spectre.Console | ||||
|  | ||||
|         internal static string[] SplitLines(this string text) | ||||
|         { | ||||
|             var result = text?.NormalizeLineEndings()?.Split(new[] { '\n' }, StringSplitOptions.None); | ||||
|             var result = text?.NormalizeNewLines()?.Split(new[] { '\n' }, StringSplitOptions.None); | ||||
|             return result ?? Array.Empty<string>(); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| @@ -28,6 +29,11 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         IAnsiConsoleInput Input { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the render pipeline. | ||||
|         /// </summary> | ||||
|         RenderPipeline Pipeline { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the buffer width of the console. | ||||
|         /// </summary> | ||||
| @@ -45,9 +51,9 @@ namespace Spectre.Console | ||||
|         void Clear(bool home); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Writes a string followed by a line terminator to the console. | ||||
|         /// Writes multiple segments to the console. | ||||
|         /// </summary> | ||||
|         /// <param name="segment">The segment to write.</param> | ||||
|         void Write(Segment segment); | ||||
|         /// <param name="segments">The segments to write.</param> | ||||
|         void Write(IEnumerable<Segment> segments); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										24
									
								
								src/Spectre.Console/InteractionSupport.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/Spectre.Console/InteractionSupport.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Determines interactivity support. | ||||
|     /// </summary> | ||||
|     public enum InteractionSupport | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Interaction support should be | ||||
|         /// detected by the system. | ||||
|         /// </summary> | ||||
|         Detect = 0, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Interactivity is supported. | ||||
|         /// </summary> | ||||
|         Yes = 1, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Interactivity is not supported. | ||||
|         /// </summary> | ||||
|         No = 2, | ||||
|     } | ||||
| } | ||||
| @@ -121,6 +121,8 @@ namespace Spectre.Console.Internal | ||||
|                             // Enabling failed. | ||||
|                             return false; | ||||
|                         } | ||||
|  | ||||
|                         isLegacy = false; | ||||
|                     } | ||||
|  | ||||
|                     return true; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Text; | ||||
| using Spectre.Console.Rendering; | ||||
| @@ -11,9 +12,11 @@ namespace Spectre.Console.Internal | ||||
|         private readonly AnsiBuilder _ansiBuilder; | ||||
|         private readonly AnsiCursor _cursor; | ||||
|         private readonly ConsoleInput _input; | ||||
|         private readonly object _lock; | ||||
|  | ||||
|         public Capabilities Capabilities { get; } | ||||
|         public Encoding Encoding { get; } | ||||
|         public RenderPipeline Pipeline { get; } | ||||
|         public IAnsiConsoleCursor Cursor => _cursor; | ||||
|         public IAnsiConsoleInput Input => _input; | ||||
|  | ||||
| @@ -49,35 +52,59 @@ namespace Spectre.Console.Internal | ||||
|  | ||||
|             Capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities)); | ||||
|             Encoding = _out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8; | ||||
|             Pipeline = new RenderPipeline(); | ||||
|  | ||||
|             _ansiBuilder = new AnsiBuilder(Capabilities, linkHasher); | ||||
|             _cursor = new AnsiCursor(this); | ||||
|             _input = new ConsoleInput(); | ||||
|             _lock = new object(); | ||||
|         } | ||||
|  | ||||
|         public void Clear(bool home) | ||||
|         { | ||||
|             Write(Segment.Control("\u001b[2J")); | ||||
|  | ||||
|             if (home) | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 Cursor.SetPosition(0, 0); | ||||
|                 Write(new[] { Segment.Control("\u001b[2J") }); | ||||
|  | ||||
|                 if (home) | ||||
|                 { | ||||
|                     Cursor.SetPosition(0, 0); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Write(Segment segment) | ||||
|         public void Write(IEnumerable<Segment> segments) | ||||
|         { | ||||
|             var parts = segment.Text.NormalizeLineEndings().Split(new[] { '\n' }); | ||||
|             foreach (var (_, _, last, part) in parts.Enumerate()) | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (!string.IsNullOrEmpty(part)) | ||||
|                 var builder = new StringBuilder(); | ||||
|                 foreach (var segment in segments) | ||||
|                 { | ||||
|                     _out.Write(_ansiBuilder.GetAnsi(part, segment.Style)); | ||||
|                     if (segment.IsControlCode) | ||||
|                     { | ||||
|                         builder.Append(segment.Text); | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     var parts = segment.Text.NormalizeNewLines().Split(new[] { '\n' }); | ||||
|                     foreach (var (_, _, last, part) in parts.Enumerate()) | ||||
|                     { | ||||
|                         if (!string.IsNullOrEmpty(part)) | ||||
|                         { | ||||
|                             builder.Append(_ansiBuilder.GetAnsi(part, segment.Style)); | ||||
|                         } | ||||
|  | ||||
|                         if (!last) | ||||
|                         { | ||||
|                             builder.Append(Environment.NewLine); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (!last) | ||||
|                 if (builder.Length > 0) | ||||
|                 { | ||||
|                     _out.Write(Environment.NewLine); | ||||
|                     _out.Write(builder.ToString()); | ||||
|                     _out.Flush(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -50,12 +50,18 @@ namespace Spectre.Console.Internal | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             var supportsInteraction = settings.Interactive == InteractionSupport.Yes; | ||||
|             if (settings.Interactive == InteractionSupport.Detect) | ||||
|             { | ||||
|                 supportsInteraction = InteractivityDetector.IsInteractive(); | ||||
|             } | ||||
|  | ||||
|             var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect | ||||
|                 ? ColorSystemDetector.Detect(supportsAnsi) | ||||
|                 : (ColorSystem)settings.ColorSystem; | ||||
|  | ||||
|             // Get the capabilities | ||||
|             var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole); | ||||
|             var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, supportsInteraction); | ||||
|  | ||||
|             // Create the renderer | ||||
|             if (supportsAnsi) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Text; | ||||
| using Spectre.Console.Rendering; | ||||
| @@ -14,6 +15,7 @@ namespace Spectre.Console.Internal | ||||
|  | ||||
|         public Capabilities Capabilities { get; } | ||||
|         public Encoding Encoding { get; } | ||||
|         public RenderPipeline Pipeline { get; } | ||||
|         public IAnsiConsoleCursor Cursor => _cursor; | ||||
|         public IAnsiConsoleInput Input => _input; | ||||
|  | ||||
| @@ -43,8 +45,9 @@ namespace Spectre.Console.Internal | ||||
|                 System.Console.SetOut(@out ?? throw new ArgumentNullException(nameof(@out))); | ||||
|             } | ||||
|  | ||||
|             Encoding = System.Console.OutputEncoding; | ||||
|             Capabilities = capabilities; | ||||
|             Encoding = System.Console.OutputEncoding; | ||||
|             Pipeline = new RenderPipeline(); | ||||
|         } | ||||
|  | ||||
|         public void Clear(bool home) | ||||
| @@ -60,14 +63,22 @@ namespace Spectre.Console.Internal | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Write(Segment segment) | ||||
|         public void Write(IEnumerable<Segment> segments) | ||||
|         { | ||||
|             if (_lastStyle?.Equals(segment.Style) != true) | ||||
|             foreach (var segment in segments) | ||||
|             { | ||||
|                 SetStyle(segment.Style); | ||||
|             } | ||||
|                 if (segment.IsControlCode) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|             System.Console.Write(segment.Text.NormalizeLineEndings(native: true)); | ||||
|                 if (_lastStyle?.Equals(segment.Style) != true) | ||||
|                 { | ||||
|                     SetStyle(segment.Style); | ||||
|                 } | ||||
|  | ||||
|                 System.Console.Write(segment.Text.NormalizeNewLines(native: true)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void SetStyle(Style style) | ||||
|   | ||||
| @@ -15,6 +15,11 @@ namespace Spectre.Console.Internal | ||||
|  | ||||
|             foreach (var (_, first, _, segment) in segments.Enumerate()) | ||||
|             { | ||||
|                 if (segment.IsControlCode) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (segment.Text == "\n" && !first) | ||||
|                 { | ||||
|                     builder.Append('\n'); | ||||
|   | ||||
							
								
								
									
										52
									
								
								src/Spectre.Console/Internal/InteractivityDetector.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/Spectre.Console/Internal/InteractivityDetector.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal static class InteractivityDetector | ||||
|     { | ||||
|         private static readonly Dictionary<string, Func<string, bool>> _environmentVariables; | ||||
|  | ||||
|         static InteractivityDetector() | ||||
|         { | ||||
|             _environmentVariables = new Dictionary<string, Func<string, bool>> | ||||
|             { | ||||
|                 { "APPVEYOR", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "bamboo_buildNumber", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "BITBUCKET_REPO_OWNER", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "BITBUCKET_REPO_SLUG", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "BITBUCKET_COMMIT", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "BITRISE_BUILD_URL", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "ContinuaCI.Version", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "CI_SERVER", v => v.Equals("yes", StringComparison.OrdinalIgnoreCase) }, // GitLab | ||||
|                 { "GITHUB_ACTIONS", v => v.Equals("true", StringComparison.OrdinalIgnoreCase) }, | ||||
|                 { "GO_SERVER_URL", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "JENKINS_URL", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "BuildRunner", v => v.Equals("MyGet", StringComparison.OrdinalIgnoreCase) }, | ||||
|                 { "TEAMCITY_VERSION", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|                 { "TF_BUILD", v => !string.IsNullOrWhiteSpace(v) }, // TFS and Azure | ||||
|                 { "TRAVIS", v => !string.IsNullOrWhiteSpace(v) }, | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public static bool IsInteractive() | ||||
|         { | ||||
|             if (!Environment.UserInteractive) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             foreach (var variable in _environmentVariables) | ||||
|             { | ||||
|                 var func = variable.Value; | ||||
|                 var value = Environment.GetEnvironmentVariable(variable.Key); | ||||
|                 if (!string.IsNullOrWhiteSpace(value) && variable.Value(value)) | ||||
|                 { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -24,7 +24,7 @@ namespace Spectre.Console.Internal | ||||
|  | ||||
|                 using (var reader = new StreamReader(stream)) | ||||
|                 { | ||||
|                     return reader.ReadToEnd().NormalizeLineEndings(); | ||||
|                     return reader.ReadToEnd().NormalizeNewLines(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -12,6 +12,11 @@ namespace Spectre.Console.Internal | ||||
|  | ||||
|             foreach (var segment in Segment.Merge(segments)) | ||||
|             { | ||||
|                 if (segment.IsControlCode) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 builder.Append(segment.Text); | ||||
|             } | ||||
|  | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/Spectre.Console/Progress/Columns/PercentageColumn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/Spectre.Console/Progress/Columns/PercentageColumn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A column showing task progress in percentage. | ||||
|     /// </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> | ||||
|         public Style Style { get; set; } = Style.Plain; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style for a completed task. | ||||
|         /// </summary> | ||||
|         public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green); | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) | ||||
|         { | ||||
|             var percentage = (int)task.Percentage; | ||||
|             var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain; | ||||
|             return new Text($"{percentage}%", style).RightAligned(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A column showing task progress as a progress bar. | ||||
|     /// </summary> | ||||
|     public sealed class ProgressBarColumn : ProgressColumn | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets the width of the column. | ||||
|         /// </summary> | ||||
|         public int? Width { get; set; } = 40; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style of completed portions of the progress bar. | ||||
|         /// </summary> | ||||
|         public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style of a finished progress bar. | ||||
|         /// </summary> | ||||
|         public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style of remaining portions of the progress bar. | ||||
|         /// </summary> | ||||
|         public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey); | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) | ||||
|         { | ||||
|             return new ProgressBar | ||||
|             { | ||||
|                 MaxValue = task.MaxValue, | ||||
|                 Value = task.Value, | ||||
|                 Width = Width, | ||||
|                 CompletedStyle = CompletedStyle, | ||||
|                 FinishedStyle = FinishedStyle, | ||||
|                 RemainingStyle = RemainingStyle, | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A column showing the remaining time of a task. | ||||
|     /// </summary> | ||||
|     public sealed class RemainingTimeColumn : ProgressColumn | ||||
|     { | ||||
|         /// <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 remaining = task.RemainingTime; | ||||
|             if (remaining == null) | ||||
|             { | ||||
|                 return new Markup("-:--:--"); | ||||
|             } | ||||
|  | ||||
|             return new Text($"{remaining.Value:h\\:mm\\:ss}", Style ?? Style.Plain); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/Spectre.Console/Progress/Columns/SpinnerColumn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/Spectre.Console/Progress/Columns/SpinnerColumn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A column showing a spinner. | ||||
|     /// </summary> | ||||
|     public sealed class SpinnerColumn : ProgressColumn | ||||
|     { | ||||
|         private const string ACCUMULATED = "SPINNER_ACCUMULATED"; | ||||
|         private const string INDEX = "SPINNER_INDEX"; | ||||
|  | ||||
|         private readonly string _ansiSequence = "⣷⣯⣟⡿⢿⣻⣽⣾"; | ||||
|         private readonly string _asciiSequence = "-\\|/-\\|/"; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style of the spinner. | ||||
|         /// </summary> | ||||
|         public Style Style { get; set; } = new Style(foreground: Color.Yellow); | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) | ||||
|         { | ||||
|             if (!task.IsStarted || task.IsFinished) | ||||
|             { | ||||
|                 return new Markup(" "); | ||||
|             } | ||||
|  | ||||
|             var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds); | ||||
|             if (accumulated >= 100) | ||||
|             { | ||||
|                 task.State.Update<double>(ACCUMULATED, _ => 0); | ||||
|                 task.State.Update<int>(INDEX, index => index + 1); | ||||
|             } | ||||
|  | ||||
|             var useAscii = context.LegacyConsole || !context.Unicode; | ||||
|             var sequence = useAscii ? _asciiSequence : _ansiSequence; | ||||
|  | ||||
|             var index = task.State.Get<int>(INDEX); | ||||
|             return new Markup(sequence[index % sequence.Length].ToString(), Style ?? Style.Plain); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A column showing the task description. | ||||
|     /// </summary> | ||||
|     public sealed class TaskDescriptionColumn : ProgressColumn | ||||
|     { | ||||
|         /// <inheritdoc/> | ||||
|         public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) | ||||
|         { | ||||
|             var text = task.Description?.RemoveNewLines()?.Trim(); | ||||
|             return new Markup(text ?? string.Empty).RightAligned(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										121
									
								
								src/Spectre.Console/Progress/Progress.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/Spectre.Console/Progress/Progress.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using Spectre.Console.Internal; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a task list. | ||||
|     /// </summary> | ||||
|     public sealed class Progress | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not task list should auto refresh. | ||||
|         /// Defaults to <c>true</c>. | ||||
|         /// </summary> | ||||
|         public bool AutoRefresh { get; set; } = true; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not the task list should | ||||
|         /// be cleared once it completes. | ||||
|         /// Defaults to <c>false</c>. | ||||
|         /// </summary> | ||||
|         public bool AutoClear { get; set; } | ||||
|  | ||||
|         internal List<ProgressColumn> Columns { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Progress"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console to render to.</param> | ||||
|         public Progress(IAnsiConsole console) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|  | ||||
|             // Initialize with default columns | ||||
|             Columns = new List<ProgressColumn> | ||||
|             { | ||||
|                 new TaskDescriptionColumn(), | ||||
|                 new ProgressBarColumn(), | ||||
|                 new PercentageColumn(), | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the progress task list. | ||||
|         /// </summary> | ||||
|         /// <param name="action">The action to execute.</param> | ||||
|         public void Start(Action<ProgressContext> action) | ||||
|         { | ||||
|             var task = StartAsync(ctx => | ||||
|             { | ||||
|                 action(ctx); | ||||
|                 return Task.CompletedTask; | ||||
|             }); | ||||
|  | ||||
|             task.GetAwaiter().GetResult(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the progress task list. | ||||
|         /// </summary> | ||||
|         /// <param name="action">The action to execute.</param> | ||||
|         /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | ||||
|         public async Task StartAsync(Func<ProgressContext, Task> action) | ||||
|         { | ||||
|             if (action is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(action)); | ||||
|             } | ||||
|  | ||||
|             var renderer = CreateRenderer(); | ||||
|             renderer.Started(); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 using (new RenderHookScope(_console, renderer)) | ||||
|                 { | ||||
|                     var context = new ProgressContext(_console, renderer); | ||||
|  | ||||
|                     if (AutoRefresh) | ||||
|                     { | ||||
|                         using (var thread = new ProgressRefreshThread(context, renderer.RefreshRate)) | ||||
|                         { | ||||
|                             await action(context).ConfigureAwait(false); | ||||
|                         } | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         await action(context).ConfigureAwait(false); | ||||
|                     } | ||||
|  | ||||
|                     context.Refresh(); | ||||
|                 } | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 renderer.Completed(AutoClear); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private ProgressRenderer CreateRenderer() | ||||
|         { | ||||
|             var caps = _console.Capabilities; | ||||
|             var interactive = caps.SupportsInteraction && caps.SupportsAnsi; | ||||
|  | ||||
|             if (interactive) | ||||
|             { | ||||
|                 var columns = new List<ProgressColumn>(Columns); | ||||
|                 return new InteractiveProgressRenderer(_console, columns); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return new NonInteractiveProgressRenderer(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/Spectre.Console/Progress/ProgressColumn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/Spectre.Console/Progress/ProgressColumn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a progress column. | ||||
|     /// </summary> | ||||
|     public abstract class ProgressColumn | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets the requested column width for the column. | ||||
|         /// </summary> | ||||
|         protected internal virtual int? ColumnWidth { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets a renderable representing the column. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The render context.</param> | ||||
|         /// <param name="task">The task.</param> | ||||
|         /// <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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										70
									
								
								src/Spectre.Console/Progress/ProgressContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/Spectre.Console/Progress/ProgressContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Internal; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a context that can be used to interact with a <see cref="Progress"/>. | ||||
|     /// </summary> | ||||
|     public sealed class ProgressContext | ||||
|     { | ||||
|         private readonly List<ProgressTask> _tasks; | ||||
|         private readonly object _taskLock; | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly ProgressRenderer _renderer; | ||||
|         private int _taskId; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets a value indicating whether or not all tasks have completed. | ||||
|         /// </summary> | ||||
|         public bool IsFinished => _tasks.All(task => task.IsFinished); | ||||
|  | ||||
|         internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer) | ||||
|         { | ||||
|             _tasks = new List<ProgressTask>(); | ||||
|             _taskLock = new object(); | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Adds a task. | ||||
|         /// </summary> | ||||
|         /// <param name="description">The task description.</param> | ||||
|         /// <param name="settings">The task settings.</param> | ||||
|         /// <returns>The task's ID.</returns> | ||||
|         public ProgressTask AddTask(string description, ProgressTaskSettings? settings = null) | ||||
|         { | ||||
|             lock (_taskLock) | ||||
|             { | ||||
|                 settings ??= new ProgressTaskSettings(); | ||||
|                 var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart); | ||||
|  | ||||
|                 _tasks.Add(task); | ||||
|                 return task; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Refreshes the current progress. | ||||
|         /// </summary> | ||||
|         public void Refresh() | ||||
|         { | ||||
|             _renderer.Update(this); | ||||
|             _console.Render(new ControlSequence(string.Empty)); | ||||
|         } | ||||
|  | ||||
|         internal void EnumerateTasks(Action<ProgressTask> action) | ||||
|         { | ||||
|             lock (_taskLock) | ||||
|             { | ||||
|                 foreach (var task in _tasks) | ||||
|                 { | ||||
|                     action(task); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/Spectre.Console/Progress/ProgressRefreshThread.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/Spectre.Console/Progress/ProgressRefreshThread.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| using System; | ||||
| using System.Threading; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal sealed class ProgressRefreshThread : IDisposable | ||||
|     { | ||||
|         private readonly ProgressContext _context; | ||||
|         private readonly TimeSpan _refreshRate; | ||||
|         private readonly ManualResetEvent _running; | ||||
|         private readonly ManualResetEvent _stopped; | ||||
|         private readonly Thread? _thread; | ||||
|  | ||||
|         public ProgressRefreshThread(ProgressContext context, TimeSpan refreshRate) | ||||
|         { | ||||
|             _context = context ?? throw new ArgumentNullException(nameof(context)); | ||||
|             _refreshRate = refreshRate; | ||||
|             _running = new ManualResetEvent(false); | ||||
|             _stopped = new ManualResetEvent(false); | ||||
|  | ||||
|             _thread = new Thread(Run); | ||||
|             _thread.IsBackground = true; | ||||
|             _thread.Start(); | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             if (_thread == null || !_running.WaitOne(0)) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             _stopped.Set(); | ||||
|             _thread.Join(); | ||||
|  | ||||
|             _stopped.Dispose(); | ||||
|             _running.Dispose(); | ||||
|         } | ||||
|  | ||||
|         private void Run() | ||||
|         { | ||||
|             _running.Set(); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 while (!_stopped.WaitOne(_refreshRate)) | ||||
|                 { | ||||
|                     _context.Refresh(); | ||||
|                 } | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 _stopped.Reset(); | ||||
|                 _running.Reset(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/Spectre.Console/Progress/ProgressRenderer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/Spectre.Console/Progress/ProgressRenderer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal abstract class ProgressRenderer : IRenderHook | ||||
|     { | ||||
|         public abstract TimeSpan RefreshRate { get; } | ||||
|  | ||||
|         public virtual void Started() | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public virtual void Completed(bool clear) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public abstract void Update(ProgressContext context); | ||||
|         public abstract IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/Spectre.Console/Progress/ProgressSample.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/Spectre.Console/Progress/ProgressSample.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal readonly struct ProgressSample | ||||
|     { | ||||
|         public double Value { get; } | ||||
|         public DateTime Timestamp { get; } | ||||
|  | ||||
|         public ProgressSample(DateTime timestamp, double value) | ||||
|         { | ||||
|             Timestamp = timestamp; | ||||
|             Value = value; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										267
									
								
								src/Spectre.Console/Progress/ProgressTask.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								src/Spectre.Console/Progress/ProgressTask.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,267 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Internal; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a progress task. | ||||
|     /// </summary> | ||||
|     public sealed class ProgressTask | ||||
|     { | ||||
|         private readonly List<ProgressSample> _samples; | ||||
|         private readonly object _lock; | ||||
|  | ||||
|         private double _maxValue; | ||||
|         private string _description; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the task ID. | ||||
|         /// </summary> | ||||
|         public int Id { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the task description. | ||||
|         /// </summary> | ||||
|         public string Description | ||||
|         { | ||||
|             get => _description; | ||||
|             set => Update(description: value); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the max value of the task. | ||||
|         /// </summary> | ||||
|         public double MaxValue | ||||
|         { | ||||
|             get => _maxValue; | ||||
|             set => Update(maxValue: value); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the value of the task. | ||||
|         /// </summary> | ||||
|         public double Value { get; private set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the start time of the task. | ||||
|         /// </summary> | ||||
|         public DateTime? StartTime { get; private set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the stop time of the task. | ||||
|         /// </summary> | ||||
|         public DateTime? StopTime { get; private set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the task state. | ||||
|         /// </summary> | ||||
|         public ProgressTaskState State { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets a value indicating whether or not the task has started. | ||||
|         /// </summary> | ||||
|         public bool IsStarted => StartTime != null; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets a value indicating whether or not the task has finished. | ||||
|         /// </summary> | ||||
|         public bool IsFinished => Value >= MaxValue; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the percentage done of the task. | ||||
|         /// </summary> | ||||
|         public double Percentage => GetPercentage(); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the speed measured in steps/second. | ||||
|         /// </summary> | ||||
|         public double? Speed => GetSpeed(); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the elapsed time. | ||||
|         /// </summary> | ||||
|         public TimeSpan? ElapsedTime => GetElapsedTime(); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the remaining time. | ||||
|         /// </summary> | ||||
|         public TimeSpan? RemainingTime => GetRemainingTime(); | ||||
|  | ||||
|         internal ProgressTask(int id, string description, double maxValue, bool autoStart) | ||||
|         { | ||||
|             _samples = new List<ProgressSample>(); | ||||
|             _lock = new object(); | ||||
|             _maxValue = maxValue; | ||||
|  | ||||
|             _description = description?.RemoveNewLines()?.Trim() ?? throw new ArgumentNullException(nameof(description)); | ||||
|             if (string.IsNullOrWhiteSpace(_description)) | ||||
|             { | ||||
|                 throw new ArgumentException("Task name cannot be empty", nameof(description)); | ||||
|             } | ||||
|  | ||||
|             Id = id; | ||||
|             State = new ProgressTaskState(); | ||||
|             Value = 0; | ||||
|             StartTime = autoStart ? DateTime.Now : null; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the task. | ||||
|         /// </summary> | ||||
|         public void StartTask() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 StartTime = DateTime.Now; | ||||
|                 StopTime = null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Stops the task. | ||||
|         /// </summary> | ||||
|         public void StopTask() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 var now = DateTime.Now; | ||||
|                 if (StartTime == null) | ||||
|                 { | ||||
|                     StartTime = now; | ||||
|                 } | ||||
|  | ||||
|                 StopTime = now; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Increments the task's value. | ||||
|         /// </summary> | ||||
|         /// <param name="value">The value to increment with.</param> | ||||
|         public void Increment(double value) | ||||
|         { | ||||
|             Update(increment: value); | ||||
|         } | ||||
|  | ||||
|         private void Update( | ||||
|             string? description = null, | ||||
|             double? maxValue = null, | ||||
|             double? increment = null) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 var startValue = Value; | ||||
|  | ||||
|                 if (description != null) | ||||
|                 { | ||||
|                     description = description?.RemoveNewLines()?.Trim(); | ||||
|                     if (string.IsNullOrWhiteSpace(description)) | ||||
|                     { | ||||
|                         throw new InvalidOperationException("Task name cannot be empty."); | ||||
|                     } | ||||
|  | ||||
|                     _description = description; | ||||
|                 } | ||||
|  | ||||
|                 if (maxValue != null) | ||||
|                 { | ||||
|                     _maxValue += maxValue.Value; | ||||
|                 } | ||||
|  | ||||
|                 if (increment != null) | ||||
|                 { | ||||
|                     Value += increment.Value; | ||||
|                 } | ||||
|  | ||||
|                 var timestamp = DateTime.Now; | ||||
|                 var threshold = timestamp - TimeSpan.FromSeconds(30); | ||||
|  | ||||
|                 // Remove samples that's too old | ||||
|                 while (_samples.Count > 0 && _samples[0].Timestamp < threshold) | ||||
|                 { | ||||
|                     _samples.RemoveAt(0); | ||||
|                 } | ||||
|  | ||||
|                 // Keep maximum of 1000 samples | ||||
|                 while (_samples.Count > 1000) | ||||
|                 { | ||||
|                     _samples.RemoveAt(0); | ||||
|                 } | ||||
|  | ||||
|                 _samples.Add(new ProgressSample(timestamp, Value - startValue)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private double GetPercentage() | ||||
|         { | ||||
|             var percentage = (Value / MaxValue) * 100; | ||||
|             percentage = Math.Min(100, Math.Max(0, percentage)); | ||||
|             return percentage; | ||||
|         } | ||||
|  | ||||
|         private double? GetSpeed() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (StartTime == null) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 if (_samples.Count == 0) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 var totalTime = _samples.Last().Timestamp - _samples[0].Timestamp; | ||||
|                 if (totalTime == TimeSpan.Zero) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 var totalCompleted = _samples.Sum(x => x.Value); | ||||
|                 return totalCompleted / totalTime.TotalSeconds; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private TimeSpan? GetElapsedTime() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (StartTime == null) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 if (StopTime != null) | ||||
|                 { | ||||
|                     return StopTime - StartTime; | ||||
|                 } | ||||
|  | ||||
|                 return DateTime.Now - StartTime; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private TimeSpan? GetRemainingTime() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (IsFinished) | ||||
|                 { | ||||
|                     return TimeSpan.Zero; | ||||
|                 } | ||||
|  | ||||
|                 var speed = GetSpeed(); | ||||
|                 if (speed == null) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 var estimate = (MaxValue - Value) / speed.Value; | ||||
|                 return TimeSpan.FromSeconds(estimate); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/Spectre.Console/Progress/ProgressTaskSettings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/Spectre.Console/Progress/ProgressTaskSettings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents settings for a progress task. | ||||
|     /// </summary> | ||||
|     public sealed class ProgressTaskSettings | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets the task's max value. | ||||
|         /// Defaults to <c>100</c>. | ||||
|         /// </summary> | ||||
|         public double MaxValue { get; set; } = 100; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not the task | ||||
|         /// will be auto started. Defaults to <c>true</c>. | ||||
|         /// </summary> | ||||
|         public bool AutoStart { get; set; } = true; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										81
									
								
								src/Spectre.Console/Progress/ProgressTaskState.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/Spectre.Console/Progress/ProgressTaskState.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents progress task state. | ||||
|     /// </summary> | ||||
|     public sealed class ProgressTaskState | ||||
|     { | ||||
|         private readonly Dictionary<string, object> _state; | ||||
|         private readonly object _lock; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProgressTaskState"/> class. | ||||
|         /// </summary> | ||||
|         public ProgressTaskState() | ||||
|         { | ||||
|             _state = new Dictionary<string, object>(); | ||||
|             _lock = new object(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the state value for the specified key. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The state value type.</typeparam> | ||||
|         /// <param name="key">The state key.</param> | ||||
|         /// <returns>The value for the specified key.</returns> | ||||
|         public T Get<T>(string key) | ||||
|             where T : struct | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (!_state.TryGetValue(key, out var value)) | ||||
|                 { | ||||
|                     return default; | ||||
|                 } | ||||
|  | ||||
|                 if (!(value is T)) | ||||
|                 { | ||||
|                     throw new InvalidOperationException("State value is of the wrong type."); | ||||
|                 } | ||||
|  | ||||
|                 return (T)value; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Updates a task state value. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The state value type.</typeparam> | ||||
|         /// <param name="key">The key.</param> | ||||
|         /// <param name="func">The transformation function.</param> | ||||
|         /// <returns>The updated value.</returns> | ||||
|         public T Update<T>(string key, Func<T, T> func) | ||||
|             where T : struct | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (func is null) | ||||
|                 { | ||||
|                     throw new ArgumentNullException(nameof(func)); | ||||
|                 } | ||||
|  | ||||
|                 var old = default(T); | ||||
|                 if (_state.TryGetValue(key, out var value)) | ||||
|                 { | ||||
|                     if (!(value is T)) | ||||
|                     { | ||||
|                         throw new InvalidOperationException("State value is of the wrong type."); | ||||
|                     } | ||||
|  | ||||
|                     old = (T)value; | ||||
|                 } | ||||
|  | ||||
|                 _state[key] = func(old); | ||||
|                 return (T)_state[key]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,109 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal sealed class InteractiveProgressRenderer : ProgressRenderer | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly List<ProgressColumn> _columns; | ||||
|         private readonly LiveRenderable _live; | ||||
|         private readonly object _lock; | ||||
|         private readonly Stopwatch _stopwatch; | ||||
|         private TimeSpan _lastUpdate; | ||||
|  | ||||
|         public override TimeSpan RefreshRate => TimeSpan.FromMilliseconds(100); | ||||
|  | ||||
|         public InteractiveProgressRenderer(IAnsiConsole console, List<ProgressColumn> columns) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _columns = columns ?? throw new ArgumentNullException(nameof(columns)); | ||||
|             _live = new LiveRenderable(); | ||||
|             _lock = new object(); | ||||
|             _stopwatch = new Stopwatch(); | ||||
|             _lastUpdate = TimeSpan.Zero; | ||||
|         } | ||||
|  | ||||
|         public override void Started() | ||||
|         { | ||||
|             _console.Cursor.Hide(); | ||||
|         } | ||||
|  | ||||
|         public override void Completed(bool clear) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (clear) | ||||
|                 { | ||||
|                     _console.Render(_live.RestoreCursor()); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     _console.WriteLine(); | ||||
|                 } | ||||
|  | ||||
|                 _console.Cursor.Show(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public override void Update(ProgressContext context) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (!_stopwatch.IsRunning) | ||||
|                 { | ||||
|                     _stopwatch.Start(); | ||||
|                 } | ||||
|  | ||||
|                 var delta = _stopwatch.Elapsed - _lastUpdate; | ||||
|                 _lastUpdate = _stopwatch.Elapsed; | ||||
|  | ||||
|                 var grid = new Grid(); | ||||
|                 for (var columnIndex = 0; columnIndex < _columns.Count; columnIndex++) | ||||
|                 { | ||||
|                     var column = new GridColumn().PadRight(1); | ||||
|                     if (_columns[columnIndex].ColumnWidth != null) | ||||
|                     { | ||||
|                         column.Width = _columns[columnIndex].ColumnWidth; | ||||
|                     } | ||||
|  | ||||
|                     // Last column? | ||||
|                     if (columnIndex == _columns.Count - 1) | ||||
|                     { | ||||
|                         column.PadRight(0); | ||||
|                     } | ||||
|  | ||||
|                     grid.AddColumn(column); | ||||
|                 } | ||||
|  | ||||
|                 // Add rows | ||||
|                 var renderContext = new RenderContext(_console.Encoding, _console.Capabilities.LegacyConsole); | ||||
|                 context.EnumerateTasks(task => | ||||
|                 { | ||||
|                     var columns = _columns.Select(column => column.Render(renderContext, task, delta)); | ||||
|                     grid.AddRow(columns.ToArray()); | ||||
|                 }); | ||||
|  | ||||
|                 _live.SetRenderable(new Padder(grid, new Padding(0, 1))); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 yield return _live.PositionCursor(); | ||||
|  | ||||
|                 foreach (var renderable in renderables) | ||||
|                 { | ||||
|                     yield return renderable; | ||||
|                 } | ||||
|  | ||||
|                 yield return _live; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,122 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal sealed class NonInteractiveProgressRenderer : ProgressRenderer | ||||
|     { | ||||
|         private const double FirstMilestone = 25; | ||||
|         private static readonly double?[] _milestones = new double?[] { FirstMilestone, 50, 75, 95, 96, 97, 98, 99, 100 }; | ||||
|  | ||||
|         private readonly Dictionary<int, double> _taskMilestones; | ||||
|         private readonly object _lock; | ||||
|         private IRenderable? _renderable; | ||||
|         private DateTime _lastUpdate; | ||||
|  | ||||
|         public override TimeSpan RefreshRate => TimeSpan.FromSeconds(1); | ||||
|  | ||||
|         public NonInteractiveProgressRenderer() | ||||
|         { | ||||
|             _taskMilestones = new Dictionary<int, double>(); | ||||
|             _lock = new object(); | ||||
|         } | ||||
|  | ||||
|         public override void Update(ProgressContext context) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 var hasStartedTasks = false; | ||||
|                 var updates = new List<(string, double)>(); | ||||
|  | ||||
|                 context.EnumerateTasks(task => | ||||
|                 { | ||||
|                     if (!task.IsStarted || task.IsFinished) | ||||
|                     { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     hasStartedTasks = true; | ||||
|  | ||||
|                     if (TryAdvance(task.Id, task.Percentage)) | ||||
|                     { | ||||
|                         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))); | ||||
|                 } | ||||
|  | ||||
|                 if (updates.Count > 0) | ||||
|                 { | ||||
|                     _lastUpdate = DateTime.Now; | ||||
|                 } | ||||
|  | ||||
|                 _renderable = BuildTaskGrid(updates); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         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; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private bool TryAdvance(int task, double percentage) | ||||
|         { | ||||
|             if (!_taskMilestones.TryGetValue(task, out var milestone)) | ||||
|             { | ||||
|                 _taskMilestones.Add(task, FirstMilestone); | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (percentage > milestone) | ||||
|             { | ||||
|                 var nextMilestone = GetNextMilestone(percentage); | ||||
|                 if (nextMilestone != null && _taskMilestones[task] != nextMilestone) | ||||
|                 { | ||||
|                     _taskMilestones[task] = nextMilestone.Value; | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         private static double? GetNextMilestone(double percentage) | ||||
|         { | ||||
|             return Array.Find(_milestones, p => p > percentage); | ||||
|         } | ||||
|  | ||||
|         private static IRenderable? BuildTaskGrid(List<(string Name, double Percentage)> updates) | ||||
|         { | ||||
|             if (updates.Count > 0) | ||||
|             { | ||||
|                 var renderables = new List<IRenderable>(); | ||||
|                 foreach (var (name, percentage) in updates) | ||||
|                 { | ||||
|                     renderables.Add(new Markup($"[blue]{name}[/]: {(int)percentage}%")); | ||||
|                 } | ||||
|  | ||||
|                 return new Rows(renderables); | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| @@ -8,7 +10,8 @@ namespace Spectre.Console | ||||
|     /// <summary> | ||||
|     /// A console recorder used to record output from a console. | ||||
|     /// </summary> | ||||
|     public sealed class Recorder : IAnsiConsole, IDisposable | ||||
|     [SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")] | ||||
|     public class Recorder : IAnsiConsole, IDisposable | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly List<Segment> _recorded; | ||||
| @@ -31,6 +34,14 @@ namespace Spectre.Console | ||||
|         /// <inheritdoc/> | ||||
|         public int Height => _console.Height; | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public RenderPipeline Pipeline => _console.Pipeline; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets a list containing all recorded segments. | ||||
|         /// </summary> | ||||
|         protected List<Segment> Recorded => _recorded; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Recorder"/> class. | ||||
|         /// </summary> | ||||
| @@ -42,6 +53,7 @@ namespace Spectre.Console | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         [SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize")] | ||||
|         public void Dispose() | ||||
|         { | ||||
|             // Only used for scoping. | ||||
| @@ -54,20 +66,25 @@ namespace Spectre.Console | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public void Write(Segment segment) | ||||
|         public void Write(IEnumerable<Segment> segments) | ||||
|         { | ||||
|             if (segment is null) | ||||
|             if (segments is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(segment)); | ||||
|                 throw new ArgumentNullException(nameof(segments)); | ||||
|             } | ||||
|  | ||||
|             // Don't record control codes. | ||||
|             if (!segment.IsControlCode) | ||||
|             { | ||||
|                 _recorded.Add(segment); | ||||
|             } | ||||
|             Record(segments); | ||||
|  | ||||
|             _console.Write(segment); | ||||
|             _console.Write(segments); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Records the specified segments. | ||||
|         /// </summary> | ||||
|         /// <param name="segments">The segments to be recorded.</param> | ||||
|         protected virtual void Record(IEnumerable<Segment> segments) | ||||
|         { | ||||
|             Recorded.AddRange(segments.Where(s => !s.IsControlCode)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|   | ||||
							
								
								
									
										18
									
								
								src/Spectre.Console/Rendering/IRenderHook.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/Spectre.Console/Rendering/IRenderHook.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a render hook. | ||||
|     /// </summary> | ||||
|     public interface IRenderHook | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Processes the specified renderables. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The render context.</param> | ||||
|         /// <param name="renderables">The renderables to process.</param> | ||||
|         /// <returns>The processed renderables.</returns> | ||||
|         IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										81
									
								
								src/Spectre.Console/Rendering/LiveRenderable.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/Spectre.Console/Rendering/LiveRenderable.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Internal; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| { | ||||
|     internal sealed class LiveRenderable : Renderable | ||||
|     { | ||||
|         private readonly object _lock = new object(); | ||||
|         private IRenderable? _renderable; | ||||
|         private int? _height; | ||||
|  | ||||
|         public void SetRenderable(IRenderable renderable) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 _renderable = renderable; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public IRenderable PositionCursor() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_height == null) | ||||
|                 { | ||||
|                     return new ControlSequence(string.Empty); | ||||
|                 } | ||||
|  | ||||
|                 return new ControlSequence("\r" + "\u001b[1A".Repeat(_height.Value - 1)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public IRenderable RestoreCursor() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_height == null) | ||||
|                 { | ||||
|                     return new ControlSequence(string.Empty); | ||||
|                 } | ||||
|  | ||||
|                 return new ControlSequence("\r\u001b[2K" + "\u001b[1A\u001b[2K".Repeat(_height.Value - 1)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_renderable != null) | ||||
|                 { | ||||
|                     var segments = _renderable.Render(context, maxWidth); | ||||
|                     var lines = Segment.SplitLines(context, segments); | ||||
|  | ||||
|                     _height = lines.Count; | ||||
|  | ||||
|                     var result = new List<Segment>(); | ||||
|                     foreach (var (_, _, last, line) in lines.Enumerate()) | ||||
|                     { | ||||
|                         foreach (var item in line) | ||||
|                         { | ||||
|                             result.Add(item); | ||||
|                         } | ||||
|  | ||||
|                         if (!last) | ||||
|                         { | ||||
|                             result.Add(Segment.LineBreak); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return result; | ||||
|                 } | ||||
|  | ||||
|                 _height = 0; | ||||
|                 return Enumerable.Empty<Segment>(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -49,7 +49,7 @@ namespace Spectre.Console.Rendering | ||||
|             Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding)); | ||||
|             LegacyConsole = legacyConsole; | ||||
|             Justification = justification; | ||||
|             Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode; | ||||
|             Unicode = Encoding.EncodingName.ContainsExact("Unicode"); | ||||
|             SingleLine = singleLine; | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										31
									
								
								src/Spectre.Console/Rendering/RenderHookScope.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/Spectre.Console/Rendering/RenderHookScope.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a render hook scope. | ||||
|     /// </summary> | ||||
|     public sealed class RenderHookScope : IDisposable | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly IRenderHook _hook; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RenderHookScope"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console to attach the render hook to.</param> | ||||
|         /// <param name="hook">The render hook.</param> | ||||
|         public RenderHookScope(IAnsiConsole console, IRenderHook hook) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _hook = hook ?? throw new ArgumentNullException(nameof(hook)); | ||||
|             _console.Pipeline.Attach(_hook); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public void Dispose() | ||||
|         { | ||||
|             _console.Pipeline.Detach(_hook); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										66
									
								
								src/Spectre.Console/Rendering/RenderPipeline.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/Spectre.Console/Rendering/RenderPipeline.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents the render pipeline. | ||||
|     /// </summary> | ||||
|     public sealed class RenderPipeline | ||||
|     { | ||||
|         private readonly List<IRenderHook> _hooks; | ||||
|         private readonly object _lock; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RenderPipeline"/> class. | ||||
|         /// </summary> | ||||
|         public RenderPipeline() | ||||
|         { | ||||
|             _hooks = new List<IRenderHook>(); | ||||
|             _lock = new object(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Attaches a new render hook onto the pipeline. | ||||
|         /// </summary> | ||||
|         /// <param name="hook">The render hook to attach.</param> | ||||
|         public void Attach(IRenderHook hook) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 _hooks.Add(hook); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Detaches a render hook from the pipeline. | ||||
|         /// </summary> | ||||
|         /// <param name="hook">The render hook to detach.</param> | ||||
|         public void Detach(IRenderHook hook) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 _hooks.Remove(hook); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Processes the specified renderables. | ||||
|         /// </summary> | ||||
|         /// <param name="context">The render context.</param> | ||||
|         /// <param name="renderables">The renderables to process.</param> | ||||
|         /// <returns>The processed renderables.</returns> | ||||
|         public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 var current = renderables; | ||||
|                 for (var index = _hooks.Count - 1; index >= 0; index--) | ||||
|                 { | ||||
|                     current = _hooks[index].Process(context, current); | ||||
|                 } | ||||
|  | ||||
|                 return current; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -72,7 +72,7 @@ namespace Spectre.Console.Rendering | ||||
|  | ||||
|         private Segment(string text, Style style, bool lineBreak, bool control) | ||||
|         { | ||||
|             Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text)); | ||||
|             Text = text?.NormalizeNewLines() ?? throw new ArgumentNullException(nameof(text)); | ||||
|             Style = style ?? throw new ArgumentNullException(nameof(style)); | ||||
|             IsLineBreak = lineBreak; | ||||
|             IsWhiteSpace = string.IsNullOrWhiteSpace(text); | ||||
| @@ -102,6 +102,11 @@ namespace Spectre.Console.Rendering | ||||
|                 throw new ArgumentNullException(nameof(context)); | ||||
|             } | ||||
|  | ||||
|             if (IsControlCode) | ||||
|             { | ||||
|                 return 0; | ||||
|             } | ||||
|  | ||||
|             return Text.CellLength(context); | ||||
|         } | ||||
|  | ||||
| @@ -477,16 +482,22 @@ namespace Spectre.Console.Rendering | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // Both control codes? | ||||
|                 if (segment.IsControlCode && previous.IsControlCode) | ||||
|                 { | ||||
|                     previous = Control(previous.Text + segment.Text); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // Same style? | ||||
|                 if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak) | ||||
|                 if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak && !previous.IsControlCode) | ||||
|                 { | ||||
|                     previous = new Segment(previous.Text + segment.Text, previous.Style); | ||||
|                     continue; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     result.Add(previous); | ||||
|                     previous = segment; | ||||
|                 } | ||||
|  | ||||
|                 result.Add(previous); | ||||
|                 previous = segment; | ||||
|             } | ||||
|  | ||||
|             if (previous != null) | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/Spectre.Console/Widgets/ControlSequence.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/Spectre.Console/Widgets/ControlSequence.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ControlSequence : Renderable | ||||
|     { | ||||
|         private readonly Segment _segment; | ||||
|  | ||||
|         public ControlSequence(string control) | ||||
|         { | ||||
|             _segment = Segment.Control(control); | ||||
|         } | ||||
|  | ||||
|         protected override Measurement Measure(RenderContext context, int maxWidth) | ||||
|         { | ||||
|             return new Measurement(0, 0); | ||||
|         } | ||||
|  | ||||
|         protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) | ||||
|         { | ||||
|             yield return _segment; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/Spectre.Console/Widgets/ProgressBar.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/Spectre.Console/Widgets/ProgressBar.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ProgressBar : Renderable | ||||
|     { | ||||
|         public double Value { get; set; } | ||||
|         public double MaxValue { get; set; } = 100; | ||||
|  | ||||
|         public int? Width { 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); | ||||
|  | ||||
|         protected override Measurement Measure(RenderContext context, int maxWidth) | ||||
|         { | ||||
|             var width = Math.Min(Width ?? maxWidth, maxWidth); | ||||
|             return new Measurement(4, width); | ||||
|         } | ||||
|  | ||||
|         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 token = !context.Unicode || context.LegacyConsole ? '-' : '━'; | ||||
|             var style = completed >= MaxValue ? FinishedStyle : CompletedStyle; | ||||
|  | ||||
|             var bars = Math.Max(0, (int)(width * (completed / MaxValue))); | ||||
|             yield return new Segment(new string(token, bars), style); | ||||
|  | ||||
|             if (bars < width) | ||||
|             { | ||||
|                 yield return new Segment(new string(token, width - bars), RemainingStyle); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -95,7 +95,7 @@ namespace Spectre.Console | ||||
|  | ||||
|         private IEnumerable<Segment> GetTitleSegments(RenderContext context, string title, int width) | ||||
|         { | ||||
|             title = title.NormalizeLineEndings().ReplaceExact("\n", " ").Trim(); | ||||
|             title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim(); | ||||
|             var markup = new Markup(title, Style); | ||||
|             return ((IRenderable)markup).Render(context.WithSingleLine(), width); | ||||
|         } | ||||
|   | ||||
| @@ -16,8 +16,8 @@ namespace Spectre.Console | ||||
|         private readonly List<TableColumn> _columns; | ||||
|         private readonly List<TableRow> _rows; | ||||
|  | ||||
|         private static Style _defaultHeadingStyle = new Style(Color.Silver); | ||||
|         private static Style _defaultCaptionStyle = new Style(Color.Grey); | ||||
|         private static readonly Style _defaultHeadingStyle = new Style(Color.Silver); | ||||
|         private static readonly Style _defaultCaptionStyle = new Style(Color.Grey); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the table columns. | ||||
| @@ -447,12 +447,10 @@ namespace Spectre.Console | ||||
|  | ||||
|         private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth) | ||||
|         { | ||||
|             var padding = column.Padding?.GetWidth() ?? 0; | ||||
|  | ||||
|             // Predetermined width? | ||||
|             if (column.Width != null) | ||||
|             { | ||||
|                 return (column.Width.Value + padding, column.Width.Value + padding); | ||||
|                 return (column.Width.Value, column.Width.Value); | ||||
|             } | ||||
|  | ||||
|             var columnIndex = _columns.IndexOf(column); | ||||
| @@ -474,6 +472,8 @@ namespace Spectre.Console | ||||
|                 maxWidths.Add(rowMeasure.Max); | ||||
|             } | ||||
|  | ||||
|             var padding = column.Padding?.GetWidth() ?? 0; | ||||
|  | ||||
|             return (minWidths.Count > 0 ? minWidths.Max() : padding, | ||||
|                     maxWidths.Count > 0 ? maxWidths.Max() : maxWidth); | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user