Repository: microsoft/terminal (language: cpp)

Issue:
Add support for horizontal scrolling sequences
# Description of the new feature/enhancement

There are a number of VT sequences that were introduced by the VT420, which provide a way for applications to scroll the screen horizontally.

There's `DECFI` (forward index) and `DECBI` (backward index), which are the horizontal equivalents of the `IND` (index) and `RI` (reverse index) operations. They move the cursor position forward or backwards by one column, but once you reach the edge of the screen (or margin area), the buffer is forced to scroll.

There's also `DECIC` (insert column) and `DECDC` (delete column), which are the horizontal equivalents of the `IL` (insert line) and `DL` (delete line) operations. As the names suggest, they insert or delete a column at the active cursor position, forcing the area to the right of that to scroll forward or backward.

My main motivation for adding them is because they're useful for optimizing side-scrolling games. But the other reason is that they're necessary to meet the requirements of the horizontal scrolling extension (along with the horizontal margin operations from PR #15084).

# Proposed technical implementation details (optional)

Once PR #15084 is done (assuming that's accepted), the implementation for these ops should be fairly trivial. It's mostly a matter of calling the existing `_ScrollRectHorizontally` method with the appropriate coordinates.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index f471a831251..496ffcf079f 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -410,6 +410,7 @@ DECANM
 DECARM
 DECAUPSS
 DECAWM
+DECBI
 DECBKM
 DECCARA
 DECCIR
@@ -418,13 +419,16 @@ DECCKSR
 DECCOLM
 DECCRA
 DECCTR
+DECDC
 DECDHL
 decdld
 DECDMAC
 DECDWL
 DECEKBD
 DECERA
+DECFI
 DECFRA
+DECIC
 DECID
 DECINVM
 DECKPAM
@@ -571,6 +575,7 @@ EDITTEXT
 EDITUPDATE
 edputil
 Efast
+efghijklmn
 EHsc
 EINS
 EJO
@@ -971,6 +976,8 @@ KLF
 KLMNO
 KLMNOPQRST
 KLMNOPQRSTQQQQQ
+KLMNOPQRSTUVWXY
+KLMNOPQRSTY
 KOK
 KPRIORITY
 KVM
@@ -1134,6 +1141,7 @@ mmsystem
 MNC
 MNOPQ
 MNOPQR
+MNOPQRSTUVWXY
 MODALFRAME
 MODERNCORE
 MONITORINFO
@@ -2044,6 +2052,7 @@ USRDLL
 utr
 UVWX
 UVWXY
+UVWXYZ
 uwa
 uwp
 uxtheme
@@ -2304,7 +2313,9 @@ YOffset
 YSubstantial
 YVIRTUALSCREEN
 YWalk
+Zab
 zabcd
+Zabcdefghijklmn
 Zabcdefghijklmnopqrstuvwxyz
 ZCmd
 ZCtrl
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index d9226c95dc4..1209780e2be 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -186,6 +186,7 @@ class ScreenBufferTests
     TEST_METHOD(InsertReplaceMode);
     TEST_METHOD(InsertChars);
     TEST_METHOD(DeleteChars);
+    TEST_METHOD(HorizontalScrollOperations);
     TEST_METHOD(ScrollingWideCharsHorizontally);
 
     TEST_METHOD(EraseScrollbackTests);
@@ -4346,6 +4347,117 @@ void ScreenBufferTests::DeleteChars()
     }
 }
 
+void ScreenBufferTests::HorizontalScrollOperations()
+{
+    BEGIN_TEST_METHOD_PROPERTIES()
+        TEST_METHOD_PROPERTY(L"Data:op", L"{0, 1, 2, 3}")
+    END_TEST_METHOD_PROPERTIES();
+
+    enum Op : int
+    {
+        DECIC,
+        DECDC,
+        DECFI,
+        DECBI
+    } op;
+    VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", (int&)op));
+
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+
+    // Set the buffer width to 40, with a centered viewport of 20.
+    const auto bufferWidth = 40;
+    const auto bufferHeight = si.GetBufferSize().Height();
+    const auto viewportStart = 10;
+    const auto viewportEnd = viewportStart + 20;
+    VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false));
+    si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true);
+
+    // Set the margin area to columns 10 to 29 and rows 14 to 19 (zero based).
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[11;30s");
+    stateMachine.ProcessString(L"\x1b[15;20r");
+    // Make sure we clear the margins on exit so they can't break other tests.
+    auto clearMargins = wil::scope_exit([&] {
+        stateMachine.ProcessString(L"\x1b[r");
+        stateMachine.ProcessString(L"\x1b[s");
+        stateMachine.ProcessString(L"\x1b[?69l");
+    });
+
+    // Fill the buffer with text. Red on Blue.
+    const auto bufferChars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn";
+    const auto bufferAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
+    _FillLines(0, 25, bufferChars, bufferAttr);
+
+    // Set the attributes that will be used to fill the revealed area.
+    auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
+    fillAttr.SetCrossedOut(true);
+    fillAttr.SetReverseVideo(true);
+    fillAttr.SetUnderlined(true);
+    si.SetAttributes(fillAttr);
+    // But note that the meta attributes are expected to be cleared.
+    auto expectedFillAttr = fillAttr;
+    expectedFillAttr.SetStandardErase();
+
+    auto& cursor = si.GetTextBuffer().GetCursor();
+    switch (op)
+    {
+    case DECIC:
+        Log::Comment(L"Insert 4 columns in the middle of the margin area.");
+        cursor.SetPosition({ 20, 17 });
+        stateMachine.ProcessString(L"\x1b[4'}");
+        VERIFY_ARE_EQUAL(til::point(20, 17), cursor.GetPosition(), L"The cursor should not move.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"KLMNOPQRST", bufferAttr),
+                       L"The margin area left of the cursor position should remain unchanged.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(20, 14, 20, L"    ", expectedFillAttr),
+                       L"4 blank columns should be inserted at the cursor position.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(24, 14, 20, L"UVWXYZ", bufferAttr),
+                       L"The area right of that should be scrolled right by 4 columns.");
+        break;
+    case DECDC:
+        Log::Comment(L"Delete 4 columns in the middle of the margin area.");
+        cursor.SetPosition({ 20, 17 });
+        stateMachine.ProcessString(L"\x1b[4'~");
+        VERIFY_ARE_EQUAL(til::point(20, 17), cursor.GetPosition(), L"The cursor should not move.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"KLMNOPQRSTYZabcd", bufferAttr),
+                       L"The area right of the cursor position should be scrolled left by 4 columns.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(26, 14, 20, L"    ", expectedFillAttr),
+                       L"4 blank columns should be inserted at the right of the margin area.");
+        break;
+    case DECFI:
+        Log::Comment(L"Forward index 4 times, 2 columns before the right margin.");
+        cursor.SetPosition({ 27, 17 });
+        stateMachine.ProcessString(L"\x1b\x39\x1b\x39\x1b\x39\x1b\x39");
+        VERIFY_ARE_EQUAL(til::point(29, 17), cursor.GetPosition(), L"The cursor should not pass the right margin.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"MNOPQRSTUVWXYZabcd", bufferAttr),
+                       L"The margin area should scroll left by 2 columns.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(28, 14, 20, L"  ", expectedFillAttr),
+                       L"2 blank columns should be inserted at the right of the margin area.");
+        break;
+    case DECBI:
+        Log::Comment(L"Back index 4 times, 2 columns before the left margin.");
+        cursor.SetPosition({ 12, 17 });
+        stateMachine.ProcessString(L"\x1b\x36\x1b\x36\x1b\x36\x1b\x36");
+        VERIFY_ARE_EQUAL(til::point(10, 17), cursor.GetPosition(), L"The cursor should not pass the left margin.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"  ", expectedFillAttr),
+                       L"2 blank columns should be inserted at the left of the margin area.");
+        VERIFY_IS_TRUE(_ValidateLinesContain(12, 14, 20, L"KLMNOPQRSTUVWXYZab", bufferAttr),
+                       L"The rest of the margin area should scroll right by 2 columns.");
+        break;
+    }
+
+    VERIFY_IS_TRUE(_ValidateLinesContain(0, 14, bufferChars, bufferAttr),
+                   L"Content above the top margin should remain unchanged.");
+    VERIFY_IS_TRUE(_ValidateLinesContain(20, 25, bufferChars, bufferAttr),
+                   L"Content below the bottom margin should remain unchanged.");
+    VERIFY_IS_TRUE(_ValidateLinesContain(0, 14, 20, L"ABCDEFGHIJ", bufferAttr),
+                   L"Content before the left margin should remain unchanged.");
+    VERIFY_IS_TRUE(_ValidateLinesContain(30, 14, 20, L"efghijklmn", bufferAttr),
+                   L"Content beyond the right margin should remain unchanged.");
+}
+
 void ScreenBufferTests::ScrollingWideCharsHorizontally()
 {
     // The point of this test is to make sure wide characters can be
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 24cce0620fa..7b8cf9d7a57 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -51,6 +51,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool ScrollDown(const VTInt distance) = 0; // SD
     virtual bool InsertLine(const VTInt distance) = 0; // IL
     virtual bool DeleteLine(const VTInt distance) = 0; // DL
+    virtual bool InsertColumn(const VTInt distance) = 0; // DECIC
+    virtual bool DeleteColumn(const VTInt distance) = 0; // DECDC
     virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM
     virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM
     virtual bool SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) = 0; // DECSTBM
@@ -59,6 +61,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool CarriageReturn() = 0; // CR
     virtual bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) = 0; // IND, NEL, LF, FF, VT
     virtual bool ReverseLineFeed() = 0; // RI
+    virtual bool BackIndex() = 0; // DECBI
+    virtual bool ForwardIndex() = 0; // DECFI
     virtual bool SetWindowTitle(std::wstring_view title) = 0; // OscWindowTitle
     virtual bool HorizontalTabSet() = 0; // HTS
     virtual bool ForwardTab(const VTInt numTabs) = 0; // CHT, HT
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 64d88723ff2..d17d3b740a8 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -571,7 +571,7 @@ bool AdaptDispatch::CursorRestoreState()
 // - delta - Distance to move (positive is down, negative is up).
 // Return Value:
 // - <none>
-void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const int32_t delta)
+void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta)
 {
     const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height());
     if (absoluteDelta < scrollRect.height())
@@ -627,7 +627,7 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec
 // - delta - Distance to move (positive is right, negative is left).
 // Return Value:
 // - <none>
-void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const int32_t delta)
+void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta)
 {
     const auto absoluteDelta = std::min(std::abs(delta), scrollRect.width());
     if (absoluteDelta < scrollRect.width())
@@ -1487,6 +1487,7 @@ bool AdaptDispatch::DeviceAttributes()
     // 1 = 132 column mode (ConHost only)
     // 6 = Selective erase
     // 7 = Soft fonts
+    // 21 = Horizontal scrolling
     // 22 = Color text
     // 23 = Greek character sets
     // 24 = Turkish character sets
@@ -1496,11 +1497,11 @@ bool AdaptDispatch::DeviceAttributes()
 
     if (_api.IsConsolePty())
     {
-        _api.ReturnResponse(L"\x1b[?61;6;7;22;23;24;28;32;42c");
+        _api.ReturnResponse(L"\x1b[?61;6;7;21;22;23;24;28;32;42c");
     }
     else
     {
-        _api.ReturnResponse(L"\x1b[?61;1;6;7;22;23;24;28;32;42c");
+        _api.ReturnResponse(L"\x1b[?61;1;6;7;21;22;23;24;28;32;42c");
     }
     return true;
 }
@@ -1693,7 +1694,7 @@ void AdaptDispatch::_ScrollMovement(const VTInt delta)
 // - True.
 bool AdaptDispatch::ScrollUp(const VTInt uiDistance)
 {
-    _ScrollMovement(-gsl::narrow_cast<int32_t>(uiDistance));
+    _ScrollMovement(-uiDistance);
     return true;
 }
 
@@ -1705,7 +1706,7 @@ bool AdaptDispatch::ScrollUp(const VTInt uiDistance)
 // - True.
 bool AdaptDispatch::ScrollDown(const VTInt uiDistance)
 {
-    _ScrollMovement(gsl::narrow_cast<int32_t>(uiDistance));
+    _ScrollMovement(uiDistance);
     return true;
 }
 
@@ -2051,7 +2052,7 @@ bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode)
 // - delta - Number of lines to modify (positive if inserting, negative if deleting).
 // Return Value:
 // - <none>
-void AdaptDispatch::_InsertDeleteLineHelper(const int32_t delta)
+void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta)
 {
     const auto viewport = _api.GetViewport();
     auto& textBuffer = _api.GetTextBuffer();
@@ -2084,7 +2085,7 @@ void AdaptDispatch::_InsertDeleteLineHelper(const int32_t delta)
 // - True.
 bool AdaptDispatch::InsertLine(const VTInt distance)
 {
-    _InsertDeleteLineHelper(gsl::narrow_cast<int32_t>(distance));
+    _InsertDeleteLineHelper(distance);
     return true;
 }
 
@@ -2102,7 +2103,58 @@ bool AdaptDispatch::InsertLine(const VTInt distance)
 // - True.
 bool AdaptDispatch::DeleteLine(const VTInt distance)
 {
-    _InsertDeleteLineHelper(-gsl::narrow_cast<int32_t>(distance));
+    _InsertDeleteLineHelper(-distance);
+    return true;
+}
+
+// Routine Description:
+// - Internal logic for adding or removing columns in the active screen buffer.
+// Parameters:
+// - delta - Number of columns to modify (positive if inserting, negative if deleting).
+// Return Value:
+// - <none>
+void AdaptDispatch::_InsertDeleteColumnHelper(const VTInt delta)
+{
+    const auto viewport = _api.GetViewport();
+    auto& textBuffer = _api.GetTextBuffer();
+    const auto bufferWidth = textBuffer.GetSize().Width();
+
+    const auto& cursor = textBuffer.GetCursor();
+    const auto col = cursor.GetPosition().x;
+    const auto row = cursor.GetPosition().y;
+
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
+    if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin)
+    {
+        // We emulate inserting and deleting by scrolling the area between the cursor and the right margin.
+        _ScrollRectHorizontally(textBuffer, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta);
+    }
+}
+
+// Routine Description:
+// - DECIC - This control function inserts one or more blank columns in the
+//    scrolling region, starting at the column that has the cursor.
+// Arguments:
+// - distance - number of columns to insert
+// Return Value:
+// - True.
+bool AdaptDispatch::InsertColumn(const VTInt distance)
+{
+    _InsertDeleteColumnHelper(distance);
+    return true;
+}
+
+// Routine Description:
+// - DECDC - This control function deletes one or more columns in the scrolling
+//    region, starting with the column that has the cursor.
+// Arguments:
+// - distance - number of columns to delete
+// Return Value:
+// - True.
+bool AdaptDispatch::DeleteColumn(const VTInt distance)
+{
+    _InsertDeleteColumnHelper(-distance);
     return true;
 }
 
@@ -2471,6 +2523,68 @@ bool AdaptDispatch::ReverseLineFeed()
     return true;
 }
 
+// Routine Description:
+// - DECBI - Moves the cursor back one column, scrolling the screen
+//    horizontally if it reaches the left margin.
+// Arguments:
+// - None
+// Return Value:
+// - True.
+bool AdaptDispatch::BackIndex()
+{
+    const auto viewport = _api.GetViewport();
+    auto& textBuffer = _api.GetTextBuffer();
+    auto& cursor = textBuffer.GetCursor();
+    const auto cursorPosition = cursor.GetPosition();
+    const auto bufferWidth = textBuffer.GetSize().Width();
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+
+    // If the cursor is at the left of the margin area, we shift the buffer right.
+    if (cursorPosition.x == leftMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
+    {
+        _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1);
+    }
+    // Otherwise we move the cursor left, but not past the start of the line.
+    else if (cursorPosition.x > 0)
+    {
+        cursor.SetXPosition(cursorPosition.x - 1);
+        _ApplyCursorMovementFlags(cursor);
+    }
+    return true;
+}
+
+// Routine Description:
+// - DECFI - Moves the cursor forward one column, scrolling the screen
+//    horizontally if it reaches the right margin.
+// Arguments:
+// - None
+// Return Value:
+// - True.
+bool AdaptDispatch::ForwardIndex()
+{
+    const auto viewport = _api.GetViewport();
+    auto& textBuffer = _api.GetTextBuffer();
+    auto& cursor = textBuffer.GetCursor();
+    const auto cursorPosition = cursor.GetPosition();
+    const auto bufferWidth = textBuffer.GetSize().Width();
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+
+    // If the cursor is at the right of the margin area, we shift the buffer left.
+    if (cursorPosition.x == rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
+    {
+        _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1);
+    }
+    // Otherwise we move the cursor right, but not past the end of the line.
+    else if (cursorPosition.x < textBuffer.GetLineWidth(cursorPosition.y) - 1)
+    {
+        cursor.SetXPosition(cursorPosition.x + 1);
+        _ApplyCursorMovementFlags(cursor);
+    }
+    return true;
+}
+
 // Routine Description:
 // - OSC Set Window Title - Sets the title of the window
 // Arguments:
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 050ea68dcd0..45f69341e1e 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -83,6 +83,8 @@ namespace Microsoft::Console::VirtualTerminal
         bool ScrollDown(const VTInt distance) override; // SD
         bool InsertLine(const VTInt distance) override; // IL
         bool DeleteLine(const VTInt distance) override; // DL
+        bool InsertColumn(const VTInt distance) override; // DECIC
+        bool DeleteColumn(const VTInt distance) override; // DECDC
         bool SetMode(const DispatchTypes::ModeParams param) override; // SM, DECSET
         bool ResetMode(const DispatchTypes::ModeParams param) override; // RM, DECRST
         bool RequestMode(const DispatchTypes::ModeParams param) override; // DECRQM
@@ -96,6 +98,8 @@ namespace Microsoft::Console::VirtualTerminal
         bool CarriageReturn() override; // CR
         bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) override; // IND, NEL, LF, FF, VT
         bool ReverseLineFeed() override; // RI
+        bool BackIndex() override; // DECBI
+        bool ForwardIndex() override; // DECFI
         bool SetWindowTitle(const std::wstring_view title) override; // OSCWindowTitle
         bool HorizontalTabSet() override; // HTS
         bool ForwardTab(const VTInt numTabs) override; // CHT, HT
@@ -218,6 +222,7 @@ namespace Microsoft::Console::VirtualTerminal
         void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta);
         void _InsertDeleteCharacterHelper(const VTInt delta);
         void _InsertDeleteLineHelper(const VTInt delta);
+        void _InsertDeleteColumnHelper(const VTInt delta);
         void _ScrollMovement(const VTInt delta);
 
         void _DoSetTopBottomScrollingMargins(const VTInt topMargin,
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index b8f1fc033a5..2fe1f216a3d 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -44,6 +44,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool ScrollDown(const VTInt /*distance*/) override { return false; } // SD
     bool InsertLine(const VTInt /*distance*/) override { return false; } // IL
     bool DeleteLine(const VTInt /*distance*/) override { return false; } // DL
+    bool InsertColumn(const VTInt /*distance*/) override { return false; } // DECIC
+    bool DeleteColumn(const VTInt /*distance*/) override { return false; } // DECDC
     bool SetKeypadMode(const bool /*applicationMode*/) override { return false; } // DECKPAM, DECKPNM
     bool SetAnsiMode(const bool /*ansiMode*/) override { return false; } // DECANM
     bool SetTopBottomScrollingMargins(const VTInt /*topMargin*/, const VTInt /*bottomMargin*/) override { return false; } // DECSTBM
@@ -52,6 +54,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool CarriageReturn() override { return false; } // CR
     bool LineFeed(const DispatchTypes::LineFeedType /*lineFeedType*/) override { return false; } // IND, NEL, LF, FF, VT
     bool ReverseLineFeed() override { return false; } // RI
+    bool BackIndex() override { return false; } // DECBI
+    bool ForwardIndex() override { return false; } // DECFI
     bool SetWindowTitle(std::wstring_view /*title*/) override { return false; } // OscWindowTitle
     bool HorizontalTabSet() override { return false; } // HTS
     bool ForwardTab(const VTInt /*numTabs*/) override { return false; } // CHT, HT
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 53d7e9e4bc6..33ee62fe09a 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -1518,7 +1518,7 @@ class AdapterTest
         _testGetSet->PrepData();
         VERIFY_IS_TRUE(_pDispatch->DeviceAttributes());
 
-        auto pwszExpectedResponse = L"\x1b[?61;1;6;7;22;23;24;28;32;42c";
+        auto pwszExpectedResponse = L"\x1b[?61;1;6;7;21;22;23;24;28;32;42c";
         _testGetSet->ValidateInputEvent(pwszExpectedResponse);
 
         Log::Comment(L"Test 2: Verify failure when ReturnResponse doesn't work.");
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 1dd61fd1f9f..b04a02ff11a 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -209,6 +209,10 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id)
         // This is the 7-bit string terminator, which is essentially a no-op.
         success = true;
         break;
+    case EscActionCodes::DECBI_BackIndex:
+        success = _dispatch->BackIndex();
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECBI);
+        break;
     case EscActionCodes::DECSC_CursorSave:
         success = _dispatch->CursorSaveState();
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSC);
@@ -217,6 +221,10 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id)
         success = _dispatch->CursorRestoreState();
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRC);
         break;
+    case EscActionCodes::DECFI_ForwardIndex:
+        success = _dispatch->ForwardIndex();
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECFI);
+        break;
     case EscActionCodes::DECKPAM_KeypadApplicationMode:
         success = _dispatch->SetKeypadMode(true);
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPAM);
@@ -700,6 +708,14 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         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::DECIC_InsertColumn:
+        success = _dispatch->InsertColumn(parameters.at(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECIC);
+        break;
+    case CsiActionCodes::DECDC_DeleteColumn:
+        success = _dispatch->DeleteColumn(parameters.at(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDC);
+        break;
     case CsiActionCodes::DECSACE_SelectAttributeChangeExtent:
         success = _dispatch->SelectAttributeChangeExtent(parameters.at(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSACE);
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 97b1237637b..0613ea3bc64 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -71,8 +71,10 @@ namespace Microsoft::Console::VirtualTerminal
 
         enum EscActionCodes : uint64_t
         {
+            DECBI_BackIndex = VTID("6"),
             DECSC_CursorSave = VTID("7"),
             DECRC_CursorRestore = VTID("8"),
+            DECFI_ForwardIndex = VTID("9"),
             DECKPAM_KeypadApplicationMode = VTID("="),
             DECKPNM_KeypadNumericMode = VTID(">"),
             IND_Index = VTID("D"),
@@ -159,6 +161,8 @@ namespace Microsoft::Console::VirtualTerminal
             DECERA_EraseRectangularArea = VTID("$z"),
             DECSERA_SelectiveEraseRectangularArea = VTID("${"),
             DECSCPP_SetColumnsPerPage = VTID("$|"),
+            DECIC_InsertColumn = VTID("'}"),
+            DECDC_DeleteColumn = VTID("'~"),
             DECSACE_SelectAttributeChangeExtent = VTID("*x"),
             DECRQCRA_RequestChecksumRectangularArea = VTID("*y"),
             DECINVM_InvokeMacro = VTID("*z"),
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index 79e3c6f2da6..98d752a3a45 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -219,8 +219,10 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[EL], "EL"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSEL], "DECSEL"),
                                       TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECBI], "DECBI"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECFI], "DECFI"),
                                       TraceLoggingUInt32(_uiTimesUsed[SM], "SM"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"),
                                       TraceLoggingUInt32(_uiTimesUsed[RM], "RM"),
@@ -293,6 +295,8 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[DECFRA], "DECFRA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECIC], "DECIC"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECDC], "DECDC"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSACE], "DECSACE"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECRQCRA], "DECRQCRA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECINVM], "DECINVM"),
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 60ad7e34215..dd71c61a104 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -46,8 +46,10 @@ namespace Microsoft::Console::VirtualTerminal
             EL,
             DECSEL,
             SGR,
+            DECBI,
             DECSC,
             DECRC,
+            DECFI,
             SM,
             DECSET,
             RM,
@@ -120,6 +122,8 @@ namespace Microsoft::Console::VirtualTerminal
             DECFRA,
             DECERA,
             DECSERA,
+            DECIC,
+            DECDC,
             DECSACE,
             DECRQCRA,
             DECINVM,


Test patch: