Repository: microsoft/terminal (language: cpp)

Issue:
Add support for the rectangular area operations
# Description of the new feature/enhancement

This is a set of functions that include [`DECFRA`](https://vt100.net/docs/vt510-rm/DECFRA.html) (fill rectangle), [`DECERA`](https://vt100.net/docs/vt510-rm/DECERA.html) (erase rectangle), [`DECSERA`](https://vt100.net/docs/vt510-rm/DECSERA.html) (selective erase rectangle), [`DECCRA`](https://vt100.net/docs/vt510-rm/DECCRA.html) (copy rectangle), [`DECCARA`](https://vt100.net/docs/vt510-rm/DECCARA.html) (change attributes), [`DECRARA`](https://vt100.net/docs/vt510-rm/DECRARA.html) (reverse attributes), and [`DECSACE`](https://vt100.net/docs/vt510-rm/DECSACE.html) (this determines the affected area of the previous two operations - it can be a rectangle or a stream of characters).

Some of these should be helpful in improving our console API support for passthrough mode (#1173 / #10001). Most useful would probably be `DECCRA`, `DECCARA`, and possibly `DECFRA`. We've also discussed using `DECCRA` to support the layering concept in issue #10810, but that would require us supporting the paging functions as well (#13892).

# Proposed technical implementation details (optional)

I've got a working POC, and some of these are fairly straightforward, but some require low level access to the buffer. That means my current implementation will probably require some refactoring to work with the `ROW` rewrite in PR #13626, so I'm happy to wait for that to merge first. And `DECSERA` depends on the new "protected" attribute added in PR #14046, so that definitely needs to merge first.

I should also note that I'm not positive about all of the implementation details, so if possible I'd like to get someone with a real terminal to confirm the correct behavior before I submit a PR.


Discussion / hints:

Fix patch:
diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp
index 0c9c7d41b88..55e3b15a5ea 100644
--- a/src/buffer/out/TextAttribute.cpp
+++ b/src/buffer/out/TextAttribute.cpp
@@ -379,7 +379,7 @@ void TextAttribute::SetDefaultBackground() noexcept
 //     except the Protected attribute.
 void TextAttribute::SetDefaultRenditionAttributes() noexcept
 {
-    _attrs &= CharacterAttributes::Protected;
+    _attrs &= ~CharacterAttributes::Rendition;
 }
 
 // Method Description:
diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp
index 97b275645fb..2a61e8902de 100644
--- a/src/buffer/out/TextAttribute.hpp
+++ b/src/buffer/out/TextAttribute.hpp
@@ -104,6 +104,10 @@ class TextAttribute final
     void SetReverseVideo(bool isReversed) noexcept;
     void SetProtected(bool isProtected) noexcept;
 
+    constexpr void SetCharacterAttributes(const CharacterAttributes attrs) noexcept
+    {
+        _attrs = attrs;
+    }
     constexpr CharacterAttributes GetCharacterAttributes() const noexcept
     {
         return _attrs;
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index d0d021c8679..186c3e01a53 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -247,6 +247,8 @@ class ScreenBufferTests
     TEST_METHOD(TestReflowBiggerLongLineWithColor);
 
     TEST_METHOD(TestDeferredMainBufferResize);
+
+    TEST_METHOD(RectangularAreaOperations);
 };
 
 void ScreenBufferTests::SingleAlternateBufferCreationTest()
@@ -7333,3 +7335,127 @@ void ScreenBufferTests::TestDeferredMainBufferResize()
     VERIFY_ARE_EQUAL(altPostResizeView, mainPostRestoreView);
     VERIFY_ARE_EQUAL(expectedSize, mainPostRestoreView);
 }
+
+void ScreenBufferTests::RectangularAreaOperations()
+{
+    BEGIN_TEST_METHOD_PROPERTIES()
+        TEST_METHOD_PROPERTY(L"Data:rectOp", L"{0, 1, 2, 3, 4, 5}")
+    END_TEST_METHOD_PROPERTIES();
+
+    enum RectOp : int
+    {
+        DECFRA,
+        DECERA,
+        DECSERA,
+        DECCARA,
+        DECRARA,
+        DECCRA
+    };
+
+    RectOp rectOp;
+    VERIFY_SUCCEEDED(TestData::TryGetValue(L"rectOp", (int&)rectOp));
+
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+
+    const auto bufferWidth = si.GetBufferSize().Width();
+    const auto bufferHeight = si.GetBufferSize().Height();
+
+    // Move the viewport down a few lines, and only cover part of the buffer width.
+    si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 20 }), true);
+    const auto viewport = si.GetViewport();
+
+    // Fill the entire buffer with Zs. Blue on Green and Underlined.
+    const auto bufferChar = L'Z';
+    auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
+    bufferAttr.SetUnderlined(true);
+    _FillLines(0, bufferHeight, bufferChar, bufferAttr);
+
+    // Set the active attributes to Red on Blue and Intense;
+    auto activeAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
+    activeAttr.SetIntense(true);
+    si.SetAttributes(activeAttr);
+
+    // The area we're targetting in all the operations below is 27;3 to 54;6.
+    // But VT coordinates use origin 1;1 so we need to subtract 1, and til::rect
+    // expects exclusive coordinates, so the bottom/right also need to add 1.
+    const auto targetArea = til::rect{ 27 - 1, viewport.Top() + 3 - 1, 54, viewport.Top() + 6 };
+
+    wchar_t expectedChar{};
+    TextAttribute expectedAttr;
+
+    // DECCARA and DECRARA can apply to both a stream of character positions or
+    // a rectangular area, but these tests only cover the rectangular option.
+    if (rectOp == DECCARA || rectOp == DECRARA)
+    {
+        Log::Comment(L"Request a rectangular change extent with DECSACE");
+        stateMachine.ProcessString(L"\033[2*x");
+    }
+
+    switch (rectOp)
+    {
+    case DECFRA:
+        Log::Comment(L"DECFRA: fill a rectangle with the active attributes and a given character value");
+        expectedAttr = activeAttr;
+        expectedChar = wchar_t{ 42 };
+        // The first parameter specifies the fill character.
+        stateMachine.ProcessString(L"\033[42;3;27;6;54$x");
+        break;
+    case DECERA:
+        Log::Comment(L"DECERA: erase a rectangle using the active colors but no rendition attributes");
+        expectedAttr = activeAttr;
+        expectedAttr.SetStandardErase();
+        expectedChar = L' ';
+        stateMachine.ProcessString(L"\033[3;27;6;54$z");
+        break;
+    case DECSERA:
+        Log::Comment(L"DECSERA: erase the text in a rectangle but leave the attributes unchanged");
+        expectedAttr = bufferAttr;
+        expectedChar = L' ';
+        stateMachine.ProcessString(L"\033[3;27;6;54${");
+        break;
+    case DECCARA:
+        Log::Comment(L"DECCARA: update the attributes in a rectangle but leave the text unchanged");
+        expectedAttr = bufferAttr;
+        expectedAttr.SetReverseVideo(true);
+        expectedChar = bufferChar;
+        // The final parameter specifies the reverse video attribute that will be set.
+        stateMachine.ProcessString(L"\033[3;27;6;54;7$r");
+        break;
+    case DECRARA:
+        Log::Comment(L"DECRARA: reverse the attributes in a rectangle but leave the text unchanged");
+        expectedAttr = bufferAttr;
+        expectedAttr.SetUnderlined(false);
+        expectedChar = bufferChar;
+        // The final parameter specifies the underline attribute that will be reversed.
+        stateMachine.ProcessString(L"\033[3;27;6;54;4$t");
+        break;
+    case DECCRA:
+        Log::Comment(L"DECCRA: copy a rectangle from the lower part of the viewport to the top");
+        expectedAttr = TextAttribute{ FOREGROUND_GREEN | BACKGROUND_RED };
+        expectedChar = L'*';
+        // Fill the lower part of the viewport with some different content.
+        _FillLines(viewport.Top() + 10, viewport.BottomExclusive(), expectedChar, expectedAttr);
+        // Copy a rectangle from that lower part up to the top with DECCRA.
+        stateMachine.ProcessString(L"\033[11;27;14;54;1;3;27;1;4$v");
+        // Reset the lower part back to it's original content.
+        _FillLines(viewport.Top() + 10, viewport.BottomExclusive(), bufferChar, bufferAttr);
+        break;
+    default:
+        VERIFY_FAIL(L"Unsupported operation");
+    }
+
+    const auto expectedChars = std::wstring(targetArea.width(), expectedChar);
+    VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.left, targetArea.top, targetArea.bottom, expectedChars, expectedAttr));
+
+    Log::Comment(L"Everything above and below the target area should remain unchanged");
+    VERIFY_IS_TRUE(_ValidateLinesContain(0, targetArea.top, bufferChar, bufferAttr));
+    VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.bottom, bufferHeight, bufferChar, bufferAttr));
+
+    Log::Comment(L"Everything to the left and right of the target area should remain unchanged");
+    const auto bufferChars = std::wstring(targetArea.left, bufferChar);
+    VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.top, targetArea.bottom, bufferChars, bufferAttr));
+    VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.right, targetArea.top, targetArea.bottom, bufferChar, bufferAttr));
+}
diff --git a/src/inc/conattrs.hpp b/src/inc/conattrs.hpp
index 85f41b48d77..f70697c33b0 100644
--- a/src/inc/conattrs.hpp
+++ b/src/inc/conattrs.hpp
@@ -27,7 +27,10 @@ enum class CharacterAttributes : uint16_t
     RightGridline = COMMON_LVB_GRID_RVERTICAL, // 0x1000
     Protected = 0x2000,
     ReverseVideo = COMMON_LVB_REVERSE_VIDEO, // 0x4000
