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 +* foo + + +- bar + + +* baz + +[?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, "HelloWorld")] + [InlineData(CursorDirection.Down, "HelloWorld")] + [InlineData(CursorDirection.Right, "HelloWorld")] + [InlineData(CursorDirection.Left, "HelloWorld")] + 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("HelloWorld"); + } + } + } + } +} 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, "HelloWorld")] + [InlineData(true, "HelloWorld")] + 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" + - "* foo\n" + - "  \n" + - "- bar\n" + - "  \n" + - "* baz\n" + - " [?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)); } }