From 4d5cbf22c01f382024aebc845c3f1bea9caac280 Mon Sep 17 00:00:00 2001 From: mgabor3141 <@mgabor3141> Date: Thu, 26 Feb 2026 18:54:18 +0100 Subject: [PATCH 1/2] client: recognize kitty keyboard protocol encoding of detach key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ; 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 #66 --- client.c | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/client.c b/client.c index 3d6d82b..a98615e 100644 --- a/client.c +++ b/client.c @@ -55,6 +55,80 @@ static void client_setup_terminal(void) { } } + +/* Kitty keyboard protocol support. + * + * Modern terminals (kitty, wezterm, foot, ghostty) and applications (fish, + * neovim, helix) increasingly use the kitty keyboard protocol, which encodes + * key presses as CSI sequences instead of raw control bytes: + * + * Ctrl-\ -> legacy: 0x1C -> kitty protocol: ESC [ 92 ; u + * + * The modifier value encodes all active modifiers plus lock keys: + * modifier = 1 + (shift:1|alt:2|ctrl:4|super:8|...|capslock:64|numlock:128) + * So Ctrl-\ with numlock on = ESC [ 92 ; 133 u. + * + * abduco's detach key detection only checks for the single-byte value, so + * detaching fails whenever the keyboard protocol is active. This adds a + * parser that recognizes the CSI u encoding with ctrl set in the modifier. + */ + +static unsigned int csi_detach_codepoint; + +static void init_csi_detach(void) { + /* Recover the base character from the control byte. + * CTRL(k) = k & 0x1F, so base = KEY_DETACH | 0x40 for @[\]^_ + * and base = KEY_DETACH | 0x60 for letters (a-z). */ + if (KEY_DETACH >= 1 && KEY_DETACH <= 26) + csi_detach_codepoint = KEY_DETACH + 0x60; + else + csi_detach_codepoint = KEY_DETACH + 0x40; +} + +/* Parse a CSI u sequence: ESC [ ; [: ] u + * Returns true if it encodes the detach key with ctrl modifier. + * Accepts any additional modifiers (numlock, capslock, etc.). */ +static bool is_csi_detach(const char *buf, ssize_t len) { + if (len < 6 || buf[0] != '\033' || buf[1] != '[') + return false; + if (buf[len - 1] != 'u') + return false; + + /* Parse codepoint */ + unsigned int codepoint = 0; + int i = 2; + while (i < len && buf[i] >= '0' && buf[i] <= '9') + codepoint = codepoint * 10 + (buf[i++] - '0'); + if (codepoint != csi_detach_codepoint) + return false; + + /* Expect semicolon */ + if (i >= len || buf[i] != ';') + return false; + i++; + + /* Parse modifier value */ + unsigned int mod = 0; + while (i < len && buf[i] >= '0' && buf[i] <= '9') + mod = mod * 10 + (buf[i++] - '0'); + + /* Skip optional event type (:1=press, :2=repeat, :3=release) */ + if (i < len && buf[i] == ':') { + i++; + while (i < len && buf[i] >= '0' && buf[i] <= '9') + i++; + } + + /* Must end with 'u' */ + if (i != len - 1) + return false; + + /* Check ctrl is set: modifier = 1 + bitmask, ctrl = bit 2 (value 4) */ + if (mod < 1) + return false; + return ((mod - 1) & 4) != 0; +} + static int client_mainloop(void) { sigset_t emptyset, blockset; sigemptyset(&emptyset); @@ -62,6 +136,8 @@ static int client_mainloop(void) { sigaddset(&blockset, SIGWINCH); sigprocmask(SIG_BLOCK, &blockset, NULL); + init_csi_detach(); + client.need_resize = true; Packet pkt = { .type = MSG_ATTACH, @@ -124,7 +200,7 @@ static int client_mainloop(void) { pkt.len = len; if (KEY_REDRAW && pkt.u.msg[0] == KEY_REDRAW) { client.need_resize = true; - } else if (pkt.u.msg[0] == KEY_DETACH) { + } else if (pkt.u.msg[0] == KEY_DETACH || is_csi_detach(pkt.u.msg, len)) { pkt.type = MSG_DETACH; pkt.len = 0; client_send_packet(&pkt); From d6551eb2262c83be8ff963c94d0b8660b7606c4c Mon Sep 17 00:00:00 2001 From: mgabor3141 <@mgabor3141> Date: Thu, 26 Feb 2026 18:54:43 +0100 Subject: [PATCH 2/2] client: tighten CSI u detach key matching 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. --- client.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/client.c b/client.c index a98615e..689aa34 100644 --- a/client.c +++ b/client.c @@ -86,8 +86,11 @@ static void init_csi_detach(void) { } /* Parse a CSI u sequence: ESC [ ; [: ] u - * Returns true if it encodes the detach key with ctrl modifier. - * Accepts any additional modifiers (numlock, capslock, etc.). */ + * Returns true if it encodes the detach key with only the ctrl modifier + * (plus any lock keys like numlock/capslock). + * + * Does NOT match if shift, alt, or super are also held, mirroring the + * legacy behavior where only the exact control byte triggers detach. */ static bool is_csi_detach(const char *buf, ssize_t len) { if (len < 6 || buf[0] != '\033' || buf[1] != '[') return false; @@ -112,21 +115,33 @@ static bool is_csi_detach(const char *buf, ssize_t len) { while (i < len && buf[i] >= '0' && buf[i] <= '9') mod = mod * 10 + (buf[i++] - '0'); - /* Skip optional event type (:1=press, :2=repeat, :3=release) */ + /* Check optional event type — only match press (:1) or + * absent (implied press), not repeat (:2) or release (:3). */ if (i < len && buf[i] == ':') { i++; + unsigned int event = 0; while (i < len && buf[i] >= '0' && buf[i] <= '9') - i++; + event = event * 10 + (buf[i++] - '0'); + if (event != 1) + return false; } /* Must end with 'u' */ if (i != len - 1) return false; - /* Check ctrl is set: modifier = 1 + bitmask, ctrl = bit 2 (value 4) */ + /* Modifier = 1 + bitmask. Check ctrl (bit 2) is set and no + * other non-lock modifiers (shift=1, alt=2, super=8, hyper=16, + * meta=32) are present. Lock keys (capslock=64, numlock=128) + * are allowed since they don't affect the key identity. */ if (mod < 1) return false; - return ((mod - 1) & 4) != 0; + unsigned int bits = mod - 1; + if (!(bits & 4)) + return false; /* ctrl not set */ + if (bits & (1|2|8|16|32)) + return false; /* shift, alt, super, hyper, or meta also held */ + return true; } static int client_mainloop(void) {