input-parser: resolve lone ESC-]/ESC-[ to Alt+]/Alt+[ on legacy terminals; surface dropped keybinding config entries - #2948
Merged
Conversation
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
force-pushed
the
claude/fix-input-parser-altbracket
branch
from
August 10, 2026 15:14
d19d604 to
3c46539
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2930
Refs #1580
Refs #1128
Motivation
Terminals without the kitty keyboard protocol transmit Alt+] as exactly
ESC ]and Alt+[ asESC [— byte-identical to the OSC and CSI introducers. Since the OSC-swallowing change in 0.4.6,feed_escapecommitted those prefixes to a string/CSI sequence unconditionally, so on legacy terminals the default Focus Next/Previous Split chords went dead — and worse:Amoved 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 viaescape_pending()/flush()once the stream goes idle (the existingESC_GRACEmachinery 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);ESC ],ESC P,ESC _,ESC ^,ESC X) with no body byte yet → flushable as Alt+`` (theStringSeqstate 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 onetracing::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: loneESC ]→ Alt+], loneESC [→ Alt+[ (and the next key is not misread as a CSI final), loneESC 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 -apath): same two properties through theClientInputParsergrace-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 plaintracing::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 --libfor thetty_input,server::input_parser, andkeybindingsmodules — all green (keybindings: 42 passed).cargo check -p fresh-editor --all-targets,cargo fmt,cargo clippy -p fresh-input-parser --all-targetsandcargo clippy -p fresh-editor(no new warnings in touched files).ESC ]/ESC [byte pairs — focus switches both ways, typing afterwards inserts normally (Ano 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