charmbracelet / charmbracelet/x

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

Open
#949 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.