Repository: microsoft/terminal (language: cpp)

Issue:
Add support for Erase Color Mode (DECECM)
# Description of the new feature/enhancement

There are a number of operations that fill the background with "standard erase attributes", which means all characters attributes are reset, but the active background color is still applied.

However, on the VT525 you can control whether that background color is used with the _Erase Color Mode_ (`DECECM`). If `DECECM` is set, those operations fill with the default colors, rather than the active colors.

This is useful when writing text with a background color that may wrap onto the next line. If that happens at the bottom of the page, the new line is completely filled with the active background color, which is usually not what you want. Setting `DECECM` can fix that.

# Proposed technical implementation details (optional)

I was thinking we could possibly just add a flag to the `TextAttribute::SetStandardErase` method that specifies whether the rendition colors should be reset along with the character attributes. Then wherever we're calling `SetStandardErase`, we lookup the `DECECM` mode and pass that as the parameter.


Discussion / hints:
@al20878 I've created another test which I'm hoping will confirm the way this mode is intended to work. This is strictly for the VT525.

https://gist.github.com/j4james/cac2cec7fe0df78e59eb52f9c8201599

This script renders a test pattern using the various scrolling and erasing operations which I think are likely to be affected. On the first pass the mode is set, and the second pass it's reset. I'd be grateful if you could take a photograph of the two renditions so I can see how they differ. The test pattern will remain visible until you press <kbd>Enter</kbd> (or whatever the VT525 equivalent).
@j4james I downloaded the script and am going to run it a bit later, will post the results. (Yeah, it's `Return` on the LK411 keyboard, while `Enter` is a keypad equivalent.)
![Set](https://user-images.githubusercontent.com/44822487/224577391-d472a45f-0e15-4235-807a-ec8335e3d432.png)
![Reset](https://user-images.githubusercontent.com/44822487/224577398-23e9b977-9571-4e4b-9d25-7d0fb2cf7a18.png)


This is brilliant! Thank you so much. This is exactly what I was hoping to see.

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 496ffcf079f..1671f48f8bd 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -424,6 +424,7 @@ DECDHL
 decdld
 DECDMAC
 DECDWL
+DECECM
 DECEKBD
 DECERA
 DECFI
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index b3c2e590e37..f73e8813f19 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -569,10 +569,10 @@ bool TextBuffer::NewlineCursor()
 //Routine Description:
 // - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable.
 //Arguments:
-// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row.
+// - fillAttributes - the attributes with which the recycled row will be initialized.
 //Return Value:
 // - true if we successfully incremented the buffer.
-bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
+bool TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes)
 {
     // FirstRow is at any given point in time the array index in the circular buffer that corresponds
     // to the logical position 0 in the window (cursor coordinates and all other coordinates).
@@ -585,13 +585,6 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
     _PruneHyperlinks();
 
     // Second, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed.
-    auto fillAttributes = _currentAttributes;
-    if (inVtMode)
-    {
-        // The VT standard requires that the new row is initialized with
-        // the current background color, but with no meta attributes set.
-        fillAttributes.SetStandardErase();
-    }
     GetRowByOffset(0).Reset(fillAttributes);
     {
         // Now proceed to increment.
@@ -851,7 +844,7 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no
     _currentAttributes = currentAttributes;
 }
 
-void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
+void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes)
 {
     const auto cursorPosition = GetCursor().GetPosition();
     const auto rowIndex = cursorPosition.y;
@@ -865,11 +858,9 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
         if (lineRendition != LineRendition::SingleWidth)
         {
             const auto fillChar = L' ';
-            auto fillAttrs = GetCurrentAttributes();
-            fillAttrs.SetStandardErase();
             const auto fillOffset = GetLineWidth(rowIndex);
             const auto fillLength = gsl::narrow<size_t>(GetSize().Width() - fillOffset);
-            const OutputCellIterator fillData{ fillChar, fillAttrs, fillLength };
+            const OutputCellIterator fillData{ fillChar, fillAttributes, fillLength };
             row.WriteCells(fillData, fillOffset, false);
             // We also need to make sure the cursor is clamped within the new width.
             GetCursor().SetPosition(ClampPositionWithinLine(cursorPosition));
diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp
index 555531a25aa..6601fbb57d5 100644
--- a/src/buffer/out/textBuffer.hpp
+++ b/src/buffer/out/textBuffer.hpp
@@ -109,7 +109,7 @@ class TextBuffer final
     bool NewlineCursor();
 
     // Scroll needs access to this to quickly rotate around the buffer.
-    bool IncrementCircularBuffer(const bool inVtMode = false);
+    bool IncrementCircularBuffer(const TextAttribute& fillAttributes = {});
 
     til::point GetLastNonSpaceCharacter(std::optional<const Microsoft::Console::Types::Viewport> viewOptional = std::nullopt) const;
 
@@ -128,7 +128,7 @@ class TextBuffer final
 
     void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept;
 
-    void SetCurrentLineRendition(const LineRendition lineRendition);
+    void SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes);
     void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept;
     LineRendition GetLineRendition(const til::CoordType row) const noexcept;
     bool IsDoubleWidthLine(const til::CoordType row) const noexcept;
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 1c8e36d72b4..1cdc4c480d9 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -124,7 +124,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void SetWorkingDirectory(std::wstring_view uri) override;
     void PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) override;
     void ShowWindow(bool showOrHide) override;
-    void UseAlternateScreenBuffer() override;
+    void UseAlternateScreenBuffer(const TextAttribute& attrs) override;
     void UseMainScreenBuffer() override;
 
     void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 28b320f638c..b76931c46dc 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -185,7 +185,7 @@ void Terminal::PlayMidiNote(const int noteNumber, const int velocity, const std:
     _pfnPlayMidiNote(noteNumber, velocity, duration);
 }
 
-void Terminal::UseAlternateScreenBuffer()
+void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs)
 {
     // the new alt buffer is exactly the size of the viewport.
     _altBufferSize = _mutableViewport.Dimensions();
@@ -197,7 +197,7 @@ void Terminal::UseAlternateScreenBuffer()
 
     // Create a new alt buffer
     _altBuffer = std::make_unique<TextBuffer>(_altBufferSize,
-                                              TextAttribute{},
+                                              attrs,
                                               cursorSize,
                                               true,
                                               _mainBuffer->GetRenderer());
diff --git a/src/host/output.cpp b/src/host/output.cpp
index 8b0ce0b7fd9..fbdb9babf84 100644
--- a/src/host/output.cpp
+++ b/src/host/output.cpp
@@ -302,7 +302,8 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source
 bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo)
 {
     // Rotate the circular buffer around and wipe out the previous final line.
-    auto fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer();
+    auto& buffer = screenInfo.GetTextBuffer();
+    auto fSuccess = buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes());
     if (fSuccess)
     {
         // Trigger a graphical update if we're active.
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 8c14921392f..202b0b53a3b 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -183,11 +183,13 @@ void ConhostInternalGetSet::SetWindowTitle(std::wstring_view title)
 // - Swaps to the alternate screen buffer. In virtual terminals, there exists both a "main"
 //     screen buffer and an alternate. This creates a new alternate, and switches to it.
 //     If there is an already existing alternate, it is discarded.
+// Arguments:
+// - attrs - the attributes the buffer is initialized with.
 // Return Value:
 // - <none>
-void ConhostInternalGetSet::UseAlternateScreenBuffer()
+void ConhostInternalGetSet::UseAlternateScreenBuffer(const TextAttribute& attrs)
 {
-    THROW_IF_NTSTATUS_FAILED(_io.GetActiveOutputBuffer().UseAlternateScreenBuffer());
+    THROW_IF_NTSTATUS_FAILED(_io.GetActiveOutputBuffer().UseAlternateScreenBuffer(attrs));
 }
 
 // Routine Description:
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 6e0120cc997..612f179c700 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -45,7 +45,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
 
     void SetWindowTitle(const std::wstring_view title) override;
 
-    void UseAlternateScreenBuffer() override;
+    void UseAlternateScreenBuffer(const TextAttribute& attrs) override;
 
     void UseMainScreenBuffer() override;
 
diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp
index 789020ebfc9..4c8d5701f00 100644
--- a/src/host/screenInfo.cpp
+++ b/src/host/screenInfo.cpp
@@ -1847,21 +1847,17 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const
 //     machine with the main buffer it belongs to.
 // TODO: MSFT:19817348 Don't create alt screenbuffer's via an out SCREEN_INFORMATION**
 // Parameters:
+// - initAttributes - the attributes the buffer is initialized with.
 // - ppsiNewScreenBuffer - a pointer to receive the newly created buffer.
 // Return value:
 // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error.
-[[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer)
+[[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(const TextAttribute& initAttributes, _Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer)
 {
     // Create new screen buffer.
     auto WindowSize = _viewport.Dimensions();
 
     const auto& existingFont = GetCurrentFont();
 
-    // The buffer needs to be initialized with the standard erase attributes,
-    // i.e. the current background color, but with no meta attributes set.
-    auto initAttributes = GetAttributes();
-    initAttributes.SetStandardErase();
-
     auto Status = SCREEN_INFORMATION::CreateInstance(WindowSize,
                                                      existingFont,
                                                      WindowSize,
@@ -1961,10 +1957,10 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain)
 //     screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already
 //     existing alternate, it is discarded. This allows applications to retain one HANDLE, and switch which buffer it points to seamlessly.
 // Parameters:
-// - None
+// - initAttributes - the attributes the buffer is initialized with.
 // Return value:
 // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error.
-[[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer()
+[[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer(const TextAttribute& initAttributes)
 {
     auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
     auto& siMain = GetMainBuffer();
@@ -1972,7 +1968,7 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain)
     _handleDeferredResize(siMain);
 
     SCREEN_INFORMATION* psiNewAltBuffer;
-    auto Status = _CreateAltBuffer(&psiNewAltBuffer);
+    auto Status = _CreateAltBuffer(initAttributes, &psiNewAltBuffer);
     if (SUCCEEDED_NTSTATUS(Status))
     {
         // if this is already an alternate buffer, we want to make the new
@@ -2269,43 +2265,23 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport,
 // - S_OK
 [[nodiscard]] HRESULT SCREEN_INFORMATION::ClearBuffer()
 {
-    const auto oldCursorPos = _textBuffer->GetCursor().GetPosition();
-    auto sNewTop = oldCursorPos.y;
-
-    auto delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height());
-    for (auto i = 0; i < delta; i++)
+    // Rotate the buffer to bring the cursor row to the top of the viewport.
+    const auto cursorPos = _textBuffer->GetCursor().GetPosition();
+    for (auto i = 0; i < cursorPos.y; i++)
     {
         _textBuffer->IncrementCircularBuffer();
-        sNewTop--;
     }
 
-    const til::point coordNewOrigin{ 0, sNewTop };
-    RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true));
-
-    // SetViewportOrigin will only move the virtual bottom down, but in this
-    // case we need to reset it to the top, so we have to update it explicitly.
-    UpdateBottom();
-
-    // Place the cursor at the same x coord, on the row that's now the top
-    RETURN_IF_FAILED(SetCursorPosition({ oldCursorPos.x, sNewTop }, false));
-
-    // Update all the rows in the current viewport with the standard erase attributes,
-    // i.e. the current background color, but with no meta attributes set.
-    auto fillAttributes = GetAttributes();
-    fillAttributes.SetStandardErase();
+    // Erase everything below that point.
+    RETURN_IF_FAILED(SetCursorPosition({ 0, 1 }, false));
+    auto& engine = reinterpret_cast<OutputStateMachineEngine&>(_stateMachine->Engine());
+    engine.Dispatch().EraseInDisplay(DispatchTypes::EraseType::ToEnd);
 
-    // +1 on the y coord because we don't want to clear the attributes of the
-    // cursor row, the one we saved.
-    til::point fillPosition{ 0, _viewport.Top() + 1 };
-    auto fillLength = gsl::narrow<size_t>(_viewport.Height() * GetBufferSize().Width());
-    auto fillData = OutputCellIterator{ fillAttributes, fillLength };
-    Write(fillData, fillPosition, false);
+    // Restore the original cursor x offset, but now on the first row.
+    RETURN_IF_FAILED(SetCursorPosition({ cursorPos.x, 0 }, false));
 
     _textBuffer->TriggerRedrawAll();
 
-    // Also reset the line rendition for the erased rows.
-    _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive());
-
     return S_OK;
 }
 
diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp
index d67e3f5684c..f74d6f32648 100644
--- a/src/host/screenInfo.hpp
+++ b/src/host/screenInfo.hpp
@@ -193,7 +193,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
 
     void MakeCursorVisible(const til::point CursorPosition);
 
-    [[nodiscard]] NTSTATUS UseAlternateScreenBuffer();
+    [[nodiscard]] NTSTATUS UseAlternateScreenBuffer(const TextAttribute& initAttributes);
     void UseMainScreenBuffer();
 
     SCREEN_INFORMATION& GetMainBuffer();
@@ -254,7 +254,8 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
     [[nodiscard]] NTSTATUS _InitializeOutputStateMachine();
     void _FreeOutputStateMachine();
 
-    [[nodiscard]] NTSTATUS _CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer);
+    [[nodiscard]] NTSTATUS _CreateAltBuffer(const TextAttribute& initAttributes,
+                                            _Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer);
 
     bool _IsAltBuffer() const;
     bool _IsInPtyMode() const;
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 1209780e2be..441394ef088 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -262,6 +262,8 @@ class ScreenBufferTests
     TEST_METHOD(CopyDoubleWidthRectangularArea);
 
     TEST_METHOD(DelayedWrapReset);
+
+    TEST_METHOD(EraseColorMode);
 };
 
 void ScreenBufferTests::SingleAlternateBufferCreationTest()
@@ -275,7 +277,7 @@ void ScreenBufferTests::SingleAlternateBufferCreationTest()
     VERIFY_IS_NULL(psiOriginal->_psiAlternateBuffer);
     VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
 
-    auto Status = psiOriginal->UseAlternateScreenBuffer();
+    auto Status = psiOriginal->UseAlternateScreenBuffer({});
     if (VERIFY_NT_SUCCESS(Status))
     {
         Log::Comment(L"First alternate buffer successfully created");
@@ -308,7 +310,7 @@ void ScreenBufferTests::MultipleAlternateBufferCreationTest()
         L"main buffer.");
 
     const auto psiOriginal = &gci.GetActiveOutputBuffer();
-    auto Status = psiOriginal->UseAlternateScreenBuffer();
+    auto Status = psiOriginal->UseAlternateScreenBuffer({});
     if (VERIFY_NT_SUCCESS(Status))
     {
         Log::Comment(L"First alternate buffer successfully created");
@@ -319,7 +321,7 @@ void ScreenBufferTests::MultipleAlternateBufferCreationTest()
         VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
         VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
 
-        Status = psiFirstAlternate->UseAlternateScreenBuffer();
+        Status = psiFirstAlternate->UseAlternateScreenBuffer({});
         if (VERIFY_NT_SUCCESS(Status))
         {
             Log::Comment(L"Second alternate buffer successfully created");
@@ -353,7 +355,7 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest()
         L"Testing creating one alternate buffer, then creating another"
         L" alternate from the main, before returning to the main buffer.");
     const auto psiOriginal = &gci.GetActiveOutputBuffer();
-    auto Status = psiOriginal->UseAlternateScreenBuffer();
+    auto Status = psiOriginal->UseAlternateScreenBuffer({});
     if (VERIFY_NT_SUCCESS(Status))
     {
         Log::Comment(L"First alternate buffer successfully created");
@@ -364,7 +366,7 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest()
         VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
         VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
 
-        Status = psiOriginal->UseAlternateScreenBuffer();
+        Status = psiOriginal->UseAlternateScreenBuffer({});
         if (VERIFY_NT_SUCCESS(Status))
         {
             Log::Comment(L"Second alternate buffer successfully created");
@@ -409,7 +411,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest()
     mainCursor.SetBlinkingAllowed(mainCursorBlinking);
 
     Log::Comment(L"Switch to the alternate buffer.");
-    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
     auto& altBuffer = gci.GetActiveOutputBuffer();
     auto& altCursor = altBuffer.GetTextBuffer().GetCursor();
     auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
@@ -901,7 +903,7 @@ void ScreenBufferTests::TestAltBufferTabStops()
     _SetTabStops(mainBuffer, expectedStops, true);
     VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(mainBuffer));
 
-    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
     auto& altBuffer = gci.GetActiveOutputBuffer();
     auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
 
@@ -2449,7 +2451,7 @@ void ScreenBufferTests::TestAltBufferCursorState()
     VERIFY_IS_NULL(original._psiAlternateBuffer);
     VERIFY_IS_NULL(original._psiMainBuffer);
 
-    auto Status = original.UseAlternateScreenBuffer();
+    auto Status = original.UseAlternateScreenBuffer({});
     if (VERIFY_NT_SUCCESS(Status))
     {
         Log::Comment(L"Alternate buffer successfully created");
@@ -2494,7 +2496,7 @@ void ScreenBufferTests::TestAltBufferVtDispatching()
     VERIFY_IS_NULL(mainBuffer._psiAlternateBuffer);
     VERIFY_IS_NULL(mainBuffer._psiMainBuffer);
 
-    auto Status = mainBuffer.UseAlternateScreenBuffer();
+    auto Status = mainBuffer.UseAlternateScreenBuffer({});
     if (VERIFY_NT_SUCCESS(Status))
     {
         Log::Comment(L"Alternate buffer successfully created");
@@ -3079,7 +3081,7 @@ void ScreenBufferTests::SetGlobalColorTable()
 
     Log::Comment(NoThrowString().Format(L"Create an alt buffer"));
 
-    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
     auto& altBuffer = gci.GetActiveOutputBuffer();
     auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
 
@@ -3182,7 +3184,7 @@ void ScreenBufferTests::SetColorTableThreeDigits()
 
     Log::Comment(NoThrowString().Format(L"Create an alt buffer"));
 
-    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+    VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
     auto& altBuffer = gci.GetActiveOutputBuffer();
     auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
 
@@ -5800,7 +5802,7 @@ void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling()
     VERIFY_IS_NULL(siMain._psiAlternateBuffer);
 
     Log::Comment(L"Create an alternate buffer");
-    if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer()))
+    if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer({})))
     {
         VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer);
         auto& altBuffer = *siMain._psiAlternateBuffer;
@@ -5965,7 +5967,7 @@ void ScreenBufferTests::ClearAlternateBuffer()
     VerifyText(siMain.GetTextBuffer());
 
     Log::Comment(L"Create an alternate buffer");
-    if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer()))
+    if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer({})))
     {
         VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer);
         auto& altBuffer = *siMain._psiAlternateBuffer;
@@ -8176,7 +8178,7 @@ void ScreenBufferTests::CopyDoubleWidthRectangularArea()
 
     // Make the second line (offset 1) double width.
     textBuffer.GetCursor().SetPosition({ 0, 1 });
-    textBuffer.SetCurrentLineRendition(LineRendition::DoubleWidth);
+    textBuffer.SetCurrentLineRendition(LineRendition::DoubleWidth, activeAttr);
 
     // Copy a segment of the top three lines with DECCRA.
     stateMachine.ProcessString(L"\033[1;31;3;50;1;4;31;1$v");
@@ -8306,3 +8308,89 @@ void ScreenBufferTests::DelayedWrapReset()
         VERIFY_ARE_EQUAL(expectedPos, actualPos);
     }
 }
+
+void ScreenBufferTests::EraseColorMode()
+{
+    BEGIN_TEST_METHOD_PROPERTIES()
+        TEST_METHOD_PROPERTY(L"Data:op", L"{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}")
+        TEST_METHOD_PROPERTY(L"Data:eraseMode", L"{false,true}")
+    END_TEST_METHOD_PROPERTIES();
+
+    bool eraseMode;
+    VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseMode", eraseMode));
+    int opIndex;
+    VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", opIndex));
+
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+    WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
+
+    const auto bufferWidth = si.GetBufferSize().Width();
+    const auto bufferHeight = si.GetBufferSize().Height();
+
+    // Set the viewport to 24 lines.
+    si.SetViewport(Viewport::FromDimensions({ 0, 0 }, { bufferWidth, 24 }), true);
+    const auto viewport = si.GetViewport();
+
+    // Fill the buffer with text. Red on Blue.
+    const auto bufferChars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn";
+    const auto bufferAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
+    _FillLines(0, bufferHeight, bufferChars, bufferAttr);
+
+    // Set the active attributes with a mix of color and meta attributes.
+    auto activeAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
+    activeAttr.SetCrossedOut(true);
+    activeAttr.SetReverseVideo(true);
+    activeAttr.SetUnderlined(true);
+    si.SetAttributes(activeAttr);
+
+    // By default, the meta attributes are expected to be cleared when erasing.
+    auto standardEraseAttr = activeAttr;
+    standardEraseAttr.SetStandardErase();
+
+    const struct
+    {
+        std::wstring_view name;
+        std::wstring_view sequence;
+        til::point startPos = {};
+        til::point erasePos = {};
+    } ops[] = {
+        { L"LF", L"\n", { 0, 23 }, { 0, 24 } }, // pans down on last row
+        { L"VT", L"\v", { 0, 23 }, { 0, 24 } }, // pans down on last row
+        { L"FF", L"\f", { 0, 23 }, { 0, 24 } }, // pans down on last row
+        { L"NEL", L"\033E", { 0, 23 }, { 0, 24 } }, // pans down on last row
+        { L"IND", L"\033D", { 0, 23 }, { 0, 24 } }, // pans down on last row
+        { L"RI", L"\033M" },
+        { L"DECBI", L"\033\066" },
+        { L"DECFI", L"\033\071", { 79, 0 }, { 79, 0 } }, // scrolls in last column
+        { L"DECIC", L"\033['}" },
+        { L"DECDC", L"\033['~", {}, { 79, 0 } }, // last column erased
+        { L"ICH", L"\033[@" },
+        { L"DCH", L"\033[P", {}, { 79, 0 } }, // last column erased
+        { L"IL", L"\033[L" },
+        { L"DL", L"\033[M", {}, { 0, 23 } }, // last row erased
+        { L"ECH", L"\033[X" },
+        { L"EL", L"\033[K" },
+        { L"ED", L"\033[J" },
+        { L"DECERA", L"\033[$z" },
+        { L"SU", L"\033[S", {}, { 0, 23 } }, // last row erased
+        { L"SD", L"\033[T" },
+    };
+    const auto& op = gsl::at(ops, opIndex);
+
+    si.GetTextBuffer().GetCursor().SetPosition(op.startPos);
+
+    Log::Comment(eraseMode ? L"Enable DECECM" : L"Disable DECECM");
+    stateMachine.ProcessString(eraseMode ? L"\033[?117h" : L"\033[?117l");
+
+    Log::Comment(NoThrowString().Format(L"Execute %s", op.name.data()));
+    stateMachine.ProcessString(op.sequence);
+
+    Log::Comment(L"Verify expected erase attributes");
+    const auto expectedEraseAttr = eraseMode ? TextAttribute{} : standardEraseAttr;
+    const auto cellData = si.GetCellDataAt(op.erasePos);
+    VERIFY_ARE_EQUAL(expectedEraseAttr, cellData->TextAttr());
+    VERIFY_ARE_EQUAL(L" ", cellData->Chars());
+}
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index f123d948428..9e342ec5f80 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -432,6 +432,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         DECNKM_NumericKeypadMode = DECPrivateMode(66),
         DECBKM_BackarrowKeyMode = DECPrivateMode(67),
         DECLRMM_LeftRightMarginMode = DECPrivateMode(69),
+        DECECM_EraseColorMode = DECPrivateMode(117),
         VT200_MOUSE_MODE = DECPrivateMode(1000),
         BUTTON_EVENT_MOUSE_MODE = DECPrivateMode(1002),
         ANY_EVENT_MOUSE_MODE = DECPrivateMode(1003),
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index cf32f308943..e2faff39616 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -60,7 +60,7 @@ namespace Microsoft::Console::VirtualTerminal
 
         virtual void WarningBell() = 0;
         virtual void SetWindowTitle(const std::wstring_view title) = 0;
-        virtual void UseAlternateScreenBuffer() = 0;
+        virtual void UseAlternateScreenBuffer(const TextAttribute& attrs) = 0;
         virtual void UseMainScreenBuffer() = 0;
 
         virtual CursorType GetUserDefaultCursorStyle() const = 0;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index d17d3b740a8..43e7f54068f 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -563,6 +563,28 @@ bool AdaptDispatch::CursorRestoreState()
     return true;
 }
 
+// Routine Description:
+// - Returns the attributes that should be used when erasing the buffer. When
+//   the Erase Color mode is set, we use the default attributes, but when reset,
+//   we use the active color attributes with the character attributes cleared.
+// Arguments:
+// - textBuffer - Target buffer that is being erased.
+// Return Value:
+// - The erase TextAttribute value.
+TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) const noexcept
+{
+    if (_modes.test(Mode::EraseColor))
+    {
+        return {};
+    }
+    else
+    {
+        auto eraseAttributes = textBuffer.GetCurrentAttributes();
+        eraseAttributes.SetStandardErase();
+        return eraseAttributes;
+    }
+}
+
 // Routine Description:
 // - Scrolls an area of the buffer in a vertical direction.
 // Arguments:
@@ -611,8 +633,7 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec
     auto eraseRect = scrollRect;
     eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta);
     eraseRect.bottom = eraseRect.top + absoluteDelta;
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
     _FillRect(textBuffer, eraseRect, L' ', eraseAttributes);
 
     // Also reset the line rendition for the erased rows.
@@ -660,8 +681,7 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r
     auto eraseRect = scrollRect;
     eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta);
     eraseRect.right = eraseRect.left + absoluteDelta;
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
     _FillRect(textBuffer, eraseRect, L' ', eraseAttributes);
 }
 
@@ -760,8 +780,7 @@ bool AdaptDispatch::EraseCharacters(const VTInt numChars)
     // The ECH control is expected to reset the delayed wrap flag.
     textBuffer.GetCursor().ResetDelayEOLWrap();
 
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
     _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, L' ', eraseAttributes);
 
     return true;
@@ -806,8 +825,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType)
     // take care of that themselves when they set the cursor position.
     textBuffer.GetCursor().ResetDelayEOLWrap();
 
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
 
     // When erasing the display, every line that is erased in full should be
     // reset to single width. When erasing to the end, this could include
@@ -846,8 +864,7 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType)
     // The EL control is expected to reset the delayed wrap flag.
     textBuffer.GetCursor().ResetDelayEOLWrap();
 
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
     switch (eraseType)
     {
     case DispatchTypes::EraseType::FromBeginning:
@@ -1292,8 +1309,7 @@ bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, cons
 {
     auto& textBuffer = _api.GetTextBuffer();
     const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions());
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
     _FillRect(textBuffer, eraseRect, L' ', eraseAttributes);
     return true;
 }
@@ -1430,7 +1446,8 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition)
     if (!_modes.test(Mode::AllowDECSLRM))
     {
         auto& textBuffer = _api.GetTextBuffer();
-        textBuffer.SetCurrentLineRendition(rendition);
+        const auto eraseAttributes = _GetEraseAttributes(textBuffer);
+        textBuffer.SetCurrentLineRendition(rendition, eraseAttributes);
         // There is some variation in how this was handled by the different DEC
         // terminals, but the STD 070 reference (on page D-13) makes it clear that
         // the delayed wrap (aka the Last Column Flag) was expected to be reset when
@@ -1748,7 +1765,8 @@ void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable)
     if (enable)
     {
         CursorSaveState();
-        _api.UseAlternateScreenBuffer();
+        const auto& textBuffer = _api.GetTextBuffer();
+        _api.UseAlternateScreenBuffer(_GetEraseAttributes(textBuffer));
         _usingAltBuffer = true;
     }
     else
@@ -1858,6 +1876,9 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
             textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom);
         }
         return true;
+    case DispatchTypes::ModeParams::DECECM_EraseColorMode:
+        _modes.set(Mode::EraseColor, enable);
+        return true;
     case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
         _terminalInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enable);
         return !_PassThroughInputModes();
@@ -1989,6 +2010,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
     case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode:
         enabled = _modes.test(Mode::AllowDECSLRM);
         break;
+    case DispatchTypes::ModeParams::DECECM_EraseColorMode:
+        enabled = _modes.test(Mode::EraseColor);
+        break;
     case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking);
         break;
@@ -2427,15 +2451,14 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c
 
         // And if the bottom margin didn't cover the full viewport, we copy the
         // lower part of the viewport down so it remains static. But for a full
-        // pan we reset the newly revealed row with the current attributes.
+        // pan we reset the newly revealed row with the erase attributes.
         if (bottomMargin < viewport.bottom - 1)
         {
             _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1);
         }
         else
         {
-            auto eraseAttributes = textBuffer.GetCurrentAttributes();
-            eraseAttributes.SetStandardErase();
+            const auto eraseAttributes = _GetEraseAttributes(textBuffer);
             textBuffer.GetRowByOffset(newPosition.y).Reset(eraseAttributes);
         }
     }
@@ -2444,7 +2467,8 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c
         // If the viewport has reached the end of the buffer, we can't pan down,
         // so we cycle the row coordinates, which effectively scrolls the buffer
         // content up. In this case we don't need to move the cursor down.
-        textBuffer.IncrementCircularBuffer(true);
+        const auto eraseAttributes = _GetEraseAttributes(textBuffer);
+        textBuffer.IncrementCircularBuffer(eraseAttributes);
         _api.NotifyBufferRotation(1);
 
         // We trigger a scroll rather than a redraw, since that's more efficient,
@@ -3192,8 +3216,7 @@ bool AdaptDispatch::_EraseAll()
     cursor.SetHasMoved(true);
 
     // Erase all the rows in the current viewport.
-    auto eraseAttributes = textBuffer.GetCurrentAttributes();
-    eraseAttributes.SetStandardErase();
+    const auto eraseAttributes = _GetEraseAttributes(textBuffer);
     _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, L' ', eraseAttributes);
 
     // Also reset the line rendition for the erased rows.
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 45f69341e1e..5af3479dc07 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -170,6 +170,7 @@ namespace Microsoft::Console::VirtualTerminal
             Column,
             AllowDECCOLM,
             AllowDECSLRM,
+            EraseColor,
             RectangularChangeExtent
         };
         enum class ScrollDirection
@@ -218,6 +219,7 @@ namespace Microsoft::Console::VirtualTerminal
         til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize);
         bool _EraseScrollback();
         bool _EraseAll();
+        TextAttribute _GetEraseAttributes(const TextBuffer& textBuffer) const noexcept;
         void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta);
         void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta);
         void _InsertDeleteCharacterHelper(const VTInt delta);
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 33ee62fe09a..af5484f78b4 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -139,7 +139,7 @@ class TestGetSet final : public ITerminalApi
         }
     }
 
-    void UseAlternateScreenBuffer() override
+    void UseAlternateScreenBuffer(const TextAttribute& /*attr*/) override
     {
         Log::Comment(L"UseAlternateScreenBuffer MOCK called...");
     }
@@ -1795,7 +1795,7 @@ class AdapterTest
         // and DECRQM would not then be applicable.
 
         BEGIN_TEST_METHOD_PROPERTIES()
-            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 69, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}")
+            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 69, 117, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}")
         END_TEST_METHOD_PROPERTIES()
 
         VTInt modeNumber;


Test patch: