fix(renderer): keep columns aligned on terminals without mode 2027 - #131
fix(renderer): keep columns aligned on terminals without mode 2027#131dgageot wants to merge 4 commits into
Conversation
Reproduces wide-glyph cursor drift with a chat-style layout with a vertical separator and sidebar. Signed-off-by: David Gageot <david.gageot@docker.com>
On terminals that don't negotiate mode 2027 (e.g. Terminal.app), cursor column drifts after wide glyphs whose width the terminal measures differently. The previous fallback only re-anchored at end of line, so anything drawn after an emoji on the same line landed at a random column. Now drift is tracked lazily and re-anchored before the next narrow cell, before ECH/EL (which act at the cursor position), and via CR before cursor moves. The overwrite-move optimisation is also disabled across wide cells. Adjacent wide glyphs still don't emit per-cell CHA. Assisted-By: Claude (Anthropic)
Under the wcwidth fallback (no mode 2027), VS16/ZWJ sequences like "⛓️💥" are stored as a single width-1 cell holding several runes, but terminals such as Terminal.app advance the cursor by more than one column. Drift detection only triggered on cells wider than 1, so everything after such a cluster was shifted. Now any cell with more than one rune also sets the drift flag. Assisted-By: Claude (Anthropic)
…ental updates On terminals without mode 2027 (e.g. Terminal.app), glyphs like emoji may be drawn wider or narrower than modeled, so cell-level diffing leaves residue or gaps. When a changed line contains wide or multi-rune cells and 2027 isn't negotiated, erase and redraw the line wholly instead of diffing. Assisted-By: Claude
|
Failing test seems flaky. Fix here: #132 |
|
hi! thank you for looking into this! your diagnosis about VS16/ZWJ clusters being invisible to a width-only check seems correct but I think the re-anchoring mechanism makes things worse rather than better. some issuesIf you draw two VS16 clusters and a separator without an incremental update: uv.NewStyledString("☹️☹️|X").Draw(scr, scr.Bounds())
s.Render(scr.RenderBuffer)
The emoji are gone on first paint. Same result for ZWJ sequences. The raw bytes from that: Each re-anchor targets the renderer's next column, but that column is inside a cluster the terminal painted wider, so the following write erases it. The re-anchor destination is computed with the same width model the terminal disagrees with, so the destination is itself wrong. Re-anchoring to a wrong column can't restore sync.
So performanceSeparately: the whole-line redraw takes a 100x30 emoji screen from 212 to about The core difficulty is that we can't compute a correct absolute column without
|
On terminals that don't negotiate Unicode grapheme width (DEC mode 2027),
such as Apple Terminal.app, emoji and other clusters may advance the cursor
by a different number of columns than the renderer models. Content drawn
after them on the same line landed at random columns, and incremental
updates left residue behind.
drift-prone glyph was drawn, instead of only at end of line
sources, not just wide cells
cell-level diffing can't trust the terminal's actual screen content there
Terminals that negotiate mode 2027 are unaffected and keep the minimal
diff path.
Tested on Terminal, iTerm and Ghostty