diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c67b41a..d116ede 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -69,14 +69,7 @@ jobs:
shell: bash
run: |
dotnet tool restore
- dotnet example info
- dotnet example tables
- dotnet example grids
- dotnet example panels
- dotnet example colors
- dotnet example emojis
- dotnet example exceptions
- dotnet example calendars
+ dotnet example --all
- name: Build
shell: bash
diff --git a/docs/input/assets/images/progress.gif b/docs/input/assets/images/progress.gif
new file mode 100644
index 0000000..7ec3868
Binary files /dev/null and b/docs/input/assets/images/progress.gif differ
diff --git a/docs/input/assets/images/progress.png b/docs/input/assets/images/progress.png
new file mode 100644
index 0000000..dd331ed
Binary files /dev/null and b/docs/input/assets/images/progress.png differ
diff --git a/docs/input/assets/images/progress_fallback.png b/docs/input/assets/images/progress_fallback.png
new file mode 100644
index 0000000..cab074d
Binary files /dev/null and b/docs/input/assets/images/progress_fallback.png differ
diff --git a/docs/input/progress.md b/docs/input/progress.md
new file mode 100644
index 0000000..ccfbbec
--- /dev/null
+++ b/docs/input/progress.md
@@ -0,0 +1,78 @@
+Title: Progress
+Order: 5
+---
+
+Spectre.Console can display information about long running tasks in the console.
+
+
+
+If the current terminal isn't considered "interactive", such as when running
+in a continuous integration system, or the terminal can't display
+ANSI control sequence, any progress will be displayed in a simpler way.
+
+
+
+# Usage
+
+```csharp
+// Synchronous
+AnsiConsole.Progress()
+ .Start(ctx =>
+ {
+ // Define tasks
+ var task1 = ctx.AddTask("[green]Reticulating splines[/]");
+ var task2 = ctx.AddTask("[green]Folding space[/]");
+
+ while(!ctx.IsFinished)
+ {
+ task1.Increment(1.5);
+ task2.Increment(0.5);
+ }
+ });
+```
+
+## Asynchronous progress
+
+If you prefer to use async/await, you can use `StartAsync` instead of `Start`.
+
+```csharp
+// Asynchronous
+await AnsiConsole.Progress()
+ .StartAsync(async ctx =>
+ {
+ // Define tasks
+ var task1 = ctx.AddTask("[green]Reticulating splines[/]");
+ var task2 = ctx.AddTask("[green]Folding space[/]");
+
+ while (!ctx.IsFinished)
+ {
+ // Simulate some work
+ await Task.Delay(250);
+
+ // Increment
+ task1.Increment(1.5);
+ task2.Increment(0.5);
+ }
+ });
+```
+
+# Configure
+
+```csharp
+// Asynchronous
+AnsiConsole.Progress()
+ .AutoRefresh(false) // Turn off auto refresh
+ .AutoClear(false) // Do not remove the task list when done
+ .Columns(new ProgressColumn[]
+ {
+ new TaskDescriptionColumn(), // Task description
+ new ProgressBarColumn(), // Progress bar
+ new PercentageColumn(), // Percentage
+ new RemainingTimeColumn(), // Remaining time
+ new SpinnerColumn(), // Spinner
+ })
+ .Start(ctx =>
+ {
+ // Omitted
+ });
+```
\ No newline at end of file
diff --git a/examples/Info/Program.cs b/examples/Info/Program.cs
index 4aa82d4..0c2f624 100644
--- a/examples/Info/Program.cs
+++ b/examples/Info/Program.cs
@@ -1,4 +1,3 @@
-using System;
using Spectre.Console;
namespace InfoExample
@@ -13,7 +12,7 @@ namespace InfoExample
.AddRow("[b]Color system[/]", $"{AnsiConsole.Capabilities.ColorSystem}")
.AddRow("[b]Supports ansi?[/]", $"{YesNo(AnsiConsole.Capabilities.SupportsAnsi)}")
.AddRow("[b]Legacy console?[/]", $"{YesNo(AnsiConsole.Capabilities.LegacyConsole)}")
- .AddRow("[b]Interactive?[/]", $"{YesNo(Environment.UserInteractive)}")
+ .AddRow("[b]Interactive?[/]", $"{YesNo(AnsiConsole.Capabilities.SupportsInteraction)}")
.AddRow("[b]Buffer width[/]", $"{AnsiConsole.Console.Width}")
.AddRow("[b]Buffer height[/]", $"{AnsiConsole.Console.Height}");
diff --git a/examples/Progress/DescriptionGenerator.cs b/examples/Progress/DescriptionGenerator.cs
new file mode 100644
index 0000000..a4f7861
--- /dev/null
+++ b/examples/Progress/DescriptionGenerator.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+
+namespace ProgressExample
+{
+ public static class DescriptionGenerator
+ {
+ private static readonly string[] _verbs = new[] { "Downloading", "Rerouting", "Retriculating", "Collapsing", "Folding", "Solving", "Colliding", "Measuring" };
+ private static readonly string[] _nouns = new[] { "internet", "splines", "space", "capacitators", "quarks", "algorithms", "data structures", "spacetime" };
+
+ private static readonly Random _random;
+ private static readonly HashSet _used;
+
+ static DescriptionGenerator()
+ {
+ _random = new Random(DateTime.Now.Millisecond);
+ _used = new HashSet();
+ }
+
+ public static bool TryGenerate(out string name)
+ {
+ var iterations = 0;
+ while (iterations < 25)
+ {
+ name = Generate();
+ if (!_used.Contains(name))
+ {
+ _used.Add(name);
+ return true;
+ }
+
+ iterations++;
+ }
+
+ name = Generate();
+ return false;
+ }
+
+ public static string Generate()
+ {
+ return _verbs[_random.Next(0, _verbs.Length)]
+ + " " + _nouns[_random.Next(0, _nouns.Length)];
+ }
+ }
+}
diff --git a/examples/Progress/Program.cs b/examples/Progress/Program.cs
new file mode 100644
index 0000000..6215b9b
--- /dev/null
+++ b/examples/Progress/Program.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Spectre.Console;
+
+namespace ProgressExample
+{
+ public static class Program
+ {
+ public static void Main()
+ {
+ AnsiConsole.MarkupLine("[yellow]Initializing warp drive[/]...");
+
+ // Show progress
+ AnsiConsole.Progress()
+ .AutoClear(false)
+ .Columns(new ProgressColumn[]
+ {
+ new TaskDescriptionColumn(), // Task description
+ new ProgressBarColumn(), // Progress bar
+ new PercentageColumn(), // Percentage
+ new RemainingTimeColumn(), // Remaining time
+ new SpinnerColumn(), // Spinner
+ })
+ .Start(ctx =>
+ {
+ var random = new Random(DateTime.Now.Millisecond);
+ var tasks = CreateTasks(ctx, random);
+
+ while (!ctx.IsFinished)
+ {
+ // Increment progress
+ foreach (var (task, increment) in tasks)
+ {
+ task.Increment(random.NextDouble() * increment);
+ }
+
+ // Write some random things to the terminal
+ if (random.NextDouble() < 0.1)
+ {
+ WriteLogMessage();
+ }
+
+ // Simulate some delay
+ Thread.Sleep(100);
+ }
+ });
+
+ // Done
+ AnsiConsole.MarkupLine("[green]Done![/]");
+ }
+
+ private static List<(ProgressTask, int)> CreateTasks(ProgressContext progress, Random random)
+ {
+ var tasks = new List<(ProgressTask, int)>();
+ while (tasks.Count < 5)
+ {
+ if (DescriptionGenerator.TryGenerate(out var name))
+ {
+ tasks.Add((progress.AddTask(name), random.Next(2, 10)));
+ }
+ }
+
+ return tasks;
+ }
+
+ private static void WriteLogMessage()
+ {
+ AnsiConsole.MarkupLine(
+ "[grey]LOG:[/] " +
+ DescriptionGenerator.Generate() +
+ "[grey]...[/]");
+ }
+ }
+}
diff --git a/examples/Progress/Progress.csproj b/examples/Progress/Progress.csproj
new file mode 100644
index 0000000..f84c230
--- /dev/null
+++ b/examples/Progress/Progress.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net5.0
+ false
+ Progress
+ Demonstrates how to show progress bars.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Prompt/Program.cs b/examples/Prompt/Program.cs
index 4cb42d8..86728e5 100644
--- a/examples/Prompt/Program.cs
+++ b/examples/Prompt/Program.cs
@@ -6,6 +6,13 @@ namespace Cursor
{
public static void Main(string[] args)
{
+ // Check if we can accept key strokes
+ if (!AnsiConsole.Capabilities.SupportsInteraction)
+ {
+ AnsiConsole.MarkupLine("[red]Environment does not support interaction.[/]");
+ return;
+ }
+
// Confirmation
if (!AnsiConsole.Confirm("Run prompt example?"))
{
diff --git a/src/.editorconfig b/src/.editorconfig
index 449cd6c..54af1cb 100644
--- a/src/.editorconfig
+++ b/src/.editorconfig
@@ -89,4 +89,7 @@ dotnet_diagnostic.RCS1227.severity = none
dotnet_diagnostic.IDE0004.severity = warning
# CA1810: Initialize reference type static fields inline
-dotnet_diagnostic.CA1810.severity = none
\ No newline at end of file
+dotnet_diagnostic.CA1810.severity = none
+
+# IDE0044: Add readonly modifier
+dotnet_diagnostic.IDE0044.severity = warning
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Expectations/ProgressBarTests.Should_Render_Progress_Bar.verified.txt b/src/Spectre.Console.Tests/Expectations/ProgressBarTests.Should_Render_Progress_Bar.verified.txt
new file mode 100644
index 0000000..d9aadad
--- /dev/null
+++ b/src/Spectre.Console.Tests/Expectations/ProgressBarTests.Should_Render_Progress_Bar.verified.txt
@@ -0,0 +1 @@
+━━━━━━━━━━━━━━━━━━━━
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Tools/PlainConsole.cs b/src/Spectre.Console.Tests/Tools/PlainConsole.cs
index f076db4..12b7c84 100644
--- a/src/Spectre.Console.Tests/Tools/PlainConsole.cs
+++ b/src/Spectre.Console.Tests/Tools/PlainConsole.cs
@@ -18,6 +18,7 @@ namespace Spectre.Console.Tests
public int Height { get; }
IAnsiConsoleInput IAnsiConsole.Input => Input;
+ public RenderPipeline Pipeline { get; }
public Decoration Decoration { get; set; }
public Color Foreground { get; set; }
@@ -31,14 +32,15 @@ namespace Spectre.Console.Tests
public PlainConsole(
int width = 80, int height = 9000, Encoding encoding = null,
bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard,
- bool legacyConsole = false)
+ bool legacyConsole = false, bool interactive = true)
{
- Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole);
+ Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, interactive);
Encoding = encoding ?? Encoding.UTF8;
Width = width;
Height = height;
Writer = new StringWriter();
Input = new TestableConsoleInput();
+ Pipeline = new RenderPipeline();
}
public void Dispose()
@@ -50,14 +52,17 @@ namespace Spectre.Console.Tests
{
}
- public void Write(Segment segment)
+ public void Write(IEnumerable segments)
{
- if (segment is null)
+ if (segments is null)
{
- throw new ArgumentNullException(nameof(segment));
+ return;
}
- Writer.Write(segment.Text);
+ foreach (var segment in segments)
+ {
+ Writer.Write(segment.Text);
+ }
}
public string WriteNormalizedException(Exception ex, ExceptionFormats formats = ExceptionFormats.Default)
diff --git a/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs b/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs
index ba986e5..0cd8122 100644
--- a/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs
+++ b/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text;
using Spectre.Console.Rendering;
@@ -19,16 +20,21 @@ namespace Spectre.Console.Tests
public int Height => _console.Height;
public IAnsiConsoleCursor Cursor => _console.Cursor;
public TestableConsoleInput Input { get; }
+ public RenderPipeline Pipeline => _console.Pipeline;
IAnsiConsoleInput IAnsiConsole.Input => Input;
- public TestableAnsiConsole(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80)
+ public TestableAnsiConsole(
+ ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes,
+ InteractionSupport interaction = InteractionSupport.Yes,
+ int width = 80)
{
_writer = new StringWriter();
_console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = ansi,
ColorSystem = (ColorSystemSupport)system,
+ Interactive = interaction,
Out = _writer,
LinkIdentityGenerator = new TestLinkIdentityGenerator(),
});
@@ -47,9 +53,17 @@ namespace Spectre.Console.Tests
_console.Clear(home);
}
- public void Write(Segment segment)
+ public void Write(IEnumerable segments)
{
- _console.Write(segment);
+ if (segments is null)
+ {
+ return;
+ }
+
+ foreach (var segment in segments)
+ {
+ _console.Write(segment);
+ }
}
}
}
diff --git a/src/Spectre.Console.Tests/Unit/ProgressTests.cs b/src/Spectre.Console.Tests/Unit/ProgressTests.cs
new file mode 100644
index 0000000..ac330c2
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/ProgressTests.cs
@@ -0,0 +1,58 @@
+using Shouldly;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit
+{
+ public sealed class ProgressTests
+ {
+ [Fact]
+ public void Should_Render_Task_Correctly()
+ {
+ // Given
+ var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10);
+
+ var progress = new Progress(console)
+ .Columns(new[] { new ProgressBarColumn() })
+ .AutoRefresh(false)
+ .AutoClear(true);
+
+ // When
+ progress.Start(ctx => ctx.AddTask("foo"));
+
+ // Then
+ console.Output
+ .NormalizeLineEndings()
+ .ShouldBe(
+ "[?25l" + // Hide cursor
+ " \n" + // Top padding
+ "[38;5;8m━━━━━━━━━━[0m\n" + // Task
+ " " + // Bottom padding
+ "[2K[1A[2K[1A[2K[?25h"); // Clear + show cursor
+ }
+
+ [Fact]
+ public void Should_Not_Auto_Clear_If_Specified()
+ {
+ // Given
+ var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10);
+
+ var progress = new Progress(console)
+ .Columns(new[] { new ProgressBarColumn() })
+ .AutoRefresh(false)
+ .AutoClear(false);
+
+ // When
+ progress.Start(ctx => ctx.AddTask("foo"));
+
+ // Then
+ console.Output
+ .NormalizeLineEndings()
+ .ShouldBe(
+ "[?25l" + // Hide cursor
+ " \n" + // Top padding
+ "[38;5;8m━━━━━━━━━━[0m\n" + // Task
+ " \n" + // Bottom padding
+ "[?25h"); // show cursor
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/RenderHookTests.cs b/src/Spectre.Console.Tests/Unit/RenderHookTests.cs
new file mode 100644
index 0000000..eeee8ec
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/RenderHookTests.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using System.Linq;
+using Shouldly;
+using Spectre.Console.Rendering;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit
+{
+ public sealed class RenderHookTests
+ {
+ private sealed class HelloRenderHook : IRenderHook
+ {
+ public IEnumerable Process(RenderContext context, IEnumerable renderables)
+ {
+ return new IRenderable[] { new Text("Hello\n") }.Concat(renderables);
+ }
+ }
+
+ [Fact]
+ public void Should_Inject_Renderable_Before_Writing_To_Console()
+ {
+ // Given
+ var console = new PlainConsole();
+ console.Pipeline.Attach(new HelloRenderHook());
+
+ // When
+ console.Render(new Text("World"));
+
+ // Then
+ console.Lines[0].ShouldBe("Hello");
+ console.Lines[1].ShouldBe("World");
+ }
+ }
+}
diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln
index a1af2ed..c4ef646 100644
--- a/src/Spectre.Console.sln
+++ b/src/Spectre.Console.sln
@@ -58,6 +58,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Canva
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Progress\Progress.csproj", "{2B712A52-40F1-4C1C-833E-7C869ACA91F3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -296,6 +298,18 @@ Global
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.Build.0 = Release|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x64.Build.0 = Debug|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x86.Build.0 = Debug|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x64.ActiveCfg = Release|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x64.Build.0 = Release|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x86.ActiveCfg = Release|Any CPU
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -318,6 +332,7 @@ Global
{6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
+ {2B712A52-40F1-4C1C-833E-7C869ACA91F3} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
diff --git a/src/Spectre.Console/AnsiConsole.Progress.cs b/src/Spectre.Console/AnsiConsole.Progress.cs
new file mode 100644
index 0000000..835fcec
--- /dev/null
+++ b/src/Spectre.Console/AnsiConsole.Progress.cs
@@ -0,0 +1,17 @@
+namespace Spectre.Console
+{
+ ///
+ /// A console capable of writing ANSI escape sequences.
+ ///
+ public static partial class AnsiConsole
+ {
+ ///
+ /// Creates a new instance.
+ ///
+ /// A instance.
+ public static Progress Progress()
+ {
+ return Console.Progress();
+ }
+ }
+}
diff --git a/src/Spectre.Console/AnsiConsoleSettings.cs b/src/Spectre.Console/AnsiConsoleSettings.cs
index 4d3dc37..83db057 100644
--- a/src/Spectre.Console/AnsiConsoleSettings.cs
+++ b/src/Spectre.Console/AnsiConsoleSettings.cs
@@ -18,6 +18,12 @@ namespace Spectre.Console
///
public ColorSystemSupport ColorSystem { get; set; }
+ ///
+ /// Gets or sets a value indicating whether or
+ /// not the console is interactive.
+ ///
+ public InteractionSupport Interactive { get; set; }
+
///
/// Gets or sets the link identity generator.
///
diff --git a/src/Spectre.Console/Capabilities.cs b/src/Spectre.Console/Capabilities.cs
index 284999c..ef4c9a6 100644
--- a/src/Spectre.Console/Capabilities.cs
+++ b/src/Spectre.Console/Capabilities.cs
@@ -36,17 +36,24 @@ namespace Spectre.Console
///
public bool LegacyConsole { get; }
+ ///
+ /// Gets or sets a value indicating whether or not the console supports interaction.
+ ///
+ public bool SupportsInteraction { get; set; }
+
///
/// Initializes a new instance of the class.
///
/// Whether or not ANSI escape sequences are supported.
/// The color system that is supported.
/// Whether or not this is a legacy console.
- public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole)
+ /// Whether or not the console supports interaction.
+ public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole, bool supportsInteraction)
{
SupportsAnsi = supportsAnsi;
ColorSystem = colorSystem;
LegacyConsole = legacyConsole;
+ SupportsInteraction = supportsInteraction;
}
///
diff --git a/src/Spectre.Console/ColorSystemSupport.cs b/src/Spectre.Console/ColorSystemSupport.cs
index cdff656..37f4aa5 100644
--- a/src/Spectre.Console/ColorSystemSupport.cs
+++ b/src/Spectre.Console/ColorSystemSupport.cs
@@ -8,31 +8,31 @@ namespace Spectre.Console
///
/// Try to detect the color system.
///
- Detect = -1,
+ Detect = 0,
///
/// No colors.
///
- NoColors = 0,
+ NoColors = 1,
///
/// Legacy, 3-bit mode.
///
- Legacy = 1,
+ Legacy = 2,
///
/// Standard, 4-bit mode.
///
- Standard = 2,
+ Standard = 3,
///
/// 8-bit mode.
///
- EightBit = 3,
+ EightBit = 4,
///
/// 24-bit mode.
///
- TrueColor = 4,
+ TrueColor = 5,
}
}
diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs
index 0a9ceb1..45ea97e 100644
--- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs
+++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs
@@ -52,8 +52,7 @@ namespace Spectre.Console
/// An array of objects to write.
public static void MarkupLine(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args)
{
- Markup(console, provider, format, args);
- console.WriteLine();
+ Markup(console, provider, format + Environment.NewLine, args);
}
}
}
diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Progress.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Progress.cs
new file mode 100644
index 0000000..69a82dc
--- /dev/null
+++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Progress.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static partial class AnsiConsoleExtensions
+ {
+ ///
+ /// Creates a new instance for the console.
+ ///
+ /// The console.
+ /// A instance.
+ public static Progress Progress(this IAnsiConsole console)
+ {
+ if (console is null)
+ {
+ throw new ArgumentNullException(nameof(console));
+ }
+
+ return new Progress(console);
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Rendering.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Rendering.cs
index 72d92b4..1d2f35a 100644
--- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Rendering.cs
+++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Rendering.cs
@@ -1,5 +1,5 @@
using System;
-using System.Linq;
+using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
@@ -26,19 +26,26 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(renderable));
}
- var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
- var segments = renderable.Render(options, console.Width).ToArray();
- segments = Segment.Merge(segments).ToArray();
+ var context = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
+ var renderables = console.Pipeline.Process(context, new[] { renderable });
- foreach (var segment in segments)
+ Render(console, context, renderables);
+ }
+
+ private static void Render(IAnsiConsole console, RenderContext options, IEnumerable renderables)
+ {
+ if (renderables is null)
{
- if (string.IsNullOrEmpty(segment.Text))
- {
- continue;
- }
-
- console.Write(segment.Text, segment.Style);
+ return;
}
+
+ var result = new List();
+ foreach (var renderable in renderables)
+ {
+ result.AddRange(renderable.Render(options, console.Width));
+ }
+
+ console.Write(Segment.Merge(result));
}
}
}
diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.cs
index aac10f7..acbf72f 100644
--- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.cs
+++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.cs
@@ -18,6 +18,26 @@ namespace Spectre.Console
return new Recorder(console);
}
+ ///
+ /// Writes the specified string value to the console.
+ ///
+ /// The console to write to.
+ /// The segment to write.
+ public static void Write(this IAnsiConsole console, Segment segment)
+ {
+ if (console is null)
+ {
+ throw new ArgumentNullException(nameof(console));
+ }
+
+ if (segment is null)
+ {
+ throw new ArgumentNullException(nameof(segment));
+ }
+
+ console.Write(new[] { segment });
+ }
+
///
/// Writes the specified string value to the console.
///
@@ -25,7 +45,7 @@ namespace Spectre.Console
/// The text to write.
public static void Write(this IAnsiConsole console, string text)
{
- Write(console, text, Style.Plain);
+ Render(console, new Text(text, Style.Plain));
}
///
@@ -36,17 +56,7 @@ namespace Spectre.Console
/// The text style.
public static void Write(this IAnsiConsole console, string text, Style style)
{
- if (console is null)
- {
- throw new ArgumentNullException(nameof(console));
- }
-
- if (text is null)
- {
- throw new ArgumentNullException(nameof(text));
- }
-
- console.Write(new Segment(text, style));
+ Render(console, new Text(text, style));
}
///
@@ -60,7 +70,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(console));
}
- console.Write(Environment.NewLine, Style.Plain);
+ Render(console, new Text(Environment.NewLine, Style.Plain));
}
///
@@ -91,8 +101,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(text));
}
- console.Write(new Segment(text, style));
- console.WriteLine();
+ console.Write(text + Environment.NewLine, style);
}
}
}
diff --git a/src/Spectre.Console/Extensions/Columns/PercentageColumnExtensions.cs b/src/Spectre.Console/Extensions/Columns/PercentageColumnExtensions.cs
new file mode 100644
index 0000000..9d68752
--- /dev/null
+++ b/src/Spectre.Console/Extensions/Columns/PercentageColumnExtensions.cs
@@ -0,0 +1,54 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class PercentageColumnExtensions
+ {
+ ///
+ /// Sets the style for a non-complete task.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static PercentageColumn Style(this PercentageColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.Style = style;
+ return column;
+ }
+
+ ///
+ /// Sets the style for a completed task.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static PercentageColumn CompletedStyle(this PercentageColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.CompletedStyle = style;
+ return column;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/Columns/ProgressBarColumnExtensions.cs b/src/Spectre.Console/Extensions/Columns/ProgressBarColumnExtensions.cs
new file mode 100644
index 0000000..a58eb0f
--- /dev/null
+++ b/src/Spectre.Console/Extensions/Columns/ProgressBarColumnExtensions.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class ProgressBarColumnExtensions
+ {
+ ///
+ /// Sets the style of completed portions of the progress bar.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static ProgressBarColumn CompletedStyle(this ProgressBarColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.CompletedStyle = style;
+ return column;
+ }
+
+ ///
+ /// Sets the style of a finished progress bar.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static ProgressBarColumn FinishedStyle(this ProgressBarColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.FinishedStyle = style;
+ return column;
+ }
+
+ ///
+ /// Sets the style of remaining portions of the progress bar.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static ProgressBarColumn RemainingStyle(this ProgressBarColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.RemainingStyle = style;
+ return column;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/Columns/RemainingTimeColumnExtensions.cs b/src/Spectre.Console/Extensions/Columns/RemainingTimeColumnExtensions.cs
new file mode 100644
index 0000000..a33a31b
--- /dev/null
+++ b/src/Spectre.Console/Extensions/Columns/RemainingTimeColumnExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class RemainingTimeColumnExtensions
+ {
+ ///
+ /// Sets the style of the remaining time text.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static RemainingTimeColumn Style(this RemainingTimeColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.Style = style;
+ return column;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/Columns/SpinnerColumnExtensions.cs b/src/Spectre.Console/Extensions/Columns/SpinnerColumnExtensions.cs
new file mode 100644
index 0000000..c33c792
--- /dev/null
+++ b/src/Spectre.Console/Extensions/Columns/SpinnerColumnExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class SpinnerColumnExtensions
+ {
+ ///
+ /// Sets the style of the spinner.
+ ///
+ /// The column.
+ /// The style.
+ /// The same instance so that multiple calls can be chained.
+ public static SpinnerColumn Style(this SpinnerColumn column, Style style)
+ {
+ if (column is null)
+ {
+ throw new ArgumentNullException(nameof(column));
+ }
+
+ if (style is null)
+ {
+ throw new ArgumentNullException(nameof(style));
+ }
+
+ column.Style = style;
+ return column;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/PanelExtensions.cs b/src/Spectre.Console/Extensions/PanelExtensions.cs
index e1901dc..9505fe5 100644
--- a/src/Spectre.Console/Extensions/PanelExtensions.cs
+++ b/src/Spectre.Console/Extensions/PanelExtensions.cs
@@ -27,7 +27,7 @@ namespace Spectre.Console
}
alignment ??= panel.Header?.Alignment;
- return Header(panel, new PanelHeader(text, alignment));
+ return Header(panel, new PanelHeader(text, alignment));
}
///
diff --git a/src/Spectre.Console/Extensions/ProgressExtensions.cs b/src/Spectre.Console/Extensions/ProgressExtensions.cs
new file mode 100644
index 0000000..cf8d8a9
--- /dev/null
+++ b/src/Spectre.Console/Extensions/ProgressExtensions.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Linq;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class ProgressExtensions
+ {
+ ///
+ /// Sets the columns to be used for an instance.
+ ///
+ /// The instance.
+ /// The columns to use.
+ /// The same instance so that multiple calls can be chained.
+ public static Progress Columns(this Progress progress, ProgressColumn[] columns)
+ {
+ if (progress is null)
+ {
+ throw new ArgumentNullException(nameof(progress));
+ }
+
+ if (columns is null)
+ {
+ throw new ArgumentNullException(nameof(columns));
+ }
+
+ if (!columns.Any())
+ {
+ throw new InvalidOperationException("At least one column must be specified.");
+ }
+
+ progress.Columns.Clear();
+ progress.Columns.AddRange(columns);
+
+ return progress;
+ }
+
+ ///
+ /// Sets whether or not auto refresh is enabled.
+ /// If disabled, you will manually have to refresh the progress.
+ ///
+ /// The instance.
+ /// Whether or not auto refresh is enabled.
+ /// The same instance so that multiple calls can be chained.
+ public static Progress AutoRefresh(this Progress progress, bool enabled)
+ {
+ if (progress is null)
+ {
+ throw new ArgumentNullException(nameof(progress));
+ }
+
+ progress.AutoRefresh = enabled;
+
+ return progress;
+ }
+
+ ///
+ /// Sets whether or not auto clear is enabled.
+ /// If enabled, the task tabled will be removed once
+ /// all tasks have completed.
+ ///
+ /// The instance.
+ /// Whether or not auto clear is enabled.
+ /// The same instance so that multiple calls can be chained.
+ public static Progress AutoClear(this Progress progress, bool enabled)
+ {
+ if (progress is null)
+ {
+ throw new ArgumentNullException(nameof(progress));
+ }
+
+ progress.AutoClear = enabled;
+
+ return progress;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/ProgressTaskExtensions.cs b/src/Spectre.Console/Extensions/ProgressTaskExtensions.cs
new file mode 100644
index 0000000..63ba2b0
--- /dev/null
+++ b/src/Spectre.Console/Extensions/ProgressTaskExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class ProgressTaskExtensions
+ {
+ ///
+ /// Sets the task description.
+ ///
+ /// The task.
+ /// The description.
+ /// The same instance so that multiple calls can be chained.
+ public static ProgressTask Description(this ProgressTask task, string description)
+ {
+ if (task is null)
+ {
+ throw new ArgumentNullException(nameof(task));
+ }
+
+ task.Description = description;
+ return task;
+ }
+
+ ///
+ /// Sets the max value of the task.
+ ///
+ /// The task.
+ /// The max value.
+ /// The same instance so that multiple calls can be chained.
+ public static ProgressTask MaxValue(this ProgressTask task, double value)
+ {
+ if (task is null)
+ {
+ throw new ArgumentNullException(nameof(task));
+ }
+
+ task.MaxValue = value;
+ return task;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/StringBuilderExtensions.cs b/src/Spectre.Console/Extensions/StringBuilderExtensions.cs
index ff394f6..0a970e0 100644
--- a/src/Spectre.Console/Extensions/StringBuilderExtensions.cs
+++ b/src/Spectre.Console/Extensions/StringBuilderExtensions.cs
@@ -1,7 +1,7 @@
using System.Globalization;
using System.Text;
-namespace Spectre.Console
+namespace Spectre.Console.Internal
{
internal static class StringBuilderExtensions
{
diff --git a/src/Spectre.Console/Extensions/StringExtensions.cs b/src/Spectre.Console/Extensions/StringExtensions.cs
index e17649a..7d5a02f 100644
--- a/src/Spectre.Console/Extensions/StringExtensions.cs
+++ b/src/Spectre.Console/Extensions/StringExtensions.cs
@@ -62,10 +62,15 @@ namespace Spectre.Console
return text;
}
- internal static string NormalizeLineEndings(this string? text, bool native = false)
+ internal static string? RemoveNewLines(this string? text)
+ {
+ return text?.ReplaceExact("\r\n", string.Empty)
+ ?.ReplaceExact("\n", string.Empty);
+ }
+
+ internal static string NormalizeNewLines(this string? text, bool native = false)
{
text = text?.ReplaceExact("\r\n", "\n");
- text = text?.ReplaceExact("\r", string.Empty);
text ??= string.Empty;
if (native && !_alreadyNormalized)
@@ -78,7 +83,7 @@ namespace Spectre.Console
internal static string[] SplitLines(this string text)
{
- var result = text?.NormalizeLineEndings()?.Split(new[] { '\n' }, StringSplitOptions.None);
+ var result = text?.NormalizeNewLines()?.Split(new[] { '\n' }, StringSplitOptions.None);
return result ?? Array.Empty();
}
diff --git a/src/Spectre.Console/IAnsiConsole.cs b/src/Spectre.Console/IAnsiConsole.cs
index 58c87e5..12785a0 100644
--- a/src/Spectre.Console/IAnsiConsole.cs
+++ b/src/Spectre.Console/IAnsiConsole.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
@@ -28,6 +29,11 @@ namespace Spectre.Console
///
IAnsiConsoleInput Input { get; }
+ ///
+ /// Gets the render pipeline.
+ ///
+ RenderPipeline Pipeline { get; }
+
///
/// Gets the buffer width of the console.
///
@@ -45,9 +51,9 @@ namespace Spectre.Console
void Clear(bool home);
///
- /// Writes a string followed by a line terminator to the console.
+ /// Writes multiple segments to the console.
///
- /// The segment to write.
- void Write(Segment segment);
+ /// The segments to write.
+ void Write(IEnumerable segments);
}
}
diff --git a/src/Spectre.Console/InteractionSupport.cs b/src/Spectre.Console/InteractionSupport.cs
new file mode 100644
index 0000000..4fb2747
--- /dev/null
+++ b/src/Spectre.Console/InteractionSupport.cs
@@ -0,0 +1,24 @@
+namespace Spectre.Console
+{
+ ///
+ /// Determines interactivity support.
+ ///
+ public enum InteractionSupport
+ {
+ ///
+ /// Interaction support should be
+ /// detected by the system.
+ ///
+ Detect = 0,
+
+ ///
+ /// Interactivity is supported.
+ ///
+ Yes = 1,
+
+ ///
+ /// Interactivity is not supported.
+ ///
+ No = 2,
+ }
+}
diff --git a/src/Spectre.Console/Internal/Ansi/AnsiDetector.cs b/src/Spectre.Console/Internal/Ansi/AnsiDetector.cs
index d678e71..5f4b132 100644
--- a/src/Spectre.Console/Internal/Ansi/AnsiDetector.cs
+++ b/src/Spectre.Console/Internal/Ansi/AnsiDetector.cs
@@ -121,6 +121,8 @@ namespace Spectre.Console.Internal
// Enabling failed.
return false;
}
+
+ isLegacy = false;
}
return true;
diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs
index 9a101d8..c41494a 100644
--- a/src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs
+++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text;
using Spectre.Console.Rendering;
@@ -11,9 +12,11 @@ namespace Spectre.Console.Internal
private readonly AnsiBuilder _ansiBuilder;
private readonly AnsiCursor _cursor;
private readonly ConsoleInput _input;
+ private readonly object _lock;
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
+ public RenderPipeline Pipeline { get; }
public IAnsiConsoleCursor Cursor => _cursor;
public IAnsiConsoleInput Input => _input;
@@ -49,35 +52,59 @@ namespace Spectre.Console.Internal
Capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
Encoding = _out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8;
+ Pipeline = new RenderPipeline();
_ansiBuilder = new AnsiBuilder(Capabilities, linkHasher);
_cursor = new AnsiCursor(this);
_input = new ConsoleInput();
+ _lock = new object();
}
public void Clear(bool home)
{
- Write(Segment.Control("\u001b[2J"));
-
- if (home)
+ lock (_lock)
{
- Cursor.SetPosition(0, 0);
+ Write(new[] { Segment.Control("\u001b[2J") });
+
+ if (home)
+ {
+ Cursor.SetPosition(0, 0);
+ }
}
}
- public void Write(Segment segment)
+ public void Write(IEnumerable segments)
{
- var parts = segment.Text.NormalizeLineEndings().Split(new[] { '\n' });
- foreach (var (_, _, last, part) in parts.Enumerate())
+ lock (_lock)
{
- if (!string.IsNullOrEmpty(part))
+ var builder = new StringBuilder();
+ foreach (var segment in segments)
{
- _out.Write(_ansiBuilder.GetAnsi(part, segment.Style));
+ if (segment.IsControlCode)
+ {
+ builder.Append(segment.Text);
+ continue;
+ }
+
+ var parts = segment.Text.NormalizeNewLines().Split(new[] { '\n' });
+ foreach (var (_, _, last, part) in parts.Enumerate())
+ {
+ if (!string.IsNullOrEmpty(part))
+ {
+ builder.Append(_ansiBuilder.GetAnsi(part, segment.Style));
+ }
+
+ if (!last)
+ {
+ builder.Append(Environment.NewLine);
+ }
+ }
}
- if (!last)
+ if (builder.Length > 0)
{
- _out.Write(Environment.NewLine);
+ _out.Write(builder.ToString());
+ _out.Flush();
}
}
}
diff --git a/src/Spectre.Console/Internal/Backends/BackendBuilder.cs b/src/Spectre.Console/Internal/Backends/BackendBuilder.cs
index 6069e2a..a2f51e2 100644
--- a/src/Spectre.Console/Internal/Backends/BackendBuilder.cs
+++ b/src/Spectre.Console/Internal/Backends/BackendBuilder.cs
@@ -50,12 +50,18 @@ namespace Spectre.Console.Internal
}
}
+ var supportsInteraction = settings.Interactive == InteractionSupport.Yes;
+ if (settings.Interactive == InteractionSupport.Detect)
+ {
+ supportsInteraction = InteractivityDetector.IsInteractive();
+ }
+
var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect
? ColorSystemDetector.Detect(supportsAnsi)
: (ColorSystem)settings.ColorSystem;
// Get the capabilities
- var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole);
+ var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, supportsInteraction);
// Create the renderer
if (supportsAnsi)
diff --git a/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs b/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs
index 81aff14..569db39 100644
--- a/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs
+++ b/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text;
using Spectre.Console.Rendering;
@@ -14,6 +15,7 @@ namespace Spectre.Console.Internal
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
+ public RenderPipeline Pipeline { get; }
public IAnsiConsoleCursor Cursor => _cursor;
public IAnsiConsoleInput Input => _input;
@@ -43,8 +45,9 @@ namespace Spectre.Console.Internal
System.Console.SetOut(@out ?? throw new ArgumentNullException(nameof(@out)));
}
- Encoding = System.Console.OutputEncoding;
Capabilities = capabilities;
+ Encoding = System.Console.OutputEncoding;
+ Pipeline = new RenderPipeline();
}
public void Clear(bool home)
@@ -60,14 +63,22 @@ namespace Spectre.Console.Internal
}
}
- public void Write(Segment segment)
+ public void Write(IEnumerable segments)
{
- if (_lastStyle?.Equals(segment.Style) != true)
+ foreach (var segment in segments)
{
- SetStyle(segment.Style);
- }
+ if (segment.IsControlCode)
+ {
+ continue;
+ }
- System.Console.Write(segment.Text.NormalizeLineEndings(native: true));
+ if (_lastStyle?.Equals(segment.Style) != true)
+ {
+ SetStyle(segment.Style);
+ }
+
+ System.Console.Write(segment.Text.NormalizeNewLines(native: true));
+ }
}
private void SetStyle(Style style)
diff --git a/src/Spectre.Console/Internal/HtmlEncoder.cs b/src/Spectre.Console/Internal/HtmlEncoder.cs
index 074a344..26b7668 100644
--- a/src/Spectre.Console/Internal/HtmlEncoder.cs
+++ b/src/Spectre.Console/Internal/HtmlEncoder.cs
@@ -15,6 +15,11 @@ namespace Spectre.Console.Internal
foreach (var (_, first, _, segment) in segments.Enumerate())
{
+ if (segment.IsControlCode)
+ {
+ continue;
+ }
+
if (segment.Text == "\n" && !first)
{
builder.Append('\n');
diff --git a/src/Spectre.Console/Internal/InteractivityDetector.cs b/src/Spectre.Console/Internal/InteractivityDetector.cs
new file mode 100644
index 0000000..c91f296
--- /dev/null
+++ b/src/Spectre.Console/Internal/InteractivityDetector.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+
+namespace Spectre.Console.Internal
+{
+ internal static class InteractivityDetector
+ {
+ private static readonly Dictionary> _environmentVariables;
+
+ static InteractivityDetector()
+ {
+ _environmentVariables = new Dictionary>
+ {
+ { "APPVEYOR", v => !string.IsNullOrWhiteSpace(v) },
+ { "bamboo_buildNumber", v => !string.IsNullOrWhiteSpace(v) },
+ { "BITBUCKET_REPO_OWNER", v => !string.IsNullOrWhiteSpace(v) },
+ { "BITBUCKET_REPO_SLUG", v => !string.IsNullOrWhiteSpace(v) },
+ { "BITBUCKET_COMMIT", v => !string.IsNullOrWhiteSpace(v) },
+ { "BITRISE_BUILD_URL", v => !string.IsNullOrWhiteSpace(v) },
+ { "ContinuaCI.Version", v => !string.IsNullOrWhiteSpace(v) },
+ { "CI_SERVER", v => v.Equals("yes", StringComparison.OrdinalIgnoreCase) }, // GitLab
+ { "GITHUB_ACTIONS", v => v.Equals("true", StringComparison.OrdinalIgnoreCase) },
+ { "GO_SERVER_URL", v => !string.IsNullOrWhiteSpace(v) },
+ { "JENKINS_URL", v => !string.IsNullOrWhiteSpace(v) },
+ { "BuildRunner", v => v.Equals("MyGet", StringComparison.OrdinalIgnoreCase) },
+ { "TEAMCITY_VERSION", v => !string.IsNullOrWhiteSpace(v) },
+ { "TF_BUILD", v => !string.IsNullOrWhiteSpace(v) }, // TFS and Azure
+ { "TRAVIS", v => !string.IsNullOrWhiteSpace(v) },
+ };
+ }
+
+ public static bool IsInteractive()
+ {
+ if (!Environment.UserInteractive)
+ {
+ return false;
+ }
+
+ foreach (var variable in _environmentVariables)
+ {
+ var func = variable.Value;
+ var value = Environment.GetEnvironmentVariable(variable.Key);
+ if (!string.IsNullOrWhiteSpace(value) && variable.Value(value))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Internal/ResourceReader.cs b/src/Spectre.Console/Internal/ResourceReader.cs
index 908707b..e41efbe 100644
--- a/src/Spectre.Console/Internal/ResourceReader.cs
+++ b/src/Spectre.Console/Internal/ResourceReader.cs
@@ -24,7 +24,7 @@ namespace Spectre.Console.Internal
using (var reader = new StreamReader(stream))
{
- return reader.ReadToEnd().NormalizeLineEndings();
+ return reader.ReadToEnd().NormalizeNewLines();
}
}
}
diff --git a/src/Spectre.Console/Internal/TextEncoder.cs b/src/Spectre.Console/Internal/TextEncoder.cs
index 2bc3adf..7261c90 100644
--- a/src/Spectre.Console/Internal/TextEncoder.cs
+++ b/src/Spectre.Console/Internal/TextEncoder.cs
@@ -12,6 +12,11 @@ namespace Spectre.Console.Internal
foreach (var segment in Segment.Merge(segments))
{
+ if (segment.IsControlCode)
+ {
+ continue;
+ }
+
builder.Append(segment.Text);
}
diff --git a/src/Spectre.Console/Progress/Columns/PercentageColumn.cs b/src/Spectre.Console/Progress/Columns/PercentageColumn.cs
new file mode 100644
index 0000000..382751c
--- /dev/null
+++ b/src/Spectre.Console/Progress/Columns/PercentageColumn.cs
@@ -0,0 +1,32 @@
+using System;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A column showing task progress in percentage.
+ ///
+ public sealed class PercentageColumn : ProgressColumn
+ {
+ ///
+ protected internal override int? ColumnWidth => 4;
+
+ ///
+ /// Gets or sets the style for a non-complete task.
+ ///
+ public Style Style { get; set; } = Style.Plain;
+
+ ///
+ /// Gets or sets the style for a completed task.
+ ///
+ public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
+
+ ///
+ public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+ {
+ var percentage = (int)task.Percentage;
+ var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
+ return new Text($"{percentage}%", style).RightAligned();
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs b/src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs
new file mode 100644
index 0000000..1164325
--- /dev/null
+++ b/src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs
@@ -0,0 +1,45 @@
+using System;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A column showing task progress as a progress bar.
+ ///
+ public sealed class ProgressBarColumn : ProgressColumn
+ {
+ ///
+ /// Gets or sets the width of the column.
+ ///
+ public int? Width { get; set; } = 40;
+
+ ///
+ /// Gets or sets the style of completed portions of the progress bar.
+ ///
+ public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
+
+ ///
+ /// Gets or sets the style of a finished progress bar.
+ ///
+ public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
+
+ ///
+ /// Gets or sets the style of remaining portions of the progress bar.
+ ///
+ public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
+
+ ///
+ public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+ {
+ return new ProgressBar
+ {
+ MaxValue = task.MaxValue,
+ Value = task.Value,
+ Width = Width,
+ CompletedStyle = CompletedStyle,
+ FinishedStyle = FinishedStyle,
+ RemainingStyle = RemainingStyle,
+ };
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs b/src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs
new file mode 100644
index 0000000..7c0a970
--- /dev/null
+++ b/src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs
@@ -0,0 +1,28 @@
+using System;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A column showing the remaining time of a task.
+ ///
+ public sealed class RemainingTimeColumn : ProgressColumn
+ {
+ ///
+ /// Gets or sets the style of the remaining time text.
+ ///
+ public Style Style { get; set; } = new Style(foreground: Color.Blue);
+
+ ///
+ public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+ {
+ var remaining = task.RemainingTime;
+ if (remaining == null)
+ {
+ return new Markup("-:--:--");
+ }
+
+ return new Text($"{remaining.Value:h\\:mm\\:ss}", Style ?? Style.Plain);
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Columns/SpinnerColumn.cs b/src/Spectre.Console/Progress/Columns/SpinnerColumn.cs
new file mode 100644
index 0000000..b32d711
--- /dev/null
+++ b/src/Spectre.Console/Progress/Columns/SpinnerColumn.cs
@@ -0,0 +1,44 @@
+using System;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A column showing a spinner.
+ ///
+ public sealed class SpinnerColumn : ProgressColumn
+ {
+ private const string ACCUMULATED = "SPINNER_ACCUMULATED";
+ private const string INDEX = "SPINNER_INDEX";
+
+ private readonly string _ansiSequence = "⣷⣯⣟⡿⢿⣻⣽⣾";
+ private readonly string _asciiSequence = "-\\|/-\\|/";
+
+ ///
+ /// Gets or sets the style of the spinner.
+ ///
+ public Style Style { get; set; } = new Style(foreground: Color.Yellow);
+
+ ///
+ public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+ {
+ if (!task.IsStarted || task.IsFinished)
+ {
+ return new Markup(" ");
+ }
+
+ var accumulated = task.State.Update(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds);
+ if (accumulated >= 100)
+ {
+ task.State.Update(ACCUMULATED, _ => 0);
+ task.State.Update(INDEX, index => index + 1);
+ }
+
+ var useAscii = context.LegacyConsole || !context.Unicode;
+ var sequence = useAscii ? _asciiSequence : _ansiSequence;
+
+ var index = task.State.Get(INDEX);
+ return new Markup(sequence[index % sequence.Length].ToString(), Style ?? Style.Plain);
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Columns/TaskDescriptionColumn.cs b/src/Spectre.Console/Progress/Columns/TaskDescriptionColumn.cs
new file mode 100644
index 0000000..eac611f
--- /dev/null
+++ b/src/Spectre.Console/Progress/Columns/TaskDescriptionColumn.cs
@@ -0,0 +1,18 @@
+using System;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A column showing the task description.
+ ///
+ public sealed class TaskDescriptionColumn : ProgressColumn
+ {
+ ///
+ public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+ {
+ var text = task.Description?.RemoveNewLines()?.Trim();
+ return new Markup(text ?? string.Empty).RightAligned();
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Progress.cs b/src/Spectre.Console/Progress/Progress.cs
new file mode 100644
index 0000000..8fde020
--- /dev/null
+++ b/src/Spectre.Console/Progress/Progress.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Spectre.Console.Internal;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents a task list.
+ ///
+ public sealed class Progress
+ {
+ private readonly IAnsiConsole _console;
+
+ ///
+ /// Gets or sets a value indicating whether or not task list should auto refresh.
+ /// Defaults to true.
+ ///
+ public bool AutoRefresh { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether or not the task list should
+ /// be cleared once it completes.
+ /// Defaults to false.
+ ///
+ public bool AutoClear { get; set; }
+
+ internal List Columns { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The console to render to.
+ public Progress(IAnsiConsole console)
+ {
+ _console = console ?? throw new ArgumentNullException(nameof(console));
+
+ // Initialize with default columns
+ Columns = new List
+ {
+ new TaskDescriptionColumn(),
+ new ProgressBarColumn(),
+ new PercentageColumn(),
+ };
+ }
+
+ ///
+ /// Starts the progress task list.
+ ///
+ /// The action to execute.
+ public void Start(Action action)
+ {
+ var task = StartAsync(ctx =>
+ {
+ action(ctx);
+ return Task.CompletedTask;
+ });
+
+ task.GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Starts the progress task list.
+ ///
+ /// The action to execute.
+ /// A representing the asynchronous operation.
+ public async Task StartAsync(Func action)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ var renderer = CreateRenderer();
+ renderer.Started();
+
+ try
+ {
+ using (new RenderHookScope(_console, renderer))
+ {
+ var context = new ProgressContext(_console, renderer);
+
+ if (AutoRefresh)
+ {
+ using (var thread = new ProgressRefreshThread(context, renderer.RefreshRate))
+ {
+ await action(context).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ await action(context).ConfigureAwait(false);
+ }
+
+ context.Refresh();
+ }
+ }
+ finally
+ {
+ renderer.Completed(AutoClear);
+ }
+ }
+
+ private ProgressRenderer CreateRenderer()
+ {
+ var caps = _console.Capabilities;
+ var interactive = caps.SupportsInteraction && caps.SupportsAnsi;
+
+ if (interactive)
+ {
+ var columns = new List(Columns);
+ return new InteractiveProgressRenderer(_console, columns);
+ }
+ else
+ {
+ return new NonInteractiveProgressRenderer();
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressColumn.cs b/src/Spectre.Console/Progress/ProgressColumn.cs
new file mode 100644
index 0000000..e72585a
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressColumn.cs
@@ -0,0 +1,25 @@
+using System;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents a progress column.
+ ///
+ public abstract class ProgressColumn
+ {
+ ///
+ /// Gets the requested column width for the column.
+ ///
+ protected internal virtual int? ColumnWidth { get; }
+
+ ///
+ /// Gets a renderable representing the column.
+ ///
+ /// The render context.
+ /// The task.
+ /// The elapsed time since last call.
+ /// A renderable representing the column.
+ public abstract IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime);
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressContext.cs b/src/Spectre.Console/Progress/ProgressContext.cs
new file mode 100644
index 0000000..4b57c2c
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressContext.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Internal;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents a context that can be used to interact with a .
+ ///
+ public sealed class ProgressContext
+ {
+ private readonly List _tasks;
+ private readonly object _taskLock;
+ private readonly IAnsiConsole _console;
+ private readonly ProgressRenderer _renderer;
+ private int _taskId;
+
+ ///
+ /// Gets a value indicating whether or not all tasks have completed.
+ ///
+ public bool IsFinished => _tasks.All(task => task.IsFinished);
+
+ internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
+ {
+ _tasks = new List();
+ _taskLock = new object();
+ _console = console ?? throw new ArgumentNullException(nameof(console));
+ _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
+ }
+
+ ///
+ /// Adds a task.
+ ///
+ /// The task description.
+ /// The task settings.
+ /// The task's ID.
+ public ProgressTask AddTask(string description, ProgressTaskSettings? settings = null)
+ {
+ lock (_taskLock)
+ {
+ settings ??= new ProgressTaskSettings();
+ var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
+
+ _tasks.Add(task);
+ return task;
+ }
+ }
+
+ ///
+ /// Refreshes the current progress.
+ ///
+ public void Refresh()
+ {
+ _renderer.Update(this);
+ _console.Render(new ControlSequence(string.Empty));
+ }
+
+ internal void EnumerateTasks(Action action)
+ {
+ lock (_taskLock)
+ {
+ foreach (var task in _tasks)
+ {
+ action(task);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressRefreshThread.cs b/src/Spectre.Console/Progress/ProgressRefreshThread.cs
new file mode 100644
index 0000000..35cd09e
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressRefreshThread.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Threading;
+
+namespace Spectre.Console.Internal
+{
+ internal sealed class ProgressRefreshThread : IDisposable
+ {
+ private readonly ProgressContext _context;
+ private readonly TimeSpan _refreshRate;
+ private readonly ManualResetEvent _running;
+ private readonly ManualResetEvent _stopped;
+ private readonly Thread? _thread;
+
+ public ProgressRefreshThread(ProgressContext context, TimeSpan refreshRate)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ _refreshRate = refreshRate;
+ _running = new ManualResetEvent(false);
+ _stopped = new ManualResetEvent(false);
+
+ _thread = new Thread(Run);
+ _thread.IsBackground = true;
+ _thread.Start();
+ }
+
+ public void Dispose()
+ {
+ if (_thread == null || !_running.WaitOne(0))
+ {
+ return;
+ }
+
+ _stopped.Set();
+ _thread.Join();
+
+ _stopped.Dispose();
+ _running.Dispose();
+ }
+
+ private void Run()
+ {
+ _running.Set();
+
+ try
+ {
+ while (!_stopped.WaitOne(_refreshRate))
+ {
+ _context.Refresh();
+ }
+ }
+ finally
+ {
+ _stopped.Reset();
+ _running.Reset();
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressRenderer.cs b/src/Spectre.Console/Progress/ProgressRenderer.cs
new file mode 100644
index 0000000..14fc8d4
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressRenderer.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console.Internal
+{
+ internal abstract class ProgressRenderer : IRenderHook
+ {
+ public abstract TimeSpan RefreshRate { get; }
+
+ public virtual void Started()
+ {
+ }
+
+ public virtual void Completed(bool clear)
+ {
+ }
+
+ public abstract void Update(ProgressContext context);
+ public abstract IEnumerable Process(RenderContext context, IEnumerable renderables);
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressSample.cs b/src/Spectre.Console/Progress/ProgressSample.cs
new file mode 100644
index 0000000..b8fc57f
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressSample.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Spectre.Console.Internal
+{
+ internal readonly struct ProgressSample
+ {
+ public double Value { get; }
+ public DateTime Timestamp { get; }
+
+ public ProgressSample(DateTime timestamp, double value)
+ {
+ Timestamp = timestamp;
+ Value = value;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressTask.cs b/src/Spectre.Console/Progress/ProgressTask.cs
new file mode 100644
index 0000000..58c3c83
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressTask.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Internal;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents a progress task.
+ ///
+ public sealed class ProgressTask
+ {
+ private readonly List _samples;
+ private readonly object _lock;
+
+ private double _maxValue;
+ private string _description;
+
+ ///
+ /// Gets the task ID.
+ ///
+ public int Id { get; }
+
+ ///
+ /// Gets or sets the task description.
+ ///
+ public string Description
+ {
+ get => _description;
+ set => Update(description: value);
+ }
+
+ ///
+ /// Gets or sets the max value of the task.
+ ///
+ public double MaxValue
+ {
+ get => _maxValue;
+ set => Update(maxValue: value);
+ }
+
+ ///
+ /// Gets the value of the task.
+ ///
+ public double Value { get; private set; }
+
+ ///
+ /// Gets the start time of the task.
+ ///
+ public DateTime? StartTime { get; private set; }
+
+ ///
+ /// Gets the stop time of the task.
+ ///
+ public DateTime? StopTime { get; private set; }
+
+ ///
+ /// Gets the task state.
+ ///
+ public ProgressTaskState State { get; }
+
+ ///
+ /// Gets a value indicating whether or not the task has started.
+ ///
+ public bool IsStarted => StartTime != null;
+
+ ///
+ /// Gets a value indicating whether or not the task has finished.
+ ///
+ public bool IsFinished => Value >= MaxValue;
+
+ ///
+ /// Gets the percentage done of the task.
+ ///
+ public double Percentage => GetPercentage();
+
+ ///
+ /// Gets the speed measured in steps/second.
+ ///
+ public double? Speed => GetSpeed();
+
+ ///
+ /// Gets the elapsed time.
+ ///
+ public TimeSpan? ElapsedTime => GetElapsedTime();
+
+ ///
+ /// Gets the remaining time.
+ ///
+ public TimeSpan? RemainingTime => GetRemainingTime();
+
+ internal ProgressTask(int id, string description, double maxValue, bool autoStart)
+ {
+ _samples = new List();
+ _lock = new object();
+ _maxValue = maxValue;
+
+ _description = description?.RemoveNewLines()?.Trim() ?? throw new ArgumentNullException(nameof(description));
+ if (string.IsNullOrWhiteSpace(_description))
+ {
+ throw new ArgumentException("Task name cannot be empty", nameof(description));
+ }
+
+ Id = id;
+ State = new ProgressTaskState();
+ Value = 0;
+ StartTime = autoStart ? DateTime.Now : null;
+ }
+
+ ///
+ /// Starts the task.
+ ///
+ public void StartTask()
+ {
+ lock (_lock)
+ {
+ StartTime = DateTime.Now;
+ StopTime = null;
+ }
+ }
+
+ ///
+ /// Stops the task.
+ ///
+ public void StopTask()
+ {
+ lock (_lock)
+ {
+ var now = DateTime.Now;
+ if (StartTime == null)
+ {
+ StartTime = now;
+ }
+
+ StopTime = now;
+ }
+ }
+
+ ///
+ /// Increments the task's value.
+ ///
+ /// The value to increment with.
+ public void Increment(double value)
+ {
+ Update(increment: value);
+ }
+
+ private void Update(
+ string? description = null,
+ double? maxValue = null,
+ double? increment = null)
+ {
+ lock (_lock)
+ {
+ var startValue = Value;
+
+ if (description != null)
+ {
+ description = description?.RemoveNewLines()?.Trim();
+ if (string.IsNullOrWhiteSpace(description))
+ {
+ throw new InvalidOperationException("Task name cannot be empty.");
+ }
+
+ _description = description;
+ }
+
+ if (maxValue != null)
+ {
+ _maxValue += maxValue.Value;
+ }
+
+ if (increment != null)
+ {
+ Value += increment.Value;
+ }
+
+ var timestamp = DateTime.Now;
+ var threshold = timestamp - TimeSpan.FromSeconds(30);
+
+ // Remove samples that's too old
+ while (_samples.Count > 0 && _samples[0].Timestamp < threshold)
+ {
+ _samples.RemoveAt(0);
+ }
+
+ // Keep maximum of 1000 samples
+ while (_samples.Count > 1000)
+ {
+ _samples.RemoveAt(0);
+ }
+
+ _samples.Add(new ProgressSample(timestamp, Value - startValue));
+ }
+ }
+
+ private double GetPercentage()
+ {
+ var percentage = (Value / MaxValue) * 100;
+ percentage = Math.Min(100, Math.Max(0, percentage));
+ return percentage;
+ }
+
+ private double? GetSpeed()
+ {
+ lock (_lock)
+ {
+ if (StartTime == null)
+ {
+ return null;
+ }
+
+ if (_samples.Count == 0)
+ {
+ return null;
+ }
+
+ var totalTime = _samples.Last().Timestamp - _samples[0].Timestamp;
+ if (totalTime == TimeSpan.Zero)
+ {
+ return null;
+ }
+
+ var totalCompleted = _samples.Sum(x => x.Value);
+ return totalCompleted / totalTime.TotalSeconds;
+ }
+ }
+
+ private TimeSpan? GetElapsedTime()
+ {
+ lock (_lock)
+ {
+ if (StartTime == null)
+ {
+ return null;
+ }
+
+ if (StopTime != null)
+ {
+ return StopTime - StartTime;
+ }
+
+ return DateTime.Now - StartTime;
+ }
+ }
+
+ private TimeSpan? GetRemainingTime()
+ {
+ lock (_lock)
+ {
+ if (IsFinished)
+ {
+ return TimeSpan.Zero;
+ }
+
+ var speed = GetSpeed();
+ if (speed == null)
+ {
+ return null;
+ }
+
+ var estimate = (MaxValue - Value) / speed.Value;
+ return TimeSpan.FromSeconds(estimate);
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressTaskSettings.cs b/src/Spectre.Console/Progress/ProgressTaskSettings.cs
new file mode 100644
index 0000000..3ceb0fb
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressTaskSettings.cs
@@ -0,0 +1,20 @@
+namespace Spectre.Console
+{
+ ///
+ /// Represents settings for a progress task.
+ ///
+ public sealed class ProgressTaskSettings
+ {
+ ///
+ /// Gets or sets the task's max value.
+ /// Defaults to 100.
+ ///
+ public double MaxValue { get; set; } = 100;
+
+ ///
+ /// Gets or sets a value indicating whether or not the task
+ /// will be auto started. Defaults to true.
+ ///
+ public bool AutoStart { get; set; } = true;
+ }
+}
diff --git a/src/Spectre.Console/Progress/ProgressTaskState.cs b/src/Spectre.Console/Progress/ProgressTaskState.cs
new file mode 100644
index 0000000..9e8a2dc
--- /dev/null
+++ b/src/Spectre.Console/Progress/ProgressTaskState.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents progress task state.
+ ///
+ public sealed class ProgressTaskState
+ {
+ private readonly Dictionary _state;
+ private readonly object _lock;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ProgressTaskState()
+ {
+ _state = new Dictionary();
+ _lock = new object();
+ }
+
+ ///
+ /// Gets the state value for the specified key.
+ ///
+ /// The state value type.
+ /// The state key.
+ /// The value for the specified key.
+ public T Get(string key)
+ where T : struct
+ {
+ lock (_lock)
+ {
+ if (!_state.TryGetValue(key, out var value))
+ {
+ return default;
+ }
+
+ if (!(value is T))
+ {
+ throw new InvalidOperationException("State value is of the wrong type.");
+ }
+
+ return (T)value;
+ }
+ }
+
+ ///
+ /// Updates a task state value.
+ ///
+ /// The state value type.
+ /// The key.
+ /// The transformation function.
+ /// The updated value.
+ public T Update(string key, Func func)
+ where T : struct
+ {
+ lock (_lock)
+ {
+ if (func is null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ var old = default(T);
+ if (_state.TryGetValue(key, out var value))
+ {
+ if (!(value is T))
+ {
+ throw new InvalidOperationException("State value is of the wrong type.");
+ }
+
+ old = (T)value;
+ }
+
+ _state[key] = func(old);
+ return (T)_state[key];
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Renderers/InteractiveProgressRenderer.cs b/src/Spectre.Console/Progress/Renderers/InteractiveProgressRenderer.cs
new file mode 100644
index 0000000..7860239
--- /dev/null
+++ b/src/Spectre.Console/Progress/Renderers/InteractiveProgressRenderer.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console.Internal
+{
+ internal sealed class InteractiveProgressRenderer : ProgressRenderer
+ {
+ private readonly IAnsiConsole _console;
+ private readonly List _columns;
+ private readonly LiveRenderable _live;
+ private readonly object _lock;
+ private readonly Stopwatch _stopwatch;
+ private TimeSpan _lastUpdate;
+
+ public override TimeSpan RefreshRate => TimeSpan.FromMilliseconds(100);
+
+ public InteractiveProgressRenderer(IAnsiConsole console, List columns)
+ {
+ _console = console ?? throw new ArgumentNullException(nameof(console));
+ _columns = columns ?? throw new ArgumentNullException(nameof(columns));
+ _live = new LiveRenderable();
+ _lock = new object();
+ _stopwatch = new Stopwatch();
+ _lastUpdate = TimeSpan.Zero;
+ }
+
+ public override void Started()
+ {
+ _console.Cursor.Hide();
+ }
+
+ public override void Completed(bool clear)
+ {
+ lock (_lock)
+ {
+ if (clear)
+ {
+ _console.Render(_live.RestoreCursor());
+ }
+ else
+ {
+ _console.WriteLine();
+ }
+
+ _console.Cursor.Show();
+ }
+ }
+
+ public override void Update(ProgressContext context)
+ {
+ lock (_lock)
+ {
+ if (!_stopwatch.IsRunning)
+ {
+ _stopwatch.Start();
+ }
+
+ var delta = _stopwatch.Elapsed - _lastUpdate;
+ _lastUpdate = _stopwatch.Elapsed;
+
+ var grid = new Grid();
+ for (var columnIndex = 0; columnIndex < _columns.Count; columnIndex++)
+ {
+ var column = new GridColumn().PadRight(1);
+ if (_columns[columnIndex].ColumnWidth != null)
+ {
+ column.Width = _columns[columnIndex].ColumnWidth;
+ }
+
+ // Last column?
+ if (columnIndex == _columns.Count - 1)
+ {
+ column.PadRight(0);
+ }
+
+ grid.AddColumn(column);
+ }
+
+ // Add rows
+ var renderContext = new RenderContext(_console.Encoding, _console.Capabilities.LegacyConsole);
+ context.EnumerateTasks(task =>
+ {
+ var columns = _columns.Select(column => column.Render(renderContext, task, delta));
+ grid.AddRow(columns.ToArray());
+ });
+
+ _live.SetRenderable(new Padder(grid, new Padding(0, 1)));
+ }
+ }
+
+ public override IEnumerable Process(RenderContext context, IEnumerable renderables)
+ {
+ lock (_lock)
+ {
+ yield return _live.PositionCursor();
+
+ foreach (var renderable in renderables)
+ {
+ yield return renderable;
+ }
+
+ yield return _live;
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Progress/Renderers/NonInteractiveProgressRenderer.cs b/src/Spectre.Console/Progress/Renderers/NonInteractiveProgressRenderer.cs
new file mode 100644
index 0000000..a15f09c
--- /dev/null
+++ b/src/Spectre.Console/Progress/Renderers/NonInteractiveProgressRenderer.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console.Internal
+{
+ internal sealed class NonInteractiveProgressRenderer : ProgressRenderer
+ {
+ private const double FirstMilestone = 25;
+ private static readonly double?[] _milestones = new double?[] { FirstMilestone, 50, 75, 95, 96, 97, 98, 99, 100 };
+
+ private readonly Dictionary _taskMilestones;
+ private readonly object _lock;
+ private IRenderable? _renderable;
+ private DateTime _lastUpdate;
+
+ public override TimeSpan RefreshRate => TimeSpan.FromSeconds(1);
+
+ public NonInteractiveProgressRenderer()
+ {
+ _taskMilestones = new Dictionary();
+ _lock = new object();
+ }
+
+ public override void Update(ProgressContext context)
+ {
+ lock (_lock)
+ {
+ var hasStartedTasks = false;
+ var updates = new List<(string, double)>();
+
+ context.EnumerateTasks(task =>
+ {
+ if (!task.IsStarted || task.IsFinished)
+ {
+ return;
+ }
+
+ hasStartedTasks = true;
+
+ if (TryAdvance(task.Id, task.Percentage))
+ {
+ updates.Add((task.Description, task.Percentage));
+ }
+ });
+
+ // Got started tasks but no updates for 30 seconds?
+ if (hasStartedTasks && updates.Count == 0 && (DateTime.Now - _lastUpdate) > TimeSpan.FromSeconds(30))
+ {
+ context.EnumerateTasks(task => updates.Add((task.Description, task.Percentage)));
+ }
+
+ if (updates.Count > 0)
+ {
+ _lastUpdate = DateTime.Now;
+ }
+
+ _renderable = BuildTaskGrid(updates);
+ }
+ }
+
+ public override IEnumerable Process(RenderContext context, IEnumerable renderables)
+ {
+ lock (_lock)
+ {
+ var result = new List();
+ result.AddRange(renderables);
+
+ if (_renderable != null)
+ {
+ result.Add(_renderable);
+ }
+
+ _renderable = null;
+
+ return result;
+ }
+ }
+
+ private bool TryAdvance(int task, double percentage)
+ {
+ if (!_taskMilestones.TryGetValue(task, out var milestone))
+ {
+ _taskMilestones.Add(task, FirstMilestone);
+ return true;
+ }
+
+ if (percentage > milestone)
+ {
+ var nextMilestone = GetNextMilestone(percentage);
+ if (nextMilestone != null && _taskMilestones[task] != nextMilestone)
+ {
+ _taskMilestones[task] = nextMilestone.Value;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static double? GetNextMilestone(double percentage)
+ {
+ return Array.Find(_milestones, p => p > percentage);
+ }
+
+ private static IRenderable? BuildTaskGrid(List<(string Name, double Percentage)> updates)
+ {
+ if (updates.Count > 0)
+ {
+ var renderables = new List();
+ foreach (var (name, percentage) in updates)
+ {
+ renderables.Add(new Markup($"[blue]{name}[/]: {(int)percentage}%"));
+ }
+
+ return new Rows(renderables);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Recorder.cs b/src/Spectre.Console/Recorder.cs
index 11271a1..f1ffadb 100644
--- a/src/Spectre.Console/Recorder.cs
+++ b/src/Spectre.Console/Recorder.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
@@ -8,7 +10,8 @@ namespace Spectre.Console
///
/// A console recorder used to record output from a console.
///
- public sealed class Recorder : IAnsiConsole, IDisposable
+ [SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
+ public class Recorder : IAnsiConsole, IDisposable
{
private readonly IAnsiConsole _console;
private readonly List _recorded;
@@ -31,6 +34,14 @@ namespace Spectre.Console
///
public int Height => _console.Height;
+ ///
+ public RenderPipeline Pipeline => _console.Pipeline;
+
+ ///
+ /// Gets a list containing all recorded segments.
+ ///
+ protected List Recorded => _recorded;
+
///
/// Initializes a new instance of the class.
///
@@ -42,6 +53,7 @@ namespace Spectre.Console
}
///
+ [SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize")]
public void Dispose()
{
// Only used for scoping.
@@ -54,20 +66,25 @@ namespace Spectre.Console
}
///
- public void Write(Segment segment)
+ public void Write(IEnumerable segments)
{
- if (segment is null)
+ if (segments is null)
{
- throw new ArgumentNullException(nameof(segment));
+ throw new ArgumentNullException(nameof(segments));
}
- // Don't record control codes.
- if (!segment.IsControlCode)
- {
- _recorded.Add(segment);
- }
+ Record(segments);
- _console.Write(segment);
+ _console.Write(segments);
+ }
+
+ ///
+ /// Records the specified segments.
+ ///
+ /// The segments to be recorded.
+ protected virtual void Record(IEnumerable segments)
+ {
+ Recorded.AddRange(segments.Where(s => !s.IsControlCode));
}
///
diff --git a/src/Spectre.Console/Rendering/IRenderHook.cs b/src/Spectre.Console/Rendering/IRenderHook.cs
new file mode 100644
index 0000000..64ed57a
--- /dev/null
+++ b/src/Spectre.Console/Rendering/IRenderHook.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Spectre.Console.Rendering
+{
+ ///
+ /// Represents a render hook.
+ ///
+ public interface IRenderHook
+ {
+ ///
+ /// Processes the specified renderables.
+ ///
+ /// The render context.
+ /// The renderables to process.
+ /// The processed renderables.
+ IEnumerable Process(RenderContext context, IEnumerable renderables);
+ }
+}
diff --git a/src/Spectre.Console/Rendering/LiveRenderable.cs b/src/Spectre.Console/Rendering/LiveRenderable.cs
new file mode 100644
index 0000000..fad49f8
--- /dev/null
+++ b/src/Spectre.Console/Rendering/LiveRenderable.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Internal;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console.Rendering
+{
+ internal sealed class LiveRenderable : Renderable
+ {
+ private readonly object _lock = new object();
+ private IRenderable? _renderable;
+ private int? _height;
+
+ public void SetRenderable(IRenderable renderable)
+ {
+ lock (_lock)
+ {
+ _renderable = renderable;
+ }
+ }
+
+ public IRenderable PositionCursor()
+ {
+ lock (_lock)
+ {
+ if (_height == null)
+ {
+ return new ControlSequence(string.Empty);
+ }
+
+ return new ControlSequence("\r" + "\u001b[1A".Repeat(_height.Value - 1));
+ }
+ }
+
+ public IRenderable RestoreCursor()
+ {
+ lock (_lock)
+ {
+ if (_height == null)
+ {
+ return new ControlSequence(string.Empty);
+ }
+
+ return new ControlSequence("\r\u001b[2K" + "\u001b[1A\u001b[2K".Repeat(_height.Value - 1));
+ }
+ }
+
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ lock (_lock)
+ {
+ if (_renderable != null)
+ {
+ var segments = _renderable.Render(context, maxWidth);
+ var lines = Segment.SplitLines(context, segments);
+
+ _height = lines.Count;
+
+ var result = new List();
+ foreach (var (_, _, last, line) in lines.Enumerate())
+ {
+ foreach (var item in line)
+ {
+ result.Add(item);
+ }
+
+ if (!last)
+ {
+ result.Add(Segment.LineBreak);
+ }
+ }
+
+ return result;
+ }
+
+ _height = 0;
+ return Enumerable.Empty();
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Rendering/RenderContext.cs b/src/Spectre.Console/Rendering/RenderContext.cs
index 644b443..c50ca6c 100644
--- a/src/Spectre.Console/Rendering/RenderContext.cs
+++ b/src/Spectre.Console/Rendering/RenderContext.cs
@@ -49,7 +49,7 @@ namespace Spectre.Console.Rendering
Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding));
LegacyConsole = legacyConsole;
Justification = justification;
- Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode;
+ Unicode = Encoding.EncodingName.ContainsExact("Unicode");
SingleLine = singleLine;
}
diff --git a/src/Spectre.Console/Rendering/RenderHookScope.cs b/src/Spectre.Console/Rendering/RenderHookScope.cs
new file mode 100644
index 0000000..c646bfb
--- /dev/null
+++ b/src/Spectre.Console/Rendering/RenderHookScope.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Spectre.Console.Rendering
+{
+ ///
+ /// Represents a render hook scope.
+ ///
+ public sealed class RenderHookScope : IDisposable
+ {
+ private readonly IAnsiConsole _console;
+ private readonly IRenderHook _hook;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The console to attach the render hook to.
+ /// The render hook.
+ public RenderHookScope(IAnsiConsole console, IRenderHook hook)
+ {
+ _console = console ?? throw new ArgumentNullException(nameof(console));
+ _hook = hook ?? throw new ArgumentNullException(nameof(hook));
+ _console.Pipeline.Attach(_hook);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _console.Pipeline.Detach(_hook);
+ }
+ }
+}
diff --git a/src/Spectre.Console/Rendering/RenderPipeline.cs b/src/Spectre.Console/Rendering/RenderPipeline.cs
new file mode 100644
index 0000000..c20ce1a
--- /dev/null
+++ b/src/Spectre.Console/Rendering/RenderPipeline.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+
+namespace Spectre.Console.Rendering
+{
+ ///
+ /// Represents the render pipeline.
+ ///
+ public sealed class RenderPipeline
+ {
+ private readonly List _hooks;
+ private readonly object _lock;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RenderPipeline()
+ {
+ _hooks = new List();
+ _lock = new object();
+ }
+
+ ///
+ /// Attaches a new render hook onto the pipeline.
+ ///
+ /// The render hook to attach.
+ public void Attach(IRenderHook hook)
+ {
+ lock (_lock)
+ {
+ _hooks.Add(hook);
+ }
+ }
+
+ ///
+ /// Detaches a render hook from the pipeline.
+ ///
+ /// The render hook to detach.
+ public void Detach(IRenderHook hook)
+ {
+ lock (_lock)
+ {
+ _hooks.Remove(hook);
+ }
+ }
+
+ ///
+ /// Processes the specified renderables.
+ ///
+ /// The render context.
+ /// The renderables to process.
+ /// The processed renderables.
+ public IEnumerable Process(RenderContext context, IEnumerable renderables)
+ {
+ lock (_lock)
+ {
+ var current = renderables;
+ for (var index = _hooks.Count - 1; index >= 0; index--)
+ {
+ current = _hooks[index].Process(context, current);
+ }
+
+ return current;
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs
index 0c3073e..46fcacd 100644
--- a/src/Spectre.Console/Rendering/Segment.cs
+++ b/src/Spectre.Console/Rendering/Segment.cs
@@ -72,7 +72,7 @@ namespace Spectre.Console.Rendering
private Segment(string text, Style style, bool lineBreak, bool control)
{
- Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
+ Text = text?.NormalizeNewLines() ?? throw new ArgumentNullException(nameof(text));
Style = style ?? throw new ArgumentNullException(nameof(style));
IsLineBreak = lineBreak;
IsWhiteSpace = string.IsNullOrWhiteSpace(text);
@@ -102,6 +102,11 @@ namespace Spectre.Console.Rendering
throw new ArgumentNullException(nameof(context));
}
+ if (IsControlCode)
+ {
+ return 0;
+ }
+
return Text.CellLength(context);
}
@@ -477,16 +482,22 @@ namespace Spectre.Console.Rendering
continue;
}
+ // Both control codes?
+ if (segment.IsControlCode && previous.IsControlCode)
+ {
+ previous = Control(previous.Text + segment.Text);
+ continue;
+ }
+
// Same style?
- if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak)
+ if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak && !previous.IsControlCode)
{
previous = new Segment(previous.Text + segment.Text, previous.Style);
+ continue;
}
- else
- {
- result.Add(previous);
- previous = segment;
- }
+
+ result.Add(previous);
+ previous = segment;
}
if (previous != null)
diff --git a/src/Spectre.Console/Widgets/ControlSequence.cs b/src/Spectre.Console/Widgets/ControlSequence.cs
new file mode 100644
index 0000000..a577f15
--- /dev/null
+++ b/src/Spectre.Console/Widgets/ControlSequence.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ internal sealed class ControlSequence : Renderable
+ {
+ private readonly Segment _segment;
+
+ public ControlSequence(string control)
+ {
+ _segment = Segment.Control(control);
+ }
+
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ return new Measurement(0, 0);
+ }
+
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ yield return _segment;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/ProgressBar.cs b/src/Spectre.Console/Widgets/ProgressBar.cs
new file mode 100644
index 0000000..3c4e742
--- /dev/null
+++ b/src/Spectre.Console/Widgets/ProgressBar.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ internal sealed class ProgressBar : Renderable
+ {
+ public double Value { get; set; }
+ public double MaxValue { get; set; } = 100;
+
+ public int? Width { get; set; }
+
+ public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
+ public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
+ public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
+
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ var width = Math.Min(Width ?? maxWidth, maxWidth);
+ return new Measurement(4, width);
+ }
+
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ var width = Math.Min(Width ?? maxWidth, maxWidth);
+ var completed = Math.Min(MaxValue, Math.Max(0, Value));
+
+ var token = !context.Unicode || context.LegacyConsole ? '-' : '━';
+ var style = completed >= MaxValue ? FinishedStyle : CompletedStyle;
+
+ var bars = Math.Max(0, (int)(width * (completed / MaxValue)));
+ yield return new Segment(new string(token, bars), style);
+
+ if (bars < width)
+ {
+ yield return new Segment(new string(token, width - bars), RemainingStyle);
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/Rule.cs b/src/Spectre.Console/Widgets/Rule.cs
index c3b9c2f..1078e2e 100644
--- a/src/Spectre.Console/Widgets/Rule.cs
+++ b/src/Spectre.Console/Widgets/Rule.cs
@@ -95,7 +95,7 @@ namespace Spectre.Console
private IEnumerable GetTitleSegments(RenderContext context, string title, int width)
{
- title = title.NormalizeLineEndings().ReplaceExact("\n", " ").Trim();
+ title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim();
var markup = new Markup(title, Style);
return ((IRenderable)markup).Render(context.WithSingleLine(), width);
}
diff --git a/src/Spectre.Console/Widgets/Table.cs b/src/Spectre.Console/Widgets/Table.cs
index f9c297b..e98f61e 100644
--- a/src/Spectre.Console/Widgets/Table.cs
+++ b/src/Spectre.Console/Widgets/Table.cs
@@ -16,8 +16,8 @@ namespace Spectre.Console
private readonly List _columns;
private readonly List _rows;
- private static Style _defaultHeadingStyle = new Style(Color.Silver);
- private static Style _defaultCaptionStyle = new Style(Color.Grey);
+ private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
+ private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
///
/// Gets the table columns.
@@ -447,12 +447,10 @@ namespace Spectre.Console
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
{
- var padding = column.Padding?.GetWidth() ?? 0;
-
// Predetermined width?
if (column.Width != null)
{
- return (column.Width.Value + padding, column.Width.Value + padding);
+ return (column.Width.Value, column.Width.Value);
}
var columnIndex = _columns.IndexOf(column);
@@ -474,6 +472,8 @@ namespace Spectre.Console
maxWidths.Add(rowMeasure.Max);
}
+ var padding = column.Padding?.GetWidth() ?? 0;
+
return (minWidths.Count > 0 ? minWidths.Max() : padding,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
}