Skip to content

Kitty keyboard protocol + macOS editing keybinds over the web terminal#79

Open
dkzlv wants to merge 8 commits into
mainfrom
dkzlv/2026-07-18-kitty-protocol-web
Open

Kitty keyboard protocol + macOS editing keybinds over the web terminal#79
dkzlv wants to merge 8 commits into
mainfrom
dkzlv/2026-07-18-kitty-protocol-web

Conversation

@dkzlv

@dkzlv dkzlv commented Jul 18, 2026

Copy link
Copy Markdown
Member

What

Two related fixes so keyboard input over blit's web terminal behaves like a native terminal.

1. Kitty keyboard protocol (CSI-u), end-to-end

Modifier+key combos (Shift+Enter, Cmd+Backspace, Ctrl+Enter, …) previously collapsed to their unmodified key because a PTY is a raw byte stream and the legacy xterm encoding has no sequence for them. This implements the modern kitty keyboard protocol, which encodes every key as CSI <codepoint>;<modifier-bitmask> u and only activates for apps that negotiate it — so combos now reach TUIs like Claude Code without regressing other apps.

  • alacritty-driver: enable kitty_keyboard; capture the CSI ? u capability-query reply (Event::PtyWrite, which we were dropping) and drain it back to the PTY; surface negotiated flags from TermMode in the frame snapshot.
  • remote: carry kitty_flags as a trailing S2C_UPDATE byte (old clients ignore it), force a frame on flag-only changes, parse defensively by payload length.
  • server: drain pty writes after driver.process(); reorder query replies so the CSI ? u answer isn't beaten by DA1 (which would read as "no kitty").
  • browser: expose Terminal.kitty_flags() to JS.
  • js/core: new kitty.ts CSI-u encoder; keyboard.ts delegates to it when flags != 0 and stays byte-identical to the legacy path when flags == 0.

2. macOS "natural text editing" keybinds

At a bare shell prompt the kitty protocol is inactive (by design), so Cmd/Option editing chords did nothing. Mirroring Ghostty's default macOS keybinds, macEditingKeybind() translates them to the legacy control bytes readline/ZLE already bind — so they work at a plain shell with no shell config, while a kitty-aware app still receives the real combo as CSI-u:

Chord Byte Action
Cmd+Backspace Ctrl+U kill to line start
Cmd+←/→ Ctrl+A / Ctrl+E line start / end
Opt+Backspace Meta-DEL delete word back
Opt+←/→ Meta-b / Meta-f word left / right

Gated on macOS and on kittyFlags === 0.

Testing

Local CI green:

  • ./bin/lint — cargo fmt, prettier, clippy (-D warnings) all clean
  • ./bin/tests — Rust workspace tests pass; JS typecheck; core 388 / react 11 / solid 11 vitest; Python + Bun fd-channel
  • nix-syntax parse

Notes

  • Base branch quality-of-life-fixes had a pre-existing prettier --check failure on docs/server.md (unrelated table drift); fixed directly on that branch and this is rebased on top.
  • Also folds in two small env fixes: pkgs.git in the dev shell and a virtual:blit-wasm module declaration for the website typecheck.

dkzlv added 8 commits July 18, 2026 12:13
The Cmd+K command palette search input lacked autocomplete-suppression
attributes, so the browser could show its native autocomplete/history
dropdown over the palette. Add autocomplete/autocorrect/autocapitalize=off
and spellcheck=false, matching the sibling overlays (Palette, Font, Remotes).
The Paste button fired from onPointerDown with e.preventDefault(), which
suppresses the synthesized click. iOS Safari (WebKit) only authorises
navigator.clipboard.readText() inside a genuine click/touch gesture, so
the read rejected silently and pasting was a no-op.

Route the Paste button through a real click via a new clickToActivate
flag (other buttons keep their snappy pointerdown behaviour), and refocus
the surface afterwards to keep the on-screen keyboard up.
Add a shared scrollbarStyle(theme) helper and apply it to every
scrollable overlay/list: the Cmd+K switcher, theme/font pickers,
remotes list, the generic OverlayPanel body, and the off-screen
terminal rail. Uses CSS scrollbar-width/scrollbar-color so the
scrollbar matches the active palette instead of the chunky native one.
The browser terminal only captured paste as text via the hidden textarea's
input(insertFromPaste) event, and Ctrl+V's preventDefault suppressed the
paste event entirely — so pasted images were silently dropped and TUIs like
Claude Code read an empty clipboard on ^V.

Intercept Ctrl+V to defer ^V, add a paste listener that forwards any
clipboard image via sendClipboard(mime, bytes) to the compositor clipboard,
then send ^V so the app reads a populated selection (transport messages are
ordered). A fallback timer preserves ^V quoted-insert when no image/paste
is present; Cmd+V / Ctrl+Shift+V text paths are unchanged.
The hidden capture textarea is kept empty, but iOS only auto-repeats
Backspace while the field still has content to delete, so a held
Backspace deleted a single character and stopped. Seed the textarea with
an NBSP filler buffer on iOS so iOS's own key-repeat streams a
deleteContentBackward per repeat; forward one DEL each and top the buffer
back up on idle (or before it runs dry mid-hold). Filler is stripped from
the typed-text and Ctrl/Alt modifier paths so it never reaches the shell.
The BLIT_EXPORT_SOCK row widened the Purpose column without realigning
the rest of the table, tripping prettier --check in CI.
…erminal

Modifier+key combos (Shift+Enter, Cmd+Backspace, Ctrl+Enter, ...) now
reach TUIs like Claude Code instead of collapsing to their unmodified
key, and macOS "natural text editing" chords work at a bare shell prompt.

Kitty keyboard protocol (CSI-u), end-to-end:
- alacritty-driver: enable kitty_keyboard; capture the CSI ? u query
  reply (Event::PtyWrite, previously dropped) and drain it back to the
  PTY; surface negotiated flags from TermMode in the frame snapshot.
- remote: carry kitty_flags as a trailing S2C_UPDATE byte (old clients
  ignore it), force a frame on flag-only changes, parse defensively.
- server: drain pty writes after driver.process(); reorder query replies
  so the CSI ? u answer isn't beaten by DA1.
- browser: expose Terminal.kitty_flags() to JS.
- js/core: new kitty.ts CSI-u encoder; keyboard.ts delegates to it when
  flags != 0 and stays byte-identical to the legacy path when flags == 0.

macOS natural text editing keybinds (browser-side, gated on kitty == 0):
- macEditingKeybind() maps Cmd/Option editing chords to the legacy
  control bytes readline/ZLE already bind (Cmd+Backspace -> Ctrl+U,
  Cmd+arrows -> Ctrl+A/E, Opt+Backspace -> Meta-DEL, Opt+arrows ->
  Meta-b/f), so they work at a plain shell without shell config, while a
  kitty-aware app still receives the real combo as CSI-u.

Also: add pkgs.git to the dev shell and a virtual:blit-wasm module
declaration for the website's typecheck.
@github-actions

Copy link
Copy Markdown

Coverage

Crate Lines Functions Regions
alacritty-driver 70.7% (732/1035) 73.4% (58/79) 72.9% (1110/1523)
browser 0.0% (0/810) 0.0% (0/66) 0.0% (0/1374)
cli 28.0% (1208/4311) 41.1% (171/416) 31.0% (2182/7033)
compositor 1.0% (93/9245) 2.0% (8/400) 1.2% (146/12422)
fonts 76.8% (486/633) 85.5% (47/55) 77.9% (922/1183)
gateway 25.7% (362/1411) 29.0% (36/124) 19.4% (449/2318)
proxy 18.3% (150/818) 20.9% (24/115) 20.4% (260/1277)
remote 72.1% (2028/2814) 82.1% (193/235) 74.7% (3850/5155)
sd-notify 73.9% (68/92) 100.0% (6/6) 83.2% (109/131)
server 19.1% (2257/11796) 34.2% (260/760) 20.6% (3772/18318)
ssh 1.9% (7/374) 3.2% (1/31) 0.7% (4/613)
webrtc-forwarder 2.7% (72/2624) 2.1% (4/187) 1.2% (50/4335)
webserver 63.5% (753/1185) 70.8% (121/171) 67.8% (1380/2034)
Total 22.1% (8216/37148) 35.1% (929/2645) 24.7% (14234/57716)

@github-actions

Copy link
Copy Markdown

🔗 Preview: https://blit-k73dskpc4-indent.vercel.app

@pcarrier

Copy link
Copy Markdown
Contributor

force a frame on flag-only changes

Hmmm?

Base automatically changed from quality-of-life-fixes to main July 20, 2026 22:49
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.

2 participants