From afcf1d442e54dec745d54b169331cff307a23735 Mon Sep 17 00:00:00 2001 From: "Dan Thompson (SBS)" Date: Thu, 18 Jul 2019 19:03:03 -0700 Subject: [PATCH] Implement XTPUSHSGR, XTPOPSGR 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. --- .github/actions/spell-check/expect/expect.txt | 3 + src/buffer/out/TextAttribute.cpp | 36 +++ src/buffer/out/TextAttribute.hpp | 5 + src/cascadia/TerminalCore/ITerminalApi.hpp | 3 + src/cascadia/TerminalCore/Terminal.hpp | 6 + 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 | 48 ++++ src/terminal/adapter/termDispatch.hpp | 3 + .../ut_adapter/Adapter.UnitTests.vcxproj | 3 + .../adapter/ut_adapter/adapterTest.cpp | 122 ++++++++++ .../parser/OutputStateMachineEngine.cpp | 109 ++++++++- .../parser/OutputStateMachineEngine.hpp | 13 +- src/terminal/parser/telemetry.cpp | 2 + src/terminal/parser/telemetry.hpp | 2 + src/types/inc/sgrStack.hpp | 115 ++++++++++ src/types/lib/types.vcxproj | 2 + src/types/lib/types.vcxproj.filters | 6 + src/types/sgrStack.cpp | 217 ++++++++++++++++++ 23 files changed, 767 insertions(+), 11 deletions(-) create mode 100644 src/types/inc/sgrStack.hpp create mode 100644 src/types/sgrStack.cpp diff --git a/.github/actions/spell-check/expect/expect.txt b/.github/actions/spell-check/expect/expect.txt index 01b64c42d786..9f255c4cbfaf 100644 --- a/.github/actions/spell-check/expect/expect.txt +++ b/.github/actions/spell-check/expect/expect.txt @@ -2368,6 +2368,7 @@ tosign touchpad towlower towupper +TParam Tpp Tpqrst tprivapi @@ -2798,6 +2799,8 @@ XSubstantial xtended xterm XTest +XTPUSHSGR +XTPOPSGR xutr xvalue XVIRTUALSCREEN diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index 371fedb2b644..8f75fe7662a6 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -81,6 +81,32 @@ bool TextAttribute::IsLegacy() const noexcept return _foreground.IsLegacy() && _background.IsLegacy(); } +// Routine Description: +// - Makes this TextAttribute's foreground color the same as the other one. +// Arguments: +// - The TextAttribute to copy the foreground color from +// Return Value: +// - +void TextAttribute::SetForegroundFrom(const TextAttribute& other) noexcept +{ + _foreground = other._foreground; + WI_ClearAllFlags(_wAttrLegacy, FG_ATTRS); + _wAttrLegacy |= (other._wAttrLegacy & FG_ATTRS); +} + +// Routine Description: +// - Makes this TextAttribute's background color the same as the other one. +// Arguments: +// - The TextAttribute to copy the background color from +// Return Value: +// - +void TextAttribute::SetBackgroundFrom(const TextAttribute& other) noexcept +{ + _background = other._background; + WI_ClearAllFlags(_wAttrLegacy, BG_ATTRS); + _wAttrLegacy |= (other._wAttrLegacy & BG_ATTRS); +} + // Routine Description: // - Calculates rgb colors based off of current color table and active modification attributes. // Arguments: @@ -255,6 +281,11 @@ bool TextAttribute::IsOverlined() const noexcept return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL); } +bool TextAttribute::IsDoublyUnderlined() const noexcept +{ + return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::DoublyUnderlined); +} + bool TextAttribute::IsReverseVideo() const noexcept { return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO); @@ -301,6 +332,11 @@ void TextAttribute::SetOverlined(bool isOverlined) noexcept WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL, isOverlined); } +void TextAttribute::SetDoublyUnderlined(bool isDoublyUnderlined) noexcept +{ + WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::DoublyUnderlined, isDoublyUnderlined); +} + void TextAttribute::SetReverseVideo(bool isReversed) noexcept { WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO, isReversed); diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index 7cec03f75e60..95580ca5436f 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -68,6 +68,9 @@ class TextAttribute final const COLORREF defaultBgColor, const bool reverseScreenMode = false) const noexcept; + void SetForegroundFrom(const TextAttribute& other) noexcept; + void SetBackgroundFrom(const TextAttribute& other) noexcept; + bool IsLeadingByte() const noexcept; bool IsTrailingByte() const noexcept; bool IsTopHorizontalDisplayed() const noexcept; @@ -96,6 +99,7 @@ class TextAttribute final bool IsCrossedOut() const noexcept; bool IsUnderlined() const noexcept; bool IsOverlined() const noexcept; + bool IsDoublyUnderlined() const noexcept; bool IsReverseVideo() const noexcept; void SetBold(bool isBold) noexcept; @@ -106,6 +110,7 @@ class TextAttribute final void SetCrossedOut(bool isCrossedOut) noexcept; void SetUnderlined(bool isUnderlined) noexcept; void SetOverlined(bool isOverlined) noexcept; + void SetDoublyUnderlined(bool isDoublyUnderlined) noexcept; void SetReverseVideo(bool isReversed) noexcept; ExtendedAttributes GetExtendedAttributes() const noexcept; diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp index d92a536c9d66..58425f48b8a7 100644 --- a/src/cascadia/TerminalCore/ITerminalApi.hpp +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -58,6 +58,9 @@ namespace Microsoft::Terminal::Core virtual bool CopyToClipboard(std::wstring_view content) noexcept = 0; + virtual bool PushGraphicsRendition(const gsl::span 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 64232ef963d3..3c3207dfd880 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/IRenderData.hpp" #include "../../terminal/parser/StateMachine.hpp" #include "../../terminal/input/terminalInput.hpp" @@ -110,6 +111,9 @@ class Microsoft::Terminal::Core::Terminal final : bool IsVtInputEnabled() const noexcept override; bool CopyToClipboard(std::wstring_view content) noexcept override; + + bool PushGraphicsRendition(const gsl::span options) noexcept override; + bool PopGraphicsRendition() noexcept override; #pragma endregion #pragma region ITerminalInput @@ -295,6 +299,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 4c7f27a88ea2..573550ca232b 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -552,3 +552,31 @@ try return true; } CATCH_LOG_RETURN_FALSE() + +// 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 gsl::span 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 640f8eb242f6..2fa84fd8bb53 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -15,6 +15,9 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool SetGraphicsRendition(const gsl::span options) noexcept override; + bool PushGraphicsRendition(const gsl::span 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 f8ff0d129dae..3832c45d07d5 100644 --- a/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp @@ -275,3 +275,13 @@ bool TerminalDispatch::SetGraphicsRendition(const gsl::span 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 f04b7906bb41..2c74037288e6 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -75,6 +75,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 = 1 0 => Foreground color. + // Ps = 1 1 => Background color. + // Ps = 2 1 => Doubly-underlined. + // + enum class SgrSaveRestoreStackOptions : unsigned int + { + Nothing = 0, + Boldness = 1, + Faintness = 2, + Italics = 3, + Underline = 4, + Blink = 5, + Negative = 7, + Invisible = 8, + CrossedOut = 9, + SaveForegroundColor = 10, + SaveBackgroundColor = 11, + DoublyUnderlined = 21, + Max = DoublyUnderlined + }; + enum class AnsiStatusType : unsigned int { OS_OperatingStatus = 5, diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 93f1dffeccdc..8db7b65b3fec 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -88,6 +88,9 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool SetGraphicsRendition(const gsl::span options) = 0; // SGR + virtual bool PushGraphicsRendition(const gsl::span options) = 0; // XTPUSHSGR + virtual bool PopGraphicsRendition() = 0; // XTPOPSGR + virtual bool SetPrivateModes(const gsl::span params) = 0; // DECSET virtual bool ResetPrivateModes(const gsl::span params) = 0; // DECRST diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 12a0c902104a..f48495d23161 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 gsl::span options) override; // SGR + bool PushGraphicsRendition(const gsl::span 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 @@ -192,6 +195,8 @@ namespace Microsoft::Console::VirtualTerminal bool _isDECCOLMAllowed; + SgrStack _sgrStack; + size_t _SetRgbColorsHelper(const gsl::span options, TextAttribute& attr, const bool isForeground) noexcept; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index a80accff61a8..95737e8e6b81 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -155,6 +155,9 @@ bool AdaptDispatch::SetGraphicsRendition(const gsl::span 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 e13579c688e5..fa8512cfc589 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -82,6 +82,9 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool SetGraphicsRendition(const gsl::span /*options*/) noexcept override { return false; } // SGR + bool PushGraphicsRendition(const gsl::span /*options*/) noexcept override { return false; } // XTPUSHSGR + bool PopGraphicsRendition() noexcept override { return false; } // XTPOPSGR + bool SetPrivateModes(const gsl::span /*params*/) noexcept override { return false; } // DECSET bool ResetPrivateModes(const gsl::span /*params*/) 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 553c9b275aca..3208d26ba7e6 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 3823a6179c51..d74c13045062 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -1558,6 +1558,128 @@ 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 + + DispatchTypes::GraphicsOptions rgOptions[16]; + DispatchTypes::SgrSaveRestoreStackOptions 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 = TextAttribute{ 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 = TextAttribute{ 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 = TextAttribute{ FOREGROUND_GREEN }; + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // First pop: + cOptions = 0; + _testGetSet->_expectedAttribute = TextAttribute{ 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 = TextAttribute{ FOREGROUND_GREEN }; + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::BoldBright; + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_GREEN }; + _testGetSet->_expectedAttribute.SetBold(true); + _testGetSet->_expectedAttribute.SetDefaultBackground(); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundBlue; + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_GREEN | BACKGROUND_BLUE }; + _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] = DispatchTypes::SgrSaveRestoreStackOptions::Boldness; + rgStackOptions[1] = DispatchTypes::SgrSaveRestoreStackOptions::SaveBackgroundColor; + rgStackOptions[2] = 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 = TextAttribute{ FOREGROUND_GREEN | BACKGROUND_GREEN }; + _testGetSet->_expectedAttribute.SetBold(true); + _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundRed; + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_RED | BACKGROUND_GREEN }; + _testGetSet->_expectedAttribute.SetBold(true); + _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + rgOptions[0] = DispatchTypes::GraphicsOptions::NotBoldOrFaint; + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_RED | BACKGROUND_GREEN }; + _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // And then restore... + cOptions = 0; + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _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 4d5b4d0c8acb..c4379dd91011 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -711,6 +711,9 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, case L' ': success = _IntermediateSpaceDispatch(wch, parameters); break; + case L'#': + success = _IntermediateHashDispatch(wch, parameters); + break; default: // If no functions to call, overall dispatch was a failure. success = false; @@ -747,7 +750,7 @@ bool OutputStateMachineEngine::_IntermediateQuestionMarkDispatch(const wchar_t w { case VTActionCodes::DECSET_PrivateModeSet: case VTActionCodes::DECRST_PrivateModeReset: - success = _GetPrivateModeParams(parameters, privateModeParams); + success = _GetTypedParams(parameters, privateModeParams); break; default: @@ -882,6 +885,66 @@ bool OutputStateMachineEngine::_IntermediateSpaceDispatch(const wchar_t wchActio return success; } +// Routine Description: +// - Handles actions that have an intermediate '#' (0x23), such as XTPUSHSGR, XTPOPSGR +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - True if handled successfully. False otherwise. +bool OutputStateMachineEngine::_IntermediateHashDispatch(const wchar_t wchAction, + const gsl::span parameters) +{ + bool success = false; + + std::vector pushPopParams; + + // Ensure that there was the right number of params + switch (wchAction) + { + case VTActionCodes::XT_PushSgr: + case VTActionCodes::XT_PushSgrAlias: + success = _GetTypedParams(parameters, pushPopParams); + break; + + case VTActionCodes::XT_PopSgr: + case VTActionCodes::XT_PopSgrAlias: + if (parameters.size() == 0) + { + // Can't supply params for XTPOPSGR. + success = true; + } + break; + + default: + // If no params to fill, param filling was successful. + success = true; + break; + } + if (success) + { + switch (wchAction) + { + case VTActionCodes::XT_PushSgr: + case VTActionCodes::XT_PushSgrAlias: + success = _dispatch->PushGraphicsRendition({ pushPopParams.data(), pushPopParams.size() }); + TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPUSHSGR); + break; + + case VTActionCodes::XT_PopSgr: + case VTActionCodes::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; + break; + } + } + return success; +} + // Routine Description: // - Triggers the Clear action to indicate that the state machine should erase // all internal state. @@ -1301,6 +1364,7 @@ bool OutputStateMachineEngine::_GetTopBottomMargins(const gsl::span parameters, - std::vector& privateModes) const +// - True if we successfully retrieved an array of strongly-typed params from the +// parameters we've stored. False otherwise. +template +bool OutputStateMachineEngine::_GetTypedParams(const gsl::span parameters, + std::vector& typedParams) const { bool success = false; - // Can't just set nothing at all if (parameters.size() > 0) { for (const auto& p : parameters) { - privateModes.push_back((DispatchTypes::PrivateModeParams)p); + // No memcpy. The parameters are size_t. The type we are converting to may be + // a different size. + // + // Note that we use gsl::narrow_cast, not gsl::narrow, because we don't want + // someone to be able to shove a too-big number in and cause a crash. Instead, + // we will detect truncation after the fact, and ignore the parameter if that + // happened. And by "ignore the parameter", we mean it won't even be put into + // typedParams (as opposed to, say, entering a 0, which may have some + // meaning). The caller can check this, if desired, by checking if the number + // of parameters they passed in equals the number of parameters they got out. + TParamType tmp = gsl::narrow_cast(p); + if (gsl::narrow_cast(tmp) == p) + { + typedParams.push_back(tmp); + } + else if (bIgnoreNarrowingConversionFailures) + { + // we ignore the parameter + } + else + { + success = false; + break; + } } + + success = true; + } + else + { success = true; } return success; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index bce98255e0e2..a7b6b2ebedf8 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -83,6 +83,8 @@ namespace Microsoft::Console::VirtualTerminal bool _IntermediateExclamationDispatch(const wchar_t wch); bool _IntermediateSpaceDispatch(const wchar_t wchAction, const gsl::span parameters); + bool _IntermediateHashDispatch(const wchar_t wchAction, + const gsl::span parameters); enum VTActionCodes : wchar_t { @@ -141,7 +143,11 @@ namespace Microsoft::Console::VirtualTerminal LS1R_LockingShift = L'~', LS2R_LockingShift = L'}', LS3R_LockingShift = L'|', - DECALN_ScreenAlignmentPattern = L'8' + DECALN_ScreenAlignmentPattern = L'8', + XT_PushSgr = L'{', + XT_PushSgrAlias = L'p', + XT_PopSgr = L'}', + XT_PopSgrAlias = L'q', }; enum Vt52ActionCodes : wchar_t @@ -212,8 +218,9 @@ namespace Microsoft::Console::VirtualTerminal bool _VerifyDeviceAttributesParams(const gsl::span parameters) const noexcept; - bool _GetPrivateModeParams(const gsl::span parameters, - std::vector& privateModes) const; + template + bool _GetTypedParams(const gsl::span parameters, + std::vector& typedParams) const; static constexpr size_t DefaultTopMargin = 0; static constexpr size_t DefaultBottomMargin = 0; diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index e7433301c4e6..39066cdeebda 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -273,6 +273,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 e33c83841b20..4b4fca535a69 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -100,6 +100,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 000000000000..66c40d30bcb2 --- /dev/null +++ b/src/types/inc/sgrStack.hpp @@ -0,0 +1,115 @@ +/*++ +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 gsl::span 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 (and we don't offset them, so the lowest bit in the bitset is + // actually not used). + typedef std::bitset(DispatchTypes::SgrSaveRestoreStackOptions::Max) + 1> AttrBitset; + + TextAttribute _CombineWithCurrentAttributes(const TextAttribute& currentAttributes, + const TextAttribute& savedAttribute, + const AttrBitset validParts) noexcept; // 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 6d5835a8bd96..4728c8d828b4 100644 --- a/src/types/lib/types.vcxproj +++ b/src/types/lib/types.vcxproj @@ -21,6 +21,7 @@ + @@ -42,6 +43,7 @@ + diff --git a/src/types/lib/types.vcxproj.filters b/src/types/lib/types.vcxproj.filters index 982dc7c38d66..be2f3eba7795 100644 --- a/src/types/lib/types.vcxproj.filters +++ b/src/types/lib/types.vcxproj.filters @@ -69,6 +69,9 @@ Source Files + + Source Files + Source Files @@ -155,6 +158,9 @@ Header Files + + Header Files + Header Files diff --git a/src/types/sgrStack.cpp b/src/types/sgrStack.cpp new file mode 100644 index 000000000000..356f8cab2781 --- /dev/null +++ b/src/types/sgrStack.cpp @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/sgrStack.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + SgrStack::SgrStack() noexcept : + _nextPushIndex{ 0 }, + _numSavedAttrs{ 0 } + { + } + + void SgrStack::Push(const TextAttribute& currentAttributes, + const gsl::span options) noexcept + { + AttrBitset validParts; + + if (options.size() == 0) + { + // We save all current attributes. + validParts.set(); // (sets all bits) + } + else + { + // Each option is encoded as a bit in validParts. Options that aren't + // supported are ignored. So if you try to save only unsupported aspects of + // the current text attributes, validParts end up as zero, and you'll do what + // is effectively an "empty" push (the subsequent pop will not change the + // current attributes), which is the correct behavior. + for (auto option : options) + { + const size_t optionAsIndex = static_cast(option); + + // Options must be specified singly; not in combination. Values that are + // out of range will be ignored. + if (optionAsIndex < validParts.size()) + { + try + { + validParts.set(optionAsIndex); + } + catch (std::out_of_range&) + { + // We should not be able to reach here: we already checked + // optionAsIndex against the size of the bitset. + RaiseFailFastException(nullptr, nullptr, 0); + } + } + } + } + + 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); + + if (restoreMe.ValidParts.all()) + { + return restoreMe.TextAttributes; + } + else + { + return _CombineWithCurrentAttributes(currentAttributes, + restoreMe.TextAttributes, + restoreMe.ValidParts); + } + } + + return currentAttributes; + } + + TextAttribute SgrStack::_CombineWithCurrentAttributes(const TextAttribute& currentAttributes, + const TextAttribute& savedAttribute, + const AttrBitset validParts) noexcept // of savedAttribute + { + 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 = 1 0 -> Foreground color. + // Ps = 1 1 -> Background color. + // Ps = 2 1 -> Doubly-underlined. + // + // (some closing braces for people with editors that get thrown off without them: }}) + // + // Note that not all of these attributes are actually supported by + // renderers/conhost, despite setters/getters on TextAttribute. + + try + { + // Boldness = 1, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Boldness))) + { + result.SetBold(savedAttribute.IsBold()); + } + + // Faintness = 2, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Faintness))) + { + result.SetFaint(savedAttribute.IsFaint()); + } + + // Italics = 3, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Italics))) + { + result.SetItalic(savedAttribute.IsItalic()); + } + + // Underline = 4, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Underline))) + { + result.SetUnderlined(savedAttribute.IsUnderlined()); + } + + // Blink = 5, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Blink))) + { + result.SetBlinking(savedAttribute.IsBlinking()); + } + + // Negative = 7, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Negative))) + { + if (savedAttribute.IsReverseVideo()) + { + if (!result.IsReverseVideo()) + { + result.Invert(); + } + } + else + { + if (result.IsReverseVideo()) + { + result.Invert(); + } + } + } + + // Invisible = 8, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::Invisible))) + { + result.SetInvisible(savedAttribute.IsInvisible()); + } + + // CrossedOut = 9, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::CrossedOut))) + { + result.SetCrossedOut(savedAttribute.IsCrossedOut()); + } + + // SaveForegroundColor = 10, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::SaveForegroundColor))) + { + result.SetForegroundFrom(savedAttribute); + } + + // SaveBackgroundColor = 11, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::SaveBackgroundColor))) + { + result.SetBackgroundFrom(savedAttribute); + } + + // DoublyUnderlined = 21, + if (validParts.test(static_cast(DispatchTypes::SgrSaveRestoreStackOptions::DoublyUnderlined))) + { + result.SetDoublyUnderlined(savedAttribute.IsDoublyUnderlined()); + } + } + catch (std::out_of_range&) + { + // We should not be able to reach here: all values passed to bitset::test are + // constants, clearly in range of the bitset. + RaiseFailFastException(nullptr, nullptr, 0); + } + + return result; + } + +}