Repository: microsoft/terminal (language: cpp)

Issue:
Add support for VT102 Insertion-Replace Mode (IRM)
### Insertion-Replacement Mode (IRM)

```
ESC  [   4   h
033 133 064 150
```

Set selects insert mode and turns INSERT on. New display characters move old display characters to the right. Characters moved past the right margin are lost.

```
ESC  [   4   l
033 133 064 154
```

Reset selects replace mode and turns INSERT off. New display characters replace old display characters at cursor position. The old character is erased.

---

#### original issue body follows.

The following is from recording command line editing in an ssh session to a freebsd environment (via FreeBSD's script). The displayed text is odd in Windows Terminal. First I show a textual form that shows the escape sequences and such in a readable form (I eliminated bell-ring characters from the file and other lines of text):

> more ~/odd_commnd_line_editing_display.txt
> ls ~/sys_typescriptsESC[1ESC[4h-ESC[4lESC[4hlESC[4lESC[4hTESC[4lESC[4hdESC[4lESC[4htESC[4lESC[4h ESC[4

A normal display in other environments (for example macOS terminal doing an ssh into FreeBSD) looks like:

> cat ~/odd_commnd_line_editing_display.txt
> ls -lTdt ~/sys_typescripts/

(The above shows the actual ls command that was executed.)

But under Windows Terminal (Preview) it shows:

> cat ~/odd_commnd_line_editing_display.txt
> ls -lTdt ~ypescripts/

This is also how it ends up looking during the command line editing,
not showing the actual text of the command to be executed: missing
some text.

The sequence is from deciding to add "-lTdt" after having typed the
path. (It was a "set -o vi" type of command line editing context.)

I was using TERM being xterm-256color on FreeBSD .



Discussion / hints:
I suspect this is because the Windows terminal doesn't yet support [Insertion-Replacement Mode (IRM)](https://vt100.net/docs/vt102-ug/chapter5.html#S5.5.2.22).
Interesting! Didn't know IRM was a thing. I'm going to repurpose this as a task to implement IRM.

Notes:

### Insertion-Replacement Mode (IRM)

```
ESC  [   4   h
033 133 064 150
```

Set selects insert mode and turns INSERT on. New display characters move old display characters to the right. Characters moved past the right margin are lost.

```
ESC  [   4   l
033 133 064 154
```

Reset selects replace mode and turns INSERT off. New display characters replace old display characters at cursor position. The old character is erased.
Yet another symptom of the same bug:

Resize the window to 63 columns. Start `mc` (Midnight Commander) on Ubuntu, compiled against the S-Lang screen drawing library (as it's done in Ubuntu).

Expected: The bottom row should display 9 buttons, each of them being 2 characters for the function key number + 5 for the action. The rightmost should be `<space>9Pu~Dn`, reaching all the way to the very end.

Actual: `<space>9Pu~D` is printed with the expected colors, and the last column remains empty, showing the terminal's background color.

What the S-Lang library does is an ancient and obsolete hack for some terminals that don't have "pending wrap" (@j4james do you know more about them?), to print in the lower right corner without the cursor automatically wrapping to the next line and shifting everything up. It prints the intended last letter (`n` in this case) in the penultimate (62nd) column, and then using IRM, prints the penultimate letter (`D`) again in the penultimate (62nd) column, expecting it to push the previously printed `n` to the 63rd.

Even at bigger window sizes, such as the normal 80 columns, `mc` misses to paint the bottom right corner using _some_ of its skins, e.g. F9 -> Options -> Appearance -> sand256 is one of these. For some mysterious reason, S-Lang decides to use different drawing method depending on some property of the skin. In this case, it repeats the aforementioned trick with space characters, whereas with most other themes, it just emits the escape sequence that wipes to the end.

This is a tiny inconsistency (almost-bug) somewhere in S-Lang, my wild guess is that it could be related to non-color attributes (in this case italic) being used there, but I don't think it's worth investigating S-Lang's behavior any further. Also, for the record: I couldn't reproduce the issue with `mc` compiled against ncurses, apparently it knows about "pending wrap" and knows that this hack is not necessary.

> What the S-Lang library does is an ancient and obsolete hack for some terminals that don't have "pending wrap" (@j4james do you know more about them?)

I know DOS and the original Windows shell didn't support "pending wrap", but then they wouldn't have supported `IRM` mode either. However, it wouldn't surprise me if there were other third-party terminal emulators for Windows that shared that wrapping behaviour, but were still able to handle `IRM`, so maybe that's the sort of thing that S-Lang is targetting. 

By the way, `IRM` is on my TODO list (assuming nobody else takes it on first). It's just that we don't yet have a handler for ANSI modes, and the existing dispatch mechanism makes that a bit of a pain to add, so that was something I was hoping to address first. There are various other issues related to parameter handling (e.g. #2101 and possibly #942), and I wanted to make sure we had something that could satisfy all of those requirements if possible, so it's probably not going to be a quick fix.
this is a a real issue. pico editor seems to use this when ran under emulated xterm.  and the characters do not move to the right like they are supposed to.

still an issue as of this day.
I'm also affected by this. Is there any known way to work around it?
I was actually looking into this recently, and I think it's going to require some architectural decisions before we can consider implementing anything.

I'd initially thought that the `AdaptDispatch` class could handle the inserting prior to writing anything, by simply scrolling the line it was about to write into by the appropriate amount. But that assumes it knows how much space the text is going to take (which it doesn't), and if the text is going to wrap onto another line, then potentially handle inserting on later lines (which may not even exist yet if vertical scrolling is required).

As things currently stand, I don't think this is possible. So either the inserting must be handled in the `PrintString` implementation itself (which is essentially the `WriteCharsLegacy` function that we don't want to touch). Or the `AdaptDispatch` class needs to take over the majority of the text layout, and have `PrintString` be a very basic operation that only writes to a single line (no inserting, wrapping, scrolling, or margin checks).

Considering the additional complications that are going to be introduced to the layout as we expand the VT functionality, I think the latter option may well be the best long term approach. But that is going to require some additional APIs, e.g. to query how much space a string of text will occupy and/or where it would be clipped.
I also have the same problem, but I have a workaround.

FreeBSD uses termcap (rather than terminfo) and the termcap entries for xterm include the 'im' and 'ei' capabilities which invoke the VT102 IRM.  In the Linux terminfo database, these capabilities are not implemented.

The workaround is to create a .termcap file in your home directory with this one entry in it:
`xterm-256color:mi@:IC=\E[%d@:ei@:ic=\E[@:im@:tc=xterm-new:`

This will use this instead for the xterm-256color terminal (which is what Windows Terminal is), and delete the 'im' and 'ei' capabilities.

Until the IRM capabilities are implemented, this works perfectly.

It's wild how IRM works with line wrapping in xterm (351). Text is pushed off the right margin, as expected, but when the inserted region is wider than will fit _it_ wraps and pushes text on the _next_ line to the right.

This is a terribly specific feature. I wonder why it was introduced!
no version of xterm has ever not supported IRM. if you're not going to implement it, please identify yourself as a VT100 instead of xterm256-color. the pending-wrap thing is a side show, don't worry about it. just please make :ei=\E[4l: and :im=\E[4h: work. i've gone back to konsole as a result of this bug.
I have to use putty because of this STILL, even though putty doesn't have perfect xterm emulation either.  For those of you who can't seem to reproduce this, your pico is actually nano, which has a workaround for this bug.

If you actually use the pico text editor, you can very easily see the behavior. it's the simplest test case.
> The workaround is to create a .termcap file in your home directory with this one entry in it:
> `xterm-256color:mi@:IC=\E[%d@:ei@:ic=\E[@:im@:tc=xterm-new:`
> Until the IRM capabilities are implemented, this works perfectly.

Thanks, your workaround fixes some weird behaviour when connecting to FreeBSD boxes. Waiting for a real fix ;-)
Marc Reymann writes:

>> ...
>
> Thanks, your workaround fixes some weird behaviour when connecting to  
> FreeBSD boxes. Waiting for a real fix ;-)

freebsd is not the only place this matters. other systems also expect that  
if the offered terminal type is xterm256-color, then IRM will work. my  
workaround is to use TERM=vt100, which means, my workaround is to use  
konsole instead.

Great!! I was dealing with this bug since April, it was really annoying. I just apply the workaround with `xterm-clear:te=\E[?1049l:ti=\E[?1049h:mi@:IC=\E[%d@:ei@:ic=\E[@:im@:tc=xterm-new:` and it worked!! I hope Microsoft implements the real fix soon!!
I'm taking a crack at this.

![irm](https://user-images.githubusercontent.com/189190/100530871-4e233c00-31ac-11eb-9ec6-bebdc3447395.gif)

It looks like we don't even have `SM`/`RM`, so there's a lot of plumbing to do.
@j4james the approach I've gone with -- which is of course subject to discussion and eventual change :smile: -- is to make TextBuffer do an inserting write at the cursor position. I've not made any architectural changes to Adapt or change whose job it is to do text layout. I tested gnome-terminal here & found that they don't do any re-wrapping, _even if_ the line was emitted wrapped. Insert mode there impacts only the physical display & not the logical paragraph/wrap state.
happy to review or test.
> make TextBuffer do an inserting write at the cursor position.

Yeah, but where are you doing that? In the `WriteCharsLegacy` implementation? And if so, that assumedly requires another `OutputMode` flag, or at least another `WC_*` flag. I just assumed we wouldn't want to be introducing more complexity there?

I was starting to think maybe now was a good time to take on #780, and incorporate the IRM functionality there. That seemed easier than worrying about how this would impact the legacy code. But maybe I'm overthinking things and you've got a simpler solution that I'm just not seeing.

> It looks like we don't even have `SM`/`RM`, so there's a lot of plumbing to do.

Yeah, that's why I wanted to get #7799 out the way first. It should at least be easer to extend the framework now than it was back then.
It would be nice if you add some spaces to see how it behaves on multi word lines.

> I'm taking a crack at this.
> 
> ![irm](https://user-images.githubusercontent.com/189190/100530871-4e233c00-31ac-11eb-9ec6-bebdc3447395.gif)
> 
> It looks like we don't even have `SM`/`RM`, so there's a lot of plumbing to do.


@j4james oh, right. Sorry to be inexact. Since all of the at-cursor writes coming out of `WriteCharsLegacy` (and any other source in conhost; see `_stream.cpp:536` for the only instance I could find) go through `SCREEN_INFORMATION::Write(OutputCellIterator)`, I put IRM determination in `SCREEN_INFORMATION` and kept it out of WCL. That keeps it away from any modes, control character handling, knowledge of backup/line wrapping state, etc.

Every other Write uses a coordinate target, but perhaps it would be prudent to rename this one to `WriteAtCursor`.

Does that seem a reasonable place?
OK that's brilliant. That's so much simpler than I was expecting. I can't see any reason why that wouldn't work. :shipit:
Happy New Year! Draft PR up in #8689.
Please ignore results in Gnome Terminal, unless they can be reproduced under real XTerm. 

IRM is designed for the case of a text editor with autowrap turned off.  This saves bandwidth.  The test case animated graphic is not a real world scenario, though it is what sending the raw characters to the terminal would do.


Connecting to a (Arch)Linux box has the problem. No IRM. If I isseu ```export TERM=vt100``` vi behaves as designed. It would be great to include this as setting. Connecting with PuTTY works fine and offers a variety of settings:
![image](https://user-images.githubusercontent.com/50140049/176836434-c5b7d076-c83f-42f5-a556-d791824ee63d.png)

Is this the root cause of cursor mode not changing?
IE: in CMD
![image](https://user-images.githubusercontent.com/30872404/180514433-56ca642f-884d-4867-ab34-8e4221f0c908.png)
vs
![image](https://user-images.githubusercontent.com/30872404/180514396-5271741a-fb7b-426b-81dc-227030077483.png)

Or should this be considered a new issue?
The feature here--IRM--is unrelated to any CMD features or console applications that use the "Windows-style" cooked input feature (Python, PowerShell without PSReadline, Command Prompt).
> I'm also affected by this. Is there any known way to work around it?

Yes it is setting the TERM variable to VT100
```
export TERM=vt100
```
> I'm also affected by this. Is there any known way to work around it?

I am just using tmux to prevent insert mode before the last space in a command. The Issue only happens to me when editing a command from the history.

`export TERM=vt100` did not change this behaviour.
Can't SSH into a lot of lab/course(from online course sites) VMs because of this.

Any workarounds for this?
`export TERM=vt100` didn't work.


Numan Zaheer Ahmed wrote on 2022-12-11 15:32:
> Can't SSH into a lot of lab/course(from Coursera, Udemy, etc.) VMs 
> because of this.
> 
> Any workarounds for this?

don't use microsoft's terminal emulator. stick with securecrt, or 
install linux or freebsd inside "hyper-v" and run your terminal sessions 
inside the client VM. neither microsoft's "terminal" or "ssh" are ready 
for prime time.

> |export TERM=vt100| did't work.

i should have been clearer. "export TERM=vt100" is what you do on the 
remote system after you log in. the sshd that launches your shell there 
is going to pass the TERM variable it gets from the ssh session. i have 
not figured out how to get this to be passes as "vt100" by the invoking 
ssh inside the powershell or command CLI on the initiating side. but if 
i override it on the remote shell, everything i invoke there hears that 
my terminal type is "vt100".

i guess coursera and udemy don't have shells and the TERM variable their 
sshd gets from your ssh has to be correct? sorry about my ignorance, i 
don't use ssh that way.

-- 
P Vixie


> don't use microsoft's terminal emulator. stick with securecrt, or install linux or freebsd inside "hyper-v" and run your terminal sessions inside the client VM. neither microsoft's "terminal" or "ssh" are ready for prime time.

It's ssh on WSL2(which is basically a VM now) so not Microsoft ssh. As for windows terminal, not using it is not solution. Good alternative console emulators/terminals are hard to find on Windows.

> |export TERM=vt100| did't work.
> i should have been clearer. "export TERM=vt100" is what you do on the remote system after you log in. the sshd that launches your shell there is going to pass the TERM variable it gets from the ssh session. i have not figured out how to get this to be passes as "vt100" by the invoking ssh inside the powershell or command CLI on the initiating side. but if i override it on the remote shell, everything i invoke there hears that my terminal type is "vt100". i guess coursera and udemy don't have shells and the TERM variable their sshd gets from your ssh has to be correct? sorry about my ignorance, i don't use ssh that way.
> […](#)
> -- P Vixie

I ran the command inside the remote system after I logged in. Still didn't work.
Thx anyways. I should have mentioned where I ran it.


Numan Zaheer Ahmed wrote on 2022-12-11 15:58:
>     ...
> 
> It's ssh on WSL2(which is basically a VM now) so not Microsoft ssh. As 
> for windows terminal, not using it is not solution. Good alternative 
> console emulators/terminals are hard to find on Windows.

if you're using WSL2 you might try mintty which claims to run there and 
is based on putty which has working xterm escape sequences like IRM.
> I ran the command inside the remote system after I logged in. Still 
> didn't work. ... Thx anyways. I should have mentioned where I ran it.

can you say more about what doesn't work and under what conditions? i 
find that once i set TERM=vt100 (or TERM=vt220) on the remote shell, all 
the text editors and screen oriented utilities stop trying to use IRM, 
and thus work passably well with Windows Terminal.

i'm using Windows Terminal Preview but i just tested and it's the same 
for Windows Terminal as shipped from the microsoft store.

-- 
P Vixie


> if you're using WSL2 you might try mintty which claims to run there and is based on putty which has working xterm escape sequences like IRM.

Recent release of mintty binary seems hard to find(and am trying to avoid MSYS2). I will try [wsltty](https://github.com/mintty/wsltty) from the developer of mintty instead.
**Update:** Same issue in mintty. Starting to think it might be WSL/Windows issue.
Additionally, mintty is used in Git Bash terminal where WSL doesn't seem to work anymore. Gives no output when I try to start WSL. 

> I ran the command inside the remote system after I logged in. Still didn't work. ... Thx anyways. I should have mentioned where I ran it.
> can you say more about what doesn't work and under what conditions?

I just SSH into the remote shell. I press and hold a character(let's say 'a' or just paste a long string). It overwrites the first character after end of line is reached instead of moving to next line.



depending on the remote shell choice, it may not notice that you've
changed the TERM variable. more reliably, the commands you run from that
shell will inherit the modified environment (TERM=vt220 or whatever). to
be sure you're getting shell-level behaviour effects warranted by your
new terminal type, do something like this:

export TERM=vt220
exec bash

or, if you're like me using csh:

setenv TERM vt220
exec tcsh

or if you're using a modern mac/os ssh server-side:

export TERM=vt220
exec zsh

and so on. but your best test is still:

export TERM=vt220
emacs -nw $somefile

if your TERM indicates IRM and your terminal doesn't implement IRM then
emacs (among other utilities) will not be able to keep your screen updated.

Numan Zaheer Ahmed wrote on 2022-12-12 13:04:
> export TERM=vt220
> exec bash

![image](https://user-images.githubusercontent.com/11459028/207158232-f1b253ca-4bb6-481e-9a49-b15647b4cbc6.png)
It works fine inside new shell but I lose UI benefits as it goes to a basic terminal.

> and so on. but your best test is still:
> export TERM=vt220
> emacs -nw $somefile
> if your TERM indicates IRM and your terminal doesn't implement IRM then
> emacs (among other utilities) will not be able to keep your screen updated.

The remote shell is a limited environment without vim, nano, emacs.
There might be a problem with the shell itself. I'll try check with their support.


**Update:** Sry about all that. There PS1 seems to be broken/bad on the remote shell. So ignore my earlier troubles. Wasn't related to VT102 IRM. Marking my messages as off-topic.
This is still open 3&&1/2yrs later?
@lhecker I see you've been assigned this 2weeks ago, is it actually something in progress finally?
The main issue here is UnicodeStorage, which is not very friendly to trying to do this. #8000 needs to finish first, and then this can be solved.
I've recently removed `UnicodeStorage`, however it doesn't actually make my implementation all that much more trivial. This is because my more important and more long term goal is to fully support Unicode (and extended grapheme clusters in turn) for conhost and Windows Terminal. Implementing IRM requires an "text insertion" primitive which may be implemented based on our existing code base, however this would build up on our existing non-Unicode support, which I'm really hesitant to do. I'd basically be writing code that I'm actively trying to remove. This is why I'm trading a bit longer development time for IRM, to write those new Unicode-supporting primitives that we will need in other areas as well, with a better integration into our long term vision.

That said, I've already begun working on this. :)
I'm hoping to finish it before the 1.17 release ships, but it's possible I might miss it due to (unrelated) personal circumstances. In either case however I'm confident to finish it within January.

Fix patch:
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 786fe399c98..706f4e3540a 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -182,6 +182,7 @@ class ScreenBufferTests
     TEST_METHOD(DontResetColorsAboveVirtualBottom);
 
     TEST_METHOD(ScrollOperations);
+    TEST_METHOD(InsertReplaceMode);
     TEST_METHOD(InsertChars);
     TEST_METHOD(DeleteChars);
     TEST_METHOD(ScrollingWideCharsHorizontally);
@@ -3782,6 +3783,82 @@ void ScreenBufferTests::ScrollOperations()
     VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr));
 }
 
+void ScreenBufferTests::InsertReplaceMode()
+{
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+
+    const auto bufferHeight = si.GetBufferSize().Height();
+    const auto viewport = si.GetViewport();
+    const auto targetRow = viewport.Top() + 5;
+    const auto targetCol = til::CoordType{ 10 };
+
+    // Fill the entire buffer with Zs. Blue on Green.
+    const auto bufferChar = L'Z';
+    const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
+    _FillLines(0, bufferHeight, bufferChar, bufferAttr);
+
+    // Fill the target row with asterisks and a range of letters at the start. Red on Blue.
+    const auto initialChars = L"ABCDEFGHIJKLMNOPQRST";
+    const auto initialAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
+    _FillLine(targetRow, L'*', initialAttr);
+    _FillLine(targetRow, initialChars, initialAttr);
+
+    // Set the attributes that will be used for the new content.
+    auto newAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
+    newAttr.SetCrossedOut(true);
+    newAttr.SetReverseVideo(true);
+    newAttr.SetUnderlined(true);
+    si.SetAttributes(newAttr);
+
+    Log::Comment(L"Write additional content into a line of text with IRM mode enabled.");
+
+    // Set the cursor position partway through the target row.
+    VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true));
+    // Enable Insert/Replace mode.
+    stateMachine.ProcessString(L"\033[4h");
+    // Write out some new content.
+    const auto newChars = L"12345";
+    stateMachine.ProcessString(newChars);
+
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, targetRow }, L"ABCDEFGHIJ", initialAttr),
+                   L"First half of the line should remain unchanged.");
+    VERIFY_IS_TRUE(_ValidateLineContains({ targetCol, targetRow }, newChars, newAttr),
+                   L"New content should be inserted at the cursor position with active attributes.");
+    VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 5, targetRow }, L"KLMNOPQRST", initialAttr),
+                   L"Second half of the line should have moved 5 columns across.");
+    VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 25, targetRow }, L'*', initialAttr),
+                   L"With the remainder of the line filled with asterisks.");
+    VERIFY_IS_TRUE(_ValidateLineContains(targetRow + 1, bufferChar, bufferAttr),
+                   L"The following line should be unaffected.");
+
+    // Fill the target row with the initial content again.
+    _FillLine(targetRow, L'*', initialAttr);
+    _FillLine(targetRow, initialChars, initialAttr);
+
+    Log::Comment(L"Write additional content into a line of text with IRM mode disabled.");
+
+    // Set the cursor position partway through the target row.
+    VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true));
+    // Disable Insert/Replace mode.
+    stateMachine.ProcessString(L"\033[4l");
+    // Write out some new content.
+    stateMachine.ProcessString(newChars);
+
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, targetRow }, L"ABCDEFGHIJ", initialAttr),
+                   L"First half of the line should remain unchanged.");
+    VERIFY_IS_TRUE(_ValidateLineContains({ targetCol, targetRow }, newChars, newAttr),
+                   L"New content should be added at the cursor position with active attributes.");
+    VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 5, targetRow }, L"PQRST", initialAttr),
+                   L"Second half of the line should have been partially overwritten.");
+    VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 25, targetRow }, L'*', initialAttr),
+                   L"With the remainder of the line filled with asterisks.");
+    VERIFY_IS_TRUE(_ValidateLineContains(targetRow + 1, bufferChar, bufferAttr),
+                   L"The following line should be unaffected.");
+}
+
 void ScreenBufferTests::InsertChars()
 {
     BEGIN_TEST_METHOD_PROPERTIES()
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index e621398b859..b20adf4bfc4 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -395,6 +395,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
 
     enum ModeParams : VTInt
     {
+        IRM_InsertReplaceMode = ANSIStandardMode(4),
         DECCKM_CursorKeysMode = DECPrivateMode(1),
         DECANM_AnsiMode = DECPrivateMode(2),
         DECCOLM_SetNumberOfColumns = DECPrivateMode(3),
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 3129b94ca49..4898a3cd942 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -89,8 +89,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR
     virtual bool PopGraphicsRendition() = 0; // XTPOPSGR
 
-    virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // DECSET
-    virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST
+    virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // SM, DECSET
+    virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // RM, DECRST
     virtual bool RequestMode(const DispatchTypes::ModeParams param) = 0; // DECRQM
 
     virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) = 0; // DSR
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 40edfe1cf96..3990fbdccaa 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -102,6 +102,20 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
         }
 
         const OutputCellIterator it(std::wstring_view{ stringPosition, string.cend() }, attributes);
