Skip to content

Commit

Permalink
Add support for restoring a DECCTR color table report (#13139)
Browse files Browse the repository at this point in the history
This PR introduces the framework for the `DECRSTS` sequence which is
used to restore terminal state reports. But to start with, I've just
implemented the `DECCTR` color table report, which provides a way for
applications to alter the terminal's color scheme.

## PR Checklist
* [x] Closes #13132
* [x] CLA signed.
* [x] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx

## Detailed Description of the Pull Request / Additional comments

I've added the functions for parsing DEC RGB and HLS color formats into
the `Utils` class, where we've got all our other color parsing routines,
since this functionality will eventually be needed in other VT protocols
like Sixel and ReGIS.

Since `DECRSTS` is a `DCS` sequence, this only works in conhost for now,
or when using the experimental passthrough mode in Windows Terminal.

## Validation Steps Performed

I've added a number of unit tests to check that the `DECCTR` report is
being interpreted as expected. This includes various edge cases (e.g.
omitted and out-of-range parameters), which I have confirmed to match
the color parsing on a real VT240 terminal.
  • Loading branch information
j4james committed Jun 3, 2022
1 parent 9dca6c2 commit c157f63
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ DECAWM
DECCKM
DECCOLM
DECCRA
DECCTR
DECDHL
decdld
DECDLD
Expand All @@ -545,7 +546,9 @@ DECREQTPARM
DECRLM
DECRQM
DECRQSS
DECRQTSR
DECRST
DECRSTS
DECSASD
DECSC
DECSCA
Expand Down Expand Up @@ -1006,6 +1009,7 @@ hkey
hkl
HKLM
hlocal
HLS
hlsl
HMENU
HMIDIOUT
Expand Down
232 changes: 232 additions & 0 deletions src/host/ut_host/ScreenBufferTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class ScreenBufferTests
TEST_METHOD(VtNewlineOutsideMargins);

TEST_METHOD(VtSetColorTable);
TEST_METHOD(VtRestoreColorTableReport);

TEST_METHOD(ResizeTraditionalDoesNotDoubleFreeAttrRows);

Expand Down Expand Up @@ -1738,6 +1739,237 @@ void ScreenBufferTests::VtSetColorTable()
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(5));
}

void ScreenBufferTests::VtRestoreColorTableReport()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();

// Set everything to white to start with.
for (auto i = 0; i < 16; i++)
{
gci.SetColorTableEntry(i, RGB(255, 255, 255));
}

// The test cases below are copied from the VT340 default color table, but
// note that our HLS conversion algorithm doesn't exactly match the VT340,
// so some of the component values may be off by 1%.

Log::Comment(L"HLS color definitions");

// HLS(0°,0%,0%) -> RGB(0,0,0)
stateMachine.ProcessString(L"\033P2$p0;1;0;0;0\033\\");
VERIFY_ARE_EQUAL(RGB(0, 0, 0), gci.GetColorTableEntry(0));

// HLS(0°,49%,59%) -> RGB(51,51,199)
stateMachine.ProcessString(L"\033P2$p1;1;0;49;59\033\\");
VERIFY_ARE_EQUAL(RGB(51, 51, 199), gci.GetColorTableEntry(1));

// HLS(120°,46%,71%) -> RGB(201,34,34)
stateMachine.ProcessString(L"\033P2$p2;1;120;46;71\033\\");
VERIFY_ARE_EQUAL(RGB(201, 34, 34), gci.GetColorTableEntry(2));

// HLS(240°,49%,59%) -> RGB(51,199,51)
stateMachine.ProcessString(L"\033P2$p3;1;240;49;59\033\\");
VERIFY_ARE_EQUAL(RGB(51, 199, 51), gci.GetColorTableEntry(3));

// HLS(60°,49%,59%) -> RGB(199,51,199)
stateMachine.ProcessString(L"\033P2$p4;1;60;49;59\033\\");
VERIFY_ARE_EQUAL(RGB(199, 51, 199), gci.GetColorTableEntry(4));

