This commit is contained in:
mgabor 2026-02-26 18:54:51 +01:00 committed by GitHub
commit f2a23c6801
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 92 additions and 1 deletions

View File

@ -55,6 +55,95 @@ 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 ; <mod> 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 [ <number> ; <number> [: <number>] u
* 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;
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');
/* 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')
event = event * 10 + (buf[i++] - '0');
if (event != 1)
return false;
}
/* Must end with 'u' */
if (i != len - 1)
return false;
/* 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;
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) {
sigset_t emptyset, blockset;
sigemptyset(&emptyset);
@ -62,6 +151,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 +215,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);