From 6140fd9ab8d902546dc47100d6e62d157d1fe8b9 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Thu, 9 Sep 2021 00:26:44 +0100 Subject: [PATCH] Add basic support for the DECRQSS settings query (#11152) This PR adds support for the `DECRQSS` (Request Selection or Setting) escape sequence, which is a standard VT query for reporting the state of various control functions. This initial implementation only supports queries for the `DECSTBM` margins, and the `SGR` graphic rendition attributes. This can be useful for certain forms of capability detection (#1040). As one example in particular, it can serve as an alternative to the `COLORTERM` environment variable for detecting truecolor support (#11057). Of the settings that can be queried by `DECRQSS`, the only other one that we could be supporting at the moment is `DECSCUSR` (Cursor Style). However, that would require passing the query through to the conpty client, which is a lot more complicated, so I thought it best to leave for a future PR. For now this gets the basic framework in place, so we are at least responding to queries, and even just supporting the `SGR` attributes query is useful in itself. Validation ---------- I've added a unit test verifying the reports for the `DECSTBM` and `SGR` settings with a range of different parameters. I've also tested the `DECSTBM` and `SGR` reports manually in _Vttest_, under menu 11.2.5.3.6 (Status-String Reports). --- .github/actions/spelling/expect/expect.txt | 1 + src/terminal/adapter/ITermDispatch.hpp | 2 + src/terminal/adapter/adaptDispatch.cpp | 146 ++++++++++++++++++ src/terminal/adapter/adaptDispatch.hpp | 5 + src/terminal/adapter/termDispatch.hpp | 2 + .../adapter/ut_adapter/adapterTest.cpp | 103 ++++++++++++ .../parser/OutputStateMachineEngine.cpp | 3 + .../parser/OutputStateMachineEngine.hpp | 1 + 8 files changed, 263 insertions(+) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 8ed94f9d496..1842360cf25 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -536,6 +536,7 @@ DECRC DECREQTPARM DECRLM DECRQM +DECRQSS DECRST DECSASD DECSC diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index b0efd81d892..c95a18d7071 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -141,6 +141,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch const DispatchTypes::DrcsFontUsage fontUsage, const VTParameter cellHeight, const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD + + virtual StringHandler RequestSetting() = 0; // DECRQSS }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() {} #pragma warning(pop) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 50db520c0a1..9bf49eeee26 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2527,6 +2527,152 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const size_t fontNumber }; } +// 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 +// sequence, which are passed to the string handler. +// Arguments: +// - None +// Return Value: +// - a function to receive the VTID of the setting being queried +ITermDispatch::StringHandler AdaptDispatch::RequestSetting() +{ + // We use a VTIDBuilder to parse the characters in the control string into + // an ID which represents the setting being queried. If the given ID isn't + // supported, we respond with an error sequence: DCS 0 $ r ST. Note that + // this is the opposite of what is documented in most DEC manuals, which + // say that 0 is for a valid response, and 1 is for an error. The correct + // interpretation is documented in the DEC STD 070 reference. + const auto idBuilder = std::make_shared(); + return [=](const auto ch) { + if (ch >= '\x40' && ch <= '\x7e') + { + const auto id = idBuilder->Finalize(ch); + switch (id) + { + case VTID('m'): + _ReportSGRSetting(); + break; + case VTID('r'): + _ReportDECSTBMSetting(); + break; + default: + _WriteResponse(L"\033P0$r\033\\"); + break; + } + return false; + } + else + { + if (ch >= '\x20' && ch <= '\x2f') + { + idBuilder->AddIntermediate(ch); + } + return true; + } + }; +} + +// Method Description: +// - Reports the current SGR attributes in response to a DECRQSS query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportSGRSetting() const +{ + // A valid response always starts with DCS 1 $ r. + // Then the '0' parameter is to reset the SGR attributes to the defaults. + std::wstring response = L"\033P1$r0"; + + TextAttribute attr; + if (_pConApi->PrivateGetTextAttributes(attr)) + { + // For each boolean attribute that is set, we add the appropriate + // parameter value to the response string. + const auto addAttribute = [&](const auto parameter, const auto enabled) { + if (enabled) + { + response += parameter; + } + }; + addAttribute(L";1", attr.IsBold()); + addAttribute(L";2", attr.IsFaint()); + addAttribute(L";3", attr.IsItalic()); + addAttribute(L";4", attr.IsUnderlined()); + addAttribute(L";5", attr.IsBlinking()); + addAttribute(L";7", attr.IsReverseVideo()); + addAttribute(L";8", attr.IsInvisible()); + addAttribute(L";9", attr.IsCrossedOut()); + addAttribute(L";21", attr.IsDoublyUnderlined()); + addAttribute(L";53", attr.IsOverlined()); + + // We also need to add the appropriate color encoding parameters for + // both the foreground and background colors. + const auto addColor = [&](const auto base, const auto color) { + const auto iterator = std::back_insert_iterator(response); + if (color.IsIndex16()) + { + const auto index = XtermToWindowsIndex(color.GetIndex()); + const auto colorParameter = base + (index >= 8 ? 60 : 0) + (index % 8); + fmt::format_to(iterator, FMT_STRING(L";{}"), colorParameter); + } + else if (color.IsIndex256()) + { + const auto index = Xterm256ToWindowsIndex(color.GetIndex()); + fmt::format_to(iterator, FMT_STRING(L";{};5;{}"), base + 8, index); + } + else if (color.IsRgb()) + { + const auto r = GetRValue(color.GetRGB()); + const auto g = GetGValue(color.GetRGB()); + const auto b = GetBValue(color.GetRGB()); + fmt::format_to(iterator, FMT_STRING(L";{};2;{};{};{}"), base + 8, r, g, b); + } + }; + addColor(30, attr.GetForeground()); + addColor(40, attr.GetBackground()); + } + + // The 'm' indicates this is an SGR response, and ST ends the sequence. + response += L"m\033\\"; + _WriteResponse(response); +} + +// Method Description: +// - Reports the DECSTBM margin range in response to a DECRQSS query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportDECSTBMSetting() const +{ + // A valid response always starts with DCS 1 $ r. + std::wstring response = L"\033P1$r"; + + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + if (_pConApi->GetConsoleScreenBufferInfoEx(csbiex)) + { + auto marginTop = _scrollMargins.Top + 1; + auto marginBottom = _scrollMargins.Bottom + 1; + // If the margin top is greater than or equal to the bottom, then the + // margins aren't actually set, so we need to return the full height + // of the window for the margin range. + if (marginTop >= marginBottom) + { + marginTop = 1; + marginBottom = csbiex.srWindow.Bottom - csbiex.srWindow.Top; + } + const auto iterator = std::back_insert_iterator(response); + fmt::format_to(iterator, FMT_STRING(L"{};{}"), marginTop, marginBottom); + } + + // The 'r' indicates this is an DECSTBM response, and ST ends the sequence. + response += L"r\033\\"; + _WriteResponse(response); +} + // Routine Description: // - Determines whether we should pass any sequence that manipulates // TerminalInput's input generator through the PTY. It encapsulates diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 7eeade5cb9a..b77345e51a7 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -140,6 +140,8 @@ namespace Microsoft::Console::VirtualTerminal const VTParameter cellHeight, const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD + StringHandler RequestSetting() override; // DECRQSS + private: enum class ScrollDirection { @@ -189,6 +191,9 @@ namespace Microsoft::Console::VirtualTerminal void _ResetTabStops() noexcept; void _InitTabStopsForWidth(const size_t width); + void _ReportSGRSetting() const; + void _ReportDECSTBMSetting() const; + bool _ShouldPassThroughInputModeChange() const; std::vector _tabStopColumns; diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 1fcb83a4e4c..16af1776e1a 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -132,4 +132,6 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons const DispatchTypes::DrcsFontUsage /*fontUsage*/, const VTParameter /*cellHeight*/, const DispatchTypes::DrcsCharsetSize /*charsetSize*/) noexcept override { return nullptr; } + + StringHandler RequestSetting() noexcept override { return nullptr; }; // DECRQSS }; diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index f65e64059b8..8760114225d 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -1990,6 +1990,109 @@ class AdapterTest VERIFY_IS_FALSE(_pDispatch.get()->RequestTerminalParameters(DispatchTypes::ReportingPermission::Unsolicited)); } + TEST_METHOD(RequestSettingsTests) + { + const auto requestSetting = [=](const std::wstring_view settingId = {}) { + const auto stringHandler = _pDispatch.get()->RequestSetting(); + for (auto ch : settingId) + { + stringHandler(ch); + } + stringHandler(L'\033'); // String terminator + }; + + Log::Comment(L"Requesting DECSTBM margins (5 to 10)."); + _testGetSet->PrepData(); + _pDispatch.get()->SetTopBottomScrollingMargins(5, 10); + requestSetting(L"r"); + _testGetSet->ValidateInputEvent(L"\033P1$r5;10r\033\\"); + + Log::Comment(L"Requesting DECSTBM margins (full screen)."); + _testGetSet->PrepData(); + // Set screen height to 25 - this will be the expected margin range. + _testGetSet->_viewport.Bottom = _testGetSet->_viewport.Top + 25; + _pDispatch.get()->SetTopBottomScrollingMargins(0, 0); + requestSetting(L"r"); + _testGetSet->ValidateInputEvent(L"\033P1$r1;25r\033\\"); + + Log::Comment(L"Requesting SGR attributes (default)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0m\033\\"); + + Log::Comment(L"Requesting SGR attributes (bold, underlined, reversed)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetBold(true); + _testGetSet->_attribute.SetUnderlined(true); + _testGetSet->_attribute.SetReverseVideo(true); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;1;4;7m\033\\"); + + Log::Comment(L"Requesting SGR attributes (faint, blinking, invisible)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetFaint(true); + _testGetSet->_attribute.SetBlinking(true); + _testGetSet->_attribute.SetInvisible(true); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;2;5;8m\033\\"); + + Log::Comment(L"Requesting SGR attributes (italic, crossed-out)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetItalic(true); + _testGetSet->_attribute.SetCrossedOut(true); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;3;9m\033\\"); + + Log::Comment(L"Requesting SGR attributes (doubly underlined, overlined)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetDoublyUnderlined(true); + _testGetSet->_attribute.SetOverlined(true); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;21;53m\033\\"); + + Log::Comment(L"Requesting SGR attributes (standard colors)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetIndexedForeground((BYTE)::XtermToWindowsIndex(3)); + _testGetSet->_attribute.SetIndexedBackground((BYTE)::XtermToWindowsIndex(6)); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;33;46m\033\\"); + + Log::Comment(L"Requesting SGR attributes (AIX colors)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetIndexedForeground((BYTE)::XtermToWindowsIndex(14)); + _testGetSet->_attribute.SetIndexedBackground((BYTE)::XtermToWindowsIndex(11)); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;96;103m\033\\"); + + Log::Comment(L"Requesting SGR attributes (ITU indexed colors)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetIndexedForeground256(123); + _testGetSet->_attribute.SetIndexedBackground256(45); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;38;5;123;48;5;45m\033\\"); + + Log::Comment(L"Requesting SGR attributes (ITU RGB colors)."); + _testGetSet->PrepData(); + _testGetSet->_attribute = {}; + _testGetSet->_attribute.SetForeground(RGB(12, 34, 56)); + _testGetSet->_attribute.SetBackground(RGB(65, 43, 21)); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;38;2;12;34;56;48;2;65;43;21m\033\\"); + + Log::Comment(L"Requesting an unsupported setting."); + _testGetSet->PrepData(); + requestSetting(L"x"); + _testGetSet->ValidateInputEvent(L"\033P0$r\033\\"); + } + TEST_METHOD(CursorKeysModeTest) { Log::Comment(L"Starting test..."); diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 953fce4154b..bc9299d0db3 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -661,6 +661,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c parameters.at(6), parameters.at(7)); break; + case DcsActionCodes::DECRQSS_RequestSetting: + handler = _dispatch->RequestSetting(); + break; default: handler = nullptr; break; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 8ef83116046..6c646249174 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -147,6 +147,7 @@ namespace Microsoft::Console::VirtualTerminal enum DcsActionCodes : uint64_t { DECDLD_DownloadDRCS = VTID("{"), + DECRQSS_RequestSetting = VTID("$q") }; enum Vt52ActionCodes : uint64_t