diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 01313efb8b4..bf4d2cc5bd5 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -171,6 +171,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch const DispatchTypes::MacroEncoding encoding) = 0; // DECDMAC virtual bool InvokeMacro(const VTInt macroId) = 0; // DECINVM + virtual bool RequestTerminalStateReport(const DispatchTypes::ReportFormat format, const VTParameter formatOption) = 0; // DECRQTSR virtual StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) = 0; // DECRSTS virtual StringHandler RequestSetting() = 0; // DECRQSS diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index d4a42e4ade4..3aca2abfdf9 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4166,6 +4166,27 @@ bool AdaptDispatch::InvokeMacro(const VTInt macroId) return true; } +// Routine Description: +// - DECRQTSR - Queries the state of the terminal. This can either be a terminal +// state report, generally covering all settable state in the terminal (with +// the exception of large data items), or a color table report. +// Arguments: +// - format - the format of the report being requested. +// - formatOption - a format-specific option. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::RequestTerminalStateReport(const DispatchTypes::ReportFormat format, const VTParameter formatOption) +{ + switch (format) + { + case DispatchTypes::ReportFormat::ColorTableReport: + _ReportColorTable(formatOption); + return true; + default: + return false; + } +} + // Method Description: // - DECRSTS - Restores the terminal state from a stream of data previously // saved with a DECRQTSR query. @@ -4184,6 +4205,48 @@ ITermDispatch::StringHandler AdaptDispatch::RestoreTerminalState(const DispatchT } } +// Method Description: +// - DECCTR - Returns the Color Table Report in response to a DECRQTSR query. +// Arguments: +// - colorModel - the color model to use in the report (1 = HLS, 2 = RGB). +// Return Value: +// - None +void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel) const +{ + using namespace std::string_view_literals; + + // A valid response always starts with DCS 2 $ s. + fmt::basic_memory_buffer response; + response.append(L"\033P2$s"sv); + + const auto modelNumber = static_cast(colorModel); + for (size_t colorNumber = 0; colorNumber < TextColor::TABLE_SIZE; colorNumber++) + { + const auto color = _renderSettings.GetColorTableEntry(colorNumber); + if (color != INVALID_COLOR) + { + response.append(colorNumber > 0 ? L"/"sv : L""sv); + auto x = 0, y = 0, z = 0; + switch (colorModel) + { + case DispatchTypes::ColorModel::HLS: + std::tie(x, y, z) = Utils::ColorToHLS(color); + break; + case DispatchTypes::ColorModel::RGB: + std::tie(x, y, z) = Utils::ColorToRGB100(color); + break; + default: + return; + } + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{};{};{};{}"), colorNumber, modelNumber, x, y, z); + } + } + + // An ST ends the sequence. + response.append(L"\033\\"sv); + _api.ReturnResponse({ response.data(), response.size() }); +} + // 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 diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 8d8cc07e0c0..cba0273665d 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -174,6 +174,7 @@ namespace Microsoft::Console::VirtualTerminal const DispatchTypes::MacroEncoding encoding) override; // DECDMAC bool InvokeMacro(const VTInt macroId) override; // DECINVM + bool RequestTerminalStateReport(const DispatchTypes::ReportFormat format, const VTParameter formatOption) override; // DECRQTSR StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) override; // DECRSTS StringHandler RequestSetting() override; // DECRQSS @@ -272,6 +273,7 @@ namespace Microsoft::Console::VirtualTerminal void _ClearAllTabStops() noexcept; void _InitTabStopsForWidth(const VTInt width); + void _ReportColorTable(const DispatchTypes::ColorModel colorModel) const; StringHandler _RestoreColorTable(); void _ReportSGRSetting() const; diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index cce60582fd5..607089545bf 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -164,6 +164,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons const DispatchTypes::MacroEncoding /*encoding*/) override { return nullptr; } // DECDMAC bool InvokeMacro(const VTInt /*macroId*/) override { return false; } // DECINVM + bool RequestTerminalStateReport(const DispatchTypes::ReportFormat /*format*/, const VTParameter /*formatOption*/) override { return false; } // DECRQTSR StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat /*format*/) override { return nullptr; }; // DECRSTS StringHandler RequestSetting() override { return nullptr; }; // DECRQSS diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 9e33ceaf7db..edd13ddab01 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -2263,6 +2263,141 @@ class AdapterTest verifyChecksumReport(L"FF8B"); } + TEST_METHOD(ColorTableReportTests) + { + _testGetSet->PrepData(); + + // The test cases below are copied from the VT525 default color table, + // but our color table holds many more values, so we're just filling the + // remaining slots with black for the purposes of this test. + auto& renderSettings = _testGetSet->_renderer._renderSettings; + renderSettings.SetColorTableEntry(0, RGB(0, 0, 0)); + renderSettings.SetColorTableEntry(1, RGB(204, 36, 36)); + renderSettings.SetColorTableEntry(2, RGB(51, 204, 51)); + renderSettings.SetColorTableEntry(3, RGB(204, 204, 51)); + renderSettings.SetColorTableEntry(4, RGB(51, 51, 204)); + renderSettings.SetColorTableEntry(5, RGB(204, 51, 204)); + renderSettings.SetColorTableEntry(6, RGB(51, 204, 204)); + renderSettings.SetColorTableEntry(7, RGB(120, 120, 120)); + renderSettings.SetColorTableEntry(8, RGB(69, 69, 69)); + renderSettings.SetColorTableEntry(9, RGB(255, 0, 0)); + renderSettings.SetColorTableEntry(10, RGB(0, 255, 0)); + renderSettings.SetColorTableEntry(11, RGB(255, 255, 0)); + renderSettings.SetColorTableEntry(12, RGB(0, 0, 255)); + renderSettings.SetColorTableEntry(13, RGB(255, 0, 255)); + renderSettings.SetColorTableEntry(14, RGB(0, 255, 255)); + renderSettings.SetColorTableEntry(15, RGB(255, 255, 255)); + for (size_t i = 16; i < TextColor::TABLE_SIZE; i++) + { + renderSettings.SetColorTableEntry(i, RGB(0, 0, 0)); + } + + // Color table reports start with a DCS $s introducer with a parameter + // value of 2, and end with an ST terminator. + const auto DECCTR = L"\033P2$s"; + const auto ST = L"\033\\"; + + Log::Comment(L"HLS color model"); + + const auto hlsColorModel = static_cast(DispatchTypes::ColorModel::HLS); + _pDispatch->RequestTerminalStateReport(DispatchTypes::ReportFormat::ColorTableReport, hlsColorModel); + + std::wstring expectedResponse = DECCTR; + // RGB(0,0,0) -> HLS(0°,0%,0%) + expectedResponse += L"0;1;0;0;0/"; + // RGB(204,36,36) -> HLS(120°,47%,70%) + expectedResponse += L"1;1;120;47;70/"; + // RGB(51,204,51) -> HLS(240°,50%,60%) + expectedResponse += L"2;1;240;50;60/"; + // RGB(204,204,51) -> HLS(180°,50%,60%) + expectedResponse += L"3;1;180;50;60/"; + // RGB(51,51,204) -> HLS(0°,50%,60%) + expectedResponse += L"4;1;0;50;60/"; + // RGB(204,51,204) -> HLS(60°,50%,60%) + expectedResponse += L"5;1;60;50;60/"; + // RGB(51,204,204) -> HLS(300°,50%,60%) + expectedResponse += L"6;1;300;50;60/"; + // RGB(120,120,120) -> HLS(0°,47%,0%) + expectedResponse += L"7;1;0;47;0/"; + // RGB(69,69,69) -> HLS(0°,27%,0%) + expectedResponse += L"8;1;0;27;0/"; + // RGB(255,0,0) -> HLS(120°,50%,100%) + expectedResponse += L"9;1;120;50;100/"; + // RGB(0,255,0) -> HLS(240°,50%,100%) + expectedResponse += L"10;1;240;50;100/"; + // RGB(255,255,0) -> HLS(180°,50%,100%) + expectedResponse += L"11;1;180;50;100/"; + // RGB(0,0,255) -> HLS(0°,50%,100%) + expectedResponse += L"12;1;0;50;100/"; + // RGB(255,0,255) -> HLS(60°,50%,100%) + expectedResponse += L"13;1;60;50;100/"; + // RGB(0,255,255) -> HLS(300°,50%,100%) + expectedResponse += L"14;1;300;50;100/"; + // RGB(255,255,255) -> HLS(0°,100%,0%) + expectedResponse += L"15;1;0;100;0/"; + // Remaining slots are black, i.e. HLS(0°,0%,0%) + for (size_t i = 16; i < TextColor::TABLE_SIZE; i++) + { + expectedResponse += std::to_wstring(i) + L";1;0;0;0"; + if (i + 1 < TextColor::TABLE_SIZE) + { + expectedResponse += L'/'; + } + } + expectedResponse += ST; + _testGetSet->ValidateInputEvent(expectedResponse.c_str()); + + Log::Comment(L"RGB color model"); + + const auto rgbColorModel = static_cast(DispatchTypes::ColorModel::RGB); + _pDispatch->RequestTerminalStateReport(DispatchTypes::ReportFormat::ColorTableReport, rgbColorModel); + + expectedResponse = DECCTR; + // RGB(0,0,0) -> RGB(0%,0%,0%) + expectedResponse += L"0;2;0;0;0/"; + // RGB(204,36,36) -> RGB(80%,14%,14%) + expectedResponse += L"1;2;80;14;14/"; + // RGB(51,204,51) -> RGB(20%,80%,20%) + expectedResponse += L"2;2;20;80;20/"; + // RGB(204,204,51) -> RGB(80%,80%,20%) + expectedResponse += L"3;2;80;80;20/"; + // RGB(51,51,204) -> RGB(20%,20%,80%) + expectedResponse += L"4;2;20;20;80/"; + // RGB(204,51,204) -> RGB(80%,20%,80%) + expectedResponse += L"5;2;80;20;80/"; + // RGB(51,204,204) -> RGB(20%,80%,80%) + expectedResponse += L"6;2;20;80;80/"; + // RGB(120,120,120) -> RGB(47%,47%,47%) + expectedResponse += L"7;2;47;47;47/"; + // RGB(69,69,69) -> RGB(27%,27%,27%) + expectedResponse += L"8;2;27;27;27/"; + // RGB(255,0,0) -> RGB(100%,0%,0%) + expectedResponse += L"9;2;100;0;0/"; + // RGB(0,255,0) -> RGB(0%,100%,0%) + expectedResponse += L"10;2;0;100;0/"; + // RGB(255,255,0) -> RGB(100%,100%,0%) + expectedResponse += L"11;2;100;100;0/"; + // RGB(0,0,255) -> RGB(0%,0%,100%) + expectedResponse += L"12;2;0;0;100/"; + // RGB(255,0,255) -> RGB(100%,0%,100%) + expectedResponse += L"13;2;100;0;100/"; + // RGB(0,255,255) -> RGB(0%,100%,100%) + expectedResponse += L"14;2;0;100;100/"; + // RGB(255,255,255) -> RGB(100%,100%,100%) + expectedResponse += L"15;2;100;100;100/"; + // Remaining slots are black, i.e. RGB(0%,0%,0%) + for (size_t i = 16; i < TextColor::TABLE_SIZE; i++) + { + expectedResponse += std::to_wstring(i) + L";2;0;0;0"; + if (i + 1 < TextColor::TABLE_SIZE) + { + expectedResponse += L'/'; + } + } + expectedResponse += ST; + _testGetSet->ValidateInputEvent(expectedResponse.c_str()); + } + TEST_METHOD(TabulationStopReportTests) { _testGetSet->PrepData(); diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 38036ce6da8..66c993ac06b 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -626,6 +626,9 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::DECCRA_CopyRectangularArea: success = _dispatch->CopyRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.at(4), parameters.at(5), parameters.at(6), parameters.at(7)); break; + case CsiActionCodes::DECRQTSR_RequestTerminalStateReport: + success = _dispatch->RequestTerminalStateReport(parameters.at(0), parameters.at(1)); + break; case CsiActionCodes::DECRQPSR_RequestPresentationStateReport: success = _dispatch->RequestPresentationStateReport(parameters.at(0)); break; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 5be20ae2513..c661bcecc6c 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -154,6 +154,7 @@ namespace Microsoft::Console::VirtualTerminal DECRQM_PrivateRequestMode = VTID("?$p"), DECCARA_ChangeAttributesRectangularArea = VTID("$r"), DECRARA_ReverseAttributesRectangularArea = VTID("$t"), + DECRQTSR_RequestTerminalStateReport = VTID("$u"), DECCRA_CopyRectangularArea = VTID("$v"), DECRQPSR_RequestPresentationStateReport = VTID("$w"), DECFRA_FillRectangularArea = VTID("$x"), diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index 18221bac2da..af730836e66 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -60,7 +60,9 @@ namespace Microsoft::Console::Utils std::optional ColorFromXTermColor(const std::wstring_view wstr) noexcept; std::optional ColorFromXParseColorSpec(const std::wstring_view wstr) noexcept; til::color ColorFromHLS(const int h, const int l, const int s) noexcept; + std::tuple ColorToHLS(const til::color color) noexcept; til::color ColorFromRGB100(const int r, const int g, const int b) noexcept; + std::tuple ColorToRGB100(const til::color color) noexcept; bool HexToUint(const wchar_t wch, unsigned int& value) noexcept; bool StringToUint(const std::wstring_view wstr, unsigned int& value); diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 0d099f7ad28..66c98a728da 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -374,6 +374,32 @@ til::color Utils::ColorFromRGB100(const int r, const int g, const int b) noexcep return { red, green, blue }; } +// Function Description: +// - Returns the RGB percentage components of a given til::color value. +// Arguments: +// - color: the color being queried +// Return Value: +// - a tuple containing the three components +std::tuple Utils::ColorToRGB100(const til::color color) noexcept +{ + // The color class components are in the range 0 to 255, so we + // need to scale them by 100/255 to obtain percentage values. We + // can optimise this conversion with a pre-created lookup table. + static constexpr auto scale255To100 = [] { + std::array lut{}; + for (size_t i = 0; i < std::size(lut); i++) + { + lut.at(i) = gsl::narrow_cast((i * 100 + 128) / 255); + } + return lut; + }(); + + const auto red = til::at(scale255To100, color.r); + const auto green = til::at(scale255To100, color.g); + const auto blue = til::at(scale255To100, color.b); + return { red, green, blue }; +} + // Routine Description: // - Constructs a til::color value from HLS components. // Arguments: @@ -424,6 +450,62 @@ til::color Utils::ColorFromHLS(const int h, const int l, const int s) noexcept return { comp3, comp2, comp1 }; // cyan to blue } +// Function Description: +// - Returns the HLS components of a given til::color value. +// Arguments: +// - color: the color being queried +// Return Value: +// - a tuple containing the three components +std::tuple Utils::ColorToHLS(const til::color color) noexcept +{ + const auto red = color.r / 255.f; + const auto green = color.g / 255.f; + const auto blue = color.b / 255.f; + + // This calculation is based on the RGB to HSL algorithm described in + // Wikipedia: https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB + // We start by calculating the maximum and minimum component values. + const auto maxComp = std::max({ red, green, blue }); + const auto minComp = std::min({ red, green, blue }); + + // The chroma value is the range of those components. + const auto chroma = maxComp - minComp; + + // And the luma is the middle of the range. But we're actually calculating + // double that value here to save on a division. + const auto luma2 = (maxComp + minComp); + + // The saturation is half the chroma value divided by min(luma, 1-luma), + // but since the luma is already doubled, we can use the chroma as is. + const auto divisor = std::min(luma2, 2.f - luma2); + const auto sat = divisor > 0 ? chroma / divisor : 0.f; + + // Finally we calculate the hue, which is represented by the angle of a + // vector to a point in a color hexagon with blue, magenta, red, yellow, + // green, and cyan at its corners. As noted above, the DEC standard has + // blue at 0°, red at 120°, and green at 240°, which is slightly different + // from the way that hue is typically mapped in modern color models. + auto hue = 0.f; + if (chroma != 0) + { + if (maxComp == red) + hue = (green - blue) / chroma + 2.f; // magenta to yellow + else if (maxComp == green) + hue = (blue - red) / chroma + 4.f; // yellow to cyan + else if (maxComp == blue) + hue = (red - green) / chroma + 6.f; // cyan to magenta + } + + // The hue value calculated above is essentially a fractional offset from the + // six hexagon corners, so it has to be scaled by 60 to get the angle value. + // Luma and saturation are percentages so must be scaled by 100, but our luma + // value is already doubled, so only needs to be scaled by 50. + const auto h = static_cast(hue * 60.f + 0.5f) % 360; + const auto l = static_cast(luma2 * 50.f + 0.5f); + const auto s = static_cast(sat * 100.f + 0.5f); + return { h, l, s }; +} + // Routine Description: // - Converts a hex character to its equivalent integer value. // Arguments: