Repository: microsoft/terminal (language: cpp)

Issue:
Add support for Presentation State reports
# Description of the new feature/enhancement

There are two reports that can be queried with the [`DECRQPSR`](https://www.vt100.net/docs/vt510-rm/DECRQPSR.html) (request presentation state) escape sequence. The one returns the active tab stops, and the other returns what they call "cursor information", which includes things like cursor position, SGR attributes, and character set designations.

There is also then a corresponding [`DECRSPS`](https://www.vt100.net/docs/vt510-rm/DECRSPS.html) sequence (restore presentation state) which can restore the saved information from the above mentioned reports.

There are a number of uses for these sequences, but the main reason I want to support them is because they're needed to meet the requirements of the TSI (terminal state interrogation) feature, which is itself a requirement for level 3 compliance.

# Proposed technical implementation details (optional)

This will require some additional work in the `TerminalOuput` class to track the actual IDs of the designated character sets so we can return that information in the report. I think most of the other information we need should be easy to access.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 3be64694a0d..21bc009112c 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -2,6 +2,7 @@ aabbcc
 ABANDONFONT
 abbcc
 ABCDEFGHIJKLMNOPQRSTUVWXY
+ABCF
 abgr
 abi
 ABORTIFHUNG
@@ -408,6 +409,7 @@ DECAUPSS
 DECAWM
 DECBKM
 DECCARA
+DECCIR
 DECCKM
 DECCKSR
 DECCOLM
@@ -439,8 +441,10 @@ DECRLM
 DECRPM
 DECRQCRA
 DECRQM
+DECRQPSR
 DECRQSS
 DECRQTSR
+DECRSPS
 decrst
 DECSACE
 DECSASD
@@ -461,6 +465,7 @@ DECSTBM
 DECSTGLT
 DECSTR
 DECSWL
+DECTABSR
 DECTCEM
 DECXCPR
 DEFAPP
@@ -814,6 +819,8 @@ hkl
 HKLM
 hlocal
 hlsl
+HMB
+HMK
 hmod
 hmodule
 hmon
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 907d9392650..eeb19db1366 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -7,9 +7,11 @@ namespace Microsoft::Console::VirtualTerminal
 {
     using VTInt = int32_t;
 
-    class VTID
+    union VTID
     {
     public:
+        VTID() = default;
+
         template<size_t Length>
         constexpr VTID(const char (&s)[Length]) :
             _value{ _FromString(s) }
@@ -17,7 +19,7 @@ namespace Microsoft::Console::VirtualTerminal
         }
 
         constexpr VTID(const uint64_t value) :
-            _value{ value }
+            _value{ value & 0x00FFFFFFFFFFFFFF }
         {
         }
 
@@ -26,6 +28,11 @@ namespace Microsoft::Console::VirtualTerminal
             return _value;
         }
 
+        constexpr const std::string_view ToString() const
+        {
+            return &_string[0];
+        }
+
         constexpr char operator[](const size_t offset) const
         {
             return SubSequence(offset)._value & 0xFF;
@@ -40,7 +47,7 @@ namespace Microsoft::Console::VirtualTerminal
         template<size_t Length>
         static constexpr uint64_t _FromString(const char (&s)[Length])
         {
-            static_assert(Length - 1 <= sizeof(_value));
+            static_assert(Length <= sizeof(_value));
             uint64_t value = 0;
             for (auto i = Length - 1; i-- > 0;)
             {
@@ -49,7 +56,12 @@ namespace Microsoft::Console::VirtualTerminal
             return value;
         }
 
-        uint64_t _value;
+        // In order for the _string to hold the correct representation of the
+        // ID stored in _value, we must be on a little endian architecture.
+        static_assert(std::endian::native == std::endian::little);
+
+        uint64_t _value = 0;
+        char _string[sizeof(_value)];
     };
 
     class VTIDBuilder
@@ -63,13 +75,13 @@ namespace Microsoft::Console::VirtualTerminal
 
         void AddIntermediate(const wchar_t intermediateChar) noexcept
         {
-            if (_idShift + CHAR_BIT >= sizeof(_idAccumulator) * CHAR_BIT)
+            if (_idShift + CHAR_BIT * 2 >= sizeof(_idAccumulator) * CHAR_BIT)
             {
                 // If there is not enough space in the accumulator to add
-                // the intermediate and still have room left for the final,
-                // then we reset the accumulator to zero. This will result
-                // in an id with all zero intermediates, which shouldn't
-                // match anything.
+                // the intermediate and still have room left for the final
+                // and null terminator, then we reset the accumulator to zero.
+                // This will result in an id with all zero intermediates,
+                // which shouldn't match anything.
                 _idAccumulator = 0;
             }
             else
@@ -539,6 +551,12 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         ColorTableReport = 2
     };
 
+    enum class PresentationReportFormat : VTInt
+    {
+        CursorInformationReport = 1,
+        TabulationStopReport = 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 67fa67bc0d3..5d27e7bb246 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -150,6 +150,9 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
 
     virtual StringHandler RequestSetting() = 0; // DECRQSS
 
+    virtual bool RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format) = 0; // DECRQPSR
+    virtual StringHandler RestorePresentationState(const DispatchTypes::PresentationReportFormat format) = 0; // DECRSPS
+
     virtual bool PlaySounds(const VTParameters parameters) = 0; // DECPS
 };
 inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() = default;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index d43fea1bb83..c6d967ea6bc 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -2446,7 +2446,7 @@ bool AdaptDispatch::LockingShiftRight(const VTInt gsetNumber)
 // - gsetNumber - The G-set that will be invoked.
 // Return value:
 // True if handled successfully. False otherwise.
-bool AdaptDispatch::SingleShift(const VTInt gsetNumber)
+bool AdaptDispatch::SingleShift(const VTInt gsetNumber) noexcept
 {
     return _termOutput.SingleShift(gsetNumber);
 }
@@ -3656,6 +3656,353 @@ void AdaptDispatch::_ReportDECSACESetting() const
     _api.ReturnResponse({ response.data(), response.size() });
 }
 
+// Routine Description:
+// - DECRQPSR - Queries the presentation state of the terminal. This can either
+//   be in the form of a cursor information report, or a tabulation stop report,
+//   depending on the requested format.
+// Arguments:
+// - format - the format of the report being requested.
+// Return Value:
+// - True if handled successfully. False otherwise.
+bool AdaptDispatch::RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format)
+{
+    switch (format)
+    {
+    case DispatchTypes::PresentationReportFormat::CursorInformationReport:
+        _ReportCursorInformation();
+        return true;
+    case DispatchTypes::PresentationReportFormat::TabulationStopReport:
+        _ReportTabStops();
+        return true;
+    default:
+        return false;
+    }
+}
+
+// Method Description:
+// - DECRSPS - Restores the presentation state from a stream of data previously
+//   saved with a DECRQPSR query.
+// Arguments:
+// - format - the format of the report being restored.
+// Return Value:
+// - a function to receive the data or nullptr if the format is unsupported.
+ITermDispatch::StringHandler AdaptDispatch::RestorePresentationState(const DispatchTypes::PresentationReportFormat format)
+{
+    switch (format)
+    {
+    case DispatchTypes::PresentationReportFormat::CursorInformationReport:
+        return _RestoreCursorInformation();
+    case DispatchTypes::PresentationReportFormat::TabulationStopReport:
+        return _RestoreTabStops();
+    default:
+        return nullptr;
+    }
+}
+
+// Method Description:
+// - DECCIR - Returns the Cursor Information Report in response to a DECRQPSR query.
+// Arguments:
+// - None
+// Return Value:
+// - None
+void AdaptDispatch::_ReportCursorInformation()
+{
+    const auto viewport = _api.GetViewport();
+    const auto& textBuffer = _api.GetTextBuffer();
+    const auto& cursor = textBuffer.GetCursor();
+    const auto attributes = textBuffer.GetCurrentAttributes();
+
+    // First pull the cursor position relative to the entire buffer out of the console.
+    til::point cursorPosition{ cursor.GetPosition() };
+
+    // Now adjust it for its position in respect to the current viewport top.
+    cursorPosition.y -= viewport.top;
+
+    // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1.
+    cursorPosition.x++;
+    cursorPosition.y++;
+
+    // If the origin mode is relative, line numbers start at top of the scrolling region.
+    if (_modes.test(Mode::Origin))
+    {
+        cursorPosition.y -= _GetVerticalMargins(viewport, false).first;
+    }
+
+    // Paging is not supported yet (GH#13892).
+    const auto pageNumber = 1;
+
+    // Only some of the rendition attributes are reported.
+    auto renditionAttributes = L'@';
+    renditionAttributes += (attributes.IsIntense() ? 1 : 0);
+    renditionAttributes += (attributes.IsUnderlined() ? 2 : 0);
+    renditionAttributes += (attributes.IsBlinking() ? 4 : 0);
+    renditionAttributes += (attributes.IsReverseVideo() ? 8 : 0);
+    renditionAttributes += (attributes.IsInvisible() ? 16 : 0);
+
+    // There is only one character attribute.
+    const auto characterAttributes = attributes.IsProtected() ? L'A' : L'@';
+
+    // Miscellaneous flags and modes.
+    auto flags = L'@';
+    flags += (_modes.test(Mode::Origin) ? 1 : 0);
+    flags += (_termOutput.IsSingleShiftPending(2) ? 2 : 0);
+    flags += (_termOutput.IsSingleShiftPending(3) ? 4 : 0);
+    flags += (cursor.IsDelayedEOLWrap() ? 8 : 0);
+
+    // Character set designations.
+    const auto leftSetNumber = _termOutput.GetLeftSetNumber();
+    const auto rightSetNumber = _termOutput.GetRightSetNumber();
+    auto charsetSizes = L'@';
+    charsetSizes += (_termOutput.GetCharsetSize(0) == 96 ? 1 : 0);
+    charsetSizes += (_termOutput.GetCharsetSize(1) == 96 ? 2 : 0);
+    charsetSizes += (_termOutput.GetCharsetSize(2) == 96 ? 4 : 0);
+    charsetSizes += (_termOutput.GetCharsetSize(3) == 96 ? 8 : 0);
+    const auto charset0 = _termOutput.GetCharsetId(0);
+    const auto charset1 = _termOutput.GetCharsetId(1);
+    const auto charset2 = _termOutput.GetCharsetId(2);
+    const auto charset3 = _termOutput.GetCharsetId(3);
+
+    // A valid response always starts with DCS 1 $ u and ends with ST.
+    const auto response = fmt::format(
+        FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"),
+        cursorPosition.y,
+        cursorPosition.x,
+        pageNumber,
+        renditionAttributes,
+        characterAttributes,
+        flags,
+        leftSetNumber,
+        rightSetNumber,
+        charsetSizes,
+        charset0.ToString(),
+        charset1.ToString(),
+        charset2.ToString(),
+        charset3.ToString());
+    _api.ReturnResponse({ response.data(), response.size() });
+}
+
+// Method Description:
+// - DECCIR - This is a parser for the Cursor Information Report received via DECRSPS.
+// Arguments:
+// - <none>
+// Return Value:
+// - a function to parse the report data.
+ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation()
+{
+    // clang-format off
+    enum Field { Row, Column, Page, SGR, Attr, Flags, GL, GR, Sizes, G0, G1, G2, G3 };
+    // clang-format on
+    constexpr til::enumset<Field> numeric{ Field::Row, Field::Column, Field::Page, Field::GL, Field::GR };
+    constexpr til::enumset<Field> flags{ Field::SGR, Field::Attr, Field::Flags, Field::Sizes };
+    constexpr til::enumset<Field> charset{ Field::G0, Field::G1, Field::G2, Field::G3 };
+    struct State
+    {
+        Field field{ Field::Row };
+        VTInt value{ 0 };
+        VTIDBuilder charsetId{};
+        std::array<bool, 4> charset96{};
+        VTParameter row{};
+        VTParameter column{};
+    };
+    auto& textBuffer = _api.GetTextBuffer();
+    return [&, state = State{}](const auto ch) mutable {
+        if (numeric.test(state.field))
+        {
+            if (ch >= '0' && ch <= '9')
+            {
+                state.value *= 10;
+                state.value += (ch - L'0');
+                state.value = std::min(state.value, MAX_PARAMETER_VALUE);
+            }
+            else if (ch == L';' || ch == AsciiChars::ESC)
+            {
+                if (state.field == Field::Row)
+                {
+                    state.row = state.value;
+                }
+                else if (state.field == Field::Column)
+                {
+                    state.column = state.value;
+                }
+                else if (state.field == Field::Page)
+                {
+                    // Paging is not supported yet (GH#13892).
+                }
+                else if (state.field == Field::GL && state.value <= 3)
+                {
+                    LockingShift(state.value);
+                }
+                else if (state.field == Field::GR && state.value <= 3)
+                {
+                    LockingShiftRight(state.value);
+                }
+                state.value = {};
+                state.field = static_cast<Field>(state.field + 1);
+            }
+        }
+        else if (flags.test(state.field))
+        {
+            // Note that there could potentially be multiple characters in a
+            // flag field, so we process the flags as soon as they're received.
+            // But for now we're only interested in the first one, so once the
+            // state.value is set, we ignore everything else until the `;`.
+            if (ch >= L'@' && ch <= '~' && !state.value)
+            {
+                state.value = ch;
+                if (state.field == Field::SGR)
+                {
+                    auto attr = textBuffer.GetCurrentAttributes();
+                    attr.SetIntense(state.value & 1);
+                    attr.SetUnderlined(state.value & 2);
+                    attr.SetBlinking(state.value & 4);
+                    attr.SetReverseVideo(state.value & 8);
+                    attr.SetInvisible(state.value & 16);
+                    textBuffer.SetCurrentAttributes(attr);
+                }
+                else if (state.field == Field::Attr)
+                {
+                    auto attr = textBuffer.GetCurrentAttributes();
+                    attr.SetProtected(state.value & 1);
+                    textBuffer.SetCurrentAttributes(attr);
+                }
+                else if (state.field == Field::Sizes)
+                {
+                    state.charset96.at(0) = state.value & 1;
+                    state.charset96.at(1) = state.value & 2;
+                    state.charset96.at(2) = state.value & 4;
+                    state.charset96.at(3) = state.value & 8;
+                }
+                else if (state.field == Field::Flags)
+                {
+                    const bool originMode = state.value & 1;
+                    const bool ss2 = state.value & 2;
+                    const bool ss3 = state.value & 4;
+                    const bool delayedEOLWrap = state.value & 8;
+                    // The cursor position is parsed at the start of the sequence,
+                    // but we only set the position once we know the origin mode.
+                    _modes.set(Mode::Origin, originMode);
+                    CursorPosition(state.row, state.column);
+                    // There can only be one single shift applied at a time, so
+                    // we'll just apply the last one that is enabled.
+                    _termOutput.SingleShift(ss3 ? 3 : (ss2 ? 2 : 0));
+                    // The EOL flag will always be reset by the cursor movement
+                    // above, so we only need to worry about setting it.
+                    if (delayedEOLWrap)
+                    {
+                        textBuffer.GetCursor().DelayEOLWrap();
+                    }
+                }
+            }
+            else if (ch == L';')
+            {
+                state.value = 0;
+                state.field = static_cast<Field>(state.field + 1);
+            }
+        }
+        else if (charset.test(state.field))
+        {
+            if (ch >= L' ' && ch <= L'/')
+            {
+                state.charsetId.AddIntermediate(ch);
+            }
+            else if (ch >= L'0' && ch <= L'~')
+            {
+                const auto id = state.charsetId.Finalize(ch);
+                const auto gset = state.field - Field::G0;
+                if (state.charset96.at(gset))
+                {
+                    Designate96Charset(gset, id);
+                }
+                else
+                {
+                    Designate94Charset(gset, id);
+                }
+                state.charsetId.Clear();
+                state.field = static_cast<Field>(state.field + 1);
+            }
+        }
+        return (ch != AsciiChars::ESC);
+    };
+}
+
+// Method Description:
+// - DECTABSR - Returns the Tabulation Stop Report in response to a DECRQPSR query.
+// Arguments:
+// - None
+// Return Value:
+// - None
+void AdaptDispatch::_ReportTabStops()
+{
+    // In order to be compatible with the original hardware terminals, we only
+    // report tab stops up to the current buffer width, even though there may
+    // be positions recorded beyond that limit.
+    const auto width = _api.GetTextBuffer().GetSize().Dimensions().width;
+    _InitTabStopsForWidth(width);
+
+    using namespace std::string_view_literals;
+
+    // A valid response always starts with DCS 2 $ u.
+    fmt::basic_memory_buffer<wchar_t, 64> response;
+    response.append(L"\033P2$u"sv);
+
+    auto need_separator = false;
+    for (auto column = 0; column < width; column++)
+    {
+        if (til::at(_tabStopColumns, column))
+        {
+            response.append(need_separator ? L"/"sv : L""sv);
+            fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{}"), column + 1);
+            need_separator = true;
+        }
+    }
+
+    // An ST ends the sequence.
+    response.append(L"\033\\"sv);
+    _api.ReturnResponse({ response.data(), response.size() });
+}
+
+// Method Description:
+// - DECTABSR - This is a parser for the Tabulation Stop Report received via DECRSPS.
+// Arguments:
+// - <none>
+// Return Value:
+// - a function to parse the report data.
+ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops()
+{
+    // In order to be compatible with the original hardware terminals, we need
+    // to be able to set tab stops up to at least 132 columns, even though the
+    // current buffer width may be less than that.
+    const auto width = std::max(_api.GetTextBuffer().GetSize().Dimensions().width, 132);
+    _ClearAllTabStops();
+    _InitTabStopsForWidth(width);
+
+    return [this, width, column = size_t{}](const auto ch) mutable {
+        if (ch >= L'0' && ch <= L'9')
+        {
+            column *= 10;
+            column += (ch - L'0');
+            column = std::min<size_t>(column, MAX_PARAMETER_VALUE);
+        }
+        else if (ch == L'/' || ch == AsciiChars::ESC)
+        {
+            // Note that column 1 is always a tab stop, so there is no
+            // need to record an entry at that offset.
+            if (column > 1u && column <= static_cast<size_t>(width))
+            {
+                _tabStopColumns.at(column - 1) = true;
+            }
+            column = 0;
+        }
+        else
+        {
+            // If we receive an unexpected character, we don't try and
+            // process any more of the input - we just abort.
+            return false;
+        }
+        return (ch != AsciiChars::ESC);
+    };
+}
+
 // Routine Description:
 // - DECPS - Plays a sequence of musical notes.
 // Arguments:
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index aab3402416f..371cdddaf92 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -104,7 +104,7 @@ namespace Microsoft::Console::VirtualTerminal
         bool Designate96Charset(const VTInt gsetNumber, const VTID charset) override; // SCS
         bool LockingShift(const VTInt gsetNumber) override; // LS0, LS1, LS2, LS3
         bool LockingShiftRight(const VTInt gsetNumber) override; // LS1R, LS2R, LS3R