// HLS(300°,49%,59%) -> RGB(51,199,199)
stateMachine.ProcessString(L"\033P2$p5;1;300;49;59\033\\");
VERIFY_ARE_EQUAL(RGB(51, 199, 199), gci.GetColorTableEntry(5));

// HLS(180°,49%,59%) -> RGB(199,199,51)
stateMachine.ProcessString(L"\033P2$p6;1;180;49;59\033\\");
VERIFY_ARE_EQUAL(RGB(199, 199, 51), gci.GetColorTableEntry(6));

// HLS(0°,46%,0%) -> RGB(117,117,117)
stateMachine.ProcessString(L"\033P2$p7;1;0;46;0\033\\");
VERIFY_ARE_EQUAL(RGB(117, 117, 117), gci.GetColorTableEntry(7));

// HLS(0°,26%,0%) -> RGB(66,66,66)
stateMachine.ProcessString(L"\033P2$p8;1;0;26;0\033\\");
VERIFY_ARE_EQUAL(RGB(66, 66, 66), gci.GetColorTableEntry(8));

// HLS(0°,46%,28%) -> RGB(84,84,150)
stateMachine.ProcessString(L"\033P2$p9;1;0;46;28\033\\");
VERIFY_ARE_EQUAL(RGB(84, 84, 150), gci.GetColorTableEntry(9));

// HLS(120°,42%,38%) -> RGB(148,66,66)
stateMachine.ProcessString(L"\033P2$p10;1;120;42;38\033\\");
VERIFY_ARE_EQUAL(RGB(148, 66, 66), gci.GetColorTableEntry(10));

// HLS(240°,46%,28%) -> RGB(84,150,84)
stateMachine.ProcessString(L"\033P2$p11;1;240;46;28\033\\");
VERIFY_ARE_EQUAL(RGB(84, 150, 84), gci.GetColorTableEntry(11));

// HLS(60°,46%,28%) -> RGB(150,84,150)
stateMachine.ProcessString(L"\033P2$p12;1;60;46;28\033\\");
VERIFY_ARE_EQUAL(RGB(150, 84, 150), gci.GetColorTableEntry(12));

// HLS(300°,46%,28%) -> RGB(84,150,150)
stateMachine.ProcessString(L"\033P2$p13;1;300;46;28\033\\");
VERIFY_ARE_EQUAL(RGB(84, 150, 150), gci.GetColorTableEntry(13));

// HLS(180°,46%,28%) -> RGB(150,150,84)
stateMachine.ProcessString(L"\033P2$p14;1;180;46;28\033\\");
VERIFY_ARE_EQUAL(RGB(150, 150, 84), gci.GetColorTableEntry(14));

// HLS(0°,79%,0%) -> RGB(201,201,201)
stateMachine.ProcessString(L"\033P2$p15;1;0;79;0\033\\");
VERIFY_ARE_EQUAL(RGB(201, 201, 201), gci.GetColorTableEntry(15));

// Reset everything to white again.
for (auto i = 0; i < 16; i++)
{
gci.SetColorTableEntry(i, RGB(255, 255, 255));
}

Log::Comment(L"RGB color definitions");

// RGB(0%,0%,0%) -> RGB(0,0,0)
stateMachine.ProcessString(L"\033P2$p0;2;0;0;0\033\\");
VERIFY_ARE_EQUAL(RGB(0, 0, 0), gci.GetColorTableEntry(0));

// RGB(20%,20%,78%) -> RGB(51,51,199)
stateMachine.ProcessString(L"\033P2$p1;2;20;20;78\033\\");
VERIFY_ARE_EQUAL(RGB(51, 51, 199), gci.GetColorTableEntry(1));

// RGB(79%,13%,13%) -> RGB(201,33,33)
stateMachine.ProcessString(L"\033P2$p2;2;79;13;13\033\\");
VERIFY_ARE_EQUAL(RGB(201, 33, 33), gci.GetColorTableEntry(2));

// RGB(20%,78%,20%) -> RGB(51,199,51)
stateMachine.ProcessString(L"\033P2$p3;2;20;78;20\033\\");
VERIFY_ARE_EQUAL(RGB(51, 199, 51), gci.GetColorTableEntry(3));

// RGB(78%,20%,78%) -> RGB(199,51,199)
stateMachine.ProcessString(L"\033P2$p4;2;78;20;78\033\\");
VERIFY_ARE_EQUAL(RGB(199, 51, 199), gci.GetColorTableEntry(4));

// RGB(20%,78%,78%) -> RGB(51,199,199)
stateMachine.ProcessString(L"\033P2$p5;2;20;78;78\033\\");
VERIFY_ARE_EQUAL(RGB(51, 199, 199), gci.GetColorTableEntry(5));

// RGB(78%,78%,20%) -> RGB(199,199,51)
stateMachine.ProcessString(L"\033P2$p6;2;78;78;20\033\\");
VERIFY_ARE_EQUAL(RGB(199, 199, 51), gci.GetColorTableEntry(6));

// RGB(46%,46%,46%) -> RGB(117,117,117)
stateMachine.ProcessString(L"\033P2$p7;2;46;46;46\033\\");
VERIFY_ARE_EQUAL(RGB(117, 117, 117), gci.GetColorTableEntry(7));

// RGB(26%,26%,26%) -> RGB(66,66,66)
stateMachine.ProcessString(L"\033P2$p8;2;26;26;26\033\\");
VERIFY_ARE_EQUAL(RGB(66, 66, 66), gci.GetColorTableEntry(8));

// RGB(33%,33%,59%) -> RGB(84,84,150)
stateMachine.ProcessString(L"\033P2$p9;2;33;33;59\033\\");
VERIFY_ARE_EQUAL(RGB(84, 84, 150), gci.GetColorTableEntry(9));

// RGB(58%,26%,26%) -> RGB(148,66,66)
stateMachine.ProcessString(L"\033P2$p10;2;58;26;26\033\\");
VERIFY_ARE_EQUAL(RGB(148, 66, 66), gci.GetColorTableEntry(10));

// RGB(33%,59%,33%) -> RGB(84,150,84)
stateMachine.ProcessString(L"\033P2$p11;2;33;59;33\033\\");
VERIFY_ARE_EQUAL(RGB(84, 150, 84), gci.GetColorTableEntry(11));

// RGB(59%,33%,59%) -> RGB(150,84,150)
stateMachine.ProcessString(L"\033P2$p12;2;59;33;59\033\\");
VERIFY_ARE_EQUAL(RGB(150, 84, 150), gci.GetColorTableEntry(12));

// RGB(33%,59%,59%) -> RGB(84,150,150)
stateMachine.ProcessString(L"\033P2$p13;2;33;59;59\033\\");
VERIFY_ARE_EQUAL(RGB(84, 150, 150), gci.GetColorTableEntry(13));

// RGB(59%,59%,33%) -> RGB(150,150,84)
stateMachine.ProcessString(L"\033P2$p14;2;59;59;33\033\\");
VERIFY_ARE_EQUAL(RGB(150, 150, 84), gci.GetColorTableEntry(14));

// RGB(79%,79%,79%) -> RGB(201,201,201)
stateMachine.ProcessString(L"\033P2$p15;2;79;79;79\033\\");
VERIFY_ARE_EQUAL(RGB(201, 201, 201), gci.GetColorTableEntry(15));

// Reset everything to white again.
for (auto i = 0; i < 16; i++)
{
gci.SetColorTableEntry(i, RGB(255, 255, 255));
}

Log::Comment(L"Multiple color definitions");

// Setting colors 0, 2, and 4 to red, green, and blue (HLS).
stateMachine.ProcessString(L"\033P2$p0;1;120;50;100/2;1;240;50;100/4;1;360;50;100\033\\");
VERIFY_ARE_EQUAL(RGB(255, 0, 0), gci.GetColorTableEntry(0));
VERIFY_ARE_EQUAL(RGB(0, 255, 0), gci.GetColorTableEntry(2));
VERIFY_ARE_EQUAL(RGB(0, 0, 255), gci.GetColorTableEntry(4));

