Make VT-100 sequences easier to understand

This commit is contained in:
Patrik Svensson 2021-03-28 17:04:03 +02:00 committed by Phil Scott
parent 20650f1e7e
commit 1ed7e65fcb
9 changed files with 275 additions and 26 deletions

View File

@ -0,0 +1,10 @@
[?25l
* foo

- bar

* baz
[?25h

View File

@ -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");
}
}
}
}
}

View File

@ -7,6 +7,23 @@ namespace Spectre.Console.Tests.Unit
{ {
public partial class AnsiConsoleTests 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] [Fact]
public void Should_Combine_Decoration_And_Colors() public void Should_Combine_Decoration_And_Colors()
{ {

View File

@ -1,13 +1,18 @@
using Shouldly; using System.Threading.Tasks;
using Spectre.Console.Testing; using Spectre.Console.Testing;
using Spectre.Verify.Extensions;
using VerifyXunit;
using Xunit; using Xunit;
namespace Spectre.Console.Tests.Unit namespace Spectre.Console.Tests.Unit
{ {
[UsesVerify]
[ExpectationPath("Widgets/Status")]
public sealed class StatusTests public sealed class StatusTests
{ {
[Fact] [Fact]
public void Should_Render_Status_Correctly() [Expectation("Render")]
public Task Should_Render_Status_Correctly()
{ {
// Given // Given
var console = new FakeAnsiConsole(ColorSystem.TrueColor, width: 10); var console = new FakeAnsiConsole(ColorSystem.TrueColor, width: 10);
@ -28,16 +33,7 @@ namespace Spectre.Console.Tests.Unit
}); });
// Then // Then
console.Output return Verifier.Verify(console.Output);
.NormalizeLineEndings()
.ShouldBe(
"[?25l \n" +
"* foo\n" +
"  \n" +
"- bar\n" +
"  \n" +
"* baz\n" +
" [?25h");
} }
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console namespace Spectre.Console
{ {
@ -49,9 +50,8 @@ namespace Spectre.Console
return text; return text;
} }
var ansiCodes = string.Join(";", result);
var ansi = result.Length > 0 var ansi = result.Length > 0
? $"\u001b[{ansiCodes}m{text}\u001b[0m" ? $"{SGR(result)}{text}{SGR(0)}"
: text; : text;
if (style.Link != null && !_profile.Capabilities.Legacy) if (style.Link != null && !_profile.Capabilities.Legacy)
@ -65,7 +65,7 @@ namespace Spectre.Console
} }
var linkId = _linkHasher.GenerateId(link, text); 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; return ansi;

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Text; using System.Text;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console namespace Spectre.Console
{ {
@ -21,11 +22,11 @@ namespace Spectre.Console
public void Clear(bool home) public void Clear(bool home)
{ {
Write(new ControlSequence("\u001b[2J")); Write(new ControlSequence(ED(2)));
if (home) if (home)
{ {
Cursor.SetPosition(0, 0); Write(new ControlSequence(CUP(0, 0)));
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console namespace Spectre.Console
{ {
@ -15,11 +16,11 @@ namespace Spectre.Console
{ {
if (show) if (show)
{ {
_backend.Write(new ControlSequence("\u001b[?25h")); _backend.Write(new ControlSequence(SM(DECTCEM)));
} }
else else
{ {
_backend.Write(new ControlSequence("\u001b[?25l")); _backend.Write(new ControlSequence(RM(DECTCEM)));
} }
} }
@ -33,23 +34,23 @@ namespace Spectre.Console
switch (direction) switch (direction)
{ {
case CursorDirection.Up: case CursorDirection.Up:
_backend.Write(new ControlSequence($"\u001b[{steps}A")); _backend.Write(new ControlSequence(CUU(steps)));
break; break;
case CursorDirection.Down: case CursorDirection.Down:
_backend.Write(new ControlSequence($"\u001b[{steps}B")); _backend.Write(new ControlSequence(CUD(steps)));
break; break;
case CursorDirection.Right: case CursorDirection.Right:
_backend.Write(new ControlSequence($"\u001b[{steps}C")); _backend.Write(new ControlSequence(CUF(steps)));
break; break;
case CursorDirection.Left: case CursorDirection.Left:
_backend.Write(new ControlSequence($"\u001b[{steps}D")); _backend.Write(new ControlSequence(CUB(steps)));
break; break;
} }
} }
public void SetPosition(int column, int line) public void SetPosition(int column, int line)
{ {
_backend.Write(new ControlSequence($"\u001b[{line};{column}H")); _backend.Write(new ControlSequence(CUP(line, column)));
} }
} }
} }

View File

@ -0,0 +1,169 @@
using System.Linq;
namespace Spectre.Console
{
internal static class AnsiSequences
{
/// <summary>
/// Introduces a control sequence that uses 8-bit characters.
/// </summary>
public const string CSI = "\u001b";
/// <summary>
/// Text cursor enable.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/DECRQM.html#T5-8"/>.
/// </remarks>
public const int DECTCEM = 25;
/// <summary>
/// This control function selects one or more character attributes at the same time.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SGR.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SGR(params int[] codes)
{
return CSI + "[" + string.Join(";", codes.Select(c => c.ToString())) + "m";
}
/// <summary>
/// This control function selects one or more character attributes at the same time.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SGR.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SGR(params byte[] codes)
{
return CSI + "[" + string.Join(";", codes.Select(c => c.ToString())) + "m";
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/ED.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string ED(int code)
{
return CSI + $"[{code}J";
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUU.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move up.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUU(int steps)
{
return CSI + $"[{steps}A";
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUD.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move down.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUD(int steps)
{
return CSI + $"[{steps}B";
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUF.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move forward.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUF(int steps)
{
return CSI + $"[{steps}C";
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUB.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move backward.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUB(int steps)
{
return CSI + $"[{steps}D";
}
/// <summary>
/// Moves the cursor to the specified position.
/// </summary>
/// <param name="line">The line to move to.</param>
/// <param name="column">The column to move to.</param>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUP.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string CUP(int line, int column)
{
return CSI + $"[{line};{column}H";
}
/// <summary>
/// Hides the cursor.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/RM.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string RM(int code)
{
return CSI + $"[?{code}l";
}
/// <summary>
/// Shows the cursor.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SM.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SM(int code)
{
return CSI + $"[?{code}h";
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/EL.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string EL(int code)
{
return CSI + $"[{code}K";
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console.Rendering namespace Spectre.Console.Rendering
{ {
@ -27,7 +28,8 @@ namespace Spectre.Console.Rendering
return new ControlSequence(string.Empty); 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(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));
} }
} }