-        bool SingleShift(const VTInt gsetNumber) override; // SS2, SS3
+        bool SingleShift(const VTInt gsetNumber) noexcept override; // SS2, SS3
         bool AcceptC1Controls(const bool enabled) override; // DECAC1
         bool SoftReset() override; // DECSTR
         bool HardReset() override; // RIS
@@ -151,6 +151,9 @@ namespace Microsoft::Console::VirtualTerminal
 
         StringHandler RequestSetting() override; // DECRQSS
 
+        bool RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format) override; // DECRQPSR
+        StringHandler RestorePresentationState(const DispatchTypes::PresentationReportFormat format) override; // DECRSPS
+
         bool PlaySounds(const VTParameters parameters) override; // DECPS
 
     private:
@@ -237,6 +240,11 @@ namespace Microsoft::Console::VirtualTerminal
         void _ReportDECSCASetting() const;
         void _ReportDECSACESetting() const;
 
+        void _ReportCursorInformation();
+        StringHandler _RestoreCursorInformation();
+        void _ReportTabStops();
+        StringHandler _RestoreTabStops();
+
         StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize);
         StringHandler _CreatePassthroughHandler();
 
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 394c5c27109..9069c892b7b 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -143,6 +143,9 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
 
     StringHandler RequestSetting() override { return nullptr; }; // DECRQSS
 
+    bool RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat /*format*/) override { return false; } // DECRQPSR
+    StringHandler RestorePresentationState(const DispatchTypes::PresentationReportFormat /*format*/) override { return nullptr; } // DECRSPS
+
     bool PlaySounds(const VTParameters /*parameters*/) override { return false; }; // DECPS
 };
 
diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp
index 8a5d4923991..f18f3512c64 100644
--- a/src/terminal/adapter/terminalOutput.cpp
+++ b/src/terminal/adapter/terminalOutput.cpp
@@ -20,12 +20,17 @@ TerminalOutput::TerminalOutput() noexcept
     _gsetTranslationTables.at(1) = Ascii;
     _gsetTranslationTables.at(2) = Ascii;
     _gsetTranslationTables.at(3) = Ascii;
+    _gsetIds.at(0) = VTID("B");
+    _gsetIds.at(1) = VTID("B");
+    _gsetIds.at(2) = VTID("B");
+    _gsetIds.at(3) = VTID("B");
 }
 
 bool TerminalOutput::Designate94Charset(size_t gsetNumber, const VTID charset)
 {
     const auto translationTable = _LookupTranslationTable94(charset);
     RETURN_BOOL_IF_FALSE(!translationTable.empty());
+    _gsetIds.at(gsetNumber) = charset;
     return _SetTranslationTable(gsetNumber, translationTable);
 }
 
@@ -33,6 +38,7 @@ bool TerminalOutput::Designate96Charset(size_t gsetNumber, const VTID charset)
 {
     const auto translationTable = _LookupTranslationTable96(charset);
     RETURN_BOOL_IF_FALSE(!translationTable.empty());
+    _gsetIds.at(gsetNumber) = charset;
     return _SetTranslationTable(gsetNumber, translationTable);
 }
 
@@ -50,6 +56,16 @@ void TerminalOutput::SetDrcs96Designation(const VTID charset)
     _drcsTranslationTable = Drcs96;
 }
 
+VTID TerminalOutput::GetCharsetId(const size_t gsetNumber) const
+{
+    return _gsetIds.at(gsetNumber);
+}
+
+size_t TerminalOutput::GetCharsetSize(const size_t gsetNumber) const
+{
+    return _gsetTranslationTables.at(gsetNumber).size() == 96 ? 96 : 94;
+}
+
 #pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning
 bool TerminalOutput::LockingShift(const size_t gsetNumber)
 {
@@ -76,13 +92,27 @@ bool TerminalOutput::LockingShiftRight(const size_t gsetNumber)
     return true;
 }
 
-#pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning
-bool TerminalOutput::SingleShift(const size_t gsetNumber)
+bool TerminalOutput::SingleShift(const size_t gsetNumber) noexcept
 {
-    _ssTranslationTable = _gsetTranslationTables.at(gsetNumber);
+    _ssSetNumber = gsetNumber;
     return true;
 }
 
+size_t TerminalOutput::GetLeftSetNumber() const noexcept
+{
+    return _glSetNumber;
+}
+
+size_t TerminalOutput::GetRightSetNumber() const noexcept
+{
+    return _grSetNumber;
+}
+
+bool TerminalOutput::IsSingleShiftPending(const size_t gsetNumber) const noexcept
+{
+    return _ssSetNumber == gsetNumber;
+}
+
 // Routine Description:
 // - Returns true if there is an active translation table, indicating that text has to come through here
 // Arguments:
@@ -91,7 +121,7 @@ bool TerminalOutput::SingleShift(const size_t gsetNumber)
 // - True if translation is required.
 bool TerminalOutput::NeedToTranslate() const noexcept
 {
-    return !_glTranslationTable.empty() || !_grTranslationTable.empty() || !_ssTranslationTable.empty();
+    return !_glTranslationTable.empty() || !_grTranslationTable.empty() || _ssSetNumber != 0;
 }
 
 void TerminalOutput::EnableGrTranslation(boolean enabled)
@@ -110,17 +140,18 @@ void TerminalOutput::EnableGrTranslation(boolean enabled)
 wchar_t TerminalOutput::TranslateKey(const wchar_t wch) const noexcept
 {
     auto wchFound = wch;
-    if (!_ssTranslationTable.empty())
+    if (_ssSetNumber == 2 || _ssSetNumber == 3)
     {
-        if (wch - 0x20u < _ssTranslationTable.size())
+        const auto ssTranslationTable = _gsetTranslationTables.at(_ssSetNumber);
+        if (wch - 0x20u < ssTranslationTable.size())
         {
-            wchFound = _ssTranslationTable.at(wch - 0x20u);
+            wchFound = ssTranslationTable.at(wch - 0x20u);
         }
-        else if (wch - 0xA0u < _ssTranslationTable.size())
+        else if (wch - 0xA0u < ssTranslationTable.size())
         {
-            wchFound = _ssTranslationTable.at(wch - 0xA0u);
+            wchFound = ssTranslationTable.at(wch - 0xA0u);
         }
-        _ssTranslationTable = {};
+        _ssSetNumber = 0;
     }
     else
     {
diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp
index aabc12657bf..3b77a3da612 100644
--- a/src/terminal/adapter/terminalOutput.hpp
+++ b/src/terminal/adapter/terminalOutput.hpp
@@ -30,9 +30,14 @@ namespace Microsoft::Console::VirtualTerminal
         bool Designate96Charset(const size_t gsetNumber, const VTID charset);
         void SetDrcs94Designation(const VTID charset);
         void SetDrcs96Designation(const VTID charset);
+        VTID GetCharsetId(const size_t gsetNumber) const;
+        size_t GetCharsetSize(const size_t gsetNumber) const;
         bool LockingShift(const size_t gsetNumber);
         bool LockingShiftRight(const size_t gsetNumber);
-        bool SingleShift(const size_t gsetNumber);
+        bool SingleShift(const size_t gsetNumber) noexcept;
+        size_t GetLeftSetNumber() const noexcept;
+        size_t GetRightSetNumber() const noexcept;
+        bool IsSingleShiftPending(const size_t gsetNumber) const noexcept;
         bool NeedToTranslate() const noexcept;
         void EnableGrTranslation(boolean enabled);
 
@@ -43,11 +48,12 @@ namespace Microsoft::Console::VirtualTerminal
         void _ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable);
 
         std::array<std::wstring_view, 4> _gsetTranslationTables;
+        std::array<VTID, 4> _gsetIds;
         size_t _glSetNumber = 0;
         size_t _grSetNumber = 2;
         std::wstring_view _glTranslationTable;
         std::wstring_view _grTranslationTable;
-        mutable std::wstring_view _ssTranslationTable;
+        mutable size_t _ssSetNumber = 0;
         boolean _grTranslationEnabled = false;
         VTID _drcsId = 0;
         std::wstring_view _drcsTranslationTable;
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 4b49d63a25a..16508a1354a 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -1903,6 +1903,209 @@ class AdapterTest
         verifyChecksumReport(L"FF8B");
     }
 
+    TEST_METHOD(TabulationStopReportTests)
+    {
+        _testGetSet->PrepData();
+        auto& textBuffer = *_testGetSet->_textBuffer;
+        auto& stateMachine = *_testGetSet->_stateMachine;
+
+        Log::Comment(L"Default tabs stops in 80-column mode");
+        VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 80, 600 }));
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u9/17/25/33/41/49/57/65/73\033\\");
+
+        Log::Comment(L"Default tabs stops in 132-column mode");
+        VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 132, 600 }));
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u9/17/25/33/41/49/57/65/73/81/89/97/105/113/121/129\033\\");
+
+        Log::Comment(L"Custom tab stops in 80 columns");
+        VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 80, 600 }));
+        _testGetSet->_stateMachine->ProcessString(L"\033P2$t30/60/120/240\033\\");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u30/60\033\\");
+
+        Log::Comment(L"After expanding width to 132 columns");
+        VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 132, 600 }));
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u30/60/120\033\\");
+        VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 80, 600 }));
+
+        Log::Comment(L"Out of order tab stops");
+        stateMachine.ProcessString(L"\033P2$t44/22/66\033\\");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u22/44/66\033\\");
+
+        Log::Comment(L"Empty tab stop are ignored");
+        stateMachine.ProcessString(L"\033P2$t3//7\033\\");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u3/7\033\\");
+
+        Log::Comment(L"'0' tab stops are ignored");
+        stateMachine.ProcessString(L"\033P2$t0/5/10\033\\");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u5/10\033\\");
+
+        Log::Comment(L"'1' tab stops are ignored");
+        stateMachine.ProcessString(L"\033P2$t1/8/18\033\\");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u8/18\033\\");
+
+        Log::Comment(L"Clear tab stops");
+        _pDispatch->TabClear(DispatchTypes::TabClearType::ClearAllColumns);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport);
+        _testGetSet->ValidateInputEvent(L"\033P2$u\033\\");
+    }
+
+    TEST_METHOD(CursorInformationReportTests)
+    {
+        _testGetSet->PrepData();
+        auto& textBuffer = *_testGetSet->_textBuffer;
+        auto& stateMachine = *_testGetSet->_stateMachine;
+        auto& termOutput = _pDispatch->_termOutput;
+        const auto viewportTop = _testGetSet->_viewport.top;
+        auto attributes = TextAttribute{};
+
+        Log::Comment(L"Initial state");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;@;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Cursor position");
+        textBuffer.GetCursor().SetPosition({ 3, viewportTop + 2 });
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;@;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Intense rendition");
+        attributes.SetIntense(true);
+        textBuffer.SetCurrentAttributes(attributes);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;A;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Underlined rendition");
+        attributes.SetUnderlined(true);
+        textBuffer.SetCurrentAttributes(attributes);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;C;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Blinking rendition");
+        attributes.SetBlinking(true);
+        textBuffer.SetCurrentAttributes(attributes);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;G;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Reverse video rendition");
+        attributes.SetReverseVideo(true);
+        textBuffer.SetCurrentAttributes(attributes);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;O;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Invisible rendition");
+        attributes.SetInvisible(true);
+        textBuffer.SetCurrentAttributes(attributes);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;_;@;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Protected attribute");
+        attributes.SetProtected(true);
+        textBuffer.SetCurrentAttributes(attributes);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;_;A;@;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Origin mode");
+        _pDispatch->SetMode(DispatchTypes::DECOM_OriginMode);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;_;A;A;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Single shift 2");
+        _pDispatch->SingleShift(2);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;_;A;C;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Single shift 3");
+        _pDispatch->SingleShift(3);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;_;A;E;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Delayed EOL wrap");
+        _pDispatch->CursorForward(999);
+        _pDispatch->Print(L'*');
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;0;2;@;BBBB\033\\");
+
+        Log::Comment(L"Locking shifts");
+        _pDispatch->LockingShift(1);
+        _pDispatch->LockingShiftRight(3);
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;@;BBBB\033\\");
+
+        Log::Comment(L"94 charset designations");
+        _pDispatch->Designate94Charset(0, "%5");
+        _pDispatch->Designate94Charset(1, "<");
+        _pDispatch->Designate94Charset(2, "0");
+        _pDispatch->Designate94Charset(3, "K");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;@;%5<0K\033\\");
+
+        Log::Comment(L"96 charset designation (G1)");
+        _pDispatch->Designate96Charset(1, "H");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;B;%5H0K\033\\");
+
+        Log::Comment(L"96 charset designation (G2)");
+        _pDispatch->Designate96Charset(2, "M");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;F;%5HMK\033\\");
+
+        Log::Comment(L"96 charset designation (G3)");
+        _pDispatch->Designate96Charset(3, "B");
+        _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport);
+        _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;N;%5HMB\033\\");
+
+        Log::Comment(L"Restore cursor position");
+        stateMachine.ProcessString(L"\033P1$t3;4;1;@;@;@;0;2;@;BBBB\033\\");
+        auto expectedPosition = til::point{ 3, viewportTop + 2 };
+        VERIFY_ARE_EQUAL(expectedPosition, textBuffer.GetCursor().GetPosition());
+
+        Log::Comment(L"Restore rendition attributes");
+        stateMachine.ProcessString(L"\033P1$t1;1;1;U;@;@;0;2;@;BBBB\033\\");
+        attributes = {};
+        attributes.SetIntense(true);
+        attributes.SetBlinking(true);
+        attributes.SetInvisible(true);
+        VERIFY_ARE_EQUAL(attributes, textBuffer.GetCurrentAttributes());
+        stateMachine.ProcessString(L"\033P1$t1;1;1;J;A;@;0;2;@;BBBB\033\\");
+        attributes = {};
+        attributes.SetUnderlined(true);
+        attributes.SetReverseVideo(true);
+        attributes.SetProtected(true);
+        VERIFY_ARE_EQUAL(attributes, textBuffer.GetCurrentAttributes());
+
+        Log::Comment(L"Restore flags");
+        stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;E;0;2;@;BBBB\033\\");
+        VERIFY_IS_TRUE(_pDispatch->_modes.test(AdaptDispatch::Mode::Origin));
+        VERIFY_IS_FALSE(termOutput.IsSingleShiftPending(2));
+        VERIFY_IS_TRUE(termOutput.IsSingleShiftPending(3));
+        VERIFY_IS_FALSE(textBuffer.GetCursor().IsDelayedEOLWrap());
+        stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;J;0;2;@;BBBB\033\\");
+        VERIFY_IS_FALSE(_pDispatch->_modes.test(AdaptDispatch::Mode::Origin));
+        VERIFY_IS_TRUE(termOutput.IsSingleShiftPending(2));
+        VERIFY_IS_FALSE(termOutput.IsSingleShiftPending(3));
+        VERIFY_IS_TRUE(textBuffer.GetCursor().IsDelayedEOLWrap());
+
+        Log::Comment(L"Restore charset configuration");
+        stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;@;3;1;H;ABCF\033\\");
+        VERIFY_ARE_EQUAL(3u, termOutput.GetLeftSetNumber());
+        VERIFY_ARE_EQUAL(1u, termOutput.GetRightSetNumber());
+        VERIFY_ARE_EQUAL(94u, termOutput.GetCharsetSize(0));
+        VERIFY_ARE_EQUAL(94u, termOutput.GetCharsetSize(1));
+        VERIFY_ARE_EQUAL(94u, termOutput.GetCharsetSize(2));
+        VERIFY_ARE_EQUAL(96u, termOutput.GetCharsetSize(3));
+        VERIFY_ARE_EQUAL(VTID("A"), termOutput.GetCharsetId(0));
+        VERIFY_ARE_EQUAL(VTID("B"), termOutput.GetCharsetId(1));
+        VERIFY_ARE_EQUAL(VTID("C"), termOutput.GetCharsetId(2));
+        VERIFY_ARE_EQUAL(VTID("F"), termOutput.GetCharsetId(3));
+    }
+
     TEST_METHOD(CursorKeysModeTest)
     {
         Log::Comment(L"Starting test...");
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 6c8c2661a08..3f191e34198 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -668,6 +668,10 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         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));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCRA);
         break;
