Repository: microsoft/terminal (language: cpp)

Issue:
Add support for the VT525 restore color table sequence
# Description of the new feature/enhancement

The VT525 terminal had a pair of sequences for saving and restoring terminal state: the `DECRQTSR` sequence could be used to request a state report from the terminal which an application could save; and then the `DECRSTS` sequences would be used to restore that saved data to the terminal, returning it to the state it was originally.

There are two kinds of reports supported by these sequences:
1. the "terminal state report", which contains most of the settable attributes, excluding large items like the DRCS fonts, and macro definitions.
2. the "color table report", which contains the color definitions for all of the palette entries in the color table.

What I'm proposing is that we add support for the color table report, but only the restore operation (for now).

The one reason I'd like to add this is so we have standard-compliant way to alter the palette. The second reason is that it will give me a chance to introduce the color conversion routines that will eventually be required for sixel.

# Proposed technical implementation details (optional)

`DECRSTS` is a `DCS` sequence, which can't pass through conpty, so for now this would only work on conhost, or using the experimental passthrough mode in Windows Terminal. That said, I am planning to try and add support for `DCS` over a regular conpty connection at some point (at least for simple cases like this). But I'm not proposing to do that now.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 2cc27be650e..673c82912b3 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -520,6 +520,7 @@ DECAWM
 DECCKM
 DECCOLM
 DECCRA
+DECCTR
 DECDHL
 decdld
 DECDLD
@@ -544,7 +545,9 @@ DECREQTPARM
 DECRLM
 DECRQM
 DECRQSS
+DECRQTSR
 DECRST
+DECRSTS
 DECSASD
 DECSC
 DECSCA
@@ -1004,6 +1007,7 @@ hkey
 hkl
 HKLM
 hlocal
+HLS
 hlsl
 HMENU
 hmod
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 75bc3eb00c6..09d037545cf 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -132,6 +132,7 @@ class ScreenBufferTests
     TEST_METHOD(VtNewlineOutsideMargins);
 
     TEST_METHOD(VtSetColorTable);
+    TEST_METHOD(VtRestoreColorTableReport);
 
     TEST_METHOD(ResizeTraditionalDoesNotDoubleFreeAttrRows);
 
@@ -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.
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 8a6e14d1864..2ae3b67a6ab 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -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,
@@ -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;
 
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index f2dcb1c2a8d..19ebcb24660 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -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
 };
 inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() {}
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index bce951e5938..ce853d47545 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -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
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index e132444182c..dac77370483 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -139,6 +139,8 @@ namespace Microsoft::Console::VirtualTerminal
                                    const VTParameter cellHeight,
                                    const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD
 
+        StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) override; // DECRSTS
+
         StringHandler RequestSetting() override; // DECRQSS
 
     private:
@@ -196,6 +198,8 @@ namespace Microsoft::Console::VirtualTerminal
         void _ResetTabStops() noexcept;
         void _InitTabStopsForWidth(const VTInt width);
 
+        StringHandler _RestoreColorTable();
+
         void _ReportSGRSetting() const;
         void _ReportDECSTBMSetting();
 
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index ad724641160..c77ff9be7c0 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -135,7 +135,9 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
                                const DispatchTypes::DrcsFontSet /*fontSet*/,
                                const DispatchTypes::DrcsFontUsage /*fontUsage*/,
                                const VTParameter /*cellHeight*/,
-                               const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; }
+                               const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; } // DECDLD
+
+    StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat /*format*/) override { return nullptr; }; // DECRSTS
 
     StringHandler RequestSetting() override { return nullptr; }; // DECRQSS
 };
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 244e7c64979..93c35cd4c8f 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -663,6 +663,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c
                                           parameters.at(6),
                                           parameters.at(7));
         break;
+    case DcsActionCodes::DECRSTS_RestoreTerminalState:
+        handler = _dispatch->RestoreTerminalState(parameters.at(0));
+        break;
     case DcsActionCodes::DECRQSS_RequestSetting:
         handler = _dispatch->RequestSetting();
         break;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 20083b474d8..5dbee8a3b3f 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -148,6 +148,7 @@ namespace Microsoft::Console::VirtualTerminal
         enum DcsActionCodes : uint64_t
         {
             DECDLD_DownloadDRCS = VTID("{"),
+            DECRSTS_RestoreTerminalState = VTID("$p"),
             DECRQSS_RequestSetting = VTID("$q")
         };
 
diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp
index 7a5eca2ff36..cef2620e806 100644
--- a/src/types/inc/utils.hpp
+++ b/src/types/inc/utils.hpp
@@ -47,6 +47,8 @@ namespace Microsoft::Console::Utils
     til::color ColorFromHexString(const std::string_view wstr);
     std::optional<til::color> ColorFromXTermColor(const std::wstring_view wstr) noexcept;
     std::optional<til::color> ColorFromXParseColorSpec(const std::wstring_view wstr) noexcept;
+    til::color ColorFromHLS(const int h, const int l, const int s) noexcept;
+    til::color ColorFromRGB100(const int r, const int g, const int b) 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 85b226d337f..c3e432204d5 100644
--- a/src/types/utils.cpp
+++ b/src/types/utils.cpp
@@ -318,6 +318,84 @@ catch (...)
     return std::nullopt;
 }
 
+// Routine Description:
+// - Constructs a til::color value from RGB percentage components.
+// Arguments:
+// - r - The red component of the color (0-100%).
+// - g - The green component of the color (0-100%).
+// - b - The blue component of the color (0-100%).
+// Return Value:
+// - The color defined by the given components.
+til::color Utils::ColorFromRGB100(const int r, const int g, const int b) noexcept
+{
+    // The color class is expecting components in the range 0 to 255,
+    // so we need to scale our percentage values by 255/100. We can
+    // optimise this conversion with a pre-created lookup table.
+    static constexpr auto scale100To255 = [] {
+        std::array<uint8_t, 101> lut{};
+        for (size_t i = 0; i < std::size(lut); i++)
+        {
+            lut.at(i) = gsl::narrow_cast<uint8_t>((i * 255 + 50) / 100);
+        }
+        return lut;
+    }();
+
+    const auto red = til::at(scale100To255, std::min<unsigned>(r, 100u));
+    const auto green = til::at(scale100To255, std::min<unsigned>(g, 100u));
+    const auto blue = til::at(scale100To255, std::min<unsigned>(b, 100u));
+    return { red, green, blue };
+}
+
+// Routine Description:
+// - Constructs a til::color value from HLS components.
+// Arguments:
+// - h - The hue component of the color (0-360°).
+// - l - The luminosity component of the color (0-100%).
+// - s - The saturation component of the color (0-100%).
+// Return Value:
+// - The color defined by the given components.
+til::color Utils::ColorFromHLS(const int h, const int l, const int s) noexcept
+{
+    const auto hue = h % 360;
+    const auto lum = gsl::narrow_cast<float>(std::min(l, 100));
+    const auto sat = gsl::narrow_cast<float>(std::min(s, 100));
+
+    // This calculation is based on the HSL to RGB algorithm described in
+    // Wikipedia: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
+    // We start by calculating the chroma value, and the point along the bottom
+    // faces of the RGB cube with the same hue and chroma as our color (x).
+    const auto chroma = (50.f - abs(lum - 50.f)) * sat / 50.f;
+    const auto x = chroma * (60 - abs(hue % 120 - 60)) / 60.f;
+
+    // We'll also need an offset added to each component to match lightness.
+    const auto lightness = lum - chroma / 2.0f;
+
+    // We use the chroma value for the brightest component, x for the second
+    // brightest, and 0 for the last. The values  are scaled by 255/100 to get
+    // them in the range 0 to 255, as required by the color class.
+    constexpr auto scale = 255.f / 100.f;
+    const auto comp1 = gsl::narrow_cast<uint8_t>((chroma + lightness) * scale + 0.5f);
+    const auto comp2 = gsl::narrow_cast<uint8_t>((x + lightness) * scale + 0.5f);
+    const auto comp3 = gsl::narrow_cast<uint8_t>((0 + lightness) * scale + 0.5f);
+
+    // Finally we order the components based on the given hue. But note that the
+    // DEC terminals used a different mapping for hue than is typical for modern
+    // color models. Blue is at 0°, red is at 120°, and green is at 240°.
+    // See DEC STD 070, ReGIS Graphics Extension, § 8.6.2.2.2, Color by Value.
+    if (hue < 60)
+        return { comp2, comp3, comp1 }; // blue to magenta
+    else if (hue < 120)
+        return { comp1, comp3, comp2 }; // magenta to red
+    else if (hue < 180)
+        return { comp1, comp2, comp3 }; // red to yellow
+    else if (hue < 240)
+        return { comp2, comp1, comp3 }; // yellow to green
+    else if (hue < 300)
+        return { comp3, comp1, comp2 }; // green to cyan
+    else
+        return { comp3, comp2, comp1 }; // cyan to blue
+}
+
 // Routine Description:
 // - Converts a hex character to its equivalent integer value.
 // Arguments:


Test patch: