Skip to content

Emacs backspace fix + buffered terminal output (one flush per frame)#105

Merged
maxlandon merged 3 commits into
masterfrom
dev
Jun 1, 2026
Merged

Emacs backspace fix + buffered terminal output (one flush per frame)#105
maxlandon merged 3 commits into
masterfrom
dev

Conversation

@maxlandon

Copy link
Copy Markdown
Member

Three commits, all GPG-signed. Ideas adapted from / evaluated against the moloch--/readline fork.

Fixes

  • fix(keymap) — In emacs mode \C-h was overridden to backward-kill-word; many terminals send ^H for Backspace, so Backspace deleted a whole word. Dropped the override so \C-h falls through to the GNU default backward-delete-char. Regression test asserts the resolved bind.

Performance

  • perf(display) — Buffer a whole render frame and flush it in a single write. Refresh() (and AcceptLine/RefreshTransient/ClearHelpers) previously emitted dozens of separate stdout writes per keystroke (hide cursor, moves, clears, prompt, line, highlights, helpers, show cursor); a terminal can show those partial frames as flicker, and each is a syscall. New internal/term buffering layer (BeginBuffer/EndBuffer/FlushBuffer/WriteString/Printf); all render-path fmt.Print sites route through it.
    • Correctness landmine handled: GetCursorPos calls term.FlushBuffer() before the ESC[6n query (unix) / console cursor read (windows), so the position is measured against on-screen output, not buffered bytes.
    • Buffering changes only when bytes are written, never which — the PTY golden-screen tests pass byte-identical. Adds buffer unit tests (coalescing, nesting, mid-frame flush).

Test-only

  • test(keymap) — Guards that matchBind's ordering is load-bearing: the default emacs keymap has 15 sequences that ConvertMeta collapses to the same key bytes with different actions, so removing the per-keystroke sort (as the fork did) would make keybind resolution depend on map iteration order. Asserts the collisions exist and that resolution stays deterministic.

Evaluated but NOT adopted

  • matchBind sort removal — unsafe (nondeterministic dispatch; see the guard test) and aimed at the wrong cost (ConvertMeta recomputed over all binds per keystroke, not the sort).
  • Cursor-position caching — passes the current suite (because ensureInputSpace maintains startRows across scrolls) but has an untested invalidation gap: WatchResize doesn't mark the cache dirty, so a terminal resize would mis-render. Not worth the Incorrect cursor/character rendering in terminal prompt #98-class risk, especially now that buffering cuts the probe's cost.

Verification

go build, go test ./... (incl. display golden-screen + -race), both unix and Windows builds, and golangci-lint run0 issues.

maxlandon and others added 3 commits June 1, 2026 01:31
emacsKeys overrode the GNU default \C-h (backward-delete-char) with backward-kill-word. Many terminals send ^H for Backspace, so Backspace deleted a whole word. Drop the override so \C-h falls through to the default backward-delete-char; add a regression test asserting the resolved bind. Ported from moloch--/readline (Will DeDominico / Moriz82).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh and the other frame entrypoints emitted dozens of separate writes to
stdout (hide cursor, moves, clears, prompt, line, highlights, helpers, show
cursor). A terminal can show those partial frames as flicker, and each is a
syscall.

Add a buffering layer in internal/term (BeginBuffer/EndBuffer/FlushBuffer/
WriteString/Printf): render entrypoints wrap their body so a whole frame
coalesces into a single write. All fmt.Print rendering sites route through
term.WriteString; term's own MoveCursor*/bracketed-paste helpers buffer for
free.

The one ordering constraint: GetCursorPos must see prior output on screen
before it reads the reply, so it calls term.FlushBuffer() before the ESC[6n
query (unix) / console cursor read (windows). Output goes to stdout, kept
distinct from termFile (stderr) used for size/cursor queries.

Buffering changes only WHEN bytes are written, never which: the PTY
golden-screen tests pass byte-identical. Adds term buffer unit tests
(coalescing, nesting, mid-frame flush). Inspired by moloch--/readline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Evaluation of removing the per-keystroke sort in matchBind: the default emacs
keymap has 15 sequences that ConvertMeta collapses to the same key bytes with
DIFFERENT actions (e.g. ESC-prefixed meta binds overlapping self-insert). The
ordering makes the exact match deterministic; dropping it would make keybind
resolution depend on Go's randomized map iteration. These tests assert the
collisions exist (so the ordering is justified) and that resolution stays
deterministic across runs — a guard against a naive 'optimization' that removes
the sort. (The real per-keystroke cost is ConvertMeta over every bind, not the
sort; addressing that is separate future work.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maxlandon maxlandon merged commit 00fcced into master Jun 1, 2026
9 checks passed
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.

1 participant