diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index e34a7625976..0c9c7d41b88 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -297,6 +297,11 @@ bool TextAttribute::IsReverseVideo() const noexcept return WI_IsFlagSet(_attrs, CharacterAttributes::ReverseVideo); } +bool TextAttribute::IsProtected() const noexcept +{ + return WI_IsFlagSet(_attrs, CharacterAttributes::Protected); +} + void TextAttribute::SetIntense(bool isIntense) noexcept { WI_UpdateFlag(_attrs, CharacterAttributes::Intense, isIntense); @@ -347,6 +352,11 @@ void TextAttribute::SetReverseVideo(bool isReversed) noexcept WI_UpdateFlag(_attrs, CharacterAttributes::ReverseVideo, isReversed); } +void TextAttribute::SetProtected(bool isProtected) noexcept +{ + WI_UpdateFlag(_attrs, CharacterAttributes::Protected, isProtected); +} + // Routine Description: // - swaps foreground and background color void TextAttribute::Invert() noexcept @@ -365,10 +375,11 @@ void TextAttribute::SetDefaultBackground() noexcept } // Method description: -// - Resets only the rendition character attributes +// - Resets only the rendition character attributes, which includes everything +// except the Protected attribute. void TextAttribute::SetDefaultRenditionAttributes() noexcept { - _attrs = CharacterAttributes::Normal; + _attrs &= CharacterAttributes::Protected; } // Method Description: diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index 83439190526..97b275645fb 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -90,6 +90,7 @@ class TextAttribute final bool IsDoublyUnderlined() const noexcept; bool IsOverlined() const noexcept; bool IsReverseVideo() const noexcept; + bool IsProtected() const noexcept; void SetIntense(bool isIntense) noexcept; void SetFaint(bool isFaint) noexcept; @@ -101,6 +102,7 @@ class TextAttribute final void SetDoublyUnderlined(bool isDoublyUnderlined) noexcept; void SetOverlined(bool isOverlined) noexcept; void SetReverseVideo(bool isReversed) noexcept; + void SetProtected(bool isProtected) noexcept; constexpr CharacterAttributes GetCharacterAttributes() const noexcept { diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 7900c2995f3..d0d021c8679 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -186,6 +186,7 @@ class ScreenBufferTests TEST_METHOD(EraseScrollbackTests); TEST_METHOD(EraseTests); + TEST_METHOD(ProtectedAttributeTests); TEST_METHOD(ScrollUpInMargins); TEST_METHOD(ScrollDownInMargins); @@ -3634,11 +3635,11 @@ bool _ValidateLineContains(int line, T expectedContent, TextAttribute expectedAt } template -auto _ValidateLinesContain(int startLine, int endLine, T expectedContent, TextAttribute expectedAttr) +auto _ValidateLinesContain(int startCol, int startLine, int endLine, T expectedContent, TextAttribute expectedAttr) { for (auto line = startLine; line < endLine; ++line) { - if (!_ValidateLineContains(line, expectedContent, expectedAttr)) + if (!_ValidateLineContains({ startCol, line }, expectedContent, expectedAttr)) { return false; } @@ -3646,6 +3647,12 @@ auto _ValidateLinesContain(int startLine, int endLine, T expectedContent, TextAt return true; }; +template +auto _ValidateLinesContain(int startLine, int endLine, T expectedContent, TextAttribute expectedAttr) +{ + return _ValidateLinesContain(0, startLine, endLine, expectedContent, expectedAttr); +} + void ScreenBufferTests::ScrollOperations() { enum ScrollType : int @@ -4196,18 +4203,27 @@ void ScreenBufferTests::EraseTests() BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:eraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType TEST_METHOD_PROPERTY(L"Data:eraseScreen", L"{false, true}") // corresponds to Line (false) or Screen (true) + TEST_METHOD_PROPERTY(L"Data:selectiveErase", L"{false, true}") END_TEST_METHOD_PROPERTIES() int eraseTypeValue; VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseType", eraseTypeValue)); bool eraseScreen; VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseScreen", eraseScreen)); + bool selectiveErase; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"selectiveErase", selectiveErase)); const auto eraseType = gsl::narrow(eraseTypeValue); std::wstringstream escapeSequence; escapeSequence << "\x1b["; + if (selectiveErase) + { + Log::Comment(L"Erasing unprotected cells only."); + escapeSequence << "?"; + } + switch (eraseType) { case DispatchTypes::EraseType::ToEnd: @@ -4247,12 +4263,24 @@ void ScreenBufferTests::EraseTests() // Move the viewport down a few lines, and only cover part of the buffer width. si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true); + const auto& viewport = si.GetViewport(); // Fill the entire buffer with Zs. Blue on Green. const auto bufferChar = L'Z'; const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; _FillLines(0, bufferHeight, bufferChar, bufferAttr); + // For selective erasure tests, we protect the first five columns. + if (selectiveErase) + { + auto protectedAttr = bufferAttr; + protectedAttr.SetProtected(true); + for (auto line = viewport.Top(); line < viewport.BottomExclusive(); ++line) + { + _FillLine(line, L"ZZZZZ", protectedAttr); + } + } + // Set the attributes that will be used to fill the erased area. auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); @@ -4262,10 +4290,15 @@ void ScreenBufferTests::EraseTests() // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; expectedFillAttr.SetStandardErase(); + // And with selective erasure the original attributes are maintained. + if (selectiveErase) + { + expectedFillAttr = bufferAttr; + } // Place the cursor in the center. const auto centerX = bufferWidth / 2; - const auto centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2; + const auto centerY = (viewport.Top() + viewport.BottomExclusive()) / 2; Log::Comment(L"Set the cursor position and perform the operation."); VERIFY_SUCCEEDED(si.SetCursorPosition({ centerX, centerY }, true)); @@ -4273,34 +4306,37 @@ void ScreenBufferTests::EraseTests() // Get cursor position and viewport range. const auto cursorPos = si.GetTextBuffer().GetCursor().GetPosition(); - const auto viewportStart = si.GetViewport().Top(); - const auto viewportEnd = si.GetViewport().BottomExclusive(); + const auto viewportStart = viewport.Top(); + const auto viewportEnd = viewport.BottomExclusive(); Log::Comment(L"Lines outside the viewport should remain unchanged."); VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr)); VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr)); + // With selective erasure, there's a protected range to account for at the start of the line. + const auto protectedOffset = selectiveErase ? 5 : 0; + // 1. Lines before cursor line if (eraseScreen && eraseType != DispatchTypes::EraseType::ToEnd) { // For eraseScreen, if we're not erasing to the end, these rows will be cleared. Log::Comment(L"Lines before the cursor line should be erased."); - VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, L' ', expectedFillAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, viewportStart, cursorPos.Y, L' ', expectedFillAttr)); } else { // Otherwise we'll be left with the original buffer content. Log::Comment(L"Lines before the cursor line should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, viewportStart, cursorPos.Y, bufferChar, bufferAttr)); } // 2. Cursor Line - auto prefixPos = til::point{ 0, cursorPos.Y }; + auto prefixPos = til::point{ protectedOffset, cursorPos.Y }; auto suffixPos = cursorPos; // When erasing from the beginning, the cursor column is included in the range. suffixPos.X += (eraseType == DispatchTypes::EraseType::FromBeginning); - size_t prefixWidth = suffixPos.X; - auto suffixWidth = bufferWidth - prefixWidth; + size_t prefixWidth = suffixPos.X - prefixPos.X; + size_t suffixWidth = bufferWidth - suffixPos.X; if (eraseType == DispatchTypes::EraseType::ToEnd) { Log::Comment(L"The start of the cursor line should remain unchanged."); @@ -4318,7 +4354,7 @@ void ScreenBufferTests::EraseTests() if (eraseType == DispatchTypes::EraseType::All) { Log::Comment(L"The entire cursor line should be erased."); - VERIFY_IS_TRUE(_ValidateLineContains(cursorPos.Y, L' ', expectedFillAttr)); + VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, L' ', expectedFillAttr)); } // 3. Lines after cursor line @@ -4326,16 +4362,85 @@ void ScreenBufferTests::EraseTests() { // For eraseScreen, if we're not erasing from the beginning, these rows will be cleared. Log::Comment(L"Lines after the cursor line should be erased."); - VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, L' ', expectedFillAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, cursorPos.Y + 1, viewportEnd, L' ', expectedFillAttr)); } else { // Otherwise we'll be left with the original buffer content. Log::Comment(L"Lines after the cursor line should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, cursorPos.Y + 1, viewportEnd, bufferChar, bufferAttr)); + } + + // 4. Protected columns + if (selectiveErase) + { + Log::Comment(L"First five columns should remain unchanged."); + auto protectedAttr = bufferAttr; + protectedAttr.SetProtected(true); + for (auto line = viewport.Top(); line < viewport.BottomExclusive(); ++line) + { + VERIFY_IS_TRUE(_ValidateLineContains(line, L"ZZZZZ", protectedAttr)); + } } } +void ScreenBufferTests::ProtectedAttributeTests() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& textBuffer = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto cursorPosition = textBuffer.GetCursor().GetPosition(); + auto unprotectedAttribute = textBuffer.GetCurrentAttributes(); + unprotectedAttribute.SetProtected(false); + auto protectedAttribute = textBuffer.GetCurrentAttributes(); + protectedAttribute.SetProtected(true); + + Log::Comment(L"DECSCA default should be unprotected"); + textBuffer.GetCursor().SetPosition(cursorPosition); + textBuffer.SetCurrentAttributes(protectedAttribute); + stateMachine.ProcessString(L"\x1b[\"qZZZZZ"); + VERIFY_IS_FALSE(textBuffer.GetCurrentAttributes().IsProtected()); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPosition, L"ZZZZZ", unprotectedAttribute)); + + Log::Comment(L"DECSCA 0 should be unprotected"); + textBuffer.GetCursor().SetPosition(cursorPosition); + textBuffer.SetCurrentAttributes(protectedAttribute); + stateMachine.ProcessString(L"\x1b[0\"qZZZZZ"); + VERIFY_IS_FALSE(textBuffer.GetCurrentAttributes().IsProtected()); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPosition, L"ZZZZZ", unprotectedAttribute)); + + Log::Comment(L"DECSCA 1 should be protected"); + textBuffer.GetCursor().SetPosition(cursorPosition); + textBuffer.SetCurrentAttributes(unprotectedAttribute); + stateMachine.ProcessString(L"\x1b[1\"qZZZZZ"); + VERIFY_IS_TRUE(textBuffer.GetCurrentAttributes().IsProtected()); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPosition, L"ZZZZZ", protectedAttribute)); + + Log::Comment(L"DECSCA 2 should be unprotected"); + textBuffer.GetCursor().SetPosition(cursorPosition); + textBuffer.SetCurrentAttributes(protectedAttribute); + stateMachine.ProcessString(L"\x1b[2\"qZZZZZ"); + VERIFY_IS_FALSE(textBuffer.GetCurrentAttributes().IsProtected()); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPosition, L"ZZZZZ", unprotectedAttribute)); + + Log::Comment(L"DECSCA 2;1 should be protected"); + textBuffer.GetCursor().SetPosition(cursorPosition); + textBuffer.SetCurrentAttributes(unprotectedAttribute); + stateMachine.ProcessString(L"\x1b[2;1\"qZZZZZ"); + VERIFY_IS_TRUE(textBuffer.GetCurrentAttributes().IsProtected()); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPosition, L"ZZZZZ", protectedAttribute)); + + Log::Comment(L"DECSCA 1;2 should be unprotected"); + textBuffer.GetCursor().SetPosition(cursorPosition); + textBuffer.SetCurrentAttributes(protectedAttribute); + stateMachine.ProcessString(L"\x1b[1;2\"qZZZZZ"); + VERIFY_IS_FALSE(textBuffer.GetCurrentAttributes().IsProtected()); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPosition, L"ZZZZZ", unprotectedAttribute)); +} + void _CommonScrollingSetup() { // Used for testing MSFT:20204600 diff --git a/src/inc/conattrs.hpp b/src/inc/conattrs.hpp index b7825db4493..85f41b48d77 100644 --- a/src/inc/conattrs.hpp +++ b/src/inc/conattrs.hpp @@ -25,7 +25,7 @@ enum class CharacterAttributes : uint16_t TopGridline = COMMON_LVB_GRID_HORIZONTAL, // 0x400 LeftGridline = COMMON_LVB_GRID_LVERTICAL, // 0x800 RightGridline = COMMON_LVB_GRID_RVERTICAL, // 0x1000 - Unused3 = 0x2000, + Protected = 0x2000, ReverseVideo = COMMON_LVB_REVERSE_VIDEO, // 0x4000 BottomGridline = COMMON_LVB_UNDERSCORE // 0x8000 }; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 5a0bf610eb6..2e4af16791a 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -330,6 +330,13 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes BrightBackgroundWhite = 107, }; + enum LogicalAttributeOptions : VTInt + { + Default = 0, + Protected = 1, + Unprotected = 2 + }; + // 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). diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 082e9607df0..678faabf6fc 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -90,9 +90,12 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool EraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // ED virtual bool EraseInLine(const DispatchTypes::EraseType eraseType) = 0; // EL virtual bool EraseCharacters(const VTInt numChars) = 0; // ECH + virtual bool SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // DECSED + virtual bool SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) = 0; // DECSEL virtual bool SetGraphicsRendition(const VTParameters options) = 0; // SGR virtual bool SetLineRendition(const LineRendition rendition) = 0; // DECSWL, DECDWL, DECDHL + virtual bool SetCharacterProtectionAttribute(const VTParameters options) = 0; // DECSCA virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR virtual bool PopGraphicsRendition() = 0; // XTPOPSGR diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index eb6546921a4..4b969767007 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -686,6 +686,103 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) } } +// Routine Description: +// - Selectively erases unprotected cells in an area of the buffer. +// Arguments: +// - textBuffer - Target buffer to be erased. +// - eraseRect - Area of the buffer that will be affected. +// Return Value: +// - +void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect) +{ + if (eraseRect) + { + for (auto row = eraseRect.top; row < eraseRect.bottom; row++) + { + auto& rowBuffer = textBuffer.GetRowByOffset(row); + const auto& attrs = rowBuffer.GetAttrRow(); + auto& chars = rowBuffer.GetCharRow(); + for (auto col = eraseRect.left; col < eraseRect.right; col++) + { + // Only unprotected cells are affected. + if (!attrs.GetAttrByColumn(col).IsProtected()) + { + // The text is cleared but the attributes are left as is. + chars.ClearGlyph(col); + textBuffer.TriggerRedraw(Viewport::FromCoord({ col, row })); + } + } + } + _api.NotifyAccessibilityChange(eraseRect); + } +} + +// Routine Description: +// - DECSED - Selectively erases unprotected cells in a portion of the viewport. +// Arguments: +// - eraseType - Determines whether to erase: +// From beginning (top-left corner) to the cursor +// From cursor to end (bottom-right corner) +// The entire viewport area +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) +{ + const auto viewport = _api.GetViewport(); + auto& textBuffer = _api.GetTextBuffer(); + const auto bufferWidth = textBuffer.GetSize().Width(); + const auto row = textBuffer.GetCursor().GetPosition().Y; + const auto col = textBuffer.GetCursor().GetPosition().X; + + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, row }); + _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + return true; + case DispatchTypes::EraseType::ToEnd: + _SelectiveEraseRect(textBuffer, { col, row, bufferWidth, row + 1 }); + _SelectiveEraseRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }); + return true; + case DispatchTypes::EraseType::All: + _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }); + return true; + default: + return false; + } +} + +// Routine Description: +// - DECSEL - Selectively erases unprotected cells on line with the cursor. +// Arguments: +// - eraseType - Determines whether to erase: +// From beginning (left edge) to the cursor +// From cursor to end (right edge) +// The entire line. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) +{ + auto& textBuffer = _api.GetTextBuffer(); + const auto row = textBuffer.GetCursor().GetPosition().Y; + const auto col = textBuffer.GetCursor().GetPosition().X; + + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + return true; + case DispatchTypes::EraseType::ToEnd: + _SelectiveEraseRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }); + return true; + case DispatchTypes::EraseType::All: + _SelectiveEraseRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); + return true; + default: + return false; + } +} + // Routine Description: // - DECSWL/DECDWL/DECDHL - Sets the line rendition attribute for the current line. // Arguments: @@ -1768,7 +1865,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled) // X All character sets G0, G1, G2, Default settings. // G3, GL, GR // X Select graphic rendition SGR Normal rendition. -// Select character attribute DECSCA Normal (erasable by DECSEL and DECSED). +// X Select character attribute DECSCA Normal (erasable by DECSEL and DECSED). // X Save cursor state DECSC Home position. // Assign user preference DECAUPSS Set selected in Set-Up. // supplemental set @@ -1802,6 +1899,7 @@ bool AdaptDispatch::SoftReset() AcceptC1Controls(false); SetGraphicsRendition({}); // Normal rendition. + SetCharacterProtectionAttribute({}); // Default (unprotected) // Reset the saved cursor state. // Note that XTerm only resets the main buffer state, but that @@ -2790,12 +2888,15 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting() const auto id = idBuilder->Finalize(ch); switch (id) { - case VTID('m'): + case VTID("m"): _ReportSGRSetting(); break; - case VTID('r'): + case VTID("r"): _ReportDECSTBMSetting(); break; + case VTID("\"q"): + _ReportDECSCASetting(); + break; default: _api.ReturnResponse(L"\033P0$r\033\\"); break; @@ -2902,6 +3003,28 @@ void AdaptDispatch::_ReportDECSTBMSetting() _api.ReturnResponse({ response.data(), response.size() }); } +// Method Description: +// - Reports the DECSCA protected attribute in response to a DECRQSS query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportDECSCASetting() const +{ + using namespace std::string_view_literals; + + // A valid response always starts with DCS 1 $ r. + fmt::basic_memory_buffer response; + response.append(L"\033P1$r"sv); + + const auto attr = _api.GetTextBuffer().GetCurrentAttributes(); + response.append(attr.IsProtected() ? L"1"sv : L"0"sv); + + // The '"q' indicates this is an DECSCA response, and ST ends the sequence. + response.append(L"\"q\033\\"sv); + _api.ReturnResponse({ response.data(), response.size() }); +} + // Routine Description: // - DECPS - Plays a sequence of musical notes. // Arguments: diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index d2c48ae9b9e..034730f2eed 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -51,10 +51,13 @@ namespace Microsoft::Console::VirtualTerminal bool EraseInDisplay(const DispatchTypes::EraseType eraseType) override; // ED bool EraseInLine(const DispatchTypes::EraseType eraseType) override; // EL bool EraseCharacters(const VTInt numChars) override; // ECH + bool SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) override; // DECSED + bool SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) override; // DECSEL bool InsertCharacter(const VTInt count) override; // ICH bool DeleteCharacter(const VTInt count) override; // DCH bool SetGraphicsRendition(const VTParameters options) override; // SGR bool SetLineRendition(const LineRendition rendition) override; // DECSWL, DECDWL, DECDHL + bool SetCharacterProtectionAttribute(const VTParameters options) override; // DECSCA bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR bool PopGraphicsRendition() override; // XTPOPSGR bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR @@ -180,6 +183,7 @@ namespace Microsoft::Console::VirtualTerminal bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const wchar_t fillChar, const TextAttribute fillAttrs); + void _SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect); void _EraseScrollback(); void _EraseAll(); void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); @@ -208,6 +212,7 @@ namespace Microsoft::Console::VirtualTerminal void _ReportSGRSetting() const; void _ReportDECSTBMSetting(); + void _ReportDECSCASetting() const; StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize); StringHandler _CreatePassthroughHandler(); diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index e1aaaaf110c..41bba96b038 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -259,6 +259,38 @@ bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) return true; } +// Routine Description: +// - DECSCA - Modifies the character protection attribute. This operation was +// originally intended to support a range of logical character attributes, +// but the protected attribute was the only one ever implemented. +// Arguments: +// - options - An array of options that will be applied in order. +// Return Value: +// - True. +bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) +{ + auto& textBuffer = _api.GetTextBuffer(); + auto attr = textBuffer.GetCurrentAttributes(); + for (size_t i = 0; i < options.size(); i++) + { + const LogicalAttributeOptions opt = options.at(i); + switch (opt) + { + case Default: + attr.SetProtected(false); + break; + case Protected: + attr.SetProtected(true); + break; + case Unprotected: + attr.SetProtected(false); + break; + } + } + textBuffer.SetCurrentAttributes(attr); + return true; +} + // Method Description: // - Saves the current text attributes to an internal stack. // Arguments: diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 7d3b302941f..607a3bc72bb 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -83,9 +83,12 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool EraseInDisplay(const DispatchTypes::EraseType /* eraseType*/) override { return false; } // ED bool EraseInLine(const DispatchTypes::EraseType /* eraseType*/) override { return false; } // EL bool EraseCharacters(const VTInt /*numChars*/) override { return false; } // ECH + bool SelectiveEraseInDisplay(const DispatchTypes::EraseType /*eraseType*/) override { return false; } // DECSED + bool SelectiveEraseInLine(const DispatchTypes::EraseType /*eraseType*/) override { return false; } // DECSEL bool SetGraphicsRendition(const VTParameters /*options*/) override { return false; } // SGR bool SetLineRendition(const LineRendition /*rendition*/) override { return false; } // DECSWL, DECDWL, DECDHL + bool SetCharacterProtectionAttribute(const VTParameters /*options*/) override { return false; } // DECSCA bool PushGraphicsRendition(const VTParameters /*options*/) override { return false; } // XTPUSHSGR bool PopGraphicsRendition() override { return false; } // XTPOPSGR diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 4cf63651160..eb7c96dd0d3 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -1619,6 +1619,21 @@ class AdapterTest requestSetting(L"m"); _testGetSet->ValidateInputEvent(L"\033P1$r0;38;2;12;34;56;48;2;65;43;21m\033\\"); + Log::Comment(L"Requesting DECSCA attributes (unprotected)."); + _testGetSet->PrepData(); + attribute = {}; + _testGetSet->_textBuffer->SetCurrentAttributes(attribute); + requestSetting(L"\"q"); + _testGetSet->ValidateInputEvent(L"\033P1$r0\"q\033\\"); + + Log::Comment(L"Requesting DECSCA attributes (protected)."); + _testGetSet->PrepData(); + attribute = {}; + attribute.SetProtected(true); + _testGetSet->_textBuffer->SetCurrentAttributes(attribute); + requestSetting(L"\"q"); + _testGetSet->ValidateInputEvent(L"\033P1$r1\"q\033\\"); + Log::Comment(L"Requesting an unsupported setting."); _testGetSet->PrepData(); requestSetting(L"x"); diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 38f79ed5b9e..771d942a704 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -494,12 +494,24 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete }); TermTelemetry::Instance().Log(TermTelemetry::Codes::ED); break; + case CsiActionCodes::DECSED_SelectiveEraseDisplay: + success = parameters.for_each([&](const auto eraseType) { + return _dispatch->SelectiveEraseInDisplay(eraseType); + }); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSED); + break; case CsiActionCodes::EL_EraseLine: success = parameters.for_each([&](const auto eraseType) { return _dispatch->EraseInLine(eraseType); }); TermTelemetry::Instance().Log(TermTelemetry::Codes::EL); break; + case CsiActionCodes::DECSEL_SelectiveEraseLine: + success = parameters.for_each([&](const auto eraseType) { + return _dispatch->SelectiveEraseInLine(eraseType); + }); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSEL); + break; case CsiActionCodes::DECSET_PrivateModeSet: success = parameters.for_each([&](const auto mode) { return _dispatch->SetMode(DispatchTypes::DECPrivateMode(mode)); @@ -606,6 +618,10 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete success = _dispatch->SoftReset(); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR); break; + case CsiActionCodes::DECSCA_SetCharacterProtectionAttribute: + success = _dispatch->SetCharacterProtectionAttribute(parameters); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSCA); + break; case CsiActionCodes::XT_PushSgr: case CsiActionCodes::XT_PushSgrAlias: success = _dispatch->PushGraphicsRendition(parameters); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 346c642d608..78c82abebbd 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -108,7 +108,9 @@ namespace Microsoft::Console::VirtualTerminal CUP_CursorPosition = VTID("H"), CHT_CursorForwardTab = VTID("I"), ED_EraseDisplay = VTID("J"), + DECSED_SelectiveEraseDisplay = VTID("?J"), EL_EraseLine = VTID("K"), + DECSEL_SelectiveEraseLine = VTID("?K"), IL_InsertLine = VTID("L"), DL_DeleteLine = VTID("M"), DCH_DeleteCharacter = VTID("P"), @@ -137,6 +139,7 @@ namespace Microsoft::Console::VirtualTerminal DECREQTPARM_RequestTerminalParameters = VTID("x"), DECSCUSR_SetCursorStyle = VTID(" q"), DECSTR_SoftReset = VTID("!p"), + DECSCA_SetCharacterProtectionAttribute = VTID("\"q"), XT_PushSgrAlias = VTID("#p"), XT_PopSgrAlias = VTID("#q"), XT_PushSgr = VTID("#{"), diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index c394faaf64b..2ba820b01bb 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -215,7 +215,9 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[CHA], "CHA"), TraceLoggingUInt32(_uiTimesUsed[CUP], "CUP"), TraceLoggingUInt32(_uiTimesUsed[ED], "ED"), + TraceLoggingUInt32(_uiTimesUsed[DECSED], "DECSED"), TraceLoggingUInt32(_uiTimesUsed[EL], "EL"), + TraceLoggingUInt32(_uiTimesUsed[DECSEL], "DECSEL"), TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"), TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"), TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"), @@ -265,6 +267,7 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[DECSTR], "DECSTR"), TraceLoggingUInt32(_uiTimesUsed[RIS], "RIS"), TraceLoggingUInt32(_uiTimesUsed[DECSCUSR], "DECSCUSR"), + TraceLoggingUInt32(_uiTimesUsed[DECSCA], "DECSCA"), TraceLoggingUInt32(_uiTimesUsed[DTTERM_WM], "DTTERM_WM"), TraceLoggingUInt32(_uiTimesUsed[OSCCT], "OscColorTable"), TraceLoggingUInt32(_uiTimesUsed[OSCSCC], "OscSetCursorColor"), diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp index 5e258d62d7b..27519a69673 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -42,7 +42,9 @@ namespace Microsoft::Console::VirtualTerminal CHA, CUP, ED, + DECSED, EL, + DECSEL, SGR, DECSC, DECRC, @@ -92,6 +94,7 @@ namespace Microsoft::Console::VirtualTerminal DECSTR, RIS, DECSCUSR, + DECSCA, DTTERM_WM, OSCCT, OSCSCC,