charmbracelet / charmbracelet/x
vt: IRM (insert mode, CSI 4 h) is not implemented — printed cells overwrite instead of shifting the line right
- Dominant language
- Go
- Stars
- 314
- Forks
- 94
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 2
Description
## Summary
`vt` implements **ICH** (`CSI n @`) but not **IRM** (Insert/Replace Mode, `CSI 4 h` / `CSI 4 l`).
Mode 4 is absent from the recognised-mode table in `resetModes()`, and `handleGrapheme` always
writes over the cell under the cursor, so a program that opens its gap with insert mode has its
characters land on top of their neighbours rather than beside them.
`x/ansi` already defines and documents the mode (`ansi.ModeInsertReplace = ANSIMode(4)`, with the
`SetModeInsertReplace` / `ResetModeInsertReplace` sequences); `vt` is the piece that never reads it.
A code search over the repo shows `vt` does not reference `ansi.ModeInsertReplace` anywhere.
## Reproduce
```go
package main
import (
"fmt"
"strings"
vt "github.com/charmbracelet/x/vt"
)
func main() {
for _, tc := range []struct{ name, in, want string }{
{"ICH (CSI n @)", "abc\x1b[3D\x1b[1@X", "Xabc"},
{"IRM (CSI 4 h)", "abc\x1b[3D\x1b[4hX\x1b[4l", "Xabc"},
} {
emu := vt.NewEmulator(20, 2)
emu.Write([]byte(tc.in))
got := strings.TrimRight(strings.Split(emu.Render(), "\n")[0], " ")
fmt.Printf("%-16s got=%-8q want=%-8q\n", tc.name, got, tc.want)
}
}
```
Both cases write `abc`, move the cursor back to column 0, and insert `X` there — once via ICH,
once via IRM. They must render the same.
```
ICH (CSI n @) got="Xabc" want="Xabc"
IRM (CSI 4 h) got="Xbc" want="Xabc" <-- the `a` was overwritten
```
**Expected:** `Xabc` in both cases. Per ECMA-48, while IRM is set a printed character is inserted at
the cursor and the rest of the line shifts right — the same effect as `ICH 1` immediately before the
character.
**Actual:** the IRM case renders `Xbc`.
## Why it bites in practice
This is not a synthetic sequence. Two real producers reach for IRM:
1. **readline.** `xterm-256color` terminfo advertises `mir` / `smir` / `rmir`, and readline uses that
path to insert a single character. Typing `a` at the front of `ls a/b/c/d` in a shell running
inside a `vt`-backed terminal draws `as a/b/c/d` instead of `als a/b/c/d`. Which of ICH and IRM
readline picks varies with how the shell was started and how the PTY was sized, so the symptom
looks intermittent. (Found this way in [cmj0121/baton#10](https://github.com/cmj0121/baton/issues/10).)
2. **Charm's own renderer.** `ultraviolet/terminal_renderer.go`'s `insertCells()` falls back to
`ansi.SetModeInsertReplace` whenever the terminal type is outside its ICH allowlist
(`xtermCaps`, driven by `$TERM`); `x/cellbuf/screen.go` does the same. So under a `TERM` such as
`rxvt-unicode-256color`, `Eterm` or `vt220`, output produced by Charm's renderer is output that
Charm's emulator cannot render correctly.
## Where it is
- `vt/mode.go` — `resetModes()` lists the recognised modes; ANSI mode 4 is not among them, so
`CSI 4 h` is stored by `handleMode` and then never consulted.
- `vt/utf8.go` — `handleGrapheme()` ends in `e.scr.SetCell(x, y, &cell)` unconditionally.
- `vt/handlers.go` — the ICH handler (`'@'`) already does the right thing via `e.scr.InsertCell(n)`.
## Suggested fix
The primitive already exists, so this looks small:
1. Add `ansi.ModeInsertReplace: ansi.ModeReset` to the `resetModes()` table so the mode is tracked.
2. In `handleGrapheme`, once the cursor position for this cell is settled (i.e. after the pending-wrap
/ `atPhantom` handling), call `e.scr.InsertCell(cell.Width)` when the mode is set, before
`SetCell`. Using the grapheme's cell width rather than 1 keeps wide characters correct.
Happy to send a PR if that shape looks right.
## Environment
- `github.com/charmbracelet/x/vt v0.0.0-20260816001655-68d539dca504` (latest at time of writing;
also reproduced on `v0.0.0-20260615092313-b57e5e6d29bb`)
- go1.26.5 darwin/arm64, macOS 26.5.2
Contributor guide
Research direction
Start in vt/mode.go at resetModes(), then read vt/utf8.go's handleGrapheme and vt/handlers.go's ICH handler. Run the vt package tests and reproduce the provided IRM example. Done means CSI 4 h makes printed cells shift right like ICH, including graphemes wider than one cell, while CSI 4 l restores overwrite behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100