mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 17:15:28 +08:00 
			
		
		
		
	Add live display support
This commit also adds functionality to LiveRenderable that should fix some problems related to vertical overflow. Closes #316 Closes #415
This commit is contained in:
		 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Phil Scott
						Phil Scott
					
				
			
			
				
	
			
			
			 Phil Scott
						Phil Scott
					
				
			
						parent
						
							5d68020abb
						
					
				
				
					commit
					3dea412785
				
			| @@ -1,4 +1,4 @@ | ||||
| Title: Live Displays | ||||
| Title: Live | ||||
| Order: 4 | ||||
| --- | ||||
|  | ||||
|   | ||||
							
								
								
									
										62
									
								
								docs/input/live/live-display.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								docs/input/live/live-display.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| Title: Live Display | ||||
| Order: 0 | ||||
| --- | ||||
|  | ||||
| Spectre.Console can update arbitrary widgets in-place.  | ||||
|  | ||||
| <?# Alert ?> | ||||
|   The live display is not  | ||||
|   thread safe, and using it together with other interactive components such as  | ||||
|   prompts, status displays or other progress displays are not supported. | ||||
| <?#/ Alert ?> | ||||
|  | ||||
| ```csharp | ||||
| var table = new Table().Centered(); | ||||
|  | ||||
| AnsiConsole.Live(table) | ||||
|     .Start(ctx =>  | ||||
|     { | ||||
|         table.AddColumn("Foo"); | ||||
|         ctx.Refresh(); | ||||
|         Thread.Sleep(1000); | ||||
|  | ||||
|         table.AddColumn("Bar"); | ||||
|         ctx.Refresh(); | ||||
|         Thread.Sleep(1000); | ||||
|     }); | ||||
| ``` | ||||
|  | ||||
| ## Asynchronous progress | ||||
|  | ||||
| If you prefer to use async/await, you can use `StartAsync` instead of `Start`. | ||||
|  | ||||
| ```csharp | ||||
| var table = new Table().Centered(); | ||||
|  | ||||
| await AnsiConsole.Live(table) | ||||
|     .StartAsync(async ctx =>  | ||||
|     { | ||||
|         table.AddColumn("Foo"); | ||||
|         ctx.Refresh(); | ||||
|         await Task.Delay(1000); | ||||
|  | ||||
|         table.AddColumn("Bar"); | ||||
|         ctx.Refresh(); | ||||
|         await Task.Delay(1000); | ||||
|     }); | ||||
| ``` | ||||
|  | ||||
| ## Configure | ||||
|  | ||||
| ```csharp | ||||
| var table = new Table().Centered(); | ||||
|  | ||||
| AnsiConsole.Live(table) | ||||
|     .AutoClear(false)   // Do not remove when done | ||||
|     .Overflow(VerticalOverflow.Ellipsis) // Show ellipsis when overflowing | ||||
|     .Cropping(VerticalOverflowCropping.Top) // Crop overflow at top | ||||
|     .Start(ctx => | ||||
|     { | ||||
|         // Omitted | ||||
|     }); | ||||
| ``` | ||||
| @@ -66,7 +66,6 @@ await AnsiConsole.Progress() | ||||
| ## Configure | ||||
|  | ||||
| ```csharp | ||||
| // Asynchronous | ||||
| AnsiConsole.Progress() | ||||
|     .AutoRefresh(false) // Turn off auto refresh | ||||
|     .AutoClear(false)   // Do not remove the task list when done | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| Title: Status | ||||
| Order: 6 | ||||
| Order: 10 | ||||
| RedirectFrom: status | ||||
| --- | ||||
|  | ||||
|   | ||||
							
								
								
									
										15
									
								
								examples/Console/Live/Live.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								examples/Console/Live/Live.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net5.0</TargetFramework> | ||||
|     <ExampleTitle>Live</ExampleTitle> | ||||
|     <ExampleDescription>Demonstrates how to do live updates.</ExampleDescription> | ||||
|     <ExampleGroup>Misc</ExampleGroup> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\Shared\Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										108
									
								
								examples/Console/Live/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								examples/Console/Live/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| using System; | ||||
| using System.Threading; | ||||
|  | ||||
| namespace Spectre.Console.Examples | ||||
| { | ||||
|     public static class Program | ||||
|     { | ||||
|         public static void Main() | ||||
|         { | ||||
|             var table = new Table().Centered(); | ||||
|  | ||||
|             // Animate | ||||
|             AnsiConsole.Live(table) | ||||
|                 .AutoClear(false) | ||||
|                 .Overflow(VerticalOverflow.Ellipsis) | ||||
|                 .Cropping(VerticalOverflowCropping.Top) | ||||
|                 .Start(ctx => | ||||
|                 { | ||||
|                     void Update(int delay, Action action) | ||||
|                     { | ||||
|                         action(); | ||||
|                         ctx.Refresh(); | ||||
|                         Thread.Sleep(delay); | ||||
|                     } | ||||
|  | ||||
|                     // Columns | ||||
|                     Update(230, () => table.AddColumn("Release date")); | ||||
|                     Update(230, () => table.AddColumn("Title")); | ||||
|                     Update(230, () => table.AddColumn("Budget")); | ||||
|                     Update(230, () => table.AddColumn("Opening Weekend")); | ||||
|                     Update(230, () => table.AddColumn("Box office")); | ||||
|  | ||||
|                     // Rows | ||||
|                     Update(70, () => table.AddRow("May 25, 1977", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IV[/]", "$11,000,000", "$1,554,475", "$775,398,007")); | ||||
|                     Update(70, () => table.AddRow("May 21, 1980", "[yellow]Star Wars[/] [grey]Ep.[/] [u]V[/]", "$18,000,000", "$4,910,483", "$547,969,004")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1983", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VI[/]", "$32,500,000", "$23,019,618", "$475,106,177")); | ||||
|                     Update(70, () => table.AddRow("May 19, 1999", "[yellow]Star Wars[/] [grey]Ep.[/] [u]I[/]", "$115,000,000", "$64,810,870", "$1,027,044,677")); | ||||
|                     Update(70, () => table.AddRow("May 16, 2002", "[yellow]Star Wars[/] [grey]Ep.[/] [u]II[/]", "$115,000,000", "$80,027,814", "$649,436,358")); | ||||
|                     Update(70, () => table.AddRow("May 19, 2005", "[yellow]Star Wars[/] [grey]Ep.[/] [u]III[/]", "$113,000,000", "$108,435,841", "$850,035,635")); | ||||
|                     Update(70, () => table.AddRow("Dec 18, 2015", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VII[/]", "$245,000,000", "$247,966,675", "$2,068,223,624")); | ||||
|                     Update(70, () => table.AddRow("Dec 15, 2017", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VIII[/]", "$317,000,000", "$220,009,584", "$1,333,539,889")); | ||||
|                     Update(70, () => table.AddRow("Dec 20, 2019", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IX[/]", "$245,000,000", "$177,383,864", "$1,074,114,248")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1977", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IV[/]", "$11,000,000", "$1,554,475", "$775,398,007")); | ||||
|                     Update(70, () => table.AddRow("May 21, 1980", "[yellow]Star Wars[/] [grey]Ep.[/] [u]V[/]", "18,000,000", "$4,910,483", "$547,969,004")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1983", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VI[/]", "$32,500,000", "$23,019,618", "$475,106,177")); | ||||
|                     Update(70, () => table.AddRow("May 19, 1999", "[yellow]Star Wars[/] [grey]Ep.[/] [u]I[/]", "$115,000,000", "$64,810,870", "$1,027,044,677")); | ||||
|                     Update(70, () => table.AddRow("May 16, 2002", "[yellow]Star Wars[/] [grey]Ep.[/] [u]II[/]", "$115,000,000", "$80,027,814", "$649,436,358")); | ||||
|                     Update(70, () => table.AddRow("May 19, 2005", "[yellow]Star Wars[/] [grey]Ep.[/] [u]III[/]", "$113,000,000", "$108,435,841", "$850,035,635")); | ||||
|                     Update(70, () => table.AddRow("Dec 18, 2015", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VII[/]", "$245,000,000", "$247,966,675", "$2,068,223,624")); | ||||
|                     Update(70, () => table.AddRow("Dec 15, 2017", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VIII[/]", "$317,000,000", "$220,009,584", "$1,333,539,889")); | ||||
|                     Update(70, () => table.AddRow("Dec 20, 2019", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IX[/]", "$245,000,000", "$177,383,864", "$1,074,114,248")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1977", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IV[/]", "$11,000,000", "$1,554,475", "$775,398,007")); | ||||
|                     Update(70, () => table.AddRow("May 21, 1980", "[yellow]Star Wars[/] [grey]Ep.[/] [u]V[/]", "18,000,000", "$4,910,483", "$547,969,004")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1983", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VI[/]", "$32,500,000", "$23,019,618", "$475,106,177")); | ||||
|                     Update(70, () => table.AddRow("May 19, 1999", "[yellow]Star Wars[/] [grey]Ep.[/] [u]I[/]", "$115,000,000", "$64,810,870", "$1,027,044,677")); | ||||
|                     Update(70, () => table.AddRow("May 16, 2002", "[yellow]Star Wars[/] [grey]Ep.[/] [u]II[/]", "$115,000,000", "$80,027,814", "$649,436,358")); | ||||
|                     Update(70, () => table.AddRow("May 19, 2005", "[yellow]Star Wars[/] [grey]Ep.[/] [u]III[/]", "$113,000,000", "$108,435,841", "$850,035,635")); | ||||
|                     Update(70, () => table.AddRow("Dec 18, 2015", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VII[/]", "$245,000,000", "$247,966,675", "$2,068,223,624")); | ||||
|                     Update(70, () => table.AddRow("Dec 15, 2017", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VIII[/]", "$317,000,000", "$220,009,584", "$1,333,539,889")); | ||||
|                     Update(70, () => table.AddRow("Dec 20, 2019", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IX[/]", "$245,000,000", "$177,383,864", "$1,074,114,248")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1977", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IV[/]", "$11,000,000", "$1,554,475", "$775,398,007")); | ||||
|                     Update(70, () => table.AddRow("May 21, 1980", "[yellow]Star Wars[/] [grey]Ep.[/] [u]V[/]", "18,000,000", "$4,910,483", "$547,969,004")); | ||||
|                     Update(70, () => table.AddRow("May 25, 1983", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VI[/]", "$32,500,000", "$23,019,618", "$475,106,177")); | ||||
|                     Update(70, () => table.AddRow("May 19, 1999", "[yellow]Star Wars[/] [grey]Ep.[/] [u]I[/]", "$115,000,000", "$64,810,870", "$1,027,044,677")); | ||||
|                     Update(70, () => table.AddRow("May 16, 2002", "[yellow]Star Wars[/] [grey]Ep.[/] [u]II[/]", "$115,000,000", "$80,027,814", "$649,436,358")); | ||||
|                     Update(70, () => table.AddRow("May 19, 2005", "[yellow]Star Wars[/] [grey]Ep.[/] [u]III[/]", "$113,000,000", "$108,435,841", "$850,035,635")); | ||||
|                     Update(70, () => table.AddRow("Dec 18, 2015", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VII[/]", "$245,000,000", "$247,966,675", "$2,068,223,624")); | ||||
|                     Update(70, () => table.AddRow("Dec 15, 2017", "[yellow]Star Wars[/] [grey]Ep.[/] [u]VIII[/]", "$317,000,000", "$220,009,584", "$1,333,539,889")); | ||||
|                     Update(70, () => table.AddRow("Dec 20, 2019", "[yellow]Star Wars[/] [grey]Ep.[/] [u]IX[/]", "$245,000,000", "$177,383,864", "$1,074,114,248")); | ||||
|  | ||||
|                     // Column footer | ||||
|                     Update(230, () => table.Columns[2].Footer("$1,633,000,000")); | ||||
|                     Update(230, () => table.Columns[3].Footer("$928,119,224")); | ||||
|                     Update(400, () => table.Columns[4].Footer("$10,318,030,576")); | ||||
|  | ||||
|                     // Column alignment | ||||
|                     Update(230, () => table.Columns[2].RightAligned()); | ||||
|                     Update(230, () => table.Columns[3].RightAligned()); | ||||
|                     Update(400, () => table.Columns[4].RightAligned()); | ||||
|  | ||||
|                     // Column titles | ||||
|                     Update(70, () => table.Columns[0].Header("[bold]Release date[/]")); | ||||
|                     Update(70, () => table.Columns[1].Header("[bold]Title[/]")); | ||||
|                     Update(70, () => table.Columns[2].Header("[red bold]Budget[/]")); | ||||
|                     Update(70, () => table.Columns[3].Header("[green bold]Opening Weekend[/]")); | ||||
|                     Update(400, () => table.Columns[4].Header("[blue bold]Box office[/]")); | ||||
|  | ||||
|                     // Footers | ||||
|                     Update(70, () => table.Columns[2].Footer("[red bold]$1,633,000,000[/]")); | ||||
|                     Update(70, () => table.Columns[3].Footer("[green bold]$928,119,224[/]")); | ||||
|                     Update(400, () => table.Columns[4].Footer("[blue bold]$10,318,030,576[/]")); | ||||
|  | ||||
|                     // Title | ||||
|                     Update(500, () => table.Title("Star Wars Movies")); | ||||
|                     Update(400, () => table.Title("[[ [yellow]Star Wars Movies[/] ]]")); | ||||
|  | ||||
|                     // Borders | ||||
|                     Update(230, () => table.BorderColor(Color.Yellow)); | ||||
|                     Update(230, () => table.MinimalBorder()); | ||||
|                     Update(230, () => table.SimpleBorder()); | ||||
|                     Update(230, () => table.SimpleHeavyBorder()); | ||||
|  | ||||
|                     // Caption | ||||
|                     Update(400, () => table.Caption("[[ [blue]THE END[/] ]]")); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -4,11 +4,7 @@ namespace Spectre.Console.Examples | ||||
|     { | ||||
|         public static void Main() | ||||
|         { | ||||
|             // Create the table. | ||||
|             var table = CreateTable(); | ||||
|  | ||||
|             // Render the table. | ||||
|             AnsiConsole.Render(table); | ||||
|             AnsiConsole.Render(CreateTable()); | ||||
|         } | ||||
|  | ||||
|         private static Table CreateTable() | ||||
|   | ||||
| @@ -88,6 +88,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "..\examples\Share | ||||
| EndProject | ||||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{0FC844AD-FCBB-4B2F-9AEC-6CB5505E49E3}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Live", "..\examples\Console\Live\Live.csproj", "{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| @@ -458,6 +460,18 @@ Global | ||||
| 		{8428A7DD-29FC-4417-9CA0-B90D34B26AB2}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{8428A7DD-29FC-4417-9CA0-B90D34B26AB2}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{8428A7DD-29FC-4417-9CA0-B90D34B26AB2}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x86.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
| @@ -493,6 +507,7 @@ Global | ||||
| 		{4C30C028-E97D-4B4C-AD17-C90F338A4DFF} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 		{A0C772BA-C5F4-451D-AA7A-4045F2FA0201} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 		{8428A7DD-29FC-4417-9CA0-B90D34B26AB2} = {A0C772BA-C5F4-451D-AA7A-4045F2FA0201} | ||||
| 		{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(ExtensibilityGlobals) = postSolution | ||||
| 		SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} | ||||
|   | ||||
							
								
								
									
										20
									
								
								src/Spectre.Console/AnsiConsole.Live.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/Spectre.Console/AnsiConsole.Live.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// A console capable of writing ANSI escape sequences. | ||||
|     /// </summary> | ||||
|     public static partial class AnsiConsole | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Creates a new <see cref="LiveDisplay"/> instance. | ||||
|         /// </summary> | ||||
|         /// <param name="target">The target renderable to update.</param> | ||||
|         /// <returns>A <see cref="LiveDisplay"/> instance.</returns> | ||||
|         public static LiveDisplay Live(IRenderable target) | ||||
|         { | ||||
|             return Console.Live(target); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/Spectre.Console/Extensions/AnsiConsoleExtensions.Live.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/Spectre.Console/Extensions/AnsiConsoleExtensions.Live.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="IAnsiConsole"/>. | ||||
|     /// </summary> | ||||
|     public static partial class AnsiConsoleExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Creates a new <see cref="LiveDisplay"/> instance for the console. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         /// <param name="target">The target renderable to update.</param> | ||||
|         /// <returns>A <see cref="LiveDisplay"/> instance.</returns> | ||||
|         public static LiveDisplay Live(this IAnsiConsole console, IRenderable target) | ||||
|         { | ||||
|             if (console is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|             } | ||||
|  | ||||
|             if (target is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(target)); | ||||
|             } | ||||
|  | ||||
|             return new LiveDisplay(console, target); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/Spectre.Console/Extensions/LiveDisplayExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/Spectre.Console/Extensions/LiveDisplayExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="LiveDisplay"/>. | ||||
|     /// </summary> | ||||
|     public static class LiveDisplayExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets whether or not auto clear is enabled. | ||||
|         /// If enabled, the live display will be cleared when done. | ||||
|         /// </summary> | ||||
|         /// <param name="live">The <see cref="LiveDisplay"/> 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 LiveDisplay AutoClear(this LiveDisplay live, bool enabled) | ||||
|         { | ||||
|             if (live is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(live)); | ||||
|             } | ||||
|  | ||||
|             live.AutoClear = enabled; | ||||
|  | ||||
|             return live; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the vertical overflow strategy. | ||||
|         /// </summary> | ||||
|         /// <param name="live">The <see cref="LiveDisplay"/> instance.</param> | ||||
|         /// <param name="overflow">The overflow strategy to use.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static LiveDisplay Overflow(this LiveDisplay live, VerticalOverflow overflow) | ||||
|         { | ||||
|             if (live is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(live)); | ||||
|             } | ||||
|  | ||||
|             live.Overflow = overflow; | ||||
|  | ||||
|             return live; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the vertical overflow cropping strategy. | ||||
|         /// </summary> | ||||
|         /// <param name="live">The <see cref="LiveDisplay"/> instance.</param> | ||||
|         /// <param name="cropping">The overflow cropping strategy to use.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static LiveDisplay Cropping(this LiveDisplay live, VerticalOverflowCropping cropping) | ||||
|         { | ||||
|             if (live is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(live)); | ||||
|             } | ||||
|  | ||||
|             live.Cropping = cropping; | ||||
|  | ||||
|             return live; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -8,11 +8,55 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     public static class TableColumnExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Sets the table column header. | ||||
|         /// </summary> | ||||
|         /// <param name="column">The table column.</param> | ||||
|         /// <param name="header">The table column header markup text.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static TableColumn Header(this TableColumn column, string header) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (header is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(header)); | ||||
|             } | ||||
|  | ||||
|             column.Header = new Markup(header); | ||||
|             return column; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the table column header. | ||||
|         /// </summary> | ||||
|         /// <param name="column">The table column.</param> | ||||
|         /// <param name="header">The table column header.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static TableColumn Header(this TableColumn column, IRenderable header) | ||||
|         { | ||||
|             if (column is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(column)); | ||||
|             } | ||||
|  | ||||
|             if (header is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(header)); | ||||
|             } | ||||
|  | ||||
|             column.Footer = header; | ||||
|             return column; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the table column footer. | ||||
|         /// </summary> | ||||
|         /// <param name="column">The table column.</param> | ||||
|         /// <param name="footer">The table column markup text.</param> | ||||
|         /// <param name="footer">The table column footer markup text.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static TableColumn Footer(this TableColumn column, string footer) | ||||
|         { | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using static Spectre.Console.AnsiSequences; | ||||
|  | ||||
| namespace Spectre.Console.Rendering | ||||
| @@ -6,12 +8,33 @@ namespace Spectre.Console.Rendering | ||||
|     internal sealed class LiveRenderable : Renderable | ||||
|     { | ||||
|         private readonly object _lock = new object(); | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private IRenderable? _renderable; | ||||
|         private SegmentShape? _shape; | ||||
|  | ||||
|         public bool HasRenderable => _renderable != null; | ||||
|         public IRenderable? Target => _renderable; | ||||
|         public bool DidOverflow { get; private set; } | ||||
|  | ||||
|         public void SetRenderable(IRenderable renderable) | ||||
|         [MemberNotNullWhen(true, nameof(Target))] | ||||
|         public bool HasRenderable => _renderable != null; | ||||
|         public VerticalOverflow Overflow { get; set; } | ||||
|         public VerticalOverflowCropping OverflowCropping { get; set; } | ||||
|  | ||||
|         public LiveRenderable(IAnsiConsole console) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|  | ||||
|             Overflow = VerticalOverflow.Ellipsis; | ||||
|             OverflowCropping = VerticalOverflowCropping.Top; | ||||
|         } | ||||
|  | ||||
|         public LiveRenderable(IAnsiConsole console, IRenderable renderable) | ||||
|             : this(console) | ||||
|         { | ||||
|             _renderable = renderable ?? throw new ArgumentNullException(nameof(renderable)); | ||||
|         } | ||||
|  | ||||
|         public void SetRenderable(IRenderable? renderable) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
| @@ -51,12 +74,61 @@ namespace Spectre.Console.Rendering | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 DidOverflow = false; | ||||
|  | ||||
|                 if (_renderable != null) | ||||
|                 { | ||||
|                     var segments = _renderable.Render(context, maxWidth); | ||||
|                     var lines = Segment.SplitLines(segments); | ||||
|  | ||||
|                     var shape = SegmentShape.Calculate(context, lines); | ||||
|                     if (shape.Height > _console.Profile.Height) | ||||
|                     { | ||||
|                         if (Overflow == VerticalOverflow.Crop) | ||||
|                         { | ||||
|                             if (OverflowCropping == VerticalOverflowCropping.Bottom) | ||||
|                             { | ||||
|                                 // Remove bottom lines | ||||
|                                 var index = Math.Min(_console.Profile.Height, lines.Count); | ||||
|                                 var count = lines.Count - index; | ||||
|                                 lines.RemoveRange(index, count); | ||||
|                             } | ||||
|                             else | ||||
|                             { | ||||
|                                 // Remove top lines | ||||
|                                 var start = lines.Count - _console.Profile.Height; | ||||
|                                 lines.RemoveRange(0, start); | ||||
|                             } | ||||
|  | ||||
|                             shape = SegmentShape.Calculate(context, lines); | ||||
|                         } | ||||
|                         else if (Overflow == VerticalOverflow.Ellipsis) | ||||
|                         { | ||||
|                             var ellipsisText = _console.Profile.Capabilities.Unicode ? "…" : "..."; | ||||
|                             var ellipsis = new SegmentLine(((IRenderable)new Markup($"[yellow]{ellipsisText}[/]")).Render(context, maxWidth)); | ||||
|  | ||||
|                             if (OverflowCropping == VerticalOverflowCropping.Bottom) | ||||
|                             { | ||||
|                                 // Remove bottom lines | ||||
|                                 var index = Math.Min(_console.Profile.Height - 1, lines.Count); | ||||
|                                 var count = lines.Count - index; | ||||
|                                 lines.RemoveRange(index, count); | ||||
|                                 lines.Add(ellipsis); | ||||
|                             } | ||||
|                             else | ||||
|                             { | ||||
|                                 // Remove top lines | ||||
|                                 var start = lines.Count - _console.Profile.Height; | ||||
|                                 lines.RemoveRange(0, start + 1); | ||||
|                                 lines.Insert(0, ellipsis); | ||||
|                             } | ||||
|  | ||||
|                             shape = SegmentShape.Calculate(context, lines); | ||||
|                         } | ||||
|  | ||||
|                         DidOverflow = true; | ||||
|                     } | ||||
|  | ||||
|                     _shape = _shape == null ? shape : _shape.Value.Inflate(shape); | ||||
|                     _shape.Value.Apply(context, ref lines); | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,22 @@ namespace Spectre.Console.Rendering | ||||
|         /// </summary> | ||||
|         public int Length => this.Sum(line => line.Text.Length); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SegmentLine"/> class. | ||||
|         /// </summary> | ||||
|         public SegmentLine() | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SegmentLine"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="segments">The segments.</param> | ||||
|         public SegmentLine(IEnumerable<Segment> segments) | ||||
|             : base(segments) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the number of cells the segment line occupies. | ||||
|         /// </summary> | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/Spectre.Console/VerticalOverflow.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/Spectre.Console/VerticalOverflow.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents vertical overflow. | ||||
|     /// </summary> | ||||
|     public enum VerticalOverflow | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Crop overflow. | ||||
|         /// </summary> | ||||
|         Crop = 0, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Add an ellipsis at the end. | ||||
|         /// </summary> | ||||
|         Ellipsis = 1, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Do not do anything about overflow. | ||||
|         /// </summary> | ||||
|         Visible = 2, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/Spectre.Console/VerticalOverflowCropping.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/Spectre.Console/VerticalOverflowCropping.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represent vertical overflow cropping. | ||||
|     /// </summary> | ||||
|     public enum VerticalOverflowCropping | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Crops the top. | ||||
|         /// </summary> | ||||
|         Top = 0, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Crops the bottom. | ||||
|         /// </summary> | ||||
|         Bottom = 1, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										126
									
								
								src/Spectre.Console/Widgets/Live/LiveDisplay.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/Spectre.Console/Widgets/Live/LiveDisplay.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a live display. | ||||
|     /// </summary> | ||||
|     public sealed class LiveDisplay | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly IRenderable _target; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not the live display should | ||||
|         /// be cleared when it's done. | ||||
|         /// Defaults to <c>false</c>. | ||||
|         /// </summary> | ||||
|         public bool AutoClear { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the vertical overflow strategy. | ||||
|         /// </summary> | ||||
|         public VerticalOverflow Overflow { get; set; } = VerticalOverflow.Ellipsis; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the vertical overflow cropping strategy. | ||||
|         /// </summary> | ||||
|         public VerticalOverflowCropping Cropping { get; set; } = VerticalOverflowCropping.Top; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LiveDisplay"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         /// <param name="target">The target renderable to update.</param> | ||||
|         public LiveDisplay(IAnsiConsole console, IRenderable target) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _target = target ?? throw new ArgumentNullException(nameof(target)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the live display. | ||||
|         /// </summary> | ||||
|         /// <param name="action">The action to execute.</param> | ||||
|         public void Start(Action<LiveDisplayContext> action) | ||||
|         { | ||||
|             var task = StartAsync(ctx => | ||||
|             { | ||||
|                 action(ctx); | ||||
|                 return Task.CompletedTask; | ||||
|             }); | ||||
|  | ||||
|             task.GetAwaiter().GetResult(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the live display. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The result type.</typeparam> | ||||
|         /// <param name="func">The action to execute.</param> | ||||
|         /// <returns>The result.</returns> | ||||
|         public T Start<T>(Func<LiveDisplayContext, T> func) | ||||
|         { | ||||
|             var task = StartAsync(ctx => Task.FromResult(func(ctx))); | ||||
|             return task.GetAwaiter().GetResult(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the live display. | ||||
|         /// </summary> | ||||
|         /// <param name="func">The action to execute.</param> | ||||
|         /// <returns>The result.</returns> | ||||
|         public async Task StartAsync(Func<LiveDisplayContext, Task> func) | ||||
|         { | ||||
|             if (func is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(func)); | ||||
|             } | ||||
|  | ||||
|             _ = await StartAsync<object?>(async ctx => | ||||
|             { | ||||
|                 await func(ctx).ConfigureAwait(false); | ||||
|                 return default; | ||||
|             }).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Starts the live display. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The result type.</typeparam> | ||||
|         /// <param name="func">The action to execute.</param> | ||||
|         /// <returns>The result.</returns> | ||||
|         public async Task<T> StartAsync<T>(Func<LiveDisplayContext, Task<T>> func) | ||||
|         { | ||||
|             if (func is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(func)); | ||||
|             } | ||||
|  | ||||
|             return await _console.RunExclusive(async () => | ||||
|             { | ||||
|                 var context = new LiveDisplayContext(_console, _target); | ||||
|                 context.SetOverflow(Overflow, Cropping); | ||||
|  | ||||
|                 var renderer = new LiveDisplayRenderer(_console, context); | ||||
|                 renderer.Started(); | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     using (new RenderHookScope(_console, renderer)) | ||||
|                     { | ||||
|                         var result = await func(context).ConfigureAwait(false); | ||||
|                         context.Refresh(); | ||||
|                         return result; | ||||
|                     } | ||||
|                 } | ||||
|                 finally | ||||
|                 { | ||||
|                     renderer.Completed(AutoClear); | ||||
|                 } | ||||
|             }).ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/Spectre.Console/Widgets/Live/LiveDisplayContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/Spectre.Console/Widgets/Live/LiveDisplayContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| using System; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a context that can be used to interact with a <see cref="LiveDisplay"/>. | ||||
|     /// </summary> | ||||
|     public sealed class LiveDisplayContext | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|  | ||||
|         internal object Lock { get; } | ||||
|         internal LiveRenderable Live { get; } | ||||
|  | ||||
|         internal LiveDisplayContext(IAnsiConsole console, IRenderable target) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|  | ||||
|             Live = new LiveRenderable(_console, target); | ||||
|             Lock = new object(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Updates the live display target. | ||||
|         /// </summary> | ||||
|         /// <param name="target">The new live display target.</param> | ||||
|         public void UpdateTarget(IRenderable? target) | ||||
|         { | ||||
|             lock (Lock) | ||||
|             { | ||||
|                 Live.SetRenderable(target); | ||||
|                 Refresh(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Refreshes the live display. | ||||
|         /// </summary> | ||||
|         public void Refresh() | ||||
|         { | ||||
|             lock (Lock) | ||||
|             { | ||||
|                 _console.Write(new ControlCode(string.Empty)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         internal void SetOverflow(VerticalOverflow overflow, VerticalOverflowCropping cropping) | ||||
|         { | ||||
|             Live.Overflow = overflow; | ||||
|             Live.OverflowCropping = cropping; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								src/Spectre.Console/Widgets/Live/LiveDisplayRenderer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/Spectre.Console/Widgets/Live/LiveDisplayRenderer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class LiveDisplayRenderer : IRenderHook | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly LiveDisplayContext _context; | ||||
|  | ||||
|         public LiveDisplayRenderer(IAnsiConsole console, LiveDisplayContext context) | ||||
|         { | ||||
|             _console = console; | ||||
|             _context = context; | ||||
|         } | ||||
|  | ||||
|         public void Started() | ||||
|         { | ||||
|             _console.Cursor.Hide(); | ||||
|         } | ||||
|  | ||||
|         public void Completed(bool autoclear) | ||||
|         { | ||||
|             lock (_context.Lock) | ||||
|             { | ||||
|                 if (autoclear) | ||||
|                 { | ||||
|                     _console.Write(_context.Live.RestoreCursor()); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (_context.Live.HasRenderable && _context.Live.DidOverflow) | ||||
|                     { | ||||
|                         // Redraw the whole live renderable | ||||
|                         _console.Write(_context.Live.RestoreCursor()); | ||||
|                         _context.Live.Overflow = VerticalOverflow.Visible; | ||||
|                         _console.Write(_context.Live.Target); | ||||
|                     } | ||||
|  | ||||
|                     _console.WriteLine(); | ||||
|                 } | ||||
|  | ||||
|                 _console.Cursor.Show(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             lock (_context.Lock) | ||||
|             { | ||||
|                 yield return _context.Live.PositionCursor(); | ||||
|  | ||||
|                 foreach (var renderable in renderables) | ||||
|                 { | ||||
|                     yield return renderable; | ||||
|                 } | ||||
|  | ||||
|                 yield return _context.Live; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -13,8 +13,8 @@ namespace Spectre.Console | ||||
|         private readonly LiveRenderable _live; | ||||
|         private readonly object _lock; | ||||
|         private readonly Stopwatch _stopwatch; | ||||
|         private readonly bool _hideCompleted; | ||||
|         private TimeSpan _lastUpdate; | ||||
|         private bool _hideCompleted; | ||||
|  | ||||
|         public override TimeSpan RefreshRate { get; } | ||||
|  | ||||
| @@ -22,7 +22,7 @@ namespace Spectre.Console | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _columns = columns ?? throw new ArgumentNullException(nameof(columns)); | ||||
|             _live = new LiveRenderable(); | ||||
|             _live = new LiveRenderable(console); | ||||
|             _lock = new object(); | ||||
|             _stopwatch = new Stopwatch(); | ||||
|             _lastUpdate = TimeSpan.Zero; | ||||
| @@ -46,6 +46,14 @@ namespace Spectre.Console | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (_live.HasRenderable && _live.DidOverflow) | ||||
|                     { | ||||
|                         // Redraw the whole live renderable | ||||
|                         _console.Write(_live.RestoreCursor()); | ||||
|                         _live.Overflow = VerticalOverflow.Visible; | ||||
|                         _console.Write(_live.Target); | ||||
|                     } | ||||
|  | ||||
|                     _console.WriteLine(); | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -7,20 +7,21 @@ namespace Spectre.Console | ||||
|     internal sealed class ListPromptRenderHook<T> : IRenderHook | ||||
|         where T : notnull | ||||
|     { | ||||
|         private readonly LiveRenderable _live; | ||||
|         private readonly object _lock; | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly Func<IRenderable> _builder; | ||||
|         private readonly LiveRenderable _live; | ||||
|         private readonly object _lock; | ||||
|         private bool _dirty; | ||||
|  | ||||
|         public ListPromptRenderHook( | ||||
|             IAnsiConsole console, | ||||
|             Func<IRenderable> builder) | ||||
|         { | ||||
|             _live = new LiveRenderable(); | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _builder = builder ?? throw new ArgumentNullException(nameof(builder)); | ||||
|  | ||||
|             _live = new LiveRenderable(console); | ||||
|             _lock = new object(); | ||||
|             _console = console; | ||||
|             _builder = builder; | ||||
|             _dirty = true; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -9,9 +9,9 @@ namespace Spectre.Console | ||||
|     public sealed class TableColumn : IColumn | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets the column header. | ||||
|         /// Gets or sets the column header. | ||||
|         /// </summary> | ||||
|         public IRenderable Header { get; } | ||||
|         public IRenderable Header { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the column footer. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user