terminal/src/renderer/vt/VtSequences.cpp
James Holderness b604117421
Standardize the color table order (#11602)
## Summary of the Pull Request

In the original implementation, we used two different orderings for the color tables. The WT color table used ANSI order, while the conhost color table used a Windows-specific order. This PR standardizes on the ANSI color order everywhere, so the usage of indexed colors is consistent across both parts of the code base, which will hopefully allow more of the code to be shared one day.

## References

This is another small step towards de-duplicating `AdaptDispatch` and `TerminalDispatch` for issue #3849, and is essentially a followup to the SGR dispatch refactoring in PR #6728.

## PR Checklist
* [x] Closes #11461
* [x] CLA signed.
* [x] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. Issue number where discussion took place: #11461

## Detailed Description of the Pull Request / Additional comments

Conhost still needs to deal with legacy attributes using Windows color order, so those values now need to be transposed to ANSI colors order when creating a `TextAttribute` object. This is done with a simple mapping table, which also handles the translation of the default color entries, so it's actually slightly faster than the original code.

And when converting `TextAttribute` values back to legacy console attributes, we were already using a mapping table to handle the narrowing of 256-color values down to 16 colors, so we just needed to adjust that table to account for the translation from ANSI to Windows, and then could make use of the same table for both 256-color and 16-color values.

There are also a few places in conhost that read from or write to the color tables, and those now need to transpose the index values. I've addressed this by creating separate `SetLegacyColorTableEntry` and `GetLegacyColorTableEntry` methods in the `Settings` class which take care of the mapping, so it's now clearer in which cases the code is dealing with legacy values, and which are ANSI values.

These methods are used in the `SetConsoleScreenBufferInfoEx` and `GetConsoleScreenBufferInfoEx` APIs, as well as a few place where color preferences are handled (the registry, shortcut links, and the properties dialog), none of which are particularly sensitive to performance. However, we also use the legacy table when looking up the default colors for rendering (which happens a lot), so I've refactored that code so the default color calculations now only occur once per frame.

The plus side of all of this is that the VT code doesn't need to do the index translation anymore, so we can finally get rid of all the calls to `XTermToWindowsIndex`, and we no longer need a separate color table initialization method for conhost, so I was able to merge a number of color initialization methods into one. We also no longer need to translate from legacy values to ANSI when generating VT sequences for conpty.

The one exception to that is the 16-color VT renderer, which uses the `TextColor::GetLegacyIndex` method to approximate 16-color equivalents for RGB and 256-color values. Since that method returns a legacy index, it still needs to be translated to ANSI before it can be used in a VT sequence. But this should be no worse than it was before.

One more special case is conhost's secret _Color Selection_ feature. That uses `Ctrl`+Number and `Alt`+Number key sequences to highlight parts of the buffer, and the mapping from number to color is based on the Windows color order. So that mapping now needs to be transposed, but that's also not performance sensitive.

The only thing that I haven't bothered to update is the trace logging code in the `Telemetry` class, which logs the first 16 entries in the color table. Those entries are now going to be in a different order, but I didn't think that would be of great concern to anyone.

## Validation Steps Performed

A lot of unit tests needed to be updated to use ANSI color constants when setting indexed colors, where before they might have been expecting values in Windows order. But this replaced a wild mix of different constants, sometimes having to use bit shifting, as well as values mapped with `XTermToWindowsIndex`, so I think the tests are a whole lot clearer now. Only a few cases have been left with literal numbers where that seemed more appropriate.

In addition to getting the unit tests working, I've also manually tested the behaviour of all the console APIs which I thought could be affected by these changes, and confirmed that they produced the same results in the new code as they did in the original implementation.

This includes:
- `WriteConsoleOutput`
- `ReadConsoleOutput`
- `SetConsoleTextAttribute` with `WriteConsoleOutputCharacter`
- `FillConsoleOutputAttribute` and `FillConsoleOutputCharacter` 
- `ScrollConsoleScreenBuffer`
- `GetConsoleScreenBufferInfo`
- `GetConsoleScreenBufferInfoEx`
- `SetConsoleScreenBufferInfoEx`

I've also manually tested changing colors via the console properties menu, the registry, and shortcut links, including setting default colors and popup colors. And I've tested that the "Quirks Mode" is still working as expected in PowerShell.

In terms of performance, I wrote a little test app that filled a 80x9999 buffer with random color combinations using `WriteConsoleOutput`, which I figured was likely to be the most performance sensitive call, and I think it now actually performs slightly better than the original implementation.

I've also tested similar code - just filling the visible window - with SGR VT sequences of various types, and the performance seems about the same as it was before.
2021-11-04 22:13:22 +00:00

473 lines
18 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "vtrenderer.hpp"
#include "../../inc/conattrs.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Render;
// Method Description:
// - Formats and writes a sequence to stop the cursor from blinking.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_StopCursorBlinking() noexcept
{
return _Write("\x1b[?12l");
}
// Method Description:
// - Formats and writes a sequence to start the cursor blinking. If it's
// hidden, this won't also show it.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_StartCursorBlinking() noexcept
{
return _Write("\x1b[?12h");
}
// Method Description:
// - Formats and writes a sequence to hide the cursor.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_HideCursor() noexcept
{
return _Write("\x1b[?25l");
}
// Method Description:
// - Formats and writes a sequence to show the cursor.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_ShowCursor() noexcept
{
return _Write("\x1b[?25h");
}
// Method Description:
// - Formats and writes a sequence to erase the remainder of the line starting
// from the cursor position.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_EraseLine() noexcept
{
// The default no-param action of erase line is erase to the right.
// telnet client doesn't understand the parameterized version,
// so emit the implicit sequence instead.
return _Write("\x1b[K");
}
// Method Description:
// - Formats and writes a sequence to either insert or delete a number of lines
// into the buffer at the current cursor location.
// Delete/insert Character removes/adds N characters from/to the buffer, and
// shifts the remaining chars in the row to the left/right, while Erase
// Character replaces N characters with spaces, and leaves the rest
// untouched.
// Arguments:
// - chars: a number of characters to erase (by overwriting with space)
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_EraseCharacter(const short chars) noexcept
{
return _WriteFormatted(FMT_COMPILE("\x1b[{}X"), chars);
}
// Method Description:
// - Moves the cursor forward (right) a number of characters.
// Arguments:
// - chars: a number of characters to move cursor right by.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_CursorForward(const short chars) noexcept
{
return _WriteFormatted(FMT_COMPILE("\x1b[{}C"), chars);
}
// Method Description:
// - Formats and writes a sequence to erase the remainder of the line starting
// from the cursor position.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_ClearScreen() noexcept
{
return _Write("\x1b[2J");
}
[[nodiscard]] HRESULT VtEngine::_ClearScrollback() noexcept
{
return _Write("\x1b[3J");
}
// Method Description:
// - Formats and writes a sequence to either insert or delete a number of lines
// into the buffer at the current cursor location.
// Arguments:
// - sLines: a number of lines to insert or delete
// - fInsertLine: true iff we should insert the lines, false to delete them.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_InsertDeleteLine(const short sLines, const bool fInsertLine) noexcept
{
if (sLines <= 0)
{
return S_OK;
}
if (sLines == 1)
{
return _Write(fInsertLine ? "\x1b[L" : "\x1b[M");
}
return _WriteFormatted(FMT_COMPILE("\x1b[{}{}"), sLines, fInsertLine ? 'L' : 'M');
}
// Method Description:
// - Formats and writes a sequence to delete a number of lines into the buffer
// at the current cursor location.
// Arguments:
// - sLines: a number of lines to insert
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_DeleteLine(const short sLines) noexcept
{
return _InsertDeleteLine(sLines, false);
}
// Method Description:
// - Formats and writes a sequence to insert a number of lines into the buffer
// at the current cursor location.
// Arguments:
// - sLines: a number of lines to insert
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_InsertLine(const short sLines) noexcept
{
return _InsertDeleteLine(sLines, true);
}
// Method Description:
// - Formats and writes a sequence to move the cursor to the specified
// coordinate position. The input coord should be in console coordinates,
// where origin=(0,0).
// Arguments:
// - coord: Console coordinates to move the cursor to.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_CursorPosition(const COORD coord) noexcept
{
// VT coords start at 1,1
COORD coordVt = coord;
coordVt.X++;
coordVt.Y++;
return _WriteFormatted(FMT_COMPILE("\x1b[{};{}H"), coordVt.Y, coordVt.X);
}
// Method Description:
// - Formats and writes a sequence to move the cursor to the origin.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_CursorHome() noexcept
{
return _Write("\x1b[H");
}
// Method Description:
// - Formats and writes a sequence to change the current text attributes to the default.
// Arguments:
// <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetGraphicsDefault() noexcept
{
return _Write("\x1b[m");
}
// Method Description:
// - Formats and writes a sequence to change the current text attributes to an
// indexed color from the 16-color table.
// Arguments:
// - index: color table index to emit as a VT sequence
// - fIsForeground: true if we should emit the foreground sequence, false for background
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetGraphicsRendition16Color(const BYTE index,
const bool fIsForeground) noexcept
{
// Always check using the foreground flags, because the bg flags constants
// are a higher byte
// Foreground sequences are in [30,37] U [90,97]
// Background sequences are in [40,47] U [100,107]
// The "dark" sequences are in the first 7 values, the bright sequences in the second set.
// Note that text brightness and boldness are different in VT. Boldness is
// handled by _SetGraphicsBoldness. Here, we can emit either bright or
// dark colors. For conhost as a terminal, it can't draw bold
// characters, so it displays "bold" as bright, and in fact most
// terminals display the bright color when displaying bolded text.
// By specifying the boldness and brightness separately, we'll make sure the
// terminal has an accurate representation of our buffer.
const auto prefix = WI_IsFlagSet(index, FOREGROUND_INTENSITY) ? (fIsForeground ? 90 : 100) : (fIsForeground ? 30 : 40);
return _WriteFormatted(FMT_COMPILE("\x1b[{}m"), prefix + (index & 7));
}
// Method Description:
// - Formats and writes a sequence to change the current text attributes to an
// indexed color from the 256-color table.
// Arguments:
// - index: color table index to emit as a VT sequence
// - fIsForeground: true if we should emit the foreground sequence, false for background
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetGraphicsRendition256Color(const BYTE index,
const bool fIsForeground) noexcept
{
return _WriteFormatted(FMT_COMPILE("\x1b[{}8;5;{}m"), fIsForeground ? '3' : '4', index);
}
// Method Description:
// - Formats and writes a sequence to change the current text attributes to an
// RGB color.
// Arguments:
// - color: The color to emit a VT sequence for
// - fIsForeground: true if we should emit the foreground sequence, false for background
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionRGBColor(const COLORREF color,
const bool fIsForeground) noexcept
{
const uint8_t r = GetRValue(color);
const uint8_t g = GetGValue(color);
const uint8_t b = GetBValue(color);
return _WriteFormatted(FMT_COMPILE("\x1b[{}8;2;{};{};{}m"), fIsForeground ? '3' : '4', r, g, b);
}
// Method Description:
// - Formats and writes a sequence to change the current text attributes to the
// default foreground or background. Does not affect the boldness of text.
// Arguments:
// - fIsForeground: true if we should emit the foreground sequence, false for background
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionDefaultColor(const bool fIsForeground) noexcept
{
return _Write(fIsForeground ? ("\x1b[39m") : ("\x1b[49m"));
}
// Method Description:
// - Formats and writes a sequence to change the terminal's window size.
// Arguments:
// - sWidth: number of columns the terminal should display
// - sHeight: number of rows the terminal should display
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_ResizeWindow(const short sWidth, const short sHeight) noexcept
{
if (sWidth < 0 || sHeight < 0)
{
return E_INVALIDARG;
}
return _WriteFormatted(FMT_COMPILE("\x1b[8;{};{}t"), sHeight, sWidth);
}
// Method Description:
// - Formats and writes a sequence to request the end terminal to tell us the
// cursor position. The terminal will reply back on the vt input handle.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_RequestCursor() noexcept
{
return _Write("\x1b[6n");
}
// Method Description:
// - Formats and writes a sequence to change the terminal's title string
// Arguments:
// - title: string to use as the new title of the window.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_ChangeTitle(_In_ const std::string& title) noexcept
{
return _WriteFormatted(FMT_COMPILE("\x1b]0;{}\x7"), title);
}
// Method Description:
// - Formats and writes a sequence to change the boldness of the following text.
// Arguments:
// - isBold: If true, we'll embolden the text. Otherwise we'll debolden the text.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetBold(const bool isBold) noexcept
{
return _Write(isBold ? "\x1b[1m" : "\x1b[22m");
}
// Method Description:
// - Formats and writes a sequence to change the faintness of the following text.
// Arguments:
// - isFaint: If true, we'll make the text faint. Otherwise we'll remove the faintness.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetFaint(const bool isFaint) noexcept
{
return _Write(isFaint ? "\x1b[2m" : "\x1b[22m");
}
// Method Description:
// - Formats and writes a sequence to change the underline of the following text.
// Arguments:
// - isUnderlined: If true, we'll underline the text. Otherwise we'll remove the underline.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetUnderlined(const bool isUnderlined) noexcept
{
return _Write(isUnderlined ? "\x1b[4m" : "\x1b[24m");
}
// Method Description:
// - Formats and writes a sequence to change the double underline of the following text.
// Arguments:
// - isUnderlined: If true, we'll doubly underline the text. Otherwise we'll remove the underline.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetDoublyUnderlined(const bool isUnderlined) noexcept
{
return _Write(isUnderlined ? "\x1b[21m" : "\x1b[24m");
}
// Method Description:
// - Formats and writes a sequence to change the overline of the following text.
// Arguments:
// - isOverlined: If true, we'll overline the text. Otherwise we'll remove the overline.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetOverlined(const bool isOverlined) noexcept
{
return _Write(isOverlined ? "\x1b[53m" : "\x1b[55m");
}
// Method Description:
// - Formats and writes a sequence to change the italics of the following text.
// Arguments:
// - isItalic: If true, we'll italicize the text. Otherwise we'll remove the italics.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetItalic(const bool isItalic) noexcept
{
return _Write(isItalic ? "\x1b[3m" : "\x1b[23m");
}
// Method Description:
// - Formats and writes a sequence to change the blinking of the following text.
// Arguments:
// - isBlinking: If true, we'll start the text blinking. Otherwise we'll stop the blinking.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetBlinking(const bool isBlinking) noexcept
{
return _Write(isBlinking ? "\x1b[5m" : "\x1b[25m");
}
// Method Description:
// - Formats and writes a sequence to change the visibility of the following text.
// Arguments:
// - isInvisible: If true, we'll make the text invisible. Otherwise we'll make it visible.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetInvisible(const bool isInvisible) noexcept
{
return _Write(isInvisible ? "\x1b[8m" : "\x1b[28m");
}
// Method Description:
// - Formats and writes a sequence to change the crossed out state of the following text.
// Arguments:
// - isCrossedOut: If true, we'll cross out the text. Otherwise we'll stop crossing out.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetCrossedOut(const bool isCrossedOut) noexcept
{
return _Write(isCrossedOut ? "\x1b[9m" : "\x1b[29m");
}
// Method Description:
// - Formats and writes a sequence to change the reversed state of the following text.
// Arguments:
// - isReversed: If true, we'll reverse the text. Otherwise we'll remove the reversed state.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetReverseVideo(const bool isReversed) noexcept
{
return _Write(isReversed ? "\x1b[7m" : "\x1b[27m");
}
// Method Description:
// - Send a sequence to the connected terminal to request win32-input-mode from
// them. This will enable the connected terminal to send us full INPUT_RECORDs
// as input. If the terminal doesn't understand this sequence, it'll just
// ignore it.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_RequestWin32Input() noexcept
{
return _Write("\x1b[?9001h");
}
// Method Description:
// - Formats and writes a sequence to add a hyperlink to the terminal buffer
// Arguments:
// - The hyperlink URI
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_SetHyperlink(const std::wstring_view& uri, const std::wstring_view& customId, const uint16_t& numberId) noexcept
{
// Opening OSC8 sequence
if (customId.empty())
{
// This is the case of auto-assigned IDs:
// send the auto-assigned ID, prefixed with the PID of this session
// (we do this so different conpty sessions do not overwrite each other's hyperlinks)
const auto sessionID = GetCurrentProcessId();
const auto uriStr = til::u16u8(uri);
return _WriteFormatted(FMT_COMPILE("\x1b]8;id={}-{};{}\x1b\\"), sessionID, numberId, uriStr);
}
else
{
// This is the case of user-defined IDs:
// send the user-defined ID, prefixed with a "u"
// (we do this so no application can accidentally override a user defined ID)
const auto uriStr = til::u16u8(uri);
const auto customIdStr = til::u16u8(customId);
return _WriteFormatted(FMT_COMPILE("\x1b]8;id=u-{};{}\x1b\\"), customIdStr, uriStr);
}
}
// Method Description:
// - Formats and writes a sequence to end a hyperlink to the terminal buffer
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_EndHyperlink() noexcept
{
// Closing OSC8 sequence
return _Write("\x1b]8;;\x1b\\");
}