+        if (_modes.test(Mode::InsertReplace))
+        {
+            // If insert-replace mode is enabled, we first measure how many cells
+            // the string will occupy, and scroll the target area right by that
+            // amount to make space for the incoming text.
+            auto measureIt = it;
+            while (measureIt && measureIt.GetCellDistance(it) < lineWidth)
+            {
+                measureIt++;
+            }
+            const auto row = cursorPosition.y;
+            const auto cellCount = measureIt.GetCellDistance(it);
+            _ScrollRectHorizontally(textBuffer, { cursorPosition.x, row, lineWidth, row + 1 }, cellCount);
+        }
         const auto itEnd = textBuffer.WriteLine(it, cursorPosition, wrapAtEOL, lineWidth - 1);
 
         if (itEnd.GetInputDistance(it) == 0)
@@ -1555,7 +1569,7 @@ bool AdaptDispatch::_PassThroughInputModes()
 }
 
 // Routine Description:
-// - Support routine for routing private mode parameters to be set/reset as flags
+// - Support routine for routing mode parameters to be set/reset as flags
 // Arguments:
 // - param - mode parameter to set/reset
 // - enable - True for set, false for unset.
@@ -1565,6 +1579,9 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
 {
     switch (param)
     {
+    case DispatchTypes::ModeParams::IRM_InsertReplaceMode:
+        _modes.set(Mode::InsertReplace, enable);
+        return true;
     case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
         _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, enable);
         return !_PassThroughInputModes();
@@ -1647,7 +1664,7 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
 }
 
 // Routine Description:
-// - DECSET - Enables the given DEC private mode params.
+// - SM/DECSET - Enables the given mode parameter (both ANSI and private).
 // Arguments:
 // - param - mode parameter to set
 // Return Value:
@@ -1658,7 +1675,7 @@ bool AdaptDispatch::SetMode(const DispatchTypes::ModeParams param)
 }
 
 // Routine Description:
-// - DECRST - Disables the given DEC private mode params.
+// - RM/DECRST - Disables the given mode parameter (both ANSI and private).
 // Arguments:
 // - param - mode parameter to reset
 // Return Value:
@@ -1681,6 +1698,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
 
     switch (param)
     {
+    case DispatchTypes::ModeParams::IRM_InsertReplaceMode:
+        enabled = _modes.test(Mode::InsertReplace);
+        break;
     case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::CursorKey);
         break;
@@ -2322,7 +2342,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled)
 //   we actually perform. As the appropriate functionality is added to our ANSI support,
 //   we should update this.
 //  X Text cursor enable          DECTCEM     Cursor enabled.
-//    Insert/replace              IRM         Replace mode.
+//  X Insert/replace              IRM         Replace mode.
 //  X Origin                      DECOM       Absolute (cursor origin at upper-left of screen.)
 //  X Autowrap                    DECAWM      Autowrap enabled (matches XTerm behavior).
 //    National replacement        DECNRCM     Multinational set.
@@ -2350,7 +2370,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled)
 bool AdaptDispatch::SoftReset()
 {
     _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled.
-    _modes.reset(Mode::Origin); // Absolute cursor addressing.
+    _modes.reset(Mode::InsertReplace, Mode::Origin); // Replace mode; Absolute cursor addressing.
     _api.SetAutoWrapMode(true); // Wrap at end of line.
     _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, false); // Normal characters.
     _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, false); // Numeric characters.
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 76dbfc05f5b..1a283a1c987 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -82,8 +82,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 SetMode(const DispatchTypes::ModeParams param) override; // DECSET
-        bool ResetMode(const DispatchTypes::ModeParams param) override; // DECRST
+        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
         bool SetKeypadMode(const bool applicationMode) override; // DECKPAM, DECKPNM
         bool SetAnsiMode(const bool ansiMode) override; // DECANM
@@ -155,6 +155,7 @@ namespace Microsoft::Console::VirtualTerminal
     private:
         enum class Mode
         {
+            InsertReplace,
             Origin,
             Column,
             AllowDECCOLM,
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 56ab5d1578d..0a3b7a688a7 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -82,8 +82,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool PushGraphicsRendition(const VTParameters /*options*/) override { return false; } // XTPUSHSGR
     bool PopGraphicsRendition() override { return false; } // XTPOPSGR
 
-    bool SetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECSET
-    bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRST
+    bool SetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // SM, DECSET
+    bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // RM, DECRST
     bool RequestMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRQM
 
     bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/, const VTParameter /*id*/) override { return false; } // DSR
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 48625cd5af7..509a16433ad 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -512,6 +512,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         });
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSEL);
         break;
+    case CsiActionCodes::SM_SetMode:
+        success = parameters.for_each([&](const auto mode) {
+            return _dispatch->SetMode(DispatchTypes::ANSIStandardMode(mode));
+        });
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::SM);
+        break;
     case CsiActionCodes::DECSET_PrivateModeSet:
         success = parameters.for_each([&](const auto mode) {
             return _dispatch->SetMode(DispatchTypes::DECPrivateMode(mode));
@@ -519,6 +525,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         //TODO: MSFT:6367459 Add specific logging for each of the DECSET/DECRST codes
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSET);
         break;
+    case CsiActionCodes::RM_ResetMode:
+        success = parameters.for_each([&](const auto mode) {
+            return _dispatch->ResetMode(DispatchTypes::ANSIStandardMode(mode));
+        });
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::RM);
+        break;
     case CsiActionCodes::DECRST_PrivateModeReset:
         success = parameters.for_each([&](const auto mode) {
             return _dispatch->ResetMode(DispatchTypes::DECPrivateMode(mode));
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index af48cf37922..83088e9a160 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -130,7 +130,9 @@ namespace Microsoft::Console::VirtualTerminal
             VPR_VerticalPositionRelative = VTID("e"),
             HVP_HorizontalVerticalPosition = VTID("f"),
             TBC_TabClear = VTID("g"),
+            SM_SetMode = VTID("h"),
             DECSET_PrivateModeSet = VTID("?h"),
+            RM_ResetMode = VTID("l"),
             DECRST_PrivateModeReset = VTID("?l"),
             SGR_SetGraphicsRendition = VTID("m"),
             DSR_DeviceStatusReport = VTID("n"),
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index 83318b1719d..56775c27999 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -221,7 +221,9 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"),
+                                      TraceLoggingUInt32(_uiTimesUsed[SM], "SM"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"),
+                                      TraceLoggingUInt32(_uiTimesUsed[RM], "RM"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECRST], "DECRST"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECKPAM], "DECKPAM"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"),
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 01eee817e0a..6de52d060e2 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -48,7 +48,9 @@ namespace Microsoft::Console::VirtualTerminal
             SGR,
             DECSC,
             DECRC,
+            SM,
             DECSET,
+            RM,
             DECRST,
             DECKPAM,
             DECKPNM,


Test patch: