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; + } + +}