fix: mirror keyboard protocol state onto the real terminal and decode encoded C-a everywhere#53
Merged
Merged
Conversation
boo ui composites sessions through a local ghostty-vt terminal, so an application's kitty keyboard enable never reached the user's real terminal (unlike boo attach, whose repaint replays it). The terminal kept legacy encodings: Shift+Enter was indistinguishable from Enter, and a kitty-mode application sat on a bare Esc waiting for the rest of a sequence that never came, so Esc only registered on the second press. The UI now mirrors the focused view's kitty flags (and modifyOtherKeys) onto the real terminal, the way an attach repaint does, and drops them while a UI prompt or key-driven mode reads byte-oriented input. The input parser learns the CSI-u encodings the mirrored terminal then produces, reusing the keys.zig kitty helpers: Ctrl+A arms the prefix, the command key after it decodes, the Esc key becomes a canonical esc event that drives the existing cancel behaviors (prompts, browse, resize, scroll snap-back) and otherwise forwards verbatim, and every other CSI-u key passes through to the session untouched.
…n quit Review follow-ups: - A second discrete kitty-encoded C-a press now dispatches the C-a C-a focus-last binding instead of staying armed; only key repeats (event 2) and releases keep the prefix armed, matching the raw-byte path. - modifyOtherKeys is no longer mirrored onto the real terminal: under it the terminal encodes C-a as CSI 27;5;97~, which the UI parser does not decode, so the prefix would stop working whenever vim and friends were focused. CSI-u decode support for that encoding is a possible follow-up. - The CSI hold grammar no longer depends on the kitty state: a CSI-u key split across reads while the mirror flips (prompt opening) replays whole instead of being mangled, and legacy F-keys (ESC [ 15 ~) replay as one chunk instead of leaking "15~" into prompts. Decoding still only happens while kitty is active. - The keyboard mirror re-syncs right after input handling, shrinking the window where a just-opened prompt still receives kitty-encoded keys. - Quitting the UI drains pending terminal input via the same restore helper a plain attach uses, so a still-held C-a C-d cannot leak an EOF into the shell underneath. - Scroll_Lock joins the kitty modifier-key set in keys.zig. - Merged duplicate prompt cancel arms, doc comments for KittyKey and InputParser.feed, and integration-test formatting cleanups.
…sers
The boo at repaint has always replayed an application's
modifyOtherKeys mode 2 onto the client's real terminal (the
formatter's keyboard extra emits CSI > 4;2m), but neither parser
decoded the resulting CSI 27;5;97~ encoding of Ctrl+A, so the prefix
silently stopped working with vim-class apps on xterm-faithful
terminals. Both deferred follow-ups land:
- keys.zig grows a Protocols struct ({kitty, modify}) replacing the
bool feed parameter in both parsers; the daemon feeds the window's
modify_other_keys_2 alongside the kitty flags, and boo ui mirrors
modifyOtherKeys onto the real terminal again (suspended during
prompts like the kitty flags) now that the parser understands it.
- parseModify decodes CSI 27;mods;cp~ (no event types, no release
handling; a repeated prefix stays armed in boo at and dispatches
C-a C-a in boo ui, matching each parser's raw-byte semantics).
- parseKitty reads the base-layout-key subfield (cp:shifted:base) and
effectiveCp matches the prefix and command keys through it when the
primary codepoint is non-ASCII, so C-a works on Cyrillic and other
non-Latin layouts under the kitty alternate-keys flag. ASCII
primaries always win, keeping AZERTY-style Latin layouts on their
legacy typed-character semantics.
- keys.zig's hold grammar loosens to any-digit codepoints (required
for base matching) with finals gated per protocol; divergence still
replays held bytes verbatim.
16 new unit tests across both parsers (modify prefix/command/repeat/
passthrough/off/split, dual-protocol decode, Cyrillic prefix+command,
AZERTY non-match, paste protection) and 3 integration tests: boo at
detach via CSI 27;5;97~ with repaint replay, boo ui modify mirror
with prompt suspension, and boo ui modify-encoded quit without leak.
Merged
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.
Problem
In
boo ui, with a kitty-protocol application focused (Claude Code and friends):\r, indistinguishable from Enter, so the app submits instead.0x1band, running in kitty mode, waits for the rest of a sequence that never comes; the next Esc flushes the first.Both work in
boo at. Two adjacent holes surfaced during review and are fixed here too:boo at's C-a prefix silently breaks under modifyOtherKeys. The attach repaint has always replayed an app'sCSI > 4;2m(vim-class apps) onto the client's real terminal, butkeys.zignever decoded the resultingCSI 27;5;97~encoding of Ctrl+A that xterm-faithful terminals then send.CSI 1092:1060:97;5u); the parsers ignored the base-layout-key subfield.Root cause (boo ui)
boo uicomposites sessions through a local ghostty-vt terminal. When the focused app enables a keyboard protocol, the enable sequence is consumed by that local terminal and never reaches the user's real terminal, which keeps sending legacy encodings.boo attachdoes not have this problem because the daemon repaint replays keyboard state onto the client's real terminal (window.zigrepaint withformatter.extra.keyboard).Fix
Ui.syncKeyboardwrites the focused view's kitty flags (CSI = flags ; 1 u) and modifyOtherKeys state (CSI > 4 ; 2/0 m) to the real terminal whenever they change, matching what an attach repaint replays. Both are suspended while a UI prompt or key-driven mode reads byte-oriented input, restored after, and re-synced immediately after input handling.keys.Protocolsstruct ({kitty, modify}) gates decoding in both parsers (keys.Parserfor the daemon,InputParserfor the UI):CSI 97;5uarms the prefix (releases, repeats, modifier keys handled per parser; in the UI a second discrete press dispatches theC-a C-afocus-last binding like a second raw0x01), the command key decodes, everything else (Shift+EnterCSI 13;2u) passes through verbatim.keys.parseModifydecodesCSI 27;mods;cp~for the prefix and command keys; the protocol has no event types, so a repeated prefix stays armed inboo atand dispatchesC-a C-ainboo ui, matching each parser's raw-byte semantics. Paste markers and F-keys share the~final but never the27marker.parseKittynow reads thecp:shifted:basesubfields andkeys.effectiveCpmatches the prefix and command keys through the base-layout key when the primary codepoint is non-ASCII (Ctrl+ф = C-a on Russian layouts). ASCII primaries always win, so AZERTY-style Latin layouts keep their legacy typed-character semantics.CSI 27upress, or a lone0x1bafter the 50ms flush) is a.escevent carrying the original bytes; it drives the existing cancel behaviors and otherwise forwards exactly as encoded. modifyOtherKeys never encodes an unmodified Esc, so no extra handling is needed there.client.restoreTty(shared with plain attach), draining held-key repeats so a still-heldC-a C-dcannot EOF the shell underneath.15~into prompts as text.Legacy terminals are unaffected: without mirrored state the parsers decode nothing new, and terminals without protocol support ignore the mirror sequences.
Testing
keys.zigandui.zig: kitty prefix arming/release/repeat, double C-a press, encoded command keys, Esc press/release/modified, split-Esc disarm, modify prefix/command/repeat/passthrough/off/split, dual-protocol decode, Cyrillic prefix+command, AZERTY non-match, paste protection, mid-hold mirror flips, single-chunk F-key replay.boo ui, Shift+Enter and Esc verbatim passthrough, kitty- and modify-encoded C-a intercepted without leaking (bothboo uiandboo at), prompts suspend and restore both mirrors,boo atreattach repaint replays>4;2m.zig build test-all(113 unit + 66 integration) passes, integration re-run 3x for flake checking;zig fmt --checkclean.Decision log
boo uisees a byte. Mirroring reproduces exactly the contractboo atprovides.CSI 27;mods;cp~: mirroring it without the decoder would have broken the C-a prefix whenever vim-class apps were focused. ghostty itself keeps Ctrl+letter legacy even in mode 2, but xterm encodes everything modified, so the decoder handles both worlds.cp:shifted:base, and matching base unconditionally would turn AZERTY's Ctrl+Q (cpq, basea) into the prefix — surprising, and different from its legacy0x11byte. Non-Latin layouts (the actual target) always have non-ASCII primaries..esccarries the original bytes so an unconsumed Esc forwards in whatever encoding the terminal used.chore: releasePRs in this repo.This PR was generated by Coder Agents on behalf of @kylecarbs.