From 0a954a536f30b7a2b0a6a3fbbaa2fd7d79ed14e9 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 4 Mar 2025 08:21:23 -0600 Subject: [PATCH] parser: handle shift+space in kkp+disambiguate Some terminals (foot, ghostty) encode shift + space as CSI 32 ; 2 u when using the kkp disambiguate flag. We need to handle this as a printable character. Use the logic from vaxis (the go version), which checks if the mods were shift only and the character is printable. Then we upper case it, set the text and the shifted char --- src/Key.zig | 6 ++++++ src/Parser.zig | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Key.zig b/src/Key.zig index 89c8dce..0a7b75f 100644 --- a/src/Key.zig +++ b/src/Key.zig @@ -13,6 +13,12 @@ pub const Modifiers = packed struct(u8) { meta: bool = false, caps_lock: bool = false, num_lock: bool = false, + + pub fn eql(self: Modifiers, other: Modifiers) bool { + const a: u8 = @bitCast(self); + const b: u8 = @bitCast(other); + return a == b; + } }; /// Flags for the Kitty Protocol. diff --git a/src/Parser.zig b/src/Parser.zig index 8455de0..911e2c9 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -592,6 +592,28 @@ inline fn parseCsi(input: []const u8, text_buf: []u8) Result { key.text = text_buf[0..total]; } + { + // We check if we have *only* shift, no text, and a printable character. This can + // happen when we have disambiguate on and a key is pressed and encoded as CSI u, + // for example shift + space can produce CSI 32 ; 2 u + const mod_test: Key.Modifiers = .{ + .shift = true, + .caps_lock = key.mods.caps_lock, + .num_lock = key.mods.num_lock, + }; + if (key.text == null and + key.mods.eql(mod_test) and + key.codepoint <= std.math.maxInt(u8) and + std.ascii.isPrint(@intCast(key.codepoint))) + { + // Encode the codepoint as upper + const upper = std.ascii.toUpper(@intCast(key.codepoint)); + const n = std.unicode.utf8Encode(upper, text_buf) catch unreachable; + key.text = text_buf[0..n]; + key.shifted_codepoint = upper; + } + } + const event: Event = if (is_release) .{ .key_release = key } else @@ -907,11 +929,12 @@ test "parse: kitty: shift+a without text reporting" { .codepoint = 'a', .shifted_codepoint = 'A', .mods = .{ .shift = true }, + .text = "A", }; const expected_event: Event = .{ .key_press = expected_key }; try testing.expectEqual(10, result.n); - try testing.expectEqual(expected_event, result.event); + try testing.expectEqualDeep(expected_event, result.event); } test "parse: kitty: alt+shift+a without text reporting" { @@ -1127,3 +1150,22 @@ test "parse(csi): mouse" { try testing.expectEqual(expected.n, result.n); try testing.expectEqual(expected.event, result.event); } + +test "parse: disambiguate shift + space" { + const alloc = testing.allocator_instance.allocator(); + const grapheme_data = try grapheme.GraphemeData.init(alloc); + defer grapheme_data.deinit(); + const input = "\x1b[32;2u"; + var parser: Parser = .{ .grapheme_data = &grapheme_data }; + const result = try parser.parse(input, alloc); + const expected_key: Key = .{ + .codepoint = ' ', + .shifted_codepoint = ' ', + .mods = .{ .shift = true }, + .text = " ", + }; + const expected_event: Event = .{ .key_press = expected_key }; + + try testing.expectEqual(7, result.n); + try testing.expectEqualDeep(expected_event, result.event); +} -- 2.51.2