Skip to content

fix: implement CBT/CHT and 1-indexed CPR (fixes #58, #94)#223

Open
falki3nec wants to merge 5 commits into
TerminalStudio:masterfrom
prismatic-cz:fix/nano-cursor
Open

fix: implement CBT/CHT and 1-indexed CPR (fixes #58, #94)#223
falki3nec wants to merge 5 commits into
TerminalStudio:masterfrom
prismatic-cz:fix/nano-cursor

Conversation

@falki3nec
Copy link
Copy Markdown

Summary

Two independent fixes restoring correct behaviour of ncurses applications (nano, vim, htop) over raw SSH without tmux.

1. Implement CSI Pn Z (CBT) and CSI Pn I (CHT)

Both sequences were absent from _csiHandlers; the parser dropped them into unknownCSI and the cursor stayed in place. nano uses CBT aggressively while redrawing syntax-highlighted portions of the current line — when ignored, nano's internal cursor model diverges from the buffer by the skipped tab-stop distance, and subsequent writes land on the wrong cells. Visible symptom: long syntax-highlighted lines morph into garbled text during plain arrow-key navigation.

Invisible inside tmux (tmux handles CBT in its own emulator layer and forwards nothing to us) and with TERM=vt100 (nano's vt100 terminfo path avoids CBT).

2. Off-by-one in CPR reply

EscapeEmitter.cursorPosition() emitted raw 0-indexed buffer coordinates, but VT100 CPR (CSI row;col R) is 1-indexed. Every CSI 6n query returned a position off by one in both axes, throwing off ncurses apps that sync from CPR.

Root cause analysis

Diagnosed by capturing raw stdout bytes during a reproducible nano session and replaying them through a headless Terminal in a regression harness, narrowing the first divergence to a CSI Z that left cursorX unchanged.

Tests

  • test/src/regression/nano_vim_cursor_test.dart — 12 hypothesis-driven tests covering scroll region, save/restore, alt-screen, autowrap, CUP, CPR
  • test/src/regression/replay_test.dart — replays a captured debug log via REPLAY_LOG env var
  • Full flutter test passes except for 2 pre-existing textScaler golden-image failures unrelated to these changes.

Fixes

Happy to split into two PRs if preferred, or adjust style.

Adam (Prismatic) added 5 commits April 13, 2026 21:22
EscapeEmitter.cursorPosition() was emitting raw 0-indexed buffer
coordinates as the CPR (Cursor Position Report) reply, but VT100
requires 1-indexed values (CSI row ; col R). Every DSR (CSI 6n) query
thus returned a position off by one in both axes.

ncurses applications like nano and vim that sync their internal cursor
model from CPR ended up editing at the wrong visible position. The bug
is invisible inside a tmux session because tmux answers CPR from its
own state and never forwards the query to the underlying emulator —
which is why the bug only manifests on raw SSH.

Adds regression tests under test/src/regression/nano_vim_cursor_test.dart
covering CPR at home, CPR after CUP, and CPR round-trip fidelity, plus
sanity checks for the other scroll-region / save-restore / autowrap
hypotheses that proved to be implemented correctly.

Fixes TerminalStudio#58, TerminalStudio#94
CSI Pn Z (Cursor Backward Tab) and CSI Pn I (Cursor Horizontal Tab)
were missing from the CSI handler table. The parser dropped them into
unknownCSI and the cursor stayed put.

nano uses CBT aggressively. When cursor is past a tab stop and nano
wants to re-render a syntax-highlighted region starting at an earlier
tab stop, it emits CSI Z to get there in one hop instead of a BS run.
With the emulator ignoring the sequence, nano's internal cursor model
diverges from the buffer's by whatever distance the skipped CBT would
have covered. Subsequent character writes then land to the right of
where nano expects and overwrite unrelated cells — the user-visible
symptom is long syntax-highlighted lines morphing into garbled text
during plain arrow-key navigation. Bug is invisible inside tmux (tmux
handles CBT in its own emulator layer and forwards nothing to xterm.dart)
and with TERM=vt100 (nano's vt100 terminfo path avoids CBT).

Also implements CHT for symmetry.

Depends on the preceding CPR off-by-one fix; both combined restore
correct nano/vim behaviour in raw SSH.

Fixes TerminalStudio#58, TerminalStudio#94
RenderTerminal._onTerminalChange was calling markNeedsLayout on every
write from the Terminal, which triggered a full Flutter layout pass
(walking the entire RenderObject tree, re-measuring every child).

For pure content writes the widget geometry is unchanged — only the
scrollback buffer grew. Layout was the hot path in TUI-heavy scenarios
(Claude Code spinners + counters, vim/nano cursor movement) and made
xterm.dart render 5–10x slower than xterm.js on the web, because
web's DOM/canvas renderer doesn't re-layout on every write.

Update scroll bounds (applyContentDimensions + stick-to-bottom
correction) directly and trigger only a repaint. Geometry changes
(widget resize, font change, viewport resize) still go through the
normal performLayout path via markNeedsLayout calls elsewhere.

Measured against a Claude Code session with ~98 small output chunks
(spinner animation + counter), this removes ~98 layout passes per
burst, which was the dominant cost on desktop.

Fixes desktop terminal feeling sluggish during TUI output.
eraseLineToCursor called eraseRange(0, _cursorX) but eraseRange end is
exclusive, so column == _cursorX was never cleared. Per VT100 / xterm
docs, "Erase in Line from start to cursor" must include the cell at
the cursor position.

Symptom: Claude Code's "Do you want to…" confirmation menu on Android
and desktop Flutter apps rendered garbled, with residual characters
bleeding through each re-drawn line. Web (xterm.js) worked correctly.

Claude Code's menu layout relies on repeated `CSI 1 K` + `CUF` + text
patterns to blit one row at a time — each frame issues ~36 CSI 1 K
calls, and each one left exactly one residual cell of stale text.

Regression tests added in test/src/regression/csi_1k_erase_test.dart
cover mid-line, first-column, last-column erase, and confirm that
CSI 0 K semantics are unchanged by the fix.
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.

Vim dosen't render properly Nano doesn't render properly

1 participant