diff --git a/docs/input/live/index.cshtml b/docs/input/live/index.cshtml index e6abc1d..ad7a0d7 100644 --- a/docs/input/live/index.cshtml +++ b/docs/input/live/index.cshtml @@ -1,4 +1,4 @@ -Title: Live Displays +Title: Live Order: 4 --- diff --git a/docs/input/live/live-display.md b/docs/input/live/live-display.md new file mode 100644 index 0000000..d7323f3 --- /dev/null +++ b/docs/input/live/live-display.md @@ -0,0 +1,62 @@ +Title: Live Display +Order: 0 +--- + +Spectre.Console can update arbitrary widgets in-place. + + + 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. + + +```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 + }); +``` \ No newline at end of file diff --git a/docs/input/live/progress.md b/docs/input/live/progress.md index 7599369..155cf7e 100644 --- a/docs/input/live/progress.md +++ b/docs/input/live/progress.md @@ -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 diff --git a/docs/input/live/status.md b/docs/input/live/status.md index 77500e7..d6f6cb3 100644 --- a/docs/input/live/status.md +++ b/docs/input/live/status.md @@ -1,5 +1,5 @@ Title: Status -Order: 6 +Order: 10 RedirectFrom: status --- diff --git a/examples/Console/Live/Live.csproj b/examples/Console/Live/Live.csproj new file mode 100644 index 0000000..c8cb17b --- /dev/null +++ b/examples/Console/Live/Live.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Live + Demonstrates how to do live updates. + Misc + + + + + + + diff --git a/examples/Console/Live/Program.cs b/examples/Console/Live/Program.cs new file mode 100644 index 0000000..7e6ef4e --- /dev/null +++ b/examples/Console/Live/Program.cs @@ -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[/] ]]")); + }); + } + } +} diff --git a/examples/Console/Tables/Program.cs b/examples/Console/Tables/Program.cs index 805cc2a..8919521 100644 --- a/examples/Console/Tables/Program.cs +++ b/examples/Console/Tables/Program.cs @@ -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() diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index 6650d9c..2e861e4 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -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} diff --git a/src/Spectre.Console/AnsiConsole.Live.cs b/src/Spectre.Console/AnsiConsole.Live.cs new file mode 100644 index 0000000..3d12717 --- /dev/null +++ b/src/Spectre.Console/AnsiConsole.Live.cs @@ -0,0 +1,20 @@ +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// A console capable of writing ANSI escape sequences. + /// + public static partial class AnsiConsole + { + /// + /// Creates a new instance. + /// + /// The target renderable to update. + /// A instance. + public static LiveDisplay Live(IRenderable target) + { + return Console.Live(target); + } + } +} diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Live.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Live.cs new file mode 100644 index 0000000..a5b1a6f --- /dev/null +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Live.cs @@ -0,0 +1,32 @@ +using System; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static partial class AnsiConsoleExtensions + { + /// + /// Creates a new instance for the console. + /// + /// The console. + /// The target renderable to update. + /// A instance. + 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); + } + } +} diff --git a/src/Spectre.Console/Extensions/LiveDisplayExtensions.cs b/src/Spectre.Console/Extensions/LiveDisplayExtensions.cs new file mode 100644 index 0000000..a5087c3 --- /dev/null +++ b/src/Spectre.Console/Extensions/LiveDisplayExtensions.cs @@ -0,0 +1,65 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class LiveDisplayExtensions + { + /// + /// Sets whether or not auto clear is enabled. + /// If enabled, the live display will be cleared when done. + /// + /// The instance. + /// Whether or not auto clear is enabled. + /// The same instance so that multiple calls can be chained. + public static LiveDisplay AutoClear(this LiveDisplay live, bool enabled) + { + if (live is null) + { + throw new ArgumentNullException(nameof(live)); + } + + live.AutoClear = enabled; + + return live; + } + + /// + /// Sets the vertical overflow strategy. + /// + /// The instance. + /// The overflow strategy to use. + /// The same instance so that multiple calls can be chained. + public static LiveDisplay Overflow(this LiveDisplay live, VerticalOverflow overflow) + { + if (live is null) + { + throw new ArgumentNullException(nameof(live)); + } + + live.Overflow = overflow; + + return live; + } + + /// + /// Sets the vertical overflow cropping strategy. + /// + /// The instance. + /// The overflow cropping strategy to use. + /// The same instance so that multiple calls can be chained. + public static LiveDisplay Cropping(this LiveDisplay live, VerticalOverflowCropping cropping) + { + if (live is null) + { + throw new ArgumentNullException(nameof(live)); + } + + live.Cropping = cropping; + + return live; + } + } +} diff --git a/src/Spectre.Console/Extensions/TableColumnExtensions.cs b/src/Spectre.Console/Extensions/TableColumnExtensions.cs index 37c8c64..9b3aa06 100644 --- a/src/Spectre.Console/Extensions/TableColumnExtensions.cs +++ b/src/Spectre.Console/Extensions/TableColumnExtensions.cs @@ -8,11 +8,55 @@ namespace Spectre.Console /// public static class TableColumnExtensions { + /// + /// Sets the table column header. + /// + /// The table column. + /// The table column header markup text. + /// The same instance so that multiple calls can be chained. + 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; + } + + /// + /// Sets the table column header. + /// + /// The table column. + /// The table column header. + /// The same instance so that multiple calls can be chained. + 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; + } + /// /// Sets the table column footer. /// /// The table column. - /// The table column markup text. + /// The table column footer markup text. /// The same instance so that multiple calls can be chained. public static TableColumn Footer(this TableColumn column, string footer) { diff --git a/src/Spectre.Console/Rendering/LiveRenderable.cs b/src/Spectre.Console/Rendering/LiveRenderable.cs index e3199c9..c4ea2f4 100644 --- a/src/Spectre.Console/Rendering/LiveRenderable.cs +++ b/src/Spectre.Console/Rendering/LiveRenderable.cs @@ -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); diff --git a/src/Spectre.Console/Rendering/SegmentLine.cs b/src/Spectre.Console/Rendering/SegmentLine.cs index 8c23aad..c47f0cd 100644 --- a/src/Spectre.Console/Rendering/SegmentLine.cs +++ b/src/Spectre.Console/Rendering/SegmentLine.cs @@ -13,6 +13,22 @@ namespace Spectre.Console.Rendering /// public int Length => this.Sum(line => line.Text.Length); + /// + /// Initializes a new instance of the class. + /// + public SegmentLine() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + public SegmentLine(IEnumerable segments) + : base(segments) + { + } + /// /// Gets the number of cells the segment line occupies. /// diff --git a/src/Spectre.Console/VerticalOverflow.cs b/src/Spectre.Console/VerticalOverflow.cs new file mode 100644 index 0000000..1556b84 --- /dev/null +++ b/src/Spectre.Console/VerticalOverflow.cs @@ -0,0 +1,23 @@ +namespace Spectre.Console +{ + /// + /// Represents vertical overflow. + /// + public enum VerticalOverflow + { + /// + /// Crop overflow. + /// + Crop = 0, + + /// + /// Add an ellipsis at the end. + /// + Ellipsis = 1, + + /// + /// Do not do anything about overflow. + /// + Visible = 2, + } +} diff --git a/src/Spectre.Console/VerticalOverflowCropping.cs b/src/Spectre.Console/VerticalOverflowCropping.cs new file mode 100644 index 0000000..1a18c87 --- /dev/null +++ b/src/Spectre.Console/VerticalOverflowCropping.cs @@ -0,0 +1,18 @@ +namespace Spectre.Console +{ + /// + /// Represent vertical overflow cropping. + /// + public enum VerticalOverflowCropping + { + /// + /// Crops the top. + /// + Top = 0, + + /// + /// Crops the bottom. + /// + Bottom = 1, + } +} diff --git a/src/Spectre.Console/Widgets/Live/LiveDisplay.cs b/src/Spectre.Console/Widgets/Live/LiveDisplay.cs new file mode 100644 index 0000000..600aa0b --- /dev/null +++ b/src/Spectre.Console/Widgets/Live/LiveDisplay.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading.Tasks; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// Represents a live display. + /// + public sealed class LiveDisplay + { + private readonly IAnsiConsole _console; + private readonly IRenderable _target; + + /// + /// Gets or sets a value indicating whether or not the live display should + /// be cleared when it's done. + /// Defaults to false. + /// + public bool AutoClear { get; set; } + + /// + /// Gets or sets the vertical overflow strategy. + /// + public VerticalOverflow Overflow { get; set; } = VerticalOverflow.Ellipsis; + + /// + /// Gets or sets the vertical overflow cropping strategy. + /// + public VerticalOverflowCropping Cropping { get; set; } = VerticalOverflowCropping.Top; + + /// + /// Initializes a new instance of the class. + /// + /// The console. + /// The target renderable to update. + public LiveDisplay(IAnsiConsole console, IRenderable target) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + _target = target ?? throw new ArgumentNullException(nameof(target)); + } + + /// + /// Starts the live display. + /// + /// The action to execute. + public void Start(Action action) + { + var task = StartAsync(ctx => + { + action(ctx); + return Task.CompletedTask; + }); + + task.GetAwaiter().GetResult(); + } + + /// + /// Starts the live display. + /// + /// The result type. + /// The action to execute. + /// The result. + public T Start(Func func) + { + var task = StartAsync(ctx => Task.FromResult(func(ctx))); + return task.GetAwaiter().GetResult(); + } + + /// + /// Starts the live display. + /// + /// The action to execute. + /// The result. + public async Task StartAsync(Func func) + { + if (func is null) + { + throw new ArgumentNullException(nameof(func)); + } + + _ = await StartAsync(async ctx => + { + await func(ctx).ConfigureAwait(false); + return default; + }).ConfigureAwait(false); + } + + /// + /// Starts the live display. + /// + /// The result type. + /// The action to execute. + /// The result. + public async Task StartAsync(Func> 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); + } + } +} diff --git a/src/Spectre.Console/Widgets/Live/LiveDisplayContext.cs b/src/Spectre.Console/Widgets/Live/LiveDisplayContext.cs new file mode 100644 index 0000000..861c0ec --- /dev/null +++ b/src/Spectre.Console/Widgets/Live/LiveDisplayContext.cs @@ -0,0 +1,54 @@ +using System; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// Represents a context that can be used to interact with a . + /// + 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(); + } + + /// + /// Updates the live display target. + /// + /// The new live display target. + public void UpdateTarget(IRenderable? target) + { + lock (Lock) + { + Live.SetRenderable(target); + Refresh(); + } + } + + /// + /// Refreshes the live display. + /// + public void Refresh() + { + lock (Lock) + { + _console.Write(new ControlCode(string.Empty)); + } + } + + internal void SetOverflow(VerticalOverflow overflow, VerticalOverflowCropping cropping) + { + Live.Overflow = overflow; + Live.OverflowCropping = cropping; + } + } +} diff --git a/src/Spectre.Console/Widgets/Live/LiveDisplayRenderer.cs b/src/Spectre.Console/Widgets/Live/LiveDisplayRenderer.cs new file mode 100644 index 0000000..2750988 --- /dev/null +++ b/src/Spectre.Console/Widgets/Live/LiveDisplayRenderer.cs @@ -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 Process(RenderContext context, IEnumerable renderables) + { + lock (_context.Lock) + { + yield return _context.Live.PositionCursor(); + + foreach (var renderable in renderables) + { + yield return renderable; + } + + yield return _context.Live; + } + } + } +} diff --git a/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs b/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs index c764531..23c23c7 100644 --- a/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs +++ b/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs @@ -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(); } diff --git a/src/Spectre.Console/Widgets/Prompt/List/ListPromptRenderHook.cs b/src/Spectre.Console/Widgets/Prompt/List/ListPromptRenderHook.cs index 6b619f3..f6b20f6 100644 --- a/src/Spectre.Console/Widgets/Prompt/List/ListPromptRenderHook.cs +++ b/src/Spectre.Console/Widgets/Prompt/List/ListPromptRenderHook.cs @@ -7,20 +7,21 @@ namespace Spectre.Console internal sealed class ListPromptRenderHook : IRenderHook where T : notnull { - private readonly LiveRenderable _live; - private readonly object _lock; private readonly IAnsiConsole _console; private readonly Func _builder; + private readonly LiveRenderable _live; + private readonly object _lock; private bool _dirty; public ListPromptRenderHook( IAnsiConsole console, Func 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; } diff --git a/src/Spectre.Console/Widgets/Table/TableColumn.cs b/src/Spectre.Console/Widgets/Table/TableColumn.cs index 6f4d938..beabf40 100644 --- a/src/Spectre.Console/Widgets/Table/TableColumn.cs +++ b/src/Spectre.Console/Widgets/Table/TableColumn.cs @@ -9,9 +9,9 @@ namespace Spectre.Console public sealed class TableColumn : IColumn { /// - /// Gets the column header. + /// Gets or sets the column header. /// - public IRenderable Header { get; } + public IRenderable Header { get; set; } /// /// Gets or sets the column footer.