feat(vt): implement IRM insert mode - #951
Open
Rohilalala wants to merge 1 commit into
Open
Conversation
`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
force-pushed
the
feat/vt-irm-insert-mode
branch
from
August 24, 2026 16:54
0bddb83 to
c6d1ab6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #949.
vtimplemented ICH (CSI n @) but not IRM (CSI 4 h/CSI 4 l). Mode 4 was missing from the table inresetModes, sohandleModestored it and nothing read it back, andhandleGraphemealways wrote over the cell under the cursor. The issue's repro, before and 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.go—ansi.ModeInsertReplacejoins 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 iscell.Widthwide, 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 unexportedinsertCellsInRow.Screen.InsertCelland the ICH handler are untouched.Two deviations from the issue's suggestion, both deliberate
1. Position instead of the cursor.
Screen.InsertCellreadss.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.insertCellsInRowtakes the positionhandleGraphemehas 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.
InsertCellArearefuses 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 outsideDECSTBM— silently, because the cell prints either way. The line a character prints on shifts whether or not it falls inside the vertical scrolling region, soinsertCellsInRownarrows 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
casestable 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,RISclearing the mode, and the DECRQM answer before and afterCSI 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=oninvt/, andgolangci-lint run --config ../.golangci.yml ./...reports only the two findings already present onmain(emulator.go:421,csi.go:39), both in files this PR does not touch.vtteststill 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 owntermTexttest helper. That is not new here: ICH produces the identical shape today, since both go throughultraviolet'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.