Skip to content

feat(vt): implement IRM insert mode - #951

Open
Rohilalala wants to merge 1 commit into
charmbracelet:mainfrom
Rohilalala:feat/vt-irm-insert-mode
Open

feat(vt): implement IRM insert mode#951
Rohilalala wants to merge 1 commit into
charmbracelet:mainfrom
Rohilalala:feat/vt-irm-insert-mode

Conversation

@Rohilalala

Copy link
Copy Markdown

Fixes #949.

vt implemented ICH (CSI n @) but not IRM (CSI 4 h / CSI 4 l). Mode 4 was missing from the table in resetModes, so handleMode stored it and nothing read it back, and handleGrapheme always wrote over the cell under the cursor. The issue's repro, before and after:

ICH  (CSI n @)   got="Xabc"   want="Xabc"
IRM  (CSI 4 h)   got="Xbc"    want="Xabc"     <- before
IRM  (CSI 4 h)   got="Xabc"   want="Xabc"     <- after

Thanks @cmj0121 for the writeup — the diagnosis and the suggested shape are yours; this follows it with two adjustments noted below.

What changed

  • vt/mode.goansi.ModeInsertReplace joins the recognised-mode table at its reset default, so the mode is tracked and DECRQM answers set/reset (\x1b[4;2$y / \x1b[4;1$y) instead of "not recognised" (\x1b[4;0$y).
  • vt/utf8.go — while the mode is set, a printed grapheme opens a gap at its own position before the cell is written. The gap is cell.Width wide, so a wide character does not come to rest on half of the cell it displaced.
  • vt/screen.go — the insertion goes through a new unexported insertCellsInRow. Screen.InsertCell and the ICH handler are untouched.

Two deviations from the issue's suggestion, both deliberate

1. Position instead of the cursor. Screen.InsertCell reads s.cur, which is the wrong place for a print that has just wrapped: the cursor still sits at the edge of the line just filled while the cell goes to column 0 of the next one. insertCellsInRow takes the position handleGrapheme has already computed. Covered by "IRM Set Inserts At The Wrapped Position" — with the cursor-based call that test renders "Zy " instead of "Zxy ".

2. Bounded by the left and right margins only. InsertCellArea refuses to shift a row outside the rectangle it is given, so passing the scroll region would have made insert mode silently degrade to replace on any line outside DECSTBM — silently, because the cell prints either way. The line a character prints on shifts whether or not it falls inside the vertical scrolling region, so insertCellsInRow narrows the scroll rectangle to the target row and keeps its horizontal bounds. Covered by "IRM Set Inserts Outside The Scroll Region".

Happy to align it with whatever you prefer if ICH's current region gating is intentional and you would rather both behave the same.

Tests

Eight cases in the existing cases table plus one for the mode report: replace as the default, a single insert, repeated inserts, a wide grapheme opening two cells, cells pushed off the right edge, insertion at the wrapped position, insertion outside the scrolling region, RIS clearing the mode, and the DECRQM answer before and after CSI 4 h.

Each one was checked against a deliberately broken implementation to make sure it fails for the right reason — dropping the mode-table entry, using the cursor-based insert, and using the scroll rectangle each turn the corresponding test red.

Verified with go test ./... -race -count=3 -shuffle=on in vt/, and golangci-lint run --config ../.golangci.yml ./... reports only the two findings already present on main (emulator.go:421, csi.go:39), both in files this PR does not touch. vttest still builds against the change.

One thing I noticed but did not fix

Printing onto the right half of a wide cell under IRM leaves an orphaned zero-width cell in the line, which will spin any loop that walks a line by cell.Width — including this package's own termText test helper. That is not new here: ICH produces the identical shape today, since both go through ultraviolet's buffer shift. It seemed like its own issue rather than something to fold into this one, but say the word and I will take it.

`vt` implemented ICH (`CSI n @`) but not IRM (`CSI 4 h` / `CSI 4 l`).
Mode 4 was missing from the table in `resetModes`, so `handleMode` stored
it and nothing ever read it back, and `handleGrapheme` wrote every cell
over whatever was under the cursor. A program that opens a gap with
insert mode therefore had its characters land on top of their neighbours:

    abc, cursor to column 0, CSI 4 h, X   rendered "Xbc", want "Xabc"

Two real producers reach for IRM rather than ICH: readline, via the
`mir`/`smir`/`rmir` capabilities that `xterm-256color` advertises, and
Charm's own renderer — `ultraviolet`'s `insertCells` and `x/cellbuf`'s
screen both fall back to `ansi.SetModeInsertReplace` when `$TERM` is
outside their ICH allowlist. Under a `TERM` like `rxvt-unicode-256color`
or `vt220` that made output from Charm's renderer unrenderable by Charm's
emulator.

Mode 4 now sits in the recognised-mode table, so DECRQM answers set/reset
for it instead of "not recognised", and a printed grapheme inserts while
the mode is set. The gap is as wide as the grapheme, so a wide character
does not come to rest on half of the cell it displaced.

The insertion goes through a new unexported `insertCellsInRow` rather
than `Screen.InsertCell`, which differs in two ways that printing needs.
It takes the position instead of reading the cursor, because a print that
wrapped lands on the next line while the cursor still sits at the edge of
the last one. And it bounds the shift by the left and right margins only:
the line a character prints on shifts whether or not that line falls
inside the vertical scrolling region, where reusing the scroll rectangle
would have let the mode quietly degrade to replace on those lines, since
the cell is printed either way. `Screen.InsertCell` and ICH are untouched.

Tests cover replace being the default, a single insert, repeated inserts,
a wide grapheme opening two cells, cells pushed off the right edge, an
insert at the wrapped position, an insert outside the scrolling region,
RIS clearing the mode, and the DECRQM report.

Fixes charmbracelet#949
@Rohilalala
Rohilalala force-pushed the feat/vt-irm-insert-mode branch from 0bddb83 to c6d1ab6 Compare August 24, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vt: IRM (insert mode, CSI 4 h) is not implemented — printed cells overwrite instead of shifting the line right

1 participant