Repository: microsoft/terminal (language: cpp)

Issue:
Add support for selective erase operations
# Description of the new feature/enhancement

I'd like to add support for the selective erase escape sequences: [`DECSED`](https://vt100.net/docs/vt510-rm/DECSED.html), [`DECSEL`](https://vt100.net/docs/vt510-rm/DECSEL.html), and [`DECSCA`](https://vt100.net/docs/vt510-rm/DECSCA.html). They provide a way of marking certain areas of the screen as "protected", and then you can easily erase all but the protected areas.

They were typically used in data entry applications, where you'd have a form with static content like the labels that you'd want protected. Then when you needed to reset the form, you could just clear the whole screen (with `DECSED`), and you wouldn't have to redraw those protected areas of the UI.

I could also see them being useful in certain gaming situations, where you might have a static background (e.g. the maze walls in Pac-Man), which you'd want to protect. Then you could easily clear out all the non-static sprites with `DECSED`, without losing the static background.

However, the main reason I wanted to add them was as a precursor to doing the rectangular area operations, since once of those ([`DECSERA`](https://vt100.net/docs/vt510-rm/DECSERA.html)) is dependent on protected attributes.

# Proposed technical implementation details (optional)

We need another bit in the `ExtendedAttributes` enum to hold the protected status of each cell, an operation to enable and disable that bit in the active attributes (this is `DECSCA`), and variants of the `EraseInDisplay` and `EraseInLine` methods that only erase the cells that don't have the protected bit set.


Discussion / hints:

Fix patch:
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<class T>
-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<class T>
+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<DispatchTypes::EraseType>(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:
+// - <none>
+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<wchar_t, 64> 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,


Test patch: