158a1708a6
This PR updates the rendering of the _underlined_ graphic rendition attribute, using the style specified in the active font, instead of just reusing the grid line at the bottom of the character cell. * Support for drawing the correct underline effect in the grid line renderer was added in #7107. There was already an `ExtendedAttributes` flag defined for the underlined state, but I needed to update the `SetUnderlined` and `IsUnderlined` methods in the `TextAttribute` class to use that flag now in place of the legacy `LVB_UNDERSCORE` attribute. This enables underlines set via a VT sequence to be tracked separately from `LVB_UNDERSCORE` grid lines set via the console API. I then needed to update the `Renderer::s_GetGridlines` method to activate the `GridLines::Underline` style when the `Underlined` attribute was set. The `GridLines::Bottom` style is still triggered by the `LVB_UNDERSCORE` attribute to produce the bottom grid line effect. Validation ---------- Because this is a change from the existing behaviour, certain unit tests that were expecting the `LVB_UNDERSCORE` to be toggled by `SGR 4` and `SGR 24` have now had to be updated to check the `Underlined` flag instead. There were also some UI Automation tests that were checking for `SGR 4` mapping to `LVB_UNDERSCORE` attribute, which I've now substituted with a test of the `SGR 53` overline attribute mapping to `LVB_GRID_HORIZONTAL`. These tests only work with legacy attributes, so they can't access the extended underline state, and I thought a replacement test that covered similar ground would be better than dropping the tests altogether. As far as the visual rendering is concerned, I've manually confirmed that the VT underline sequences now draw the underline in the correct position and style, while grid lines output via the console API are still displayed in their original form. Closes #2915
435 lines
17 KiB
C#
435 lines
17 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace VTApp
|
|
{
|
|
class Program
|
|
{
|
|
static string CSI = ((char)0x1b) + "[";
|
|
static void Main(string[] args)
|
|
{
|
|
Console.WindowHeight = 25;
|
|
Console.BufferHeight = 9000;
|
|
Console.WindowWidth = 80;
|
|
Console.BufferWidth = 80;
|
|
|
|
Console.WriteLine("VT Tester");
|
|
|
|
while (true)
|
|
{
|
|
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
|
|
|
|
switch (keyInfo.KeyChar)
|
|
{
|
|
case '\x1b': // escape
|
|
// this case is for receiving replies from the console host
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.Append(keyInfo.KeyChar);
|
|
|
|
keyInfo = Console.ReadKey(true);
|
|
|
|
// 40-7E are the "dispatch" characters meaning the sequence is done.
|
|
// 0x5B '[' is expected after the escape. So ignore that. We don't know a of a sequence terminated with it, so it also continues the loop.
|
|
// keep collecting characters as the "reply" until then
|
|
while (keyInfo.KeyChar < 0x40 || keyInfo.KeyChar > 0x7E || keyInfo.KeyChar == '[')
|
|
{
|
|
builder.Append(keyInfo.KeyChar);
|
|
|
|
keyInfo = Console.ReadKey(true);
|
|
}
|
|
builder.Append(keyInfo.KeyChar);
|
|
|
|
Console.Title = string.Format(CultureInfo.InvariantCulture, "Response Received: {0}", builder.ToString());
|
|
break;
|
|
case '\x8': // backspace
|
|
Console.Write('\x8');
|
|
break;
|
|
case '\x9': // horizontal tab (tab key)
|
|
Console.Write('\x9');
|
|
break;
|
|
case 'A':
|
|
case 'B':
|
|
case 'C':
|
|
case 'D':
|
|
case 'E':
|
|
case 'F':
|
|
case 'P':
|
|
case 'S':
|
|
case 'T':
|
|
case 'L':
|
|
case 'M':
|
|
Console.Write(CSI);
|
|
Console.Write(keyInfo.KeyChar);
|
|
break;
|
|
case 'O':
|
|
Console.Write(CSI);
|
|
Console.Write("@");
|
|
break;
|
|
case 'G':
|
|
Console.Write(CSI);
|
|
Console.Write('1');
|
|
Console.Write('4');
|
|
Console.Write('G');
|
|
break;
|
|
case 'v':
|
|
Console.Write(CSI);
|
|
Console.Write('1');
|
|
Console.Write('4');
|
|
Console.Write('d');
|
|
break;
|
|
case 'H':
|
|
Console.Write(CSI);
|
|
Console.Write('5');
|
|
Console.Write(';');
|
|
Console.Write('1');
|
|
Console.Write('H');
|
|
break;
|
|
case 'h':
|
|
Console.Write(CSI);
|
|
Console.Write('?');
|
|
Console.Write('2');
|
|
Console.Write('5');
|
|
Console.Write('h');
|
|
break;
|
|
case 'l':
|
|
Console.Write(CSI);
|
|
Console.Write('?');
|
|
Console.Write('2');
|
|
Console.Write('5');
|
|
Console.Write('l');
|
|
break;
|
|
case '7':
|
|
Console.Write((char)0x1b);
|
|
Console.Write('7');
|
|
break;
|
|
case '8':
|
|
Console.Write((char)0x1b);
|
|
Console.Write('8');
|
|
break;
|
|
case 'y':
|
|
Console.Write(CSI);
|
|
Console.Write('s');
|
|
break;
|
|
case 'u':
|
|
Console.Write(CSI);
|
|
Console.Write('u');
|
|
break;
|
|
case '~':
|
|
// move to top left corner
|
|
Console.Write(CSI);
|
|
Console.Write('H');
|
|
|
|
// write out a ton of Zs
|
|
for (int i = 0; i < 24; i++)
|
|
{
|
|
for (int j = 0; j < 80; j++)
|
|
{
|
|
Console.Write("Z");
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < 79; j++)
|
|
{
|
|
Console.Write("Z");
|
|
}
|
|
|
|
// move to middle-ish
|
|
Console.Write(CSI);
|
|
Console.Write("15");
|
|
Console.Write(';');
|
|
Console.Write("15");
|
|
Console.Write('H');
|
|
break;
|
|
case 'J':
|
|
Console.Write(CSI);
|
|
Console.Write("J");
|
|
break;
|
|
case 'j':
|
|
Console.Write(CSI);
|
|
Console.Write("1");
|
|
Console.Write("J");
|
|
break;
|
|
case 'K':
|
|
Console.Write(CSI);
|
|
Console.Write("K");
|
|
break;
|
|
case 'k':
|
|
Console.Write(CSI);
|
|
Console.Write("1");
|
|
Console.Write("K");
|
|
break;
|
|
case '0':
|
|
Console.Write(CSI);
|
|
Console.Write("m");
|
|
break;
|
|
case '1':
|
|
Console.Write(CSI);
|
|
Console.Write("1m");
|
|
break;
|
|
case '2':
|
|
Console.Write(CSI);
|
|
Console.Write("32m");
|
|
break;
|
|
case '3':
|
|
Console.Write(CSI);
|
|
Console.Write("33m");
|
|
break;
|
|
case '4':
|
|
Console.Write(CSI);
|
|
Console.Write("34m");
|
|
break;
|
|
case '5':
|
|
Console.Write(CSI);
|
|
Console.Write("35m");
|
|
break;
|
|
case '6':
|
|
Console.Write(CSI);
|
|
Console.Write("36m");
|
|
break;
|
|
case '!':
|
|
Console.Write(CSI);
|
|
Console.Write("91m");
|
|
break;
|
|
case '@':
|
|
Console.Write(CSI);
|
|
Console.Write("94m");
|
|
break;
|
|
case '#':
|
|
Console.Write(CSI);
|
|
Console.Write("96m");
|
|
break;
|
|
case '$':
|
|
Console.Write(CSI);
|
|
Console.Write("101m");
|
|
break;
|
|
case '%':
|
|
Console.Write(CSI);
|
|
Console.Write("104m");
|
|
break;
|
|
case '^':
|
|
Console.Write(CSI);
|
|
Console.Write("106m");
|
|
break;
|
|
case 'Q':
|
|
Console.Write(CSI);
|
|
Console.Write("40m");
|
|
break;
|
|
case 'W':
|
|
Console.Write(CSI);
|
|
Console.Write("47m");
|
|
break;
|
|
case 'q':
|
|
Console.Write(CSI);
|
|
Console.Write("41m");
|
|
break;
|
|
case 'w':
|
|
Console.Write(CSI);
|
|
Console.Write("43m");
|
|
break;
|
|
case 'e':
|
|
Console.Write(CSI);
|
|
Console.Write("53m");
|
|
break;
|
|
case 'd':
|
|
Console.Write(CSI);
|
|
Console.Write("55m");
|
|
break;
|
|
case 'r':
|
|
Console.Write(CSI);
|
|
Console.Write("7m");
|
|
break;
|
|
case 'f':
|
|
Console.Write(CSI);
|
|
Console.Write("27m");
|
|
break;
|
|
case 'R':
|
|
Console.Write(CSI);
|
|
Console.Write("6n");
|
|
break;
|
|
case 'c':
|
|
Console.Write(CSI);
|
|
Console.Write("0c");
|
|
break;
|
|
case '9':
|
|
Console.Write(CSI);
|
|
Console.Write("1;37;43;53m");
|
|
break;
|
|
case '(':
|
|
Console.Write(CSI);
|
|
Console.Write("39m");
|
|
break;
|
|
case ')':
|
|
Console.Write(CSI);
|
|
Console.Write("49m");
|
|
break;
|
|
case '<':
|
|
Console.Write('\xD'); // carriage return \r
|
|
break;
|
|
case '>':
|
|
Console.Write('\xA'); // line feed/new line \n
|
|
break;
|
|
case '`':
|
|
Console.Write("z");
|
|
break;
|
|
case '-':
|
|
{
|
|
IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_OUTPUT_HANDLE);
|
|
|
|
int mode;
|
|
|
|
if (Pinvoke.GetConsoleMode(hCon, out mode))
|
|
{
|
|
if ((mode & Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0)
|
|
{
|
|
mode &= ~Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
|
}
|
|
else
|
|
{
|
|
mode |= Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
|
}
|
|
|
|
Pinvoke.SetConsoleMode(hCon, mode);
|
|
}
|
|
break;
|
|
}
|
|
case '_':
|
|
{
|
|
IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_INPUT_HANDLE);
|
|
|
|
int mode;
|
|
if (Pinvoke.GetConsoleMode(hCon, out mode))
|
|
{
|
|
if ((mode & Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT) != 0)
|
|
{
|
|
mode &= ~Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT;
|
|
}
|
|
else
|
|
{
|
|
mode |= Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT;
|
|
}
|
|
mode &= ~Pinvoke.ENABLE_PROCESSED_INPUT;
|
|
Pinvoke.SetConsoleMode(hCon, mode);
|
|
}
|
|
break;
|
|
}
|
|
case 's':
|
|
Console.Write(CSI);
|
|
Console.Write("5S");
|
|
break;
|
|
case 't':
|
|
Console.Write(CSI);
|
|
Console.Write("5T");
|
|
break;
|
|
case '\'':
|
|
Console.Write(CSI);
|
|
Console.Write("3L");
|
|
break;
|
|
case '"':
|
|
Console.Write(CSI);
|
|
Console.Write("3M");
|
|
break;
|
|
case '\\':
|
|
Console.Write(CSI);
|
|
Console.Write("?3l");
|
|
break;
|
|
case '|':
|
|
Console.Write(CSI);
|
|
Console.Write("?3h");
|
|
break;
|
|
case '&':
|
|
Console.Write(CSI);
|
|
Console.Write("0;0r");
|
|
break;
|
|
case '*':
|
|
Console.Write(CSI + "3;1H");
|
|
Console.Write(CSI + "1;42m");
|
|
Console.Write("VVVVVVVVVVVVVVVV");
|
|
Console.Write(CSI + "4;1H");
|
|
Console.Write(CSI + "43m");
|
|
Console.Write("----------------");
|
|
Console.Write(CSI + "12;1H");
|
|
Console.Write(CSI + "44m");
|
|
Console.Write("----------------");
|
|
Console.Write(CSI + "13;1H");
|
|
Console.Write(CSI + "45m");
|
|
Console.Write("^^^^^^^^^^^^^^^^");
|
|
Console.Write(CSI + "m");
|
|
Console.Write(CSI + "1m");
|
|
Console.Write(CSI + "5;2Ha");
|
|
Console.Write(CSI + "6;3Hb");
|
|
Console.Write(CSI + "7;4Hc");
|
|
Console.Write(CSI + "8;5Hd");
|
|
Console.Write(CSI + "9;6He");
|
|
Console.Write(CSI + "10;7Hf");
|
|
Console.Write(CSI + "11;8Hg");
|
|
Console.Write(CSI + "12;9Hh");
|
|
Console.Write(CSI + "m");
|
|
Console.Write(CSI + "4;12r");
|
|
break;
|
|
case '{':
|
|
// move to top left corner
|
|
Console.Write(CSI);
|
|
Console.Write('H');
|
|
|
|
// write out a ton of Zs
|
|
for (int i = 0; i < 24; i++)
|
|
{
|
|
for (int j = 0; j < 80; j++)
|
|
{
|
|
if (j == 0)
|
|
Console.Write(i % 10);
|
|
else
|
|
Console.Write("Z");
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < 79; j++)
|
|
{
|
|
Console.Write("Z");
|
|
}
|
|
|
|
// move to middle-ish
|
|
Console.Write(CSI);
|
|
Console.Write("15");
|
|
Console.Write(';');
|
|
Console.Write("15");
|
|
Console.Write('H');
|
|
break;
|
|
case '=': //Go to the v2 app
|
|
VTApp2.Program.Main2(args);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class Pinvoke
|
|
{
|
|
[DllImport("kernel32.dll")]
|
|
public static extern bool SetConsoleMode(IntPtr hConsoleHandle, int dwMode);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out int lpMode);
|
|
|
|
public const int ENABLE_PROCESSED_OUTPUT = 0x1;
|
|
public const int ENABLE_WRAP_AT_EOL_OUTPUT = 0x2;
|
|
public const int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4;
|
|
|
|
public const int ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
public const int ENABLE_PROCESSED_INPUT = 0x0001;
|
|
public const int STD_INPUT_HANDLE = -10;
|
|
public const int STD_OUTPUT_HANDLE = -11;
|
|
|
|
[DllImport("kernel32.dll")]
|
|
public static extern IntPtr GetStdHandle(int nStdHandle);
|
|
}
|
|
}
|