-    BottomGridline = COMMON_LVB_UNDERSCORE // 0x8000
+    BottomGridline = COMMON_LVB_UNDERSCORE, // 0x8000
+
+    All = 0xFFFF, // All character attributes
+    Rendition = All & ~Protected // Only rendition attributes (everything except Protected)
 };
 DEFINE_ENUM_FLAG_OPERATORS(CharacterAttributes);
 
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 78a8ff9da07..62edc781191 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -259,6 +259,13 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         Scrollback = 3
     };
 
+    enum class ChangeExtent : VTInt
+    {
+        Default = 0,
+        Stream = 1,
+        Rectangle = 2
+    };
+
     enum class TaskbarState : VTInt
     {
         Clear = 0,
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 70c9e1e6f02..fb16f0eb821 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -93,6 +93,14 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // DECSED
     virtual bool SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) = 0; // DECSEL
 
+    virtual bool ChangeAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs) = 0; // DECCARA
+    virtual bool ReverseAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs) = 0; // DECRARA
+    virtual bool CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage) = 0; // DECCRA
+    virtual bool FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECFRA
+    virtual bool EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECERA
+    virtual bool SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECSERA
+    virtual bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) = 0; // DECSACE
+
     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
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 7e5fb800f8d..95c205e9707 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -5,6 +5,7 @@
 
 #include "adaptDispatch.hpp"
 #include "../../renderer/base/renderer.hpp"
+#include "../../types/inc/GlyphWidth.hpp"
 #include "../../types/inc/Viewport.hpp"
 #include "../../types/inc/utils.hpp"
 #include "../../inc/unicode.hpp"
@@ -22,6 +23,7 @@ AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettin
     _usingAltBuffer(false),
     _isOriginModeRelative(false), // by default, the DECOM origin mode is absolute.
     _isDECCOLMAllowed(false), // by default, DECCOLM is not allowed.
+    _isChangeExtentRectangular(false),
     _termOutput()
 {
 }
@@ -783,6 +785,373 @@ bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseTyp
     }
 }
 