// Setting colors 1, 3, and 5 to red, green, and blue (RGB).
stateMachine.ProcessString(L"\033P2$p1;2;100;0;0/3;2;0;100;0/5;2;0;0;100\033\\");
VERIFY_ARE_EQUAL(RGB(255, 0, 0), gci.GetColorTableEntry(1));
VERIFY_ARE_EQUAL(RGB(0, 255, 0), gci.GetColorTableEntry(3));
VERIFY_ARE_EQUAL(RGB(0, 0, 255), gci.GetColorTableEntry(5));

// The interpretation of omitted and out of range parameter values is based
// on the VT240 and VT340 sixel implementations. It is assumed that color
// parsing is handled in the same way for other operations.

Log::Comment(L"Omitted parameter values");

// Omitted hue interpreted as 0° (blue)
stateMachine.ProcessString(L"\033P2$p6;1;;50;100\033\\");
VERIFY_ARE_EQUAL(RGB(0, 0, 255), gci.GetColorTableEntry(6));

// Omitted luminosity interpreted as 0% (black)
stateMachine.ProcessString(L"\033P2$p7;1;120;;100\033\\");
VERIFY_ARE_EQUAL(RGB(0, 0, 0), gci.GetColorTableEntry(7));

// Omitted saturation interpreted as 0% (gray)
stateMachine.ProcessString(L"\033P2$p8;1;120;50\033\\");
VERIFY_ARE_EQUAL(RGB(128, 128, 128), gci.GetColorTableEntry(8));

// Omitted red component interpreted as 0%
stateMachine.ProcessString(L"\033P2$p6;2;;50;100\033\\");
VERIFY_ARE_EQUAL(RGB(0, 128, 255), gci.GetColorTableEntry(6));

// Omitted green component interpreted as 0%
stateMachine.ProcessString(L"\033P2$p7;2;50;;100\033\\");
VERIFY_ARE_EQUAL(RGB(128, 0, 255), gci.GetColorTableEntry(7));

// Omitted blue component interpreted as 0%
stateMachine.ProcessString(L"\033P2$p8;2;50;100\033\\");
VERIFY_ARE_EQUAL(RGB(128, 255, 0), gci.GetColorTableEntry(8));

Log::Comment(L"Out of range parameter values");

// Hue wraps at 360°, so 480° interpreted as 120° (red)
stateMachine.ProcessString(L"\033P2$p9;1;480;50;100\033\\");
VERIFY_ARE_EQUAL(RGB(255, 0, 0), gci.GetColorTableEntry(9));

// Luminosity is clamped at 100%, so 150% interpreted as 100%
stateMachine.ProcessString(L"\033P2$p10;1;240;150;100\033\\");
VERIFY_ARE_EQUAL(RGB(255, 255, 255), gci.GetColorTableEntry(10));

// Saturation is clamped at 100%, so 120% interpreted as 100%
stateMachine.ProcessString(L"\033P2$p11;1;0;50;120\033\\");
VERIFY_ARE_EQUAL(RGB(0, 0, 255), gci.GetColorTableEntry(11));

// Red component is clamped at 100%, so 150% interpreted as 100%
stateMachine.ProcessString(L"\033P2$p12;2;150;0;0\033\\");
VERIFY_ARE_EQUAL(RGB(255, 0, 0), gci.GetColorTableEntry(12));

// Green component is clamped at 100%, so 150% interpreted as 100%
stateMachine.ProcessString(L"\033P2$p13;2;0;150;0\033\\");
VERIFY_ARE_EQUAL(RGB(0, 255, 0), gci.GetColorTableEntry(13));

// Blue component is clamped at 100%, so 150% interpreted as 100%
stateMachine.ProcessString(L"\033P2$p14;2;0;0;150\033\\");
VERIFY_ARE_EQUAL(RGB(0, 0, 255), gci.GetColorTableEntry(14));
}

void ScreenBufferTests::ResizeTraditionalDoesNotDoubleFreeAttrRows()
{
// there is not much to verify here, this test passes if the console doesn't crash.
Expand Down
12 changes: 12 additions & 0 deletions src/terminal/adapter/DispatchTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
WindowFrame = 2,
};

enum class ColorModel : VTInt
{
HLS = 1,
RGB = 2,
};

enum class EraseType : VTInt
{
ToEnd = 0,
Expand Down Expand Up @@ -481,6 +487,12 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
Size96 = 1
};

enum class ReportFormat : VTInt
{
TerminalStateReport = 1,
ColorTableReport = 2
};

constexpr VTInt s_sDECCOLMSetColumns = 132;
constexpr VTInt s_sDECCOLMResetColumns = 80;

Expand Down
2 changes: 2 additions & 0 deletions src/terminal/adapter/ITermDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
const VTParameter cellHeight,
const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD

virtual StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) = 0; // DECRSTS

virtual StringHandler RequestSetting() = 0; // DECRQSS

virtual bool PlaySounds(const VTParameters parameters) = 0; // DECPS
Expand Down
73 changes: 73 additions & 0 deletions src/terminal/adapter/adaptDispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2552,6 +2552,79 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
};
}

// Method Description:
// - DECRSTS - Restores the terminal state from a stream of data previously
// saved with a DECRQTSR query.
// Arguments:
// - format - the format of the state report being restored.
// Return Value:
// - a function to receive the data or nullptr if the format is unsupported.
ITermDispatch::StringHandler AdaptDispatch::RestoreTerminalState(const DispatchTypes::ReportFormat format)
{
switch (format)
{
case DispatchTypes::ReportFormat::ColorTableReport:
return _RestoreColorTable();
default:
return nullptr;
}
}

// Method Description:
// - DECCTR - This is a parser for the Color Table Report received via DECRSTS.
// The report contains a list of color definitions separated with a slash
// character. Each definition consists of 5 parameters: Pc;Pu;Px;Py;Pz
// - Pc is the color number.
// - Pu is the color model (1 = HLS, 2 = RGB).
// - Px, Py, and Pz are component values in the color model.
// Arguments:
// - <none>
// Return Value:
// - a function to parse the report data.
ITermDispatch::StringHandler AdaptDispatch::_RestoreColorTable()
{
return [this, parameter = VTInt{}, parameters = std::vector<VTParameter>{}](const auto ch) mutable {
if (ch >= L'0' && ch <= L'9')
{
parameter *= 10;
parameter += (ch - L'0');
parameter = std::min(parameter, MAX_PARAMETER_VALUE);
}
else if (ch == L';')
{
if (parameters.size() < 5)
{
parameters.push_back(parameter);
}
parameter = 0;
}
else if (ch == L'/' || ch == AsciiChars::ESC)
{
parameters.push_back(parameter);
const auto colorParameters = VTParameters{ parameters.data(), parameters.size() };
const auto colorNumber = colorParameters.at(0).value_or(0);
if (colorNumber < TextColor::TABLE_SIZE)
{
const auto colorModel = DispatchTypes::ColorModel{ colorParameters.at(1) };
const auto x = colorParameters.at(2).value_or(0);
const auto y = colorParameters.at(3).value_or(0);
const auto z = colorParameters.at(4).value_or(0);
if (colorModel == DispatchTypes::ColorModel::HLS)
{
SetColorTableEntry(colorNumber, Utils::ColorFromHLS(x, y, z));
}
else if (colorModel == DispatchTypes::ColorModel::RGB)
{
SetColorTableEntry(colorNumber, Utils::ColorFromRGB100(x, y, z));
}
}
parameters.clear();
parameter = 0;
}
return (ch != AsciiChars::ESC);
};
}

// Method Description:
// - DECRQSS - Requests the state of a VT setting. The value being queried is
// identified by the intermediate and final characters of its control
Expand Down

0 comments on commit c157f63

Please sign in to comment.