Skip to content

client: recognize kitty keyboard protocol encoding of detach key#71

Open
mgabor3141 wants to merge 2 commits into
martanne:masterfrom
mgabor3141:kitty-keyboard-protocol
Open

client: recognize kitty keyboard protocol encoding of detach key#71
mgabor3141 wants to merge 2 commits into
martanne:masterfrom
mgabor3141:kitty-keyboard-protocol

Conversation

@mgabor3141

Copy link
Copy Markdown

Problem

The detach key (default Ctrl-\) does not work when the kitty keyboard protocol is active. This affects an increasing number of terminal + application combinations:

Terminals: kitty, wezterm, foot, ghostty, rio
Applications that enable it: fish shell, neovim, helix, kakoune

When the protocol is active, Ctrl-\ is sent as a CSI u sequence (ESC [ 92 ; <mod> u) instead of the raw byte 0x1C. Since client_mainloop() only checks pkt.u.msg[0] == KEY_DETACH (a single-byte comparison), the detach key is silently ignored.

Reproducing

# In kitty (or any terminal supporting the kitty keyboard protocol):
abduco -c test fish   # or: abduco -c test nvim
# Press Ctrl-\ — nothing happens
# But if you run `cat` first, then press Ctrl-\ — it detaches,
# because cat does not enable the keyboard protocol

Subtle gotcha: numlock

The modifier field in the kitty protocol encodes all active modifiers including lock keys: modifier = 1 + (shift:1 | alt:2 | ctrl:4 | super:8 | ... | capslock:64 | numlock:128).

So Ctrl-\ with numlock on is ESC[92;133u (not ESC[92;5u). A simple string match against a fixed sequence won't work — the modifier must be parsed as a bitmask.

Solution

Adds a small CSI u parser (is_csi_detach()) alongside the existing single-byte check. It:

  • Extracts the Unicode codepoint and verifies it matches the configured detach key
  • Parses the modifier value as a bitmask and checks that ctrl (bit 2) is set
  • Ignores other modifier bits (numlock, capslock, shift, etc.)
  • Handles the optional event type suffix (:1 press, :2 repeat, :3 release)
  • Works with any detach key configured via -e

The change is backward-compatible — the legacy single-byte check remains, so terminals not using the protocol are unaffected.

Closes #66

@mgabor3141

Copy link
Copy Markdown
Author

Note on read boundaries: A reviewer pointed out that both this patch and the existing single-byte detach check assume the key event arrives as a complete, self-contained read(). In theory, a CSI sequence could be split across two reads (e.g., over a congested SSH link), or coalesced with subsequent input into a single read.

This is a pre-existing architectural limitation — the original pkt.u.msg[0] == KEY_DETACH has the same assumption and has worked in practice for 10+ years. This patch intentionally keeps the same approach for consistency rather than introducing a buffering/state machine just for the CSI path.

If there's interest in making the input handling more robust across the board, both the legacy and CSI detach checks could benefit from a small ring buffer that scans across read boundaries. Happy to do that work in a follow-up if desired.

@mgabor3141
mgabor3141 force-pushed the kitty-keyboard-protocol branch 2 times, most recently from e78e808 to 8059781 Compare February 26, 2026 17:51
mgabor3141 added 2 commits February 26, 2026 18:54
Modern terminals (kitty, wezterm, foot, ghostty) and shell/editor
applications (fish, neovim, helix) increasingly use the kitty keyboard
protocol, which encodes key presses as CSI u sequences instead of raw
control bytes:

  Ctrl-\  legacy: 0x1C  ->  kitty protocol: ESC [ 92 ; <mod> u

The modifier field encodes all active modifiers including lock keys:
  modifier = 1 + (shift:1|alt:2|ctrl:4|super:8|...|capslock:64|numlock:128)

For example, Ctrl-\ with numlock on produces ESC[92;133u, not 0x1C.
Since abduco only checked for the single-byte value, the detach key was
silently ignored whenever the keyboard protocol was active — which is
the default state at a fish shell prompt or inside neovim/helix.

This adds a CSI u parser alongside the existing single-byte check in the
client read loop. The parser:
- Extracts the Unicode codepoint and verifies it matches the detach key
- Extracts the modifier bitmask and checks that ctrl (bit 2) is set
- Ignores other modifier bits (numlock, capslock, shift, etc.)
- Handles the optional event type suffix (:1 press, :2 repeat, :3 release)
- Works with any detach key configured via -e

Closes martanne#66
Restrict the kitty keyboard protocol detach matcher to better mirror
the legacy single-byte behavior:

- Only match key press events (:1 or absent), not repeat (:2) or
  release (:3).  In practice the press triggers detach before the
  release arrives, but this is more correct.

- Reject if shift, alt, super, hyper, or meta are also held.  The
  legacy byte for e.g. Ctrl-\ is distinct from Ctrl+Shift-\, so
  extra non-lock modifiers should not trigger detach.  Lock keys
  (numlock, capslock) are still allowed since they do not affect key
  identity.
@mgabor3141
mgabor3141 force-pushed the kitty-keyboard-protocol branch from 8059781 to d6551eb Compare February 26, 2026 17:54
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.

Detach when running vim

1 participant