diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Status/Render.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Status/Render.Output.verified.txt
new file mode 100644
index 0000000..debe239
--- /dev/null
+++ b/src/Spectre.Console.Tests/Expectations/Widgets/Status/Render.Output.verified.txt
@@ -0,0 +1,10 @@
+[?25l
+[38;5;11m*[0m foo
+
+[2A
+[38;5;11m-[0m bar
+
+[2A
+[38;5;11m*[0m baz
+
+[2K[1A[2K[1A[2K[?25h
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs
new file mode 100644
index 0000000..fbbc4b7
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs
@@ -0,0 +1,52 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit
+{
+ public partial class AnsiConsoleTests
+ {
+ public sealed class Cursor
+ {
+ public sealed class TheMoveMethod
+ {
+ [Theory]
+ [InlineData(CursorDirection.Up, "Hello[2AWorld")]
+ [InlineData(CursorDirection.Down, "Hello[2BWorld")]
+ [InlineData(CursorDirection.Right, "Hello[2CWorld")]
+ [InlineData(CursorDirection.Left, "Hello[2DWorld")]
+ public void Should_Return_Correct_Ansi_Code(CursorDirection direction, string expected)
+ {
+ // Given
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+
+ // When
+ console.Write("Hello");
+ console.Cursor.Move(direction, 2);
+ console.Write("World");
+
+ // Then
+ console.Output.ShouldBe(expected);
+ }
+ }
+
+ public sealed class TheSetPositionMethod
+ {
+ [Fact]
+ public void Should_Return_Correct_Ansi_Code()
+ {
+ // Given
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+
+ // When
+ console.Write("Hello");
+ console.Cursor.SetPosition(5, 3);
+ console.Write("World");
+
+ // Then
+ console.Output.ShouldBe("Hello[3;5HWorld");
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
index 9d2ab16..2b9ed39 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
@@ -7,6 +7,23 @@ namespace Spectre.Console.Tests.Unit
{
public partial class AnsiConsoleTests
{
+ [Theory]
+ [InlineData(false, "Hello[2JWorld")]
+ [InlineData(true, "Hello[2J[0;0HWorld")]
+ public void Should_Clear_Screen(bool home, string expected)
+ {
+ // Given
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
+
+ // When
+ console.Write("Hello");
+ console.Clear(home);
+ console.Write("World");
+
+ // Then
+ console.Output.ShouldBe(expected);
+ }
+
[Fact]
public void Should_Combine_Decoration_And_Colors()
{
diff --git a/src/Spectre.Console.Tests/Unit/StatusTests.cs b/src/Spectre.Console.Tests/Unit/StatusTests.cs
index 6ec3bd1..e223813 100644
--- a/src/Spectre.Console.Tests/Unit/StatusTests.cs
+++ b/src/Spectre.Console.Tests/Unit/StatusTests.cs
@@ -1,13 +1,18 @@
-using Shouldly;
+using System.Threading.Tasks;
using Spectre.Console.Testing;
+using Spectre.Verify.Extensions;
+using VerifyXunit;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
+ [UsesVerify]
+ [ExpectationPath("Widgets/Status")]
public sealed class StatusTests
{
[Fact]
- public void Should_Render_Status_Correctly()
+ [Expectation("Render")]
+ public Task Should_Render_Status_Correctly()
{
// Given
var console = new FakeAnsiConsole(ColorSystem.TrueColor, width: 10);
@@ -28,16 +33,7 @@ namespace Spectre.Console.Tests.Unit
});
// Then
- console.Output
- .NormalizeLineEndings()
- .ShouldBe(
- "[?25l \n" +
- "[38;5;11m*[0m foo\n" +
- " [1A[1A \n" +
- "[38;5;11m-[0m bar\n" +
- " [1A[1A \n" +
- "[38;5;11m*[0m baz\n" +
- " [2K[1A[2K[1A[2K[?25h");
+ return Verifier.Verify(console.Output);
}
}
}
diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs
index 85c9ae8..bf5b0cf 100644
--- a/src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs
+++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using static Spectre.Console.AnsiSequences;
namespace Spectre.Console
{
@@ -49,9 +50,8 @@ namespace Spectre.Console
return text;
}
- var ansiCodes = string.Join(";", result);
var ansi = result.Length > 0
- ? $"\u001b[{ansiCodes}m{text}\u001b[0m"
+ ? $"{SGR(result)}{text}{SGR(0)}"
: text;
if (style.Link != null && !_profile.Capabilities.Legacy)
@@ -65,7 +65,7 @@ namespace Spectre.Console
}
var linkId = _linkHasher.GenerateId(link, text);
- ansi = $"\u001b]8;id={linkId};{link}\u001b\\{ansi}\u001b]8;;\u001b\\";
+ ansi = $"{CSI}]8;id={linkId};{link}{CSI}\\{ansi}{CSI}]8;;{CSI}\\";
}
return ansi;
diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs
index b077bb2..f4698ba 100644
--- a/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs
+++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs
@@ -1,6 +1,7 @@
using System;
using System.Text;
using Spectre.Console.Rendering;
+using static Spectre.Console.AnsiSequences;
namespace Spectre.Console
{
@@ -21,11 +22,11 @@ namespace Spectre.Console
public void Clear(bool home)
{
- Write(new ControlSequence("\u001b[2J"));
+ Write(new ControlSequence(ED(2)));
if (home)
{
- Cursor.SetPosition(0, 0);
+ Write(new ControlSequence(CUP(0, 0)));
}
}
diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleCursor.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleCursor.cs
index d0f110f..9c6aa8c 100644
--- a/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleCursor.cs
+++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleCursor.cs
@@ -1,4 +1,5 @@
using System;
+using static Spectre.Console.AnsiSequences;
namespace Spectre.Console
{
@@ -15,11 +16,11 @@ namespace Spectre.Console
{
if (show)
{
- _backend.Write(new ControlSequence("\u001b[?25h"));
+ _backend.Write(new ControlSequence(SM(DECTCEM)));
}
else
{
- _backend.Write(new ControlSequence("\u001b[?25l"));
+ _backend.Write(new ControlSequence(RM(DECTCEM)));
}
}
@@ -33,23 +34,23 @@ namespace Spectre.Console
switch (direction)
{
case CursorDirection.Up:
- _backend.Write(new ControlSequence($"\u001b[{steps}A"));
+ _backend.Write(new ControlSequence(CUU(steps)));
break;
case CursorDirection.Down:
- _backend.Write(new ControlSequence($"\u001b[{steps}B"));
+ _backend.Write(new ControlSequence(CUD(steps)));
break;
case CursorDirection.Right:
- _backend.Write(new ControlSequence($"\u001b[{steps}C"));
+ _backend.Write(new ControlSequence(CUF(steps)));
break;
case CursorDirection.Left:
- _backend.Write(new ControlSequence($"\u001b[{steps}D"));
+ _backend.Write(new ControlSequence(CUB(steps)));
break;
}
}
public void SetPosition(int column, int line)
{
- _backend.Write(new ControlSequence($"\u001b[{line};{column}H"));
+ _backend.Write(new ControlSequence(CUP(line, column)));
}
}
}
diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiSequences.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiSequences.cs
new file mode 100644
index 0000000..9e99516
--- /dev/null
+++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiSequences.cs
@@ -0,0 +1,169 @@
+using System.Linq;
+
+namespace Spectre.Console
+{
+ internal static class AnsiSequences
+ {
+ ///
+ /// Introduces a control sequence that uses 8-bit characters.
+ ///
+ public const string CSI = "\u001b";
+
+ ///
+ /// Text cursor enable.
+ ///
+ ///
+ /// See .
+ ///
+ public const int DECTCEM = 25;
+
+ ///
+ /// This control function selects one or more character attributes at the same time.
+ ///
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string SGR(params int[] codes)
+ {
+ return CSI + "[" + string.Join(";", codes.Select(c => c.ToString())) + "m";
+ }
+
+ ///
+ /// This control function selects one or more character attributes at the same time.
+ ///
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string SGR(params byte[] codes)
+ {
+ return CSI + "[" + string.Join(";", codes.Select(c => c.ToString())) + "m";
+ }
+
+ ///
+ /// This control function erases characters from part or all of the display.
+ /// When you erase complete lines, they become single-height, single-width lines,
+ /// with all visual character attributes cleared.
+ /// ED works inside or outside the scrolling margins.
+ ///
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string ED(int code)
+ {
+ return CSI + $"[{code}J";
+ }
+
+ ///
+ /// Moves the cursor up a specified number of lines in the same column.
+ /// The cursor stops at the top margin.
+ /// If the cursor is already above the top margin, then the cursor stops at the top line.
+ ///
+ ///
+ /// See .
+ ///
+ /// The number of steps to move up.
+ /// The ANSI escape code.
+ public static string CUU(int steps)
+ {
+ return CSI + $"[{steps}A";
+ }
+
+ ///
+ /// This control function moves the cursor down a specified number of lines in the same column.
+ /// The cursor stops at the bottom margin.
+ /// If the cursor is already below the bottom margin, then the cursor stops at the bottom line.
+ ///
+ ///
+ /// See .
+ ///
+ /// The number of steps to move down.
+ /// The ANSI escape code.
+ public static string CUD(int steps)
+ {
+ return CSI + $"[{steps}B";
+ }
+
+ ///
+ /// This control function moves the cursor to the right by a specified number of columns.
+ /// The cursor stops at the right border of the page.
+ ///
+ ///
+ /// See .
+ ///
+ /// The number of steps to move forward.
+ /// The ANSI escape code.
+ public static string CUF(int steps)
+ {
+ return CSI + $"[{steps}C";
+ }
+
+ ///
+ /// This control function moves the cursor to the left by a specified number of columns.
+ /// The cursor stops at the left border of the page.
+ ///
+ ///
+ /// See .
+ ///
+ /// The number of steps to move backward.
+ /// The ANSI escape code.
+ public static string CUB(int steps)
+ {
+ return CSI + $"[{steps}D";
+ }
+
+ ///
+ /// Moves the cursor to the specified position.
+ ///
+ /// The line to move to.
+ /// The column to move to.
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string CUP(int line, int column)
+ {
+ return CSI + $"[{line};{column}H";
+ }
+
+ ///
+ /// Hides the cursor.
+ ///
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string RM(int code)
+ {
+ return CSI + $"[?{code}l";
+ }
+
+ ///
+ /// Shows the cursor.
+ ///
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string SM(int code)
+ {
+ return CSI + $"[?{code}h";
+ }
+
+ ///
+ /// This control function erases characters on the line that has the cursor.
+ /// EL clears all character attributes from erased character positions.
+ /// EL works inside or outside the scrolling margins.
+ ///
+ ///
+ /// See .
+ ///
+ /// The ANSI escape code.
+ public static string EL(int code)
+ {
+ return CSI + $"[{code}K";
+ }
+ }
+}
diff --git a/src/Spectre.Console/Rendering/LiveRenderable.cs b/src/Spectre.Console/Rendering/LiveRenderable.cs
index 86a0895..582f16a 100644
--- a/src/Spectre.Console/Rendering/LiveRenderable.cs
+++ b/src/Spectre.Console/Rendering/LiveRenderable.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using static Spectre.Console.AnsiSequences;
namespace Spectre.Console.Rendering
{
@@ -27,7 +28,8 @@ namespace Spectre.Console.Rendering
return new ControlSequence(string.Empty);
}
- return new ControlSequence("\r" + "\u001b[1A".Repeat(_shape.Value.Height - 1));
+ var linesToMoveUp = _shape.Value.Height - 1;
+ return new ControlSequence("\r" + CUU(linesToMoveUp));
}
}
@@ -40,7 +42,8 @@ namespace Spectre.Console.Rendering
return new ControlSequence(string.Empty);
}
- return new ControlSequence("\r\u001b[2K" + "\u001b[1A\u001b[2K".Repeat(_shape.Value.Height - 1));
+ var linesToClear = _shape.Value.Height - 1;
+ return new ControlSequence("\r" + EL(2) + (CUU(1) + EL(2)).Repeat(linesToClear));
}
}