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.
+
+# 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
+ });
+```
\ 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