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
{
[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()
{

View File

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

View File

@ -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;

View File

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

View File

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

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