From 72cbe5907810a2751e6214ba544e2939adb01ecd Mon Sep 17 00:00:00 2001 From: Dan Thompson Date: Wed, 17 Feb 2021 18:31:52 -0800 Subject: [PATCH] Add support for XTPUSHSGR / XTPOPSGR (#1978) Implement the `XTPUSHSGR` and `XTPOPSGR` control sequences (see #1796). This change adds a new pair of methods to `ITermDispatch`: `PushGraphicsRendition` and `PopGraphicsRendition`, and then plumbs the change through `AdaptDispatch`, `TerminalDispatch`, `ITerminalApi` and `TerminalApi`. The stack logic is encapsulated in the `SgrStack` class, to allow it to be reused between the two APIs (`AdaptDispatch` and `TerminalDispatch`). Like xterm, only ten levels of nesting are supported. The stack is implemented as a "ring stack": if you push when the stack is full, the bottom of the stack will be dropped to make room. Partial pushes (see the description of `XTPUSHSGR` in Issue #1796) are implemented per xterm spec. ## Validation Steps Performed Tests added, plus manual verification of the feature. Closes #1796 --- .github/actions/spelling/expect/expect.txt | 2 + src/cascadia/TerminalCore/ITerminalApi.hpp | 3 + src/cascadia/TerminalCore/Terminal.hpp | 7 + src/cascadia/TerminalCore/TerminalApi.cpp | 28 +++ .../TerminalCore/TerminalDispatch.hpp | 3 + .../TerminalCore/TerminalDispatchGraphics.cpp | 10 + src/terminal/adapter/DispatchTypes.hpp | 34 +++ src/terminal/adapter/ITermDispatch.hpp | 3 + src/terminal/adapter/adaptDispatch.hpp | 5 + .../adapter/adaptDispatchGraphics.cpp | 45 ++++ src/terminal/adapter/termDispatch.hpp | 3 + .../ut_adapter/Adapter.UnitTests.vcxproj | 3 + .../adapter/ut_adapter/adapterTest.cpp | 138 ++++++++++++ .../parser/OutputStateMachineEngine.cpp | 13 ++ .../parser/OutputStateMachineEngine.hpp | 6 +- src/terminal/parser/telemetry.cpp | 2 + src/terminal/parser/telemetry.hpp | 2 + src/types/inc/sgrStack.hpp | 114 ++++++++++ src/types/lib/types.vcxproj | 2 + src/types/lib/types.vcxproj.filters | 6 + src/types/sgrStack.cpp | 212 ++++++++++++++++++ src/types/sources.inc | 1 + 22 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 src/types/inc/sgrStack.hpp create mode 100644 src/types/sgrStack.cpp diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 3df6051f12d..8b0943584f9 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2843,6 +2843,8 @@ XSubstantial xtended xterm XTest +XTPUSHSGR +XTPOPSGR xunit xutr xvalue diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp index 0ea02440d15..bc1c63fd89a 100644 --- a/src/cascadia/TerminalCore/ITerminalApi.hpp +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -70,6 +70,9 @@ namespace Microsoft::Terminal::Core virtual bool SetWorkingDirectory(std::wstring_view uri) noexcept = 0; virtual std::wstring_view GetWorkingDirectory() noexcept = 0; + virtual bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept = 0; + virtual bool PopGraphicsRendition() noexcept = 0; + protected: ITerminalApi() = default; }; diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 7def2cf72a8..fac17b22979 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -6,6 +6,7 @@ #include #include "../../buffer/out/textBuffer.hpp" +#include "../../types/inc/sgrStack.hpp" #include "../../renderer/inc/BlinkingState.hpp" #include "../../terminal/parser/StateMachine.hpp" #include "../../terminal/input/terminalInput.hpp" @@ -124,6 +125,10 @@ class Microsoft::Terminal::Core::Terminal final : bool SetTaskbarProgress(const size_t state, const size_t progress) noexcept override; bool SetWorkingDirectory(std::wstring_view uri) noexcept override; std::wstring_view GetWorkingDirectory() noexcept override; + + bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override; + bool PopGraphicsRendition() noexcept override; + #pragma endregion #pragma region ITerminalInput @@ -347,6 +352,8 @@ class Microsoft::Terminal::Core::Terminal final : COORD _ConvertToBufferCell(const COORD viewportPos) const; #pragma endregion + Microsoft::Console::VirtualTerminal::SgrStack _sgrStack; + #ifdef UNIT_TESTING friend class TerminalCoreUnitTests::TerminalBufferTests; friend class TerminalCoreUnitTests::TerminalApiTest; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 9bbf8901d62..aaf61778dbf 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -640,3 +640,31 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept { return _workingDirectory; } + +// Method Description: +// - Saves the current text attributes to an internal stack. +// Arguments: +// - options, cOptions: if present, specify which portions of the current text attributes +// should be saved. Only a small subset of GraphicsOptions are actually supported; +// others are ignored. If no options are specified, all attributes are stored. +// Return Value: +// - true +bool Terminal::PushGraphicsRendition(const VTParameters options) noexcept +{ + _sgrStack.Push(_buffer->GetCurrentAttributes(), options); + return true; +} + +// Method Description: +// - Restores text attributes from the internal stack. If only portions of text attributes +// were saved, combines those with the current attributes. +// Arguments: +// - +// Return Value: +// - true +bool Terminal::PopGraphicsRendition() noexcept +{ + const TextAttribute current = _buffer->GetCurrentAttributes(); + _buffer->SetCurrentAttributes(_sgrStack.Pop(current)); + return true; +} diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp index c36e5b48e0d..d01beae8081 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -18,6 +18,9 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool SetGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override; + bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override; + bool PopGraphicsRendition() noexcept override; + bool CursorPosition(const size_t line, const size_t column) noexcept override; // CUP diff --git a/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp index 35755bf9f2a..c70d61b0554 100644 --- a/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp @@ -276,3 +276,13 @@ bool TerminalDispatch::SetGraphicsRendition(const VTParameters options) noexcept _terminalApi.SetTextAttributes(attr); return true; } + +bool TerminalDispatch::PushGraphicsRendition(const VTParameters options) noexcept +{ + return _terminalApi.PushGraphicsRendition(options); +} + +bool TerminalDispatch::PopGraphicsRendition() noexcept +{ + return _terminalApi.PopGraphicsRendition(); +} diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 36817734882..e28dde68252 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -311,6 +311,40 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes BrightBackgroundWhite = 107, }; + // Many of these correspond directly to SGR parameters (the GraphicsOptions enum), but + // these are distinct (notably 10 and 11, which as SGR parameters would select fonts, + // are used here to indicate that the foreground/background colors should be saved). + // From xterm's ctlseqs doc for XTPUSHSGR: + // + // Ps = 1 => Bold. + // Ps = 2 => Faint. + // Ps = 3 => Italicized. + // Ps = 4 => Underlined. + // Ps = 5 => Blink. + // Ps = 7 => Inverse. + // Ps = 8 => Invisible. + // Ps = 9 => Crossed-out characters. + // Ps = 2 1 => Doubly-underlined. + // Ps = 3 0 => Foreground color. + // Ps = 3 1 => Background color. + // + enum class SgrSaveRestoreStackOptions : size_t + { + All = 0, + Boldness = 1, + Faintness = 2, + Italics = 3, + Underline = 4, + Blink = 5, + Negative = 7, + Invisible = 8, + CrossedOut = 9, + DoublyUnderlined = 21, + SaveForegroundColor = 30, + SaveBackgroundColor = 31, + Max = SaveBackgroundColor + }; + enum class AnsiStatusType : size_t { OS_OperatingStatus = 5, diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 92f9765139d..e18f0cd46ab 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -89,6 +89,9 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool SetGraphicsRendition(const VTParameters options) = 0; // SGR + virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR + virtual bool PopGraphicsRendition() = 0; // XTPOPSGR + virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // DECSET virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index deb468da4a2..77f17f5b987 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -19,6 +19,7 @@ Author(s): #include "conGetSet.hpp" #include "adaptDefaults.hpp" #include "terminalOutput.hpp" +#include "..\..\types\inc\sgrStack.hpp" namespace Microsoft::Console::VirtualTerminal { @@ -56,6 +57,8 @@ namespace Microsoft::Console::VirtualTerminal bool InsertCharacter(const size_t count) override; // ICH bool DeleteCharacter(const size_t count) override; // DCH bool SetGraphicsRendition(const VTParameters options) override; // SGR + bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR + bool PopGraphicsRendition() override; // XTPOPSGR bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR bool DeviceAttributes() override; // DA1 bool SecondaryDeviceAttributes() override; // DA2 @@ -199,6 +202,8 @@ namespace Microsoft::Console::VirtualTerminal bool _isDECCOLMAllowed; + SgrStack _sgrStack; + size_t _SetRgbColorsHelper(const VTParameters options, TextAttribute& attr, const bool isForeground) noexcept; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index 65ff4e2b98f..66e419b1c28 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -286,3 +286,48 @@ bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) return success; } + +// Method Description: +// - Saves the current text attributes to an internal stack. +// Arguments: +// - options: if not empty, specify which portions of the current text attributes should +// be saved. Options that are not supported are ignored. If no options are specified, +// all attributes are stored. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) +{ + bool success = true; + TextAttribute currentAttributes; + + success = _pConApi->PrivateGetTextAttributes(currentAttributes); + + if (success) + { + _sgrStack.Push(currentAttributes, options); + } + + return success; +} + +// Method Description: +// - Restores text attributes from the internal stack. If only portions of text attributes +// were saved, combines those with the current attributes. +// Arguments: +// - +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::PopGraphicsRendition() +{ + bool success = true; + TextAttribute currentAttributes; + + success = _pConApi->PrivateGetTextAttributes(currentAttributes); + + if (success) + { + success = _pConApi->PrivateSetTextAttributes(_sgrStack.Pop(currentAttributes)); + } + + return success; +} diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index fac375b1f9b..3fb4b0d2d65 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -83,6 +83,9 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool SetGraphicsRendition(const VTParameters /*options*/) noexcept override { return false; } // SGR + bool PushGraphicsRendition(const VTParameters /*options*/) noexcept override { return false; } // XTPUSHSGR + bool PopGraphicsRendition() noexcept override { return false; } // XTPOPSGR + bool SetMode(const DispatchTypes::ModeParams /*param*/) noexcept override { return false; } // DECSET bool ResetMode(const DispatchTypes::ModeParams /*param*/) noexcept override { return false; } // DECRST diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj index 553c9b275ac..3208d26ba7e 100644 --- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj @@ -57,6 +57,9 @@ {dcf55140-ef6a-4736-a403-957e4f7430bb} + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index bc1b1b01123..5773833117a 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -1599,6 +1599,144 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch.get()->SetGraphicsRendition({ rgOptions, cOptions })); } + TEST_METHOD(GraphicsPushPopTests) + { + Log::Comment(L"Starting test..."); + + _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + + VTParameter rgOptions[16]; + VTParameter rgStackOptions[16]; + size_t cOptions = 1; + + Log::Comment(L"Test 1: Basic push and pop"); + + rgOptions[0] = DispatchTypes::GraphicsOptions::Off; + _testGetSet->_expectedAttribute = {}; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + cOptions = 0; + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + Log::Comment(L"Test 2: Push, change color, pop"); + + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundCyan; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(3); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + cOptions = 0; + _testGetSet->_expectedAttribute = {}; + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + Log::Comment(L"Test 3: two pushes (nested) and pops"); + + // First push: + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundRed; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // Second push: + cOptions = 0; + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundGreen; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // First pop: + cOptions = 0; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + // Second pop: + cOptions = 0; + _testGetSet->_expectedAttribute = {}; + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + Log::Comment(L"Test 4: Save and restore partial attributes"); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundGreen; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::BoldBright; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN); + _testGetSet->_expectedAttribute.SetBold(true); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundBlue; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN); + _testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_BLUE >> 4); + _testGetSet->_expectedAttribute.SetBold(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // Push, specifying that we only want to save the background, the boldness, and double-underline-ness: + cOptions = 3; + rgStackOptions[0] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::Boldness; + rgStackOptions[1] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::SaveBackgroundColor; + rgStackOptions[2] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::DoublyUnderlined; + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + // Now change everything... + cOptions = 2; + rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundGreen; + rgOptions[1] = DispatchTypes::GraphicsOptions::DoublyUnderlined; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_GREEN); + _testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_GREEN >> 4); + _testGetSet->_expectedAttribute.SetBold(true); + _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundRed; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED); + _testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_GREEN >> 4); + _testGetSet->_expectedAttribute.SetBold(true); + _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + rgOptions[0] = DispatchTypes::GraphicsOptions::NotBoldOrFaint; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED); + _testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_GREEN >> 4); + _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // And then restore... + cOptions = 0; + _testGetSet->_expectedAttribute = {}; + _testGetSet->_expectedAttribute.SetIndexedForeground(FOREGROUND_RED); + _testGetSet->_expectedAttribute.SetIndexedBackground(BACKGROUND_BLUE >> 4); + _testGetSet->_expectedAttribute.SetBold(true); + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + } + TEST_METHOD(GraphicsPersistBrightnessTests) { Log::Comment(L"Starting test..."); diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 049dfcaf443..a214f5fd1f7 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -589,6 +589,19 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete success = _dispatch->SoftReset(); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR); break; + + case CsiActionCodes::XT_PushSgr: + case CsiActionCodes::XT_PushSgrAlias: + success = _dispatch->PushGraphicsRendition(parameters); + TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPUSHSGR); + break; + + case CsiActionCodes::XT_PopSgr: + case CsiActionCodes::XT_PopSgrAlias: + success = _dispatch->PopGraphicsRendition(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPOPSGR); + break; + default: // If no functions to call, overall dispatch was a failure. success = false; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 69436840ca5..1ad03b643ed 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -131,7 +131,11 @@ namespace Microsoft::Console::VirtualTerminal DECREQTPARM_RequestTerminalParameters = VTID("x"), DECSCUSR_SetCursorStyle = VTID(" q"), DECSTR_SoftReset = VTID("!p"), - DECSCPP_SetColumnsPerPage = VTID("$|") + XT_PushSgrAlias = VTID("#p"), + XT_PopSgrAlias = VTID("#q"), + XT_PushSgr = VTID("#{"), + XT_PopSgr = VTID("#}"), + DECSCPP_SetColumnsPerPage = VTID("$|"), }; enum Vt52ActionCodes : uint64_t diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index 445b1e1d3fd..d1a6da50cba 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -274,6 +274,8 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"), TraceLoggingUInt32(_uiTimesUsed[REP], "REP"), TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"), + TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"), + TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"), TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"), TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange")); } diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp index 0da17fbc4fb..f472852a224 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -101,6 +101,8 @@ namespace Microsoft::Console::VirtualTerminal OSCBG, DECALN, OSCSCB, + XTPUSHSGR, + XTPOPSGR, // Only use this last enum as a count of the number of codes. NUMBER_OF_CODES }; diff --git a/src/types/inc/sgrStack.hpp b/src/types/inc/sgrStack.hpp new file mode 100644 index 00000000000..84a9a42861e --- /dev/null +++ b/src/types/inc/sgrStack.hpp @@ -0,0 +1,114 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- sgrStack.hpp + +Abstract: +- Encapsulates logic for the XTPUSHSGR / XTPOPSGR VT control sequences, which save and + restore text attributes on a stack. + +--*/ + +#pragma once + +#include "..\..\buffer\out\TextAttribute.hpp" +#include "..\..\terminal\adapter\DispatchTypes.hpp" +#include + +namespace Microsoft::Console::VirtualTerminal +{ + class SgrStack + { + public: + SgrStack() noexcept; + + // Method Description: + // - Saves the specified text attributes onto an internal stack. + // Arguments: + // - currentAttributes - The attributes to save onto the stack. + // - options - If none supplied, the full attributes are saved. Else only the + // specified parts of currentAttributes are saved. + // Return Value: + // - + void Push(const TextAttribute& currentAttributes, + const VTParameters options) noexcept; + + // Method Description: + // - Restores text attributes by removing from the top of the internal stack, + // combining them with the supplied currentAttributes, if appropriate. + // Arguments: + // - currentAttributes - The current text attributes. If only a portion of + // attributes were saved on the internal stack, then those attributes will be + // combined with the currentAttributes passed in to form the return value. + // Return Value: + // - The TextAttribute that has been removed from the top of the stack, possibly + // combined with currentAttributes. + const TextAttribute Pop(const TextAttribute& currentAttributes) noexcept; + + // Xterm allows the save stack to go ten deep, so we'll follow suit. + static constexpr int c_MaxStoredSgrPushes = 10; + + private: + // Note the +1 in the size of the bitset: this is because we use the + // SgrSaveRestoreStackOptions enum values as bitset flags, so they are naturally + // one-based. + typedef std::bitset(DispatchTypes::SgrSaveRestoreStackOptions::Max) + 1> AttrBitset; + + TextAttribute _CombineWithCurrentAttributes(const TextAttribute& currentAttributes, + const TextAttribute& savedAttribute, + const AttrBitset validParts); // valid parts of savedAttribute + + struct SavedSgrAttributes + { + TextAttribute TextAttributes; + AttrBitset ValidParts; // flags that indicate which parts of TextAttributes are meaningful + }; + + // The number of "save slots" on the stack is limited (let's say there are N). So + // there are a couple of problems to think about: what to do about apps that try + // to do more pushes than will fit, and how to recover from garbage (such as + // accidentally running "cat" on a binary file that looks like lots of pushes). + // + // Dealing with more pops than pushes is simple: just ignore pops when the stack + // is empty. + // + // But how should we handle doing more pushes than are supported by the storage? + // + // One approach might be to ignore pushes once the stack is full. Things won't + // look right while the number of outstanding pushes is above the stack, but once + // it gets popped back down into range, things start working again. Put another + // way: with a traditional stack, the first N pushes work, and the last N pops + // work. But that introduces a burden: you have to do something (lots of pops) in + // order to recover from garbage. (There are strategies that could be employed to + // place an upper bound on how many pops are required (say K), but it's still + // something that /must/ be done to recover from a blown stack.) + // + // An alternative approach is a "ring stack": if you do another push when the + // stack is already full, it just drops the bottom of the stack. With this + // strategy, the last N pushes work, and the first N pops work. And the advantage + // of this approach is that there is no "recovery procedure" necessary: if you + // want a clean slate, you can just declare a clean slate--you will always have N + // slots for pushes and pops in front of you. + // + // A ring stack will also lead to apps that are friendlier to cross-app + // pushes/pops. + // + // Consider using a traditional stack. In that case, an app might be tempted to + // always begin by issuing a bunch of pops (K), in order to ensure they have a + // clean state. However, apps that behave that way would not work well with + // cross-app push/pops (e.g. I push before I ssh to my remote system, and will pop + // when after closing the connection, and during the connection I'll run apps on + // the remote host which might also do pushes and pops). By using a ring stack, an + // app does not need to do /anything/ to start in a "clean state"--an app can + // ALWAYS consider its initial state to be clean. + // + // So we've chosen to use a "ring stack", because it is simplest for apps to deal + // with. + + int _nextPushIndex; // will wrap around once the stack is full + int _numSavedAttrs; // how much of _storedSgrAttributes is actually in use + std::array _storedSgrAttributes; + }; +} diff --git a/src/types/lib/types.vcxproj b/src/types/lib/types.vcxproj index b96b1755ba1..e401b6cd9c4 100644 --- a/src/types/lib/types.vcxproj +++ b/src/types/lib/types.vcxproj @@ -22,6 +22,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/src/types/lib/types.vcxproj.filters b/src/types/lib/types.vcxproj.filters index f3b8cf61a6b..efb34ad3735 100644 --- a/src/types/lib/types.vcxproj.filters +++ b/src/types/lib/types.vcxproj.filters @@ -72,6 +72,9 @@ Source Files + + Source Files + Source Files @@ -161,6 +164,9 @@ Header Files + + Header Files + Header Files diff --git a/src/types/sgrStack.cpp b/src/types/sgrStack.cpp new file mode 100644 index 00000000000..42abe8ed409 --- /dev/null +++ b/src/types/sgrStack.cpp @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/sgrStack.hpp" + +using namespace Microsoft::Console::VirtualTerminal::DispatchTypes; + +namespace Microsoft::Console::VirtualTerminal +{ + SgrStack::SgrStack() noexcept : + _nextPushIndex{ 0 }, + _numSavedAttrs{ 0 } + { + } + + void SgrStack::Push(const TextAttribute& currentAttributes, + const VTParameters options) noexcept + { + AttrBitset validParts; + + try + { + if (options.empty()) + { + // We save all current attributes. + validParts.set(static_cast(SgrSaveRestoreStackOptions::All)); + } + else + { + // Each option is encoded as a bit in validParts. All options (that fit) are + // encoded; options that aren't supported are ignored when read back (popped). + // So if you try to save only unsupported aspects of the current text + // attributes, you'll do what is effectively an "empty" push (the subsequent + // pop will not change the current attributes), which is the correct behavior. + + for (size_t i = 0; i < options.size(); i++) + { + const size_t optionAsIndex = options.at(i).value_or(0); + + // Options must be specified singly; not in combination. Values that are + // out of range will be ignored. + if (optionAsIndex < validParts.size()) + { + validParts.set(optionAsIndex); + } + } + } + } + catch (...) + { + // The static analyzer knows that the bitset operations can throw + // std::out_of_range. However, we know that won't happen, because we pre-check + // that everything should be in range. So we plan to never execute this + // failfast: + FAIL_FAST_CAUGHT_EXCEPTION(); + } + + if (_numSavedAttrs < gsl::narrow(_storedSgrAttributes.size())) + { + _numSavedAttrs++; + } + + _storedSgrAttributes.at(_nextPushIndex) = { currentAttributes, validParts }; + _nextPushIndex = (_nextPushIndex + 1) % gsl::narrow(_storedSgrAttributes.size()); + } + + const TextAttribute SgrStack::Pop(const TextAttribute& currentAttributes) noexcept + { + if (_numSavedAttrs > 0) + { + _numSavedAttrs--; + + if (_nextPushIndex == 0) + { + _nextPushIndex = gsl::narrow(_storedSgrAttributes.size() - 1); + } + else + { + _nextPushIndex--; + } + + SavedSgrAttributes& restoreMe = _storedSgrAttributes.at(_nextPushIndex); + + try + { + if (restoreMe.ValidParts.test(static_cast(SgrSaveRestoreStackOptions::All))) + { + return restoreMe.TextAttributes; + } + else + { + return _CombineWithCurrentAttributes(currentAttributes, + restoreMe.TextAttributes, + restoreMe.ValidParts); + } + } + catch (...) + { + // The static analyzer knows that the bitset operations can throw + // std::out_of_range. However, we know that won't happen, because we + // pre-check that everything should be in range. So we plan to never + // execute this failfast: + FAIL_FAST_CAUGHT_EXCEPTION(); + } + } + + return currentAttributes; + } + + TextAttribute SgrStack::_CombineWithCurrentAttributes(const TextAttribute& currentAttributes, + const TextAttribute& savedAttribute, + const AttrBitset validParts) // of savedAttribute + { + // If we are restoring all attributes, we should have just taken savedAttribute + // before we even got here. + FAIL_FAST_IF(validParts.test(static_cast(SgrSaveRestoreStackOptions::All))); + + TextAttribute result = currentAttributes; + + // From xterm documentation: + // + // CSI # { + // CSI Ps ; Ps # { + // Push video attributes onto stack (XTPUSHSGR), xterm. The + // optional parameters correspond to the SGR encoding for video + // attributes, except for colors (which do not have a unique SGR + // code): + // Ps = 1 -> Bold. + // Ps = 2 -> Faint. + // Ps = 3 -> Italicized. + // Ps = 4 -> Underlined. + // Ps = 5 -> Blink. + // Ps = 7 -> Inverse. + // Ps = 8 -> Invisible. + // Ps = 9 -> Crossed-out characters. + // Ps = 2 1 -> Doubly-underlined. + // Ps = 3 0 -> Foreground color. + // Ps = 3 1 -> Background color. + // + // (some closing braces for people with editors that get thrown off without them: }}) + + // Boldness = 1, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Boldness))) + { + result.SetBold(savedAttribute.IsBold()); + } + + // Faintness = 2, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Faintness))) + { + result.SetFaint(savedAttribute.IsFaint()); + } + + // Italics = 3, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Italics))) + { + result.SetItalic(savedAttribute.IsItalic()); + } + + // Underline = 4, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Underline))) + { + result.SetUnderlined(savedAttribute.IsUnderlined()); + } + + // Blink = 5, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Blink))) + { + result.SetBlinking(savedAttribute.IsBlinking()); + } + + // Negative = 7, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Negative))) + { + result.SetReverseVideo(savedAttribute.IsReverseVideo()); + } + + // Invisible = 8, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::Invisible))) + { + result.SetInvisible(savedAttribute.IsInvisible()); + } + + // CrossedOut = 9, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::CrossedOut))) + { + result.SetCrossedOut(savedAttribute.IsCrossedOut()); + } + + // DoublyUnderlined = 21, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::DoublyUnderlined))) + { + result.SetDoublyUnderlined(savedAttribute.IsDoublyUnderlined()); + } + + // SaveForegroundColor = 30, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::SaveForegroundColor))) + { + result.SetForeground(savedAttribute.GetForeground()); + } + + // SaveBackgroundColor = 31, + if (validParts.test(static_cast(SgrSaveRestoreStackOptions::SaveBackgroundColor))) + { + result.SetBackground(savedAttribute.GetBackground()); + } + + return result; + } + +} diff --git a/src/types/sources.inc b/src/types/sources.inc index 90cf7f591ce..601ecf3a194 100644 --- a/src/types/sources.inc +++ b/src/types/sources.inc @@ -44,6 +44,7 @@ SOURCES= \ ..\utils.cpp \ ..\ThemeUtils.cpp \ ..\ScreenInfoUiaProviderBase.cpp \ + ..\sgrStack.cpp \ ..\UiaTextRangeBase.cpp \ ..\UiaTracing.cpp \ ..\TermControlUiaProvider.cpp \