Skip to content

input-parser: resolve lone ESC-]/ESC-[ to Alt+]/Alt+[ on legacy terminals; surface dropped keybinding config entries - #2948

Merged
sinelaw merged 2 commits into
masterfrom
claude/fix-input-parser-altbracket
Aug 10, 2026
Merged

input-parser: resolve lone ESC-]/ESC-[ to Alt+]/Alt+[ on legacy terminals; surface dropped keybinding config entries#2948
sinelaw merged 2 commits into
masterfrom
claude/fix-input-parser-altbracket

Conversation

@sinelaw

@sinelaw sinelaw commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Fixes #2930
Refs #1580
Refs #1128

Motivation

Terminals without the kitty keyboard protocol transmit Alt+] as exactly ESC ] and Alt+[ as ESC [ — byte-identical to the OSC and CSI introducers. Since the OSC-swallowing change in 0.4.6, feed_escape committed those prefixes to a string/CSI sequence unconditionally, so on legacy terminals the default Focus Next/Previous Split chords went dead — and worse:

  • Alt+] entered the OSC-swallowing state and ate every subsequent keystroke until a stray BEL/ST arrived;
  • after Alt+[, the next typed key was misparsed as a CSI final byte (typing A moved the cursor up).

This is #2930, and also the root cause of the remaining "Alt+] doesn't switch splits from the search panel" symptom in #1580 (the key never reaches the keymap, in any context).

The fix

No new timers, states, or heuristics: the parser already disambiguates a bare ESC (Escape key vs. sequence head) by letting the callers resolve it via escape_pending() / flush() once the stream goes idle (the existing ESC_GRACE machinery in the tty reader and the session-client parser). This PR extends which prefixes participate in that existing rule:

  • ESC [ with no parameter byte yet → flushable as Alt+[ (parameter presence was already tracked by the CSI buffer);
  • a string introducer (ESC ], ESC P, ESC _, ESC ^, ESC X) with no body byte yet → flushable as Alt+`` (the StringSeq state now remembers its introducer until the first body byte).

The first payload byte commits the sequence exactly as before, so genuine OSC/CSI traffic — whose payload always rides in the same write burst as its introducer — is still swallowed whole, including when a read boundary splits it right after the introducer (the continuation then arrives within the grace window, never at idle). The risk profile is identical to the pre-existing bare-ESC rule. Both callers (TtyReader, ClientInputParser) pick this up with zero changes to their code.

Secondary: silently dropped keybinding entries (#1128)

While the primary Ctrl+* symptom in #1128 is the terminal's legacy encoding stripping modifiers (not fixable editor-side; kitty-protocol terminals work), the investigation surfaced that a config keybinding whose key name doesn't parse ("asterisk", "kp_multiply", …) was dropped with no feedback anywhere. Per review feedback this is now diagnosed via logging only (no status-bar involvement): when an entry — single-key or a key inside a chord — fails to parse, the resolver emits one tracing::warn! at parse-failure time naming the offending key string and its action, drops that entry, and continues loading the rest of the config.

Tests

All reproducers fail without the fix and pass with it (verified by running the new tests against the unpatched parser: 3 failed, then green after the fix).

New tests:

  • fresh-input-parser: lone ESC ] → Alt+], lone ESC [ → Alt+[ (and the next key is not misread as a CSI final), lone ESC P/_/^/X → their Alt chords, OSC with payload in the same burst is not flushable, OSC split right after the introducer with the payload following is still swallowed, CSI with parameters is not flushable, empty OSC terminated in a later chunk is swallowed.
  • services::tty_input: lone introducers resolve to the Alt chords on idle and typing works afterwards; an OSC reply split across reads is still swallowed.
  • server::input_parser (fresh -a path): same two properties through the ClientInputParser grace-timer flow, driven without sleeping.
  • input::keybindings: entries with an unknown key name (single-key and chord) are dropped without half-registering anything, while a valid entry later in the same config still loads and resolves (the warning itself is a plain tracing::warn!; there is no log-capture helper for lib unit tests, so the test asserts the drop/continue behavior).

Ran:

  • cargo test -p fresh-input-parser — 121 passed (includes all prior OSC-swallowing/split-invariance regression tests and proptests, unchanged and green).
  • cargo test -p fresh-editor --lib for the tty_input, server::input_parser, and keybindings modules — all green (keybindings: 42 passed).
  • cargo check -p fresh-editor --all-targets, cargo fmt, cargo clippy -p fresh-input-parser --all-targets and cargo clippy -p fresh-editor (no new warnings in touched files).
  • Interactive tmux check with a debug build: opened a file, split vertically, sent raw ESC ] / ESC [ byte pairs — focus switches both ways, typing afterwards inserts normally (A no longer becomes Up), and a complete OSC reply sent as one burst is still swallowed without leaking text.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y


Generated by Claude Code

claude added 2 commits August 10, 2026 14:40
A terminal without the kitty keyboard protocol transmits Alt+] as
exactly `ESC ]` and Alt+[ as `ESC [` — byte-identical to the OSC and
CSI introducers. Since the OSC-swallowing fix shipped in 0.4.6,
`feed_escape` unconditionally committed those prefixes to StringSeq /
Csi, so on legacy terminals the default Focus Next/Previous Split
chords went dead: Alt+] additionally swallowed every subsequent
keystroke until a stray BEL/ST, and after Alt+[ the next typed key was
misparsed as a CSI final byte (typing `A` moved the cursor up).

Fix by extending the existing bare-ESC disambiguation machinery —
`escape_pending()` / `flush()` plus the callers' unchanged ESC_GRACE
idle rule — to the introducer prefixes: a bare `ESC [` (no parameter
byte yet) and a bare string introducer (`ESC ]`, `ESC P`, `ESC _`,
`ESC ^`, `ESC X`; no body byte yet) are now flushable, resolving to
the legacy Alt chord once the stream goes idle. The first payload byte
commits the sequence as before, so genuine OSC/CSI traffic — whose
payload always rides in the same write burst as its introducer — is
still swallowed whole, including when a read boundary splits it right
after the introducer (the payload then lands within the grace window,
never at idle). No timer or caller changes: both the tty reader and
the session-client parser pick the new behavior up through the same
two methods they already use for the bare-ESC case.

Fixes #2930
Refs #1580 (the "Alt+] doesn't switch splits from the search panel"
symptom there has this same root cause)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
A keybinding whose key name does not parse (`"key": "asterisk"`,
`"kp_multiply"`, …) was dropped while loading the config with no feedback
anywhere, so users had no way to learn why their binding never fired
(issue #1128). Emit one tracing warning per rejected entry, naming the
offending key and its action, and keep loading the rest of the config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
@sinelaw
sinelaw force-pushed the claude/fix-input-parser-altbracket branch from d19d604 to 3c46539 Compare August 10, 2026 15:14
@sinelaw
sinelaw merged commit 0919b22 into master Aug 10, 2026
10 checks passed
@sinelaw
sinelaw deleted the claude/fix-input-parser-altbracket branch August 10, 2026 16:53
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.

switching splits regreshon

2 participants