+// Routine Description:
+// - Changes the attributes of each cell in a rectangular area of the buffer.
+// Arguments:
+// - textBuffer - Target buffer to be changed.
+// - changeRect - A rectangular area of the buffer that will be affected.
+// - changeOps - Changes that will be applied to each of the attributes.
+// Return Value:
+// - <none>
+void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps)
+{
+    if (changeRect)
+    {
+        for (auto row = changeRect.top; row < changeRect.bottom; row++)
+        {
+            auto& rowBuffer = textBuffer.GetRowByOffset(row);
+            auto& attrs = rowBuffer.GetAttrRow();
+            for (auto col = changeRect.left; col < changeRect.right; col++)
+            {
+                auto attr = attrs.GetAttrByColumn(col);
+                auto characterAttributes = attr.GetCharacterAttributes();
+                characterAttributes &= changeOps.andAttrMask;
+                characterAttributes ^= changeOps.xorAttrMask;
+                attr.SetCharacterAttributes(characterAttributes);
+                if (changeOps.foreground)
+                {
+                    attr.SetForeground(*changeOps.foreground);
+                }
+                if (changeOps.background)
+                {
+                    attr.SetBackground(*changeOps.background);
+                }
+                attrs.Replace(col, col + 1, attr);
+            }
+        }
+        textBuffer.TriggerRedraw(Viewport::FromExclusive(changeRect));
+        _api.NotifyAccessibilityChange(changeRect);
+    }
+}
+
+// Routine Description:
+// - Changes the attributes of each cell in an area of the buffer.
+// Arguments:
+// - changeArea - Area of the buffer that will be affected. This may be
+//     interpreted as a rectangle or a stream depending on the state of the
+//     _isChangeExtentRectangular field.
+// - changeOps - Changes that will be applied to each of the attributes.
+// Return Value:
+// - <none>
+void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps)
+{
+    auto& textBuffer = _api.GetTextBuffer();
+    const auto bufferSize = textBuffer.GetSize().Dimensions();
+    const auto changeRect = _CalculateRectArea(changeArea.top, changeArea.left, changeArea.bottom, changeArea.right, bufferSize);
+    const auto lineCount = changeRect.height();
+
+    // If the change extent is rectangular, we can apply the change with a
+    // single call. The same is true for a stream extent that is only one line.
+    if (_isChangeExtentRectangular || lineCount == 1)
+    {
+        _ChangeRectAttributes(textBuffer, changeRect, changeOps);
+    }
+    // If the stream extent is more than one line we require three passes. The
+    // top line is altered from the left offset up to the end of the line. The
+    // bottom line is altered from the start up to the right offset. All the
+    // lines inbetween have their entire length altered. The right coordinate
+    // must be greater than the left, otherwise the operation is ignored.
+    else if (lineCount > 1 && changeRect.right > changeRect.left)
+    {
+        const auto bufferWidth = bufferSize.width;
+        _ChangeRectAttributes(textBuffer, { changeRect.origin(), til::size{ bufferWidth - changeRect.left, 1 } }, changeOps);
+        _ChangeRectAttributes(textBuffer, { { 0, changeRect.top + 1 }, til::size{ bufferWidth, lineCount - 2 } }, changeOps);
+        _ChangeRectAttributes(textBuffer, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps);
+    }
+}
+
+// Routine Description:
+// - Helper method to caculate the applicable buffer coordinates for use with
+//   the various rectangular area operations.
+// Arguments:
+// - top - The first row of the area.
+// - left - The first column of the area.
+// - bottom - The last row of the area (inclusive).
+// - right - The last column of the area (inclusive).
+// - bufferSize - The size of the target buffer.
+// Return value:
+// - An exclusive rect with the absolute buffer coordinates.
+til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize)
+{
+    const auto viewport = _api.GetViewport();
+
+    // We start by calculating the margin offsets and maximum dimensions.
+    // If the origin mode isn't set, we use the viewport extent.
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false);
+    const auto yOffset = _isOriginModeRelative ? topMargin : 0;
+    const auto yMaximum = _isOriginModeRelative ? bottomMargin + 1 : viewport.height();
+    const auto xMaximum = bufferSize.width;
+
+    auto fillRect = til::inclusive_rect{};
+    fillRect.left = left;
+    fillRect.top = top + yOffset;
+    // Right and bottom default to the maximum dimensions.
+    fillRect.right = (right ? right : xMaximum);
+    fillRect.bottom = (bottom ? bottom + yOffset : yMaximum);
+
+    // We also clamp everything to the maximum dimensions, and subtract 1
+    // to convert from VT coordinates which have an origin of 1;1.
+    fillRect.left = std::min(fillRect.left, xMaximum) - 1;
+    fillRect.right = std::min(fillRect.right, xMaximum) - 1;
+    fillRect.top = std::min(fillRect.top, yMaximum) - 1;
+    fillRect.bottom = std::min(fillRect.bottom, yMaximum) - 1;
+
+    // To get absolute coordinates we offset with the viewport top.
+    fillRect.top += viewport.top;
+    fillRect.bottom += viewport.top;
+
+    return til::rect{ fillRect };
+}
+
+// Routine Description:
+// - DECCARA - Changes the attributes in a rectangular area. The affected range
+//   is dependent on the change extent setting defined by DECSACE.
+// Arguments:
+// - top - The first row of the area.
+// - left - The first column of the area.
+// - bottom - The last row of the area (inclusive).
+// - right - The last column of the area (inclusive).
+// - attrs - The rendition attributes that will be applied to the area.
+// Return Value:
+// - True.
+bool AdaptDispatch::ChangeAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs)
+{
+    auto changeOps = ChangeOps{};
+
+    // We apply the attribute parameters to two TextAttribute instances: one
+    // with no character attributes set, and one with all attributes set. This
+    // provides us with an OR mask and an AND mask which can then be applied to
+    // each cell to set and reset the appropriate attribute bits.
+    auto allAttrsOff = TextAttribute{};
+    auto allAttrsOn = TextAttribute{ 0, 0 };
+    allAttrsOn.SetCharacterAttributes(CharacterAttributes::All);
+    _ApplyGraphicsOptions(attrs, allAttrsOff);
+    _ApplyGraphicsOptions(attrs, allAttrsOn);
+    const auto orAttrMask = allAttrsOff.GetCharacterAttributes();
+    const auto andAttrMask = allAttrsOn.GetCharacterAttributes();
+    // But to minimize the required ops, which we share with the DECRARA control
+    // below, we want to use an XOR rather than OR. For that to work, we have to
+    // combine the AND mask with the inverse of the OR mask in advance.
+    changeOps.andAttrMask = andAttrMask & ~orAttrMask;
+    changeOps.xorAttrMask = orAttrMask;
+
+    // We also make use of the two TextAttributes calculated above to determine
+    // whether colors need to be applied. Since allAttrsOff started off with
+    // default colors, and allAttrsOn started with black, we know something has
+    // been set if the former is no longer default, or the latter is now default.
+    const auto foreground = allAttrsOff.GetForeground();
+    const auto background = allAttrsOff.GetBackground();
+    const auto foregroundChanged = !foreground.IsDefault() || allAttrsOn.GetForeground().IsDefault();
+    const auto backgroundChanged = !background.IsDefault() || allAttrsOn.GetBackground().IsDefault();
+    changeOps.foreground = foregroundChanged ? std::optional{ foreground } : std::nullopt;
+    changeOps.background = backgroundChanged ? std::optional{ background } : std::nullopt;
+
+    _ChangeRectOrStreamAttributes({ left, top, right, bottom }, changeOps);
+
+    return true;
+}
+
+// Routine Description:
+// - DECRARA - Reverses the attributes in a rectangular area. The affected range
+//   is dependent on the change extent setting defined by DECSACE.
+// Arguments:
+// - top - The first row of the area.
+// - left - The first column of the area.
+// - bottom - The last row of the area (inclusive).
+// - right - The last column of the area (inclusive).
+// - attrs - The rendition attributes that will be applied to the area.
+// Return Value:
+// - True.
+bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs)
+{
+    // In order to create a mask of the attributes that we want to reverse, we
+    // need to go through the options one by one, applying each of them to an
+    // empty TextAttribute object from which can extract the effected bits. We
+    // then combine them with XOR, because if we're reversing the same attribute
+    // twice, we'd expect the two instances to cancel each other out.
+    auto reverseMask = CharacterAttributes::Normal;
+    if (!attrs.empty())
+    {
+        for (size_t i = 0; i < attrs.size();)
+        {
+            // A zero or default option is a special case that reverses all the
+            // rendition bits. But note that this shouldn't be triggered by an
+            // empty attribute list, so we explicitly exclude that case in
+            // the empty check above.
+            if (attrs.at(i).value_or(0) == 0)
+            {
+                reverseMask ^= CharacterAttributes::Rendition;
+                i++;
+            }
+            else
+            {
+                auto allAttrsOff = TextAttribute{};
+                i += _ApplyGraphicsOption(attrs, i, allAttrsOff);
+                reverseMask ^= allAttrsOff.GetCharacterAttributes();
+            }
+        }
+    }
+
+    // If the accumulated mask ends up blank, there's nothing for us to do.
+    if (reverseMask != CharacterAttributes::Normal)
+    {
+        _ChangeRectOrStreamAttributes({ left, top, right, bottom }, { .xorAttrMask = reverseMask });
+    }
+
+    return true;
+}
+
+// Routine Description:
+// - DECCRA - Copys a rectangular area from one part of the buffer to another.
+// Arguments:
+// - top - The first row of the source area.
+// - left - The first column of the source area.
+// - bottom - The last row of the source area (inclusive).
+// - right - The last column of the source area (inclusive).
+// - page - The source page number (unused for now).
+// - dstTop - The first row of the destination.
+// - dstLeft - The first column of the destination.
+// - dstPage - The destination page number (unused for now).
+// Return Value:
+// - True.
+bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt /*page*/, const VTInt dstTop, const VTInt dstLeft, const VTInt /*dstPage*/)
+{
+    // GH#13892 We don't yet support the paging extension, so for now we ignore
+    // the page parameters. This is the same as if the maximum page count was 1.
+
+    auto& textBuffer = _api.GetTextBuffer();
+    const auto bufferSize = textBuffer.GetSize().Dimensions();
+    const auto srcRect = _CalculateRectArea(top, left, bottom, right, bufferSize);
+    const auto dstBottom = dstTop + srcRect.height() - 1;
+    const auto dstRight = dstLeft + srcRect.width() - 1;
+    const auto dstRect = _CalculateRectArea(dstTop, dstLeft, dstBottom, dstRight, bufferSize);
+
+    if (dstRect && dstRect.origin() != srcRect.origin())
+    {
+        // If the source is bigger than the available space at the destination
+        // it needs to be clipped, so we only care about the destination size.
+        const auto srcView = Viewport::FromDimensions(srcRect.origin(), dstRect.size());
+        const auto dstView = Viewport::FromDimensions(dstRect.origin(), dstRect.size());
+        const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView);
+        auto srcPos = srcView.GetWalkOrigin(walkDirection);
+        auto dstPos = dstView.GetWalkOrigin(walkDirection);
+        do
+        {
+            // If the source position is offscreen (which can occur on double
+            // width lines), then we shouldn't copy anything to the destination.
+            if (srcPos.x < textBuffer.GetLineWidth(srcPos.y))
+            {
+                const auto data = OutputCell(*textBuffer.GetCellDataAt(srcPos));
+                textBuffer.Write(OutputCellIterator({ &data, 1 }), dstPos);
+            }
+            srcView.WalkInBounds(srcPos, walkDirection);
+        } while (dstView.WalkInBounds(dstPos, walkDirection));
+        _api.NotifyAccessibilityChange(dstRect);
+    }
+
+    return true;
+}
+
+// Routine Description:
+// - DECFRA - Fills a rectangular area with the given character and using the
+//     currently active rendition attributes.
+// Arguments:
+// - ch - The ordinal value of the character used to fill the area.
+// - top - The first row of the area.
+// - left - The first column of the area.
+// - bottom - The last row of the area (inclusive).
+// - right - The last column of the area (inclusive).
+// Return Value:
+// - True.
+bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
+{
+    auto& textBuffer = _api.GetTextBuffer();
+    auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions());
+
+    // The standard only allows for characters in the range of the GL and GR
+    // character set tables, but we also support additional Unicode characters
+    // from the BMP if the code page is UTF-8. Default and 0 are treated as 32.
+    const auto charValue = ch.value_or(0) == 0 ? 32 : ch.value();
+    const auto glChar = (charValue >= 32 && charValue <= 126);
+    const auto grChar = (charValue >= 160 && charValue <= 255);
+    const auto unicodeChar = (charValue >= 256 && charValue <= 65535 && _api.GetConsoleOutputCP() == CP_UTF8);
+    if (glChar || grChar || unicodeChar)
+    {
+        const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast<wchar_t>(charValue));
+        const auto fillAttributes = textBuffer.GetCurrentAttributes();
+        if (IsGlyphFullWidth(fillChar))
+        {
+            // If the fill char is full width, we need to halve the width of the
+            // fill area, otherwise it'll occupy twice as much space as expected.
+            fillRect.right = fillRect.left + fillRect.width() / 2;
+        }
+        _FillRect(textBuffer, fillRect, fillChar, fillAttributes);
+    }
+
+    return true;
+}
+
+// Routine Description:
+// - DECERA - Erases a rectangular area, replacing all cells with a space
+//     character and the default rendition attributes.
+// Arguments:
+// - top - The first row of the area.
+// - left - The first column of the area.
+// - bottom - The last row of the area (inclusive).
+// - right - The last column of the area (inclusive).
+// Return Value:
+// - True.
+bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
+{
+    auto& textBuffer = _api.GetTextBuffer();
+    const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions());
+    auto eraseAttributes = textBuffer.GetCurrentAttributes();
+    eraseAttributes.SetStandardErase();
+    _FillRect(textBuffer, eraseRect, L' ', eraseAttributes);
+    return true;
+}
+
+// Routine Description:
+// - DECSERA - Selectively erases a rectangular area, replacing unprotected
+//     cells with a space character, but retaining the rendition attributes.
+// Arguments:
+// - top - The first row of the area.
+// - left - The first column of the area.
+// - bottom - The last row of the area (inclusive).
+// - right - The last column of the area (inclusive).
+// Return Value:
+// - True.
+bool AdaptDispatch::SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right)
+{
+    auto& textBuffer = _api.GetTextBuffer();
+    const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions());
+    _SelectiveEraseRect(textBuffer, eraseRect);
+    return true;
+}
+
+// Routine Description:
+// - DECSACE - Selects the format of the character range that will be affected
+//   by the DECCARA and DECRARA attribute operations.
+// Arguments:
+// - changeExtent - Whether the character range is a stream or a rectangle.
+// Return value:
+// - True if handled successfully. False otherwise.
+bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) noexcept
+{
+    switch (changeExtent)
+    {
+    case DispatchTypes::ChangeExtent::Default:
+    case DispatchTypes::ChangeExtent::Stream:
+        _isChangeExtentRectangular = false;
+        return true;
+    case DispatchTypes::ChangeExtent::Rectangle:
+        _isChangeExtentRectangular = true;
+        return true;
+    default:
+        return false;
+    }
+}
+
 // Routine Description:
 // - DECSWL/DECDWL/DECDHL - Sets the line rendition attribute for the current line.
 // Arguments:
@@ -1975,6 +2344,9 @@ bool AdaptDispatch::HardReset()
     _renderer.UpdateSoftFont({}, {}, false);
     _fontBuffer = nullptr;
 
+    // Reset the attribute change extent (DECSACE) to a stream.
+    _isChangeExtentRectangular = false;
+
     // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to
     // make the state machine propagate this RIS sequence to the connected
     // terminal application. We've reset our state, but the connected terminal
@@ -2906,6 +3278,9 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
             case VTID("\"q"):
                 _ReportDECSCASetting();
                 break;
+            case VTID("*x"):
+                _ReportDECSACESetting();
+                break;
             default:
                 _api.ReturnResponse(L"\033P0$r\033\\");
                 break;
@@ -3034,6 +3409,28 @@ void AdaptDispatch::_ReportDECSCASetting() const
     _api.ReturnResponse({ response.data(), response.size() });
 }
 
+// Method Description:
+// - Reports the DECSACE change extent in response to a DECRQSS query.
+// Arguments:
+// - None
+// Return Value:
+// - None
+void AdaptDispatch::_ReportDECSACESetting() 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(_isChangeExtentRectangular ? L"2"sv : L"1"sv);
+
+    // The '*x' indicates this is an DECSACE response, and ST ends the sequence.
+    response.append(L"*x\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 034730f2eed..3e8c02a8f08 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -55,6 +55,13 @@ namespace Microsoft::Console::VirtualTerminal
         bool SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) override; // DECSEL
         bool InsertCharacter(const VTInt count) override; // ICH
         bool DeleteCharacter(const VTInt count) override; // DCH
+        bool ChangeAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs) override; // DECCARA
+        bool ReverseAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs) override; // DECRARA
+        bool CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage) override; // DECCRA
+        bool FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) override; // DECFRA
+        bool EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) override; // DECERA
+        bool SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) override; // DECSERA
+        bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) noexcept override; // DECSACE
         bool SetGraphicsRendition(const VTParameters options) override; // SGR
         bool SetLineRendition(const LineRendition rendition) override; // DECSWL, DECDWL, DECDHL
         bool SetCharacterProtectionAttribute(const VTParameters options) override; // DECSCA
@@ -178,12 +185,22 @@ namespace Microsoft::Console::VirtualTerminal
             static constexpr Offset Backward(const VTInt value) { return { -value, false }; };
             static constexpr Offset Unchanged() { return Forward(0); };
         };
+        struct ChangeOps
+        {
+            CharacterAttributes andAttrMask = CharacterAttributes::All;
+            CharacterAttributes xorAttrMask = CharacterAttributes::Normal;
+            std::optional<TextColor> foreground;
+            std::optional<TextColor> background;
+        };
 
         std::pair<int, int> _GetVerticalMargins(const til::rect& viewport, const bool absolute);
         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 _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps);
+        void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps);
+        til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize);
         void _EraseScrollback();
         void _EraseAll();
         void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta);
@@ -213,6 +230,7 @@ namespace Microsoft::Console::VirtualTerminal
         void _ReportSGRSetting() const;
         void _ReportDECSTBMSetting();
         void _ReportDECSCASetting() const;
+        void _ReportDECSACESetting() const;
 
         StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize);
         StringHandler _CreatePassthroughHandler();
@@ -239,13 +257,18 @@ namespace Microsoft::Console::VirtualTerminal
         til::inclusive_rect _scrollMargins;
 
         bool _isOriginModeRelative;
-
         bool _isDECCOLMAllowed;
+        bool _isChangeExtentRectangular;
 
         SgrStack _sgrStack;
 
         size_t _SetRgbColorsHelper(const VTParameters options,
                                    TextAttribute& attr,
                                    const bool isForeground) noexcept;
+        size_t _ApplyGraphicsOption(const VTParameters options,
+                                    const size_t optionIndex,
+                                    TextAttribute& attr) noexcept;
+        void _ApplyGraphicsOptions(const VTParameters options,
+                                   TextAttribute& attr) noexcept;
     };
 }
diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp
index 41bba96b038..1f351089b3a 100644
--- a/src/terminal/adapter/adaptDispatchGraphics.cpp
+++ b/src/terminal/adapter/adaptDispatchGraphics.cpp
@@ -62,6 +62,210 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options,
     return optionsConsumed;
 }
 
+// Routine Description:
+// - Helper to apply a single graphic rendition option to an attribute.
+// Arguments:
+// - options - An array of options.
+// - optionIndex - The start index of the option that will be applied.
+// - attr - The attribute that will be updated with the applied option.
+// Return Value:
+// - The number of entries in the array that were consumed.
+size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options,
+                                           const size_t optionIndex,
+                                           TextAttribute& attr) noexcept
+{
+    const GraphicsOptions opt = options.at(optionIndex);
+    switch (opt)
+    {
+    case Off:
+        attr.SetDefaultForeground();
+        attr.SetDefaultBackground();
+        attr.SetDefaultRenditionAttributes();
+        return 1;
+    case ForegroundDefault:
+        attr.SetDefaultForeground();
+        return 1;
+    case BackgroundDefault:
+        attr.SetDefaultBackground();
+        return 1;
+    case Intense:
+        attr.SetIntense(true);
+        return 1;
+    case RGBColorOrFaint:
+        attr.SetFaint(true);
+        return 1;
+    case NotIntenseOrFaint:
+        attr.SetIntense(false);
+        attr.SetFaint(false);
+        return 1;
+    case Italics:
+        attr.SetItalic(true);
+        return 1;
+    case NotItalics:
+        attr.SetItalic(false);
+        return 1;
+    case BlinkOrXterm256Index:
+    case RapidBlink: // We just interpret rapid blink as an alias of blink.
+        attr.SetBlinking(true);
+        return 1;
+    case Steady:
+        attr.SetBlinking(false);
+        return 1;
+    case Invisible:
+        attr.SetInvisible(true);
+        return 1;
+    case Visible:
+        attr.SetInvisible(false);
+        return 1;
+    case CrossedOut:
+        attr.SetCrossedOut(true);
+        return 1;
+    case NotCrossedOut:
+        attr.SetCrossedOut(false);
+        return 1;
+    case Negative:
+        attr.SetReverseVideo(true);
+        return 1;
+    case Positive:
+        attr.SetReverseVideo(false);
+        return 1;
+    case Underline:
+        attr.SetUnderlined(true);
+        return 1;
+    case DoublyUnderlined:
+        attr.SetDoublyUnderlined(true);
+        return 1;
+    case NoUnderline:
+        attr.SetUnderlined(false);
+        attr.SetDoublyUnderlined(false);
+        return 1;
+    case Overline:
+        attr.SetOverlined(true);
+        return 1;
+    case NoOverline:
+        attr.SetOverlined(false);
+        return 1;
+    case ForegroundBlack:
+        attr.SetIndexedForeground(TextColor::DARK_BLACK);
+        return 1;
+    case ForegroundBlue:
+        attr.SetIndexedForeground(TextColor::DARK_BLUE);
+        return 1;
+    case ForegroundGreen:
+        attr.SetIndexedForeground(TextColor::DARK_GREEN);
+        return 1;
+    case ForegroundCyan:
+        attr.SetIndexedForeground(TextColor::DARK_CYAN);
+        return 1;
+    case ForegroundRed:
+        attr.SetIndexedForeground(TextColor::DARK_RED);
+        return 1;
+    case ForegroundMagenta:
+        attr.SetIndexedForeground(TextColor::DARK_MAGENTA);
+        return 1;
+    case ForegroundYellow:
+        attr.SetIndexedForeground(TextColor::DARK_YELLOW);
+        return 1;
+    case ForegroundWhite:
+        attr.SetIndexedForeground(TextColor::DARK_WHITE);
+        return 1;
+    case BackgroundBlack:
+        attr.SetIndexedBackground(TextColor::DARK_BLACK);
+        return 1;
+    case BackgroundBlue:
+        attr.SetIndexedBackground(TextColor::DARK_BLUE);
+        return 1;
+    case BackgroundGreen:
+        attr.SetIndexedBackground(TextColor::DARK_GREEN);
+        return 1;
+    case BackgroundCyan:
+        attr.SetIndexedBackground(TextColor::DARK_CYAN);
+        return 1;
+    case BackgroundRed:
+        attr.SetIndexedBackground(TextColor::DARK_RED);
+        return 1;
+    case BackgroundMagenta:
+        attr.SetIndexedBackground(TextColor::DARK_MAGENTA);
+        return 1;
+    case BackgroundYellow:
+        attr.SetIndexedBackground(TextColor::DARK_YELLOW);
+        return 1;
+    case BackgroundWhite:
+        attr.SetIndexedBackground(TextColor::DARK_WHITE);
+        return 1;
+    case BrightForegroundBlack:
+        attr.SetIndexedForeground(TextColor::BRIGHT_BLACK);
+        return 1;
+    case BrightForegroundBlue:
+        attr.SetIndexedForeground(TextColor::BRIGHT_BLUE);
+        return 1;
+    case BrightForegroundGreen:
+        attr.SetIndexedForeground(TextColor::BRIGHT_GREEN);
+        return 1;
+    case BrightForegroundCyan:
+        attr.SetIndexedForeground(TextColor::BRIGHT_CYAN);
+        return 1;
+    case BrightForegroundRed:
+        attr.SetIndexedForeground(TextColor::BRIGHT_RED);
+        return 1;
+    case BrightForegroundMagenta:
+        attr.SetIndexedForeground(TextColor::BRIGHT_MAGENTA);
+        return 1;
+    case BrightForegroundYellow:
+        attr.SetIndexedForeground(TextColor::BRIGHT_YELLOW);
+        return 1;
+    case BrightForegroundWhite:
+        attr.SetIndexedForeground(TextColor::BRIGHT_WHITE);
+        return 1;
+    case BrightBackgroundBlack:
+        attr.SetIndexedBackground(TextColor::BRIGHT_BLACK);
+        return 1;
+    case BrightBackgroundBlue:
+        attr.SetIndexedBackground(TextColor::BRIGHT_BLUE);
+        return 1;
+    case BrightBackgroundGreen:
+        attr.SetIndexedBackground(TextColor::BRIGHT_GREEN);
+        return 1;
+    case BrightBackgroundCyan:
+        attr.SetIndexedBackground(TextColor::BRIGHT_CYAN);
+        return 1;
+    case BrightBackgroundRed:
+        attr.SetIndexedBackground(TextColor::BRIGHT_RED);
+        return 1;
+    case BrightBackgroundMagenta:
+        attr.SetIndexedBackground(TextColor::BRIGHT_MAGENTA);
+        return 1;
+    case BrightBackgroundYellow:
+        attr.SetIndexedBackground(TextColor::BRIGHT_YELLOW);
+        return 1;
+    case BrightBackgroundWhite:
+        attr.SetIndexedBackground(TextColor::BRIGHT_WHITE);
+        return 1;
+    case ForegroundExtended:
+        return 1 + _SetRgbColorsHelper(options.subspan(optionIndex + 1), attr, true);
+    case BackgroundExtended:
+        return 1 + _SetRgbColorsHelper(options.subspan(optionIndex + 1), attr, false);
+    default:
+        return 1;
+    }
+}
+
+// Routine Description:
+// - Helper to apply a number of graphic rendition options to an attribute.
+// Arguments:
+// - options - An array of options that will be applied in sequence.
+// - attr - The attribute that will be updated with the applied options.
+// Return Value:
+// - <none>
+void AdaptDispatch::_ApplyGraphicsOptions(const VTParameters options,
+                                          TextAttribute& attr) noexcept
+{
+    for (size_t i = 0; i < options.size();)
+    {
+        i += _ApplyGraphicsOption(options, i, attr);
+    }
+}
+
 // Routine Description:
 // - SGR - Modifies the graphical rendering options applied to the next
 //   characters written into the buffer.
@@ -75,187 +279,8 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options,
 bool AdaptDispatch::SetGraphicsRendition(const VTParameters options)
 {
     auto attr = _api.GetTextBuffer().GetCurrentAttributes();
-
-    // Run through the graphics options and apply them
-    for (size_t i = 0; i < options.size(); i++)
-    {
-        const GraphicsOptions opt = options.at(i);
-        switch (opt)
-        {
-        case Off:
-            attr.SetDefaultForeground();
-            attr.SetDefaultBackground();
-            attr.SetDefaultRenditionAttributes();
-            break;
-        case ForegroundDefault:
-            attr.SetDefaultForeground();
-            break;
-        case BackgroundDefault:
-            attr.SetDefaultBackground();
-            break;
-        case Intense:
-            attr.SetIntense(true);
-            break;
-        case RGBColorOrFaint:
-            attr.SetFaint(true);
-            break;
-        case NotIntenseOrFaint:
-            attr.SetIntense(false);
-            attr.SetFaint(false);
-            break;
-        case Italics:
-            attr.SetItalic(true);
-            break;
-        case NotItalics:
-            attr.SetItalic(false);
-            break;
-        case BlinkOrXterm256Index:
-        case RapidBlink: // We just interpret rapid blink as an alias of blink.
-            attr.SetBlinking(true);
-            break;
-        case Steady:
-            attr.SetBlinking(false);
-            break;
-        case Invisible:
-            attr.SetInvisible(true);
-            break;
-        case Visible:
-            attr.SetInvisible(false);
-            break;
-        case CrossedOut:
-            attr.SetCrossedOut(true);
-            break;
-        case NotCrossedOut:
-            attr.SetCrossedOut(false);
-            break;
-        case Negative:
-            attr.SetReverseVideo(true);
-            break;
-        case Positive:
-            attr.SetReverseVideo(false);
-            break;
-        case Underline:
-            attr.SetUnderlined(true);
-            break;
-        case DoublyUnderlined:
-            attr.SetDoublyUnderlined(true);
-            break;
-        case NoUnderline:
-            attr.SetUnderlined(false);
-            attr.SetDoublyUnderlined(false);
-            break;
-        case Overline:
-            attr.SetOverlined(true);
-            break;
-        case NoOverline:
-            attr.SetOverlined(false);
-            break;
-        case ForegroundBlack:
-            attr.SetIndexedForeground(TextColor::DARK_BLACK);
-            break;
-        case ForegroundBlue:
-            attr.SetIndexedForeground(TextColor::DARK_BLUE);
-            break;
-        case ForegroundGreen:
-            attr.SetIndexedForeground(TextColor::DARK_GREEN);
-            break;
-        case ForegroundCyan:
-            attr.SetIndexedForeground(TextColor::DARK_CYAN);
-            break;
-        case ForegroundRed:
-            attr.SetIndexedForeground(TextColor::DARK_RED);
-            break;
-        case ForegroundMagenta:
-            attr.SetIndexedForeground(TextColor::DARK_MAGENTA);
-            break;
-        case ForegroundYellow:
-            attr.SetIndexedForeground(TextColor::DARK_YELLOW);
-            break;
-        case ForegroundWhite:
-            attr.SetIndexedForeground(TextColor::DARK_WHITE);
-            break;
-        case BackgroundBlack:
-            attr.SetIndexedBackground(TextColor::DARK_BLACK);
-            break;
-        case BackgroundBlue:
-            attr.SetIndexedBackground(TextColor::DARK_BLUE);
-            break;
-        case BackgroundGreen:
-            attr.SetIndexedBackground(TextColor::DARK_GREEN);
-            break;
-        case BackgroundCyan:
-            attr.SetIndexedBackground(TextColor::DARK_CYAN);
-            break;
-        case BackgroundRed:
-            attr.SetIndexedBackground(TextColor::DARK_RED);
-            break;
-        case BackgroundMagenta:
-            attr.SetIndexedBackground(TextColor::DARK_MAGENTA);
-            break;
-        case BackgroundYellow:
-            attr.SetIndexedBackground(TextColor::DARK_YELLOW);
-            break;
-        case BackgroundWhite:
-            attr.SetIndexedBackground(TextColor::DARK_WHITE);
-            break;
-        case BrightForegroundBlack:
-            attr.SetIndexedForeground(TextColor::BRIGHT_BLACK);
-            break;
-        case BrightForegroundBlue:
-            attr.SetIndexedForeground(TextColor::BRIGHT_BLUE);
-            break;
-        case BrightForegroundGreen:
-            attr.SetIndexedForeground(TextColor::BRIGHT_GREEN);
-            break;
-        case BrightForegroundCyan:
-            attr.SetIndexedForeground(TextColor::BRIGHT_CYAN);
-            break;
-        case BrightForegroundRed:
-            attr.SetIndexedForeground(TextColor::BRIGHT_RED);
-            break;
-        case BrightForegroundMagenta:
-            attr.SetIndexedForeground(TextColor::BRIGHT_MAGENTA);
-            break;
-        case BrightForegroundYellow:
-            attr.SetIndexedForeground(TextColor::BRIGHT_YELLOW);
-            break;
-        case BrightForegroundWhite:
-            attr.SetIndexedForeground(TextColor::BRIGHT_WHITE);
-            break;
-        case BrightBackgroundBlack:
-            attr.SetIndexedBackground(TextColor::BRIGHT_BLACK);
-            break;
-        case BrightBackgroundBlue:
-            attr.SetIndexedBackground(TextColor::BRIGHT_BLUE);
-            break;
-        case BrightBackgroundGreen:
-            attr.SetIndexedBackground(TextColor::BRIGHT_GREEN);
-            break;
-        case BrightBackgroundCyan:
-            attr.SetIndexedBackground(TextColor::BRIGHT_CYAN);
-            break;
-        case BrightBackgroundRed:
-            attr.SetIndexedBackground(TextColor::BRIGHT_RED);
-            break;
-        case BrightBackgroundMagenta:
-            attr.SetIndexedBackground(TextColor::BRIGHT_MAGENTA);
-            break;
-        case BrightBackgroundYellow:
-            attr.SetIndexedBackground(TextColor::BRIGHT_YELLOW);
-            break;
-        case BrightBackgroundWhite:
-            attr.SetIndexedBackground(TextColor::BRIGHT_WHITE);
-            break;
-        case ForegroundExtended:
-            i += _SetRgbColorsHelper(options.subspan(i + 1), attr, true);
-            break;
-        case BackgroundExtended:
-            i += _SetRgbColorsHelper(options.subspan(i + 1), attr, false);
-            break;
-        }
-    }
+    _ApplyGraphicsOptions(options, attr);
     _api.SetTextAttributes(attr);
-
     return true;
 }
 
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 607a3bc72bb..2f84f59cf2e 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -86,6 +86,14 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool SelectiveEraseInDisplay(const DispatchTypes::EraseType /*eraseType*/) override { return false; } // DECSED
     bool SelectiveEraseInLine(const DispatchTypes::EraseType /*eraseType*/) override { return false; } // DECSEL
 