+    case CsiActionCodes::DECRQPSR_RequestPresentationStateReport:
+        success = _dispatch->RequestPresentationStateReport(parameters.at(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRQPSR);
+        break;
     case CsiActionCodes::DECFRA_FillRectangularArea:
         success = _dispatch->FillRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2), parameters.at(3).value_or(0), parameters.at(4).value_or(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECFRA);
@@ -752,6 +756,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c
     case DcsActionCodes::DECRQSS_RequestSetting:
         handler = _dispatch->RequestSetting();
         break;
+    case DcsActionCodes::DECRSPS_RestorePresentationState:
+        handler = _dispatch->RestorePresentationState(parameters.at(0));
+        break;
     default:
         handler = nullptr;
         break;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 72ff5bbe91c..468cf4e6700 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -154,6 +154,7 @@ namespace Microsoft::Console::VirtualTerminal
             DECCARA_ChangeAttributesRectangularArea = VTID("$r"),
             DECRARA_ReverseAttributesRectangularArea = VTID("$t"),
             DECCRA_CopyRectangularArea = VTID("$v"),
+            DECRQPSR_RequestPresentationStateReport = VTID("$w"),
             DECFRA_FillRectangularArea = VTID("$x"),
             DECERA_EraseRectangularArea = VTID("$z"),
             DECSERA_SelectiveEraseRectangularArea = VTID("${"),
@@ -170,7 +171,8 @@ namespace Microsoft::Console::VirtualTerminal
             DECDLD_DownloadDRCS = VTID("{"),
             DECDMAC_DefineMacro = VTID("!z"),
             DECRSTS_RestoreTerminalState = VTID("$p"),
-            DECRQSS_RequestSetting = VTID("$q")
+            DECRQSS_RequestSetting = VTID("$q"),
+            DECRSPS_RestorePresentationState = VTID("$t"),
         };
 
         enum Vt52ActionCodes : uint64_t
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index 474e49b5947..f5dbc68ecd6 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -289,6 +289,7 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[DECCARA], "DECCARA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECRARA], "DECRARA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECCRA], "DECCRA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECRQPSR], "DECRQPSR"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECFRA], "DECFRA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"),
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 63d850675e1..79132ec8241 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -116,6 +116,7 @@ namespace Microsoft::Console::VirtualTerminal
             DECCARA,
             DECRARA,
             DECCRA,
+            DECRQPSR,
             DECFRA,
             DECERA,
             DECSERA,


Test patch: