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 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 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: +// - +// 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 numeric{ Field::Row, Field::Column, Field::Page, Field::GL, Field::GR }; + constexpr til::enumset flags{ Field::SGR, Field::Attr, Field::Flags, Field::Sizes }; + constexpr til::enumset charset{ Field::G0, Field::G1, Field::G2, Field::G3 }; + struct State + { + Field field{ Field::Row }; + VTInt value{ 0 }; + VTIDBuilder charsetId{}; + std::array 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(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(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(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 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: +// - +// 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(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(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 _gsetTranslationTables; + std::array _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,