+    bool ChangeAttributesRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/, const VTParameters /*attrs*/) override { return false; } // DECCARA
+    bool ReverseAttributesRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/, const VTParameters /*attrs*/) override { return false; } // DECRARA
+    bool CopyRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/, const VTInt /*page*/, const VTInt /*dstTop*/, const VTInt /*dstLeft*/, const VTInt /*dstPage*/) override { return false; } // DECCRA
+    bool FillRectangularArea(const VTParameter /*ch*/, const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/) override { return false; } // DECFRA
+    bool EraseRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/) override { return false; } // DECERA
+    bool SelectiveEraseRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/) override { return false; } // DECSERA
+    bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent /*changeExtent*/) override { return false; } // DECSACE
+
     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
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index d6227383e53..6fc371c8ab4 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -632,6 +632,34 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         success = _dispatch->PopGraphicsRendition();
         TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPOPSGR);
         break;
+    case CsiActionCodes::DECCARA_ChangeAttributesRectangularArea:
+        success = _dispatch->ChangeAttributesRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.subspan(4));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCARA);
+        break;
+    case CsiActionCodes::DECRARA_ReverseAttributesRectangularArea:
+        success = _dispatch->ReverseAttributesRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.subspan(4));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRARA);
+        break;
+    case CsiActionCodes::DECCRA_CopyRectangularArea:
+        success = _dispatch->CopyRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.at(4), parameters.at(5), parameters.at(6), parameters.at(7));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCRA);
+        break;
+    case CsiActionCodes::DECFRA_FillRectangularArea:
+        success = _dispatch->FillRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2), parameters.at(3).value_or(0), parameters.at(4).value_or(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECFRA);
+        break;
+    case CsiActionCodes::DECERA_EraseRectangularArea:
+        success = _dispatch->EraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECERA);
+        break;
+    case CsiActionCodes::DECSERA_SelectiveEraseRectangularArea:
+        success = _dispatch->SelectiveEraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSERA);
+        break;
+    case CsiActionCodes::DECSACE_SelectAttributeChangeExtent:
+        success = _dispatch->SelectAttributeChangeExtent(parameters.at(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSACE);
+        break;
     case CsiActionCodes::DECAC_AssignColor:
         success = _dispatch->AssignColor(parameters.at(0), parameters.at(1).value_or(0), parameters.at(2).value_or(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC);
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index eb29ca9de5d..67b44891e76 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -146,7 +146,14 @@ namespace Microsoft::Console::VirtualTerminal
             XT_PopSgrAlias = VTID("#q"),
             XT_PushSgr = VTID("#{"),
             XT_PopSgr = VTID("#}"),
+            DECCARA_ChangeAttributesRectangularArea = VTID("$r"),
+            DECRARA_ReverseAttributesRectangularArea = VTID("$t"),
+            DECCRA_CopyRectangularArea = VTID("$v"),
+            DECFRA_FillRectangularArea = VTID("$x"),
+            DECERA_EraseRectangularArea = VTID("$z"),
+            DECSERA_SelectiveEraseRectangularArea = VTID("${"),
             DECSCPP_SetColumnsPerPage = VTID("$|"),
+            DECSACE_SelectAttributeChangeExtent = VTID("*x"),
             DECAC_AssignColor = VTID(",|"),
             DECPS_PlaySound = VTID(",~")
         };
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index 2ba820b01bb..4812fb77544 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -283,6 +283,13 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"),
                                       TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"),
                                       TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECCARA], "DECCARA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECRARA], "DECRARA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECCRA], "DECCRA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECFRA], "DECFRA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECSACE], "DECSACE"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECPS], "DECPS"),
                                       TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"),
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 27519a69673..283a163402b 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -110,6 +110,13 @@ namespace Microsoft::Console::VirtualTerminal
             OSCSCB,
             XTPUSHSGR,
             XTPOPSGR,
+            DECCARA,
+            DECRARA,
+            DECCRA,
+            DECFRA,
+            DECERA,
+            DECSERA,
+            DECSACE,
             DECAC,
             DECPS,
             // Only use this last enum as a count of the number of codes.


Test patch: