From c084c144988b93db2037e8edee0734ed570e9e77 Mon Sep 17 00:00:00 2001 From: adi-IL Date: Mon, 31 Aug 2026 02:24:54 +0530 Subject: [PATCH] fix(input): decode kitty key events with event type suffix for all keys When terminals using the Kitty progressive keyboard protocol report key events with an event-type suffix (such as press :1, repeat :2, or release :3 in ESC[;:u), the parser previously matched only keycode 27 (Escape). As a result, colon-qualified event reports for other keys (such as Backspace 127, Enter 13, and Tab 9 sent by terminals like Ghostty) were dropped. Generalize the event type decoding stage: - Store the keycode in param2 and pack raw modifiers into the upper bits of param before transitioning to the event type stage. - Dispatch press (1) and repeat (2) actions through kittyUnicodeKeyAction for all keycodes, while ignoring release events (3). --- src/ui/input/escape_parser.zig | 26 ++++++++++++++------------ src/ui/input/runtime.zig | 8 ++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/ui/input/escape_parser.zig b/src/ui/input/escape_parser.zig index 60837b570..0a3c11e9e 100644 --- a/src/ui/input/escape_parser.zig +++ b/src/ui/input/escape_parser.zig @@ -566,11 +566,11 @@ pub fn consumeInputEscapeByteWithMouse( return null; } - // Ghostty can report a Kitty key event type as a colon-qualified - // modifier, e.g. `ESC[27;1:1u` for an Escape key press. - if (byte == ':' and param2.* == 27) { - param2.* = if (param.* > 0) param.* - 1 else 0; - param.* = 0; + // Kitty keyboard protocol reports key event types as a colon-qualified + // modifier, e.g. `ESC[;:u`. + if (byte == ':') { + const raw_modifiers = if (param.* > 0) param.* - 1 else 0; + param.* = raw_modifiers << 4; setBaseEscapeStage(stage, kitty_escape_event_type_stage); return null; } @@ -656,19 +656,21 @@ pub fn consumeInputEscapeByteWithMouse( return beginControlSequenceDiscard(stage, param, param2, mouse, byte); }, - // Kitty key reports with an event type: `ESC[27;modifier:event-type u`. - // Only press and repeat are actionable; release must not close a panel. + // Kitty key reports with an event type: `ESC[;modifier:event-type u`. + // Only press (1) and repeat (2) are actionable; release (3) must not trigger actions. kitty_escape_event_type_stage => { if (byte >= '0' and byte <= '9') { - appendCsiDigitSaturating(param, byte); + param.* = (param.* & 0xFFF0) | (byte - '0'); return null; } if (byte == 'u') { - const modifiers = param2.*; - const event_type = param.*; + const meta_prefixed = hasMetaPrefix(stage.*); + const keycode = param2.*; + const modifiers = param.* >> 4; + const event_type = param.* & 0x0F; resetMouseEscapeDecode(stage, param, param2, mouse); - if (event_type == 1 or event_type == 2) { - return kittyUnicodeKeyAction(27, modifiers, false); + if (event_type == 1 or event_type == 2 or event_type == 0) { + return kittyUnicodeKeyAction(keycode, modifiers, meta_prefixed); } return .ignore; } diff --git a/src/ui/input/runtime.zig b/src/ui/input/runtime.zig index 47cafdb92..7d2cd9d36 100644 --- a/src/ui/input/runtime.zig +++ b/src/ui/input/runtime.zig @@ -2322,12 +2322,20 @@ test "input escape parser resolves unmodified kitty Backspace to a delete byte" try expectEscapeAction("[127u", .{ .remapped_byte = 127 }); // Modifier param is bitmask+1: ;1 = none, ;3 = Alt, ;9 = Super. try expectEscapeAction("[127;1u", .{ .remapped_byte = 127 }); + try expectEscapeAction("[127;1:1u", .{ .remapped_byte = 127 }); + try expectEscapeAction("[127;1:2u", .{ .remapped_byte = 127 }); + try expectEscapeAction("[127;1:3u", .ignore); try expectEscapeAction("[127;3u", .delete_word_left); + try expectEscapeAction("[127;3:1u", .delete_word_left); try expectEscapeAction("[127;9u", .delete_to_line_start); try expectEscapeAction("[13u", .{ .remapped_byte = '\r' }); try expectEscapeAction("[13;1u", .{ .remapped_byte = '\r' }); + try expectEscapeAction("[13;1:1u", .{ .remapped_byte = '\r' }); + try expectEscapeAction("[13;1:3u", .ignore); try expectEscapeAction("[9u", .{ .remapped_byte = '\t' }); try expectEscapeAction("[9;1u", .{ .remapped_byte = '\t' }); + try expectEscapeAction("[9;1:1u", .{ .remapped_byte = '\t' }); + try expectEscapeAction("[9;1:3u", .ignore); } test "input escape parser resolves unmapped single-parameter CSI u to ignore" {