From c80b1bed7591bb307e6701c0fe0f96d106485db3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 21:27:53 -0700 Subject: [PATCH 01/12] terminal: add Kitty key flags, stack --- src/terminal/kitty.zig | 108 +++++++++++++++++++++++++++++++++++++++++ src/terminal/main.zig | 1 + 2 files changed, 109 insertions(+) create mode 100644 src/terminal/kitty.zig diff --git a/src/terminal/kitty.zig b/src/terminal/kitty.zig new file mode 100644 index 000000000..cfba9c405 --- /dev/null +++ b/src/terminal/kitty.zig @@ -0,0 +1,108 @@ +//! Types and functions related to Kitty protocols. +//! +//! Documentation for the Kitty keyboard protocol: +//! https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement + +const std = @import("std"); + +/// Stack for the key flags. This implements the push/pop behavior +/// of the CSI > u and CSI < u sequences. We implement the stack as +/// fixed size to avoid heap allocation. +pub const KeyFlagStack = struct { + const len = 8; + + flags: [len]KeyFlags = .{.{}} ** len, + idx: u3 = 0, + + /// Return the current stack value + pub fn current(self: KeyFlagStack) KeyFlags { + return self.flags[self.idx]; + } + + /// Push a new set of flags onto the stack. If the stack is full + /// then the oldest entry is evicted. + pub fn push(self: *KeyFlagStack, flags: KeyFlags) void { + // Overflow and wrap around if we're full, which evicts + // the oldest entry. + self.idx +%= 1; + self.flags[self.idx] = flags; + } + + /// Pop `n` entries from the stack. This will just wrap around + /// if `n` is greater than the amount in the stack. + pub fn pop(self: *KeyFlagStack, n: usize) void { + // If n is more than our length then we just reset the stack. + // This also avoids a DoS vector where a malicious client + // could send a huge number of pop commands to waste cpu. + if (n >= self.flags.len) { + self.idx = 0; + self.flags = .{.{}} ** len; + return; + } + + for (0..n) |_| { + self.flags[self.idx] = .{}; + self.idx -%= 1; + } + } + + // Make sure we the overflow works as expected + test { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.idx = stack.flags.len - 1; + stack.idx +%= 1; + try testing.expect(stack.idx == 0); + + stack.idx = 0; + stack.idx -%= 1; + try testing.expect(stack.idx == stack.flags.len - 1); + } +}; + +/// The possible flags for the Kitty keyboard protocol. +pub const KeyFlags = packed struct(u5) { + disambiguate: bool = false, + report_events: bool = false, + report_alternates: bool = false, + report_all: bool = false, + report_associated: bool = false, + + pub fn int(self: KeyFlags) u5 { + return @bitCast(self); + } + + // Its easy to get packed struct ordering wrong so this test checks. + test { + const testing = std.testing; + + try testing.expectEqual( + @as(u5, 0b1), + (KeyFlags{ .disambiguate = true }).int(), + ); + try testing.expectEqual( + @as(u5, 0b10), + (KeyFlags{ .report_events = true }).int(), + ); + } +}; + +test "KeyFlagStack: push pop" { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.push(.{ .disambiguate = true }); + try testing.expectEqual( + KeyFlags{ .disambiguate = true }, + stack.current(), + ); + + stack.pop(1); + try testing.expectEqual(KeyFlags{}, stack.current()); +} + +test "KeyFlagStack: pop big number" { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.pop(100); + try testing.expectEqual(KeyFlags{}, stack.current()); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 3de537656..a1b6f6c59 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -7,6 +7,7 @@ const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); +pub const kitty = @import("kitty.zig"); pub const modes = @import("modes.zig"); pub const parse_table = @import("parse_table.zig"); From a9d7e0eb7f3b87fd8c47b2fabd29a1bc53e342fb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 21:41:44 -0700 Subject: [PATCH 02/12] terminal: parse kitty query, push, pop keyboard flags --- src/terminal/Screen.zig | 4 ++++ src/terminal/stream.zig | 43 +++++++++++++++++++++++++++++++++++++++++ src/termio/Exec.zig | 28 +++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 42394f7f7..8da118584 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -58,6 +58,7 @@ const utf8proc = @import("utf8proc"); const trace = @import("tracy").trace; const sgr = @import("sgr.zig"); const color = @import("color.zig"); +const kitty = @import("kitty.zig"); const point = @import("point.zig"); const CircBuf = @import("circ_buf.zig").CircBuf; const Selection = @import("Selection.zig"); @@ -861,6 +862,9 @@ saved_cursor: Cursor = .{}, /// The selection for this screen (if any). selection: ?Selection = null, +/// The kitty keyboard settings. +kitty_keyboard: kitty.KeyFlagStack = .{}, + /// Initialize a new screen. pub fn init( alloc: Allocator, diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 0ddfbb5bf..a152b1bc9 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -4,6 +4,7 @@ const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); +const kitty = @import("kitty.zig"); const modes = @import("modes.zig"); const osc = @import("osc.zig"); const sgr = @import("sgr.zig"); @@ -644,6 +645,48 @@ pub fn Stream(comptime Handler: type) type { ), }, + // Kitty keyboard protocol + 'u' => switch (action.intermediates.len) { + 1 => switch (action.intermediates[0]) { + '?' => if (@hasDecl(T, "queryKittyKeyboard")) { + try self.handler.queryKittyKeyboard(); + }, + + '>' => if (@hasDecl(T, "pushKittyKeyboard")) push: { + const flags: u5 = if (action.params.len == 1) + std.math.cast(u5, action.params[0]) orelse { + log.warn("invalid pushKittyKeyboard command: {}", .{action}); + break :push; + } + else + 0; + + try self.handler.pushKittyKeyboard(@bitCast(flags)); + }, + + '<' => if (@hasDecl(T, "popKittyKeyboard")) { + const number: u16 = if (action.params.len == 1) + action.params[0] + else + 0; + + try self.handler.popKittyKeyboard(number); + }, + + '=' => @panic("TODO! DO NOT MERGE"), + + else => log.warn( + "unknown CSI s with intermediate: {}", + .{action}, + ), + }, + + else => log.warn( + "ignoring unimplemented CSI u: {}", + .{action}, + ), + }, + // ICH - Insert Blanks // TODO: test '@' => if (@hasDecl(T, "insertBlanks")) switch (action.params.len) { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d4d858957..91b955753 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1407,6 +1407,34 @@ const StreamHandler = struct { self.terminal.fullReset(); } + pub fn queryKittyKeyboard(self: *StreamHandler) !void { + // log.debug("querying kitty keyboard mode", .{}); + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1b[?{}u", .{ + self.terminal.screen.kitty_keyboard.current().int(), + }); + + self.messageWriter(.{ + .write_small = .{ + .data = data, + .len = @intCast(resp.len), + }, + }); + } + + pub fn pushKittyKeyboard( + self: *StreamHandler, + flags: terminal.kitty.KeyFlags, + ) !void { + // log.debug("pushing kitty keyboard mode: {}", .{flags}); + self.terminal.screen.kitty_keyboard.push(flags); + } + + pub fn popKittyKeyboard(self: *StreamHandler, n: u16) !void { + // log.debug("popping kitty keyboard mode", .{}); + self.terminal.screen.kitty_keyboard.pop(@intCast(n)); + } + //------------------------------------------------------------------------- // OSC From af4ede40f107b64b0dd52193568effab2883c00b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 21:50:24 -0700 Subject: [PATCH 03/12] terminal: implement CSI = u for setting kitty keyboard flags --- src/terminal/kitty.zig | 46 +++++++++++++++++++++++++++++++++++++++++ src/terminal/stream.zig | 30 ++++++++++++++++++++++++++- src/termio/Exec.zig | 9 ++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/terminal/kitty.zig b/src/terminal/kitty.zig index cfba9c405..35f92da02 100644 --- a/src/terminal/kitty.zig +++ b/src/terminal/kitty.zig @@ -19,6 +19,24 @@ pub const KeyFlagStack = struct { return self.flags[self.idx]; } + /// Perform the "set" operation as described in the spec for + /// the CSI = u sequence. + pub fn set( + self: *KeyFlagStack, + mode: KeySetMode, + v: KeyFlags, + ) void { + switch (mode) { + .set => self.flags[self.idx] = v, + .@"or" => self.flags[self.idx] = @bitCast( + self.flags[self.idx].int() | v.int(), + ), + .not => self.flags[self.idx] = @bitCast( + self.flags[self.idx].int() & ~v.int(), + ), + } + } + /// Push a new set of flags onto the stack. If the stack is full /// then the oldest entry is evicted. pub fn push(self: *KeyFlagStack, flags: KeyFlags) void { @@ -87,6 +105,9 @@ pub const KeyFlags = packed struct(u5) { } }; +/// The possible modes for setting the key flags. +pub const KeySetMode = enum { set, @"or", not }; + test "KeyFlagStack: push pop" { const testing = std.testing; var stack: KeyFlagStack = .{}; @@ -106,3 +127,28 @@ test "KeyFlagStack: pop big number" { stack.pop(100); try testing.expectEqual(KeyFlags{}, stack.current()); } + +test "KeyFlagStack: set" { + const testing = std.testing; + var stack: KeyFlagStack = .{}; + stack.set(.set, .{ .disambiguate = true }); + try testing.expectEqual( + KeyFlags{ .disambiguate = true }, + stack.current(), + ); + + stack.set(.@"or", .{ .report_events = true }); + try testing.expectEqual( + KeyFlags{ + .disambiguate = true, + .report_events = true, + }, + stack.current(), + ); + + stack.set(.not, .{ .report_events = true }); + try testing.expectEqual( + KeyFlags{ .disambiguate = true }, + stack.current(), + ); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a152b1bc9..7dd75513f 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -673,7 +673,35 @@ pub fn Stream(comptime Handler: type) type { try self.handler.popKittyKeyboard(number); }, - '=' => @panic("TODO! DO NOT MERGE"), + '=' => if (@hasDecl(T, "setKittyKeyboard")) set: { + const flags: u5 = if (action.params.len >= 1) + std.math.cast(u5, action.params[0]) orelse { + log.warn("invalid setKittyKeyboard command: {}", .{action}); + break :set; + } + else + 0; + + const number: u16 = if (action.params.len >= 2) + action.params[1] + else + 1; + + const mode: kitty.KeySetMode = switch (number) { + 0 => .set, + 1 => .@"or", + 2 => .not, + else => { + log.warn("invalid setKittyKeyboard command: {}", .{action}); + break :set; + }, + }; + + try self.handler.setKittyKeyboard( + mode, + @bitCast(flags), + ); + }, else => log.warn( "unknown CSI s with intermediate: {}", diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 91b955753..cbb4f0fd4 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1435,6 +1435,15 @@ const StreamHandler = struct { self.terminal.screen.kitty_keyboard.pop(@intCast(n)); } + pub fn setKittyKeyboard( + self: *StreamHandler, + mode: terminal.kitty.KeySetMode, + flags: terminal.kitty.KeyFlags, + ) !void { + // log.debug("setting kitty keyboard mode: {} {}", .{mode, flags}); + self.terminal.screen.kitty_keyboard.set(mode, flags); + } + //------------------------------------------------------------------------- // OSC From f60066a64dcd7d2729f3a3e5e1bccc2cc728a135 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 21:55:14 -0700 Subject: [PATCH 04/12] terminfo: add fullkbd for kitty keyboard protocol --- src/terminfo/ghostty.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/terminfo/ghostty.zig b/src/terminfo/ghostty.zig index 2e37bfc64..15f40466d 100644 --- a/src/terminfo/ghostty.zig +++ b/src/terminfo/ghostty.zig @@ -67,8 +67,7 @@ pub const ghostty: Source = .{ // Full keyboard support using Kitty's keyboard protocol: // https://sw.kovidgoyal.net/kitty/keyboard-protocol/ - // Commented out because we don't yet support this. - // .{ .name = "fullkbd", .value = .{ .boolean = {} } }, + .{ .name = "fullkbd", .value = .{ .boolean = {} } }, // Number of colors in the color palette. .{ .name = "colors", .value = .{ .numeric = 256 } }, From b42178bf34ac4c81fd6039d0c442b5914951c3ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 22:08:05 -0700 Subject: [PATCH 05/12] input: add kitty keymap data --- src/input.zig | 1 + src/input/kitty.zig | 117 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/input/kitty.zig diff --git a/src/input.zig b/src/input.zig index f382386c6..21a8702d6 100644 --- a/src/input.zig +++ b/src/input.zig @@ -5,6 +5,7 @@ pub usingnamespace @import("input/mouse.zig"); pub usingnamespace @import("input/key.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); +pub const kitty = @import("input/kitty.zig"); pub const Binding = @import("input/Binding.zig"); pub const KeyEncoder = @import("input/KeyEncoder.zig"); pub const SplitDirection = Binding.Action.SplitDirection; diff --git a/src/input/kitty.zig b/src/input/kitty.zig new file mode 100644 index 000000000..e0120ab32 --- /dev/null +++ b/src/input/kitty.zig @@ -0,0 +1,117 @@ +const std = @import("std"); +const key = @import("key.zig"); + +/// A single entry in the kitty keymap data. There are only ~100 entries +/// so the recommendation is to just use a linear search to find the entry +/// for a given key. +pub const Entry = struct { + key: key.Key, + code: u16, + final: u8, + modifier: bool, +}; + +/// The full list of entries for the current platform. +pub const entries: []const Entry = entries: { + var result: [raw_entries.len]Entry = undefined; + for (raw_entries, 0..) |raw, i| { + result[i] = .{ + .key = raw[0], + .code = raw[1], + .final = raw[2], + .modifier = raw[3], + }; + } + break :entries &result; +}; + +/// Raw entry is the tuple form of an entry for easy human management. +/// This should never be used in a real program so it is not pub. For +/// real programs, use `entries` which has properly typed, structured data. +const RawEntry = struct { key.Key, u16, u8, bool }; + +/// The raw data for how to map keys to Kitty data. Based on the information: +/// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions +/// And the exact table is ported from Foot: +/// https://codeberg.org/dnkl/foot/src/branch/master/kitty-keymap.h +/// +/// Note that we currently don't support all the same keysyms as Kitty, +/// but we can add them as we add support. +const raw_entries: []const RawEntry = &.{ + .{ .backspace, 127, 'u', false }, + .{ .tab, 9, 'u', false }, + .{ .enter, 13, 'u', false }, + .{ .pause, 57362, 'u', false }, + .{ .scroll_lock, 57359, 'u', false }, + .{ .escape, 27, 'u', false }, + .{ .home, 1, 'H', false }, + .{ .left, 1, 'D', false }, + .{ .up, 1, 'A', false }, + .{ .right, 1, 'C', false }, + .{ .down, 1, 'B', false }, + .{ .end, 1, 'F', false }, + .{ .print_screen, 57361, 'u', false }, + .{ .insert, 2, '~', false }, + .{ .num_lock, 57360, 'u', true }, + + .{ .kp_enter, 57414, 'u', false }, + .{ .kp_multiply, 57411, 'u', false }, + .{ .kp_add, 57413, 'u', false }, + .{ .kp_subtract, 57412, 'u', false }, + .{ .kp_decimal, 57409, 'u', false }, + .{ .kp_divide, 57410, 'u', false }, + .{ .kp_0, 57399, 'u', false }, + .{ .kp_1, 57400, 'u', false }, + .{ .kp_2, 57401, 'u', false }, + .{ .kp_3, 57402, 'u', false }, + .{ .kp_4, 57403, 'u', false }, + .{ .kp_5, 57404, 'u', false }, + .{ .kp_6, 57405, 'u', false }, + .{ .kp_7, 57406, 'u', false }, + .{ .kp_8, 57407, 'u', false }, + .{ .kp_9, 57408, 'u', false }, + .{ .kp_equal, 57415, 'u', false }, + + .{ .f1, 1, 'P', false }, + .{ .f2, 1, 'Q', false }, + .{ .f3, 13, '~', false }, + .{ .f4, 1, 'S', false }, + .{ .f5, 15, '~', false }, + .{ .f6, 17, '~', false }, + .{ .f7, 18, '~', false }, + .{ .f8, 19, '~', false }, + .{ .f9, 20, '~', false }, + .{ .f10, 21, '~', false }, + .{ .f11, 23, '~', false }, + .{ .f12, 24, '~', false }, + .{ .f13, 57376, 'u', false }, + .{ .f14, 57377, 'u', false }, + .{ .f15, 57378, 'u', false }, + .{ .f16, 57379, 'u', false }, + .{ .f17, 57380, 'u', false }, + .{ .f18, 57381, 'u', false }, + .{ .f19, 57382, 'u', false }, + .{ .f20, 57383, 'u', false }, + .{ .f21, 57384, 'u', false }, + .{ .f22, 57385, 'u', false }, + .{ .f23, 57386, 'u', false }, + .{ .f24, 57387, 'u', false }, + .{ .f25, 57388, 'u', false }, + + .{ .left_shift, 57441, 'u', true }, + .{ .right_shift, 57447, 'u', true }, + .{ .left_control, 57442, 'u', true }, + .{ .right_control, 57448, 'u', true }, + .{ .caps_lock, 57358, 'u', true }, + .{ .left_super, 57444, 'u', true }, + .{ .right_super, 57450, 'u', true }, + .{ .left_alt, 57443, 'u', true }, + .{ .right_alt, 57449, 'u', true }, + + .{ .delete, 3, '~', false }, +}; + +test { + // To force comptime to test it + _ = entries; +} From c5177f6609ce1d681cff9afbf00a28fce0344105 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 22:08:16 -0700 Subject: [PATCH 06/12] input: begin kitty key encoding logic (not working yet) --- src/input/KeyEncoder.zig | 218 +++++++++++++++++++++++++++++++++++---- 1 file changed, 199 insertions(+), 19 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 11deeb942..f154f4daf 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -10,6 +10,10 @@ const testing = std.testing; const key = @import("key.zig"); const function_keys = @import("function_keys.zig"); +const terminal = @import("../terminal/main.zig"); +const KittyEntry = @import("kitty.zig").Entry; +const kitty_entries = @import("kitty.zig").entries; +const KittyFlags = terminal.kitty.KeyFlags; event: key.KeyEvent, @@ -18,6 +22,66 @@ alt_esc_prefix: bool = false, cursor_key_application: bool = false, keypad_key_application: bool = false, modify_other_keys_state_2: bool = false, +kitty_flags: KittyFlags = .{}, + +/// Perform Kitty keyboard protocol encoding of the key event. +pub fn kitty( + self: *const KeyEncoder, + buf: []u8, +) ![]const u8 { + // This should never happen but we'll check anyway. + if (self.kitty_flags.int() == 0) return try self.legacy(buf); + + // We only processed "press" events unless report events is active + if (self.event.action != .press and !self.kitty_flags.report_events) + return ""; + + const all_mods = self.event.mods; + const effective_mods = self.event.effectiveMods(); + const binding_mods = effective_mods.binding(); + + // Find the entry for this key in the kitty table. + const entry: ?KittyEntry = entry: { + for (kitty_entries) |entry| { + if (entry.key == self.event.key) break :entry entry; + } + + break :entry null; + }; + + // Quote: + // The only exceptions are the Enter, Tab and Backspace keys which + // still generate the same bytes as in legacy mode this is to allow the + // user to type and execute commands in the shell such as reset after a + // program that sets this mode crashes without clearing it. + if (effective_mods.empty()) { + switch (self.event.key) { + .enter => return try copyToBuf(buf, "\r"), + .tab => return try copyToBuf(buf, "\t"), + .backspace => return try copyToBuf(buf, "\x7F"), + else => {}, + } + } + + // Send plain-text non-modified text directly to the terminal. + // We don't send release events because those are specially encoded. + if (self.event.utf8.len > 0 and + binding_mods.empty() and + self.event.action != .release) + { + return try copyToBuf(buf, self.event.utf8); + } + + const kitty_mods = KittyMods.fromInput(all_mods); + const final_entry = entry orelse { + // TODO: we need to look it up + return ""; + }; + _ = kitty_mods; + _ = final_entry; + + return "X"; +} /// Perform legacy encoding of the key event. "Legacy" in this case /// is referring to the behavior of traditional terminals, plus @@ -312,34 +376,150 @@ const CsiUMods = packed struct(u3) { const raw: u4 = @intCast(self.int()); return raw + 1; } + + test "modifer sequence values" { + // This is all sort of trivially seen by looking at the code but + // we want to make sure we never regress this. + var mods: CsiUMods = .{}; + try testing.expectEqual(@as(u4, 1), mods.seqInt()); + + mods = .{ .shift = true }; + try testing.expectEqual(@as(u4, 2), mods.seqInt()); + + mods = .{ .alt = true }; + try testing.expectEqual(@as(u4, 3), mods.seqInt()); + + mods = .{ .ctrl = true }; + try testing.expectEqual(@as(u4, 5), mods.seqInt()); + + mods = .{ .alt = true, .shift = true }; + try testing.expectEqual(@as(u4, 4), mods.seqInt()); + + mods = .{ .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u4, 6), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true }; + try testing.expectEqual(@as(u4, 7), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u4, 8), mods.seqInt()); + } }; -test "modifer sequence values" { - // This is all sort of trivially seen by looking at the code but - // we want to make sure we never regress this. - var mods: CsiUMods = .{}; - try testing.expectEqual(@as(u4, 1), mods.seqInt()); +/// This is the bitfields for Kitty modifiers. +const KittyMods = packed struct(u8) { + shift: bool = false, + alt: bool = false, + ctrl: bool = false, + super: bool = false, + hyper: bool = false, + meta: bool = false, + caps_lock: bool = false, + num_lock: bool = false, - mods = .{ .shift = true }; - try testing.expectEqual(@as(u4, 2), mods.seqInt()); + /// Convert an input mods value into the CSI u mods value. + pub fn fromInput(mods: key.Mods) KittyMods { + return .{ + .shift = mods.shift, + .alt = mods.alt, + .ctrl = mods.ctrl, + .super = mods.super, + .caps_lock = mods.caps_lock, + .num_lock = mods.num_lock, + }; + } - mods = .{ .alt = true }; - try testing.expectEqual(@as(u4, 3), mods.seqInt()); + /// Returns the raw int value of this packed struct. + pub fn int(self: KittyMods) u8 { + return @bitCast(self); + } - mods = .{ .ctrl = true }; - try testing.expectEqual(@as(u4, 5), mods.seqInt()); + /// Returns the integer value sent as part of the Kitty sequence. + /// This adds 1 to the bitmask value as described in the spec. + pub fn seqInt(self: KittyMods) u9 { + const raw: u9 = @intCast(self.int()); + return raw + 1; + } - mods = .{ .alt = true, .shift = true }; - try testing.expectEqual(@as(u4, 4), mods.seqInt()); + test "modifer sequence values" { + // This is all sort of trivially seen by looking at the code but + // we want to make sure we never regress this. + var mods: KittyMods = .{}; + try testing.expectEqual(@as(u9, 1), mods.seqInt()); - mods = .{ .ctrl = true, .shift = true }; - try testing.expectEqual(@as(u4, 6), mods.seqInt()); + mods = .{ .shift = true }; + try testing.expectEqual(@as(u9, 2), mods.seqInt()); - mods = .{ .alt = true, .ctrl = true }; - try testing.expectEqual(@as(u4, 7), mods.seqInt()); + mods = .{ .alt = true }; + try testing.expectEqual(@as(u9, 3), mods.seqInt()); - mods = .{ .alt = true, .ctrl = true, .shift = true }; - try testing.expectEqual(@as(u4, 8), mods.seqInt()); + mods = .{ .ctrl = true }; + try testing.expectEqual(@as(u9, 5), mods.seqInt()); + + mods = .{ .alt = true, .shift = true }; + try testing.expectEqual(@as(u9, 4), mods.seqInt()); + + mods = .{ .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u9, 6), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true }; + try testing.expectEqual(@as(u9, 7), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u9, 8), mods.seqInt()); + } +}; + +/// Represents a kitty key sequence and has helpers for encoding it. +/// The sequence from the Kitty specification: +/// +/// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u +const KittySequence = struct { + key: u16, +}; + +test "kitty: plain text" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .a, + .mods = .{}, + .utf8 = "abcd", + }, + + .kitty_flags = .{ .disambiguate = true }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("abcd", actual); +} + +test "kitty: enter, backspace, tab" { + var buf: [128]u8 = undefined; + { + var enc: KeyEncoder = .{ + .event = .{ .key = .enter, .mods = .{}, .utf8 = "" }, + .kitty_flags = .{ .disambiguate = true }, + }; + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\r", actual); + } + { + var enc: KeyEncoder = .{ + .event = .{ .key = .backspace, .mods = .{}, .utf8 = "" }, + .kitty_flags = .{ .disambiguate = true }, + }; + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x7f", actual); + } + { + var enc: KeyEncoder = .{ + .event = .{ .key = .tab, .mods = .{}, .utf8 = "" }, + .kitty_flags = .{ .disambiguate = true }, + }; + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\t", actual); + } } test "legacy: ctrl+alt+c" { From 91ba53b081406108c3e4c0b2051d793d295c5148 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Aug 2023 08:43:40 -0700 Subject: [PATCH 07/12] input: KittySequence for encoding sequences --- src/input/KeyEncoder.zig | 179 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index f154f4daf..7dccd806a 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -476,8 +476,187 @@ const KittyMods = packed struct(u8) { /// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u const KittySequence = struct { key: u16, + final: u8, + event: Event = .none, + alternates: []const u16 = &.{}, + mods: KittyMods = .{}, + text: []const u8 = "", + + /// Values for the event code (see "event-type" in above comment). + /// Note that Kitty omits the ":1" for the press event but other + /// terminals include it. We'll include it. + const Event = enum(u2) { + none = 0, + press = 1, + repeat = 2, + release = 3, + }; + + pub fn encode(self: KittySequence, buf: []u8) ![]const u8 { + if (self.final == 'u' or self.final == '~') return try self.encodeFull(buf); + return try self.encodeSpecial(buf); + } + + fn encodeFull(self: KittySequence, buf: []u8) ![]const u8 { + // Boilerplate to basically create a string builder that writes + // over our buffer (but no more). + var fba = std.heap.FixedBufferAllocator.init(buf); + const alloc = fba.allocator(); + var builder = try std.ArrayListUnmanaged(u8).initCapacity(alloc, buf.len); + const writer = builder.writer(alloc); + + // Key section + try writer.print("\x1B[{d}", .{self.key}); + for (self.alternates) |alt| try writer.print(":{d}", .{alt}); + + // Mods and events section + const mods = self.mods.seqInt(); + var emit_prior = false; + if (self.event != .none) { + try writer.print(";{d}:{d}", .{ mods, @intFromEnum(self.event) }); + emit_prior = true; + } else if (mods > 1) { + try writer.print(";{d}", .{mods}); + emit_prior = true; + } + + // Text section + if (self.text.len > 0) { + // We need to add our ";". We need to add two if we didn't emit + // the modifier section. + if (!emit_prior) try writer.writeByte(';'); + try writer.writeByte(';'); + + // First one has no prefix + const view = try std.unicode.Utf8View.init(self.text); + var it = view.iterator(); + if (it.nextCodepoint()) |cp| { + try writer.print("{d}", .{cp}); + } + while (it.nextCodepoint()) |cp| { + try writer.print(":{d}", .{cp}); + } + } + + try writer.print("{c}", .{self.final}); + return builder.items; + } + + fn encodeSpecial(self: KittySequence, buf: []u8) ![]const u8 { + const mods = self.mods.seqInt(); + if (self.event != .none) { + return try std.fmt.bufPrint(buf, "\x1B[1;{d}:{d}{c}", .{ + mods, + @intFromEnum(self.event), + self.final, + }); + } + + if (mods > 1) { + return try std.fmt.bufPrint(buf, "\x1B[1;{d}{c}", .{ + mods, + self.final, + }); + } + + return try std.fmt.bufPrint(buf, "\x1B[{c}", .{self.final}); + } }; +test "KittySequence: backspace" { + var buf: [128]u8 = undefined; + + // Plain + { + var seq: KittySequence = .{ .key = 127, .final = 'u' }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[127u", actual); + } + + // Release event + { + var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[127;1:3u", actual); + } + + // Shift + { + var seq: KittySequence = .{ + .key = 127, + .final = 'u', + .mods = .{ .shift = true }, + }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[127;2u", actual); + } +} + +test "KittySequence: text" { + var buf: [128]u8 = undefined; + + // Plain + { + var seq: KittySequence = .{ + .key = 127, + .final = 'u', + .text = "A", + }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[127;;65u", actual); + } + + // Release + { + var seq: KittySequence = .{ + .key = 127, + .final = 'u', + .event = .release, + .text = "A", + }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[127;1:3;65u", actual); + } + + // Shift + { + var seq: KittySequence = .{ + .key = 127, + .final = 'u', + .mods = .{ .shift = true }, + .text = "A", + }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[127;2;65u", actual); + } +} + +test "KittySequence: special no mods" { + var buf: [128]u8 = undefined; + var seq: KittySequence = .{ .key = 1, .final = 'A' }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[A", actual); +} + +test "KittySequence: special mods only" { + var buf: [128]u8 = undefined; + var seq: KittySequence = .{ .key = 1, .final = 'A', .mods = .{ .shift = true } }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[1;2A", actual); +} + +test "KittySequence: special mods and event" { + var buf: [128]u8 = undefined; + var seq: KittySequence = .{ + .key = 1, + .final = 'A', + .event = .release, + .mods = .{ .shift = true }, + }; + const actual = try seq.encode(&buf); + try testing.expectEqualStrings("\x1B[1;2:3A", actual); +} + test "kitty: plain text" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ From fb9dc74b29c4cf97c42394dff07f0af0c1b12ac0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Aug 2023 08:55:37 -0700 Subject: [PATCH 08/12] input: lot more Kitty encoding logic --- src/input/KeyEncoder.zig | 121 ++++++++++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 27 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 7dccd806a..6be1fbdd0 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -41,7 +41,7 @@ pub fn kitty( const binding_mods = effective_mods.binding(); // Find the entry for this key in the kitty table. - const entry: ?KittyEntry = entry: { + const entry_: ?KittyEntry = entry: { for (kitty_entries) |entry| { if (entry.key == self.event.key) break :entry entry; } @@ -49,38 +49,75 @@ pub fn kitty( break :entry null; }; - // Quote: - // The only exceptions are the Enter, Tab and Backspace keys which - // still generate the same bytes as in legacy mode this is to allow the - // user to type and execute commands in the shell such as reset after a - // program that sets this mode crashes without clearing it. - if (effective_mods.empty()) { - switch (self.event.key) { - .enter => return try copyToBuf(buf, "\r"), - .tab => return try copyToBuf(buf, "\t"), - .backspace => return try copyToBuf(buf, "\x7F"), - else => {}, + preprocessing: { + // When composing, the only keys sent are plain modifiers. + if (self.event.composing) { + if (entry_) |entry| { + if (entry.modifier) break :preprocessing; + } + + return ""; + } + + // If we're reporting all then we always send CSI sequences. + if (!self.kitty_flags.report_all) { + // Quote: + // The only exceptions are the Enter, Tab and Backspace keys which + // still generate the same bytes as in legacy mode this is to allow the + // user to type and execute commands in the shell such as reset after a + // program that sets this mode crashes without clearing it. + // + // Quote ("report all" mode): + // Note that all keys are reported as escape codes, including Enter, + // Tab, Backspace etc. + if (effective_mods.empty()) { + switch (self.event.key) { + .enter => return try copyToBuf(buf, "\r"), + .tab => return try copyToBuf(buf, "\t"), + .backspace => return try copyToBuf(buf, "\x7F"), + else => {}, + } + } + + // Send plain-text non-modified text directly to the terminal. + // We don't send release events because those are specially encoded. + if (self.event.utf8.len > 0 and + binding_mods.empty() and + self.event.action != .release) + { + return try copyToBuf(buf, self.event.utf8); + } } } - // Send plain-text non-modified text directly to the terminal. - // We don't send release events because those are specially encoded. - if (self.event.utf8.len > 0 and - binding_mods.empty() and - self.event.action != .release) - { - return try copyToBuf(buf, self.event.utf8); - } - - const kitty_mods = KittyMods.fromInput(all_mods); - const final_entry = entry orelse { + const final_entry = entry_ orelse { // TODO: we need to look it up return ""; }; - _ = kitty_mods; - _ = final_entry; - return "X"; + const seq: KittySequence = seq: { + var seq: KittySequence = .{ + .key = final_entry.code, + .final = final_entry.final, + .mods = KittyMods.fromInput(all_mods), + }; + + if (self.kitty_flags.report_events) { + seq.event = switch (self.event.action) { + .press => .press, + .release => .release, + .repeat => .repeat, + }; + } + + if (self.kitty_flags.report_associated) { + seq.text = self.event.utf8; + } + + break :seq seq; + }; + + return try seq.encode(buf); } /// Perform legacy encoding of the key event. "Legacy" in this case @@ -477,9 +514,9 @@ const KittyMods = packed struct(u8) { const KittySequence = struct { key: u16, final: u8, + mods: KittyMods = .{}, event: Event = .none, alternates: []const u16 = &.{}, - mods: KittyMods = .{}, text: []const u8 = "", /// Values for the event code (see "event-type" in above comment). @@ -701,6 +738,36 @@ test "kitty: enter, backspace, tab" { } } +test "kitty: composing with no modifier" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .a, + .mods = .{ .shift = true }, + .composing = true, + }, + .kitty_flags = .{ .disambiguate = true }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("", actual); +} + +test "kitty: composing with modifier" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .left_shift, + .mods = .{ .shift = true }, + .composing = true, + }, + .kitty_flags = .{ .disambiguate = true }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[57441;2u", actual); +} + test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ From 37daf028049c08386eff77f102164e298614359d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Aug 2023 09:02:43 -0700 Subject: [PATCH 09/12] core: use Kitty encoding if enabled --- src/Surface.zig | 3 ++- src/input/KeyEncoder.zig | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c16987b70..e9f766385 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1037,11 +1037,12 @@ pub fn keyCallback( .cursor_key_application = t.modes.get(.cursor_keys), .keypad_key_application = t.modes.get(.keypad_keys), .modify_other_keys_state_2 = t.flags.modify_other_keys_2, + .kitty_flags = t.screen.kitty_keyboard.current(), }; }; var data: termio.Message.WriteReq.Small.Array = undefined; - const seq = try enc.legacy(&data); + const seq = try enc.encode(&data); if (seq.len == 0) return false; _ = self.io_thread.mailbox.push(.{ diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 6be1fbdd0..4fa760314 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -24,8 +24,17 @@ keypad_key_application: bool = false, modify_other_keys_state_2: bool = false, kitty_flags: KittyFlags = .{}, +/// Perform the proper encoding depending on the terminal state. +pub fn encode( + self: *const KeyEncoder, + buf: []u8, +) ![]const u8 { + if (self.kitty_flags.int() != 0) return try self.kitty(buf); + return try self.legacy(buf); +} + /// Perform Kitty keyboard protocol encoding of the key event. -pub fn kitty( +fn kitty( self: *const KeyEncoder, buf: []u8, ) ![]const u8 { @@ -126,7 +135,7 @@ pub fn kitty( /// These together combine the legacy protocol because they're all /// meant to be extensions that do not change any existing behavior /// and therefore safe to combine. -pub fn legacy( +fn legacy( self: *const KeyEncoder, buf: []u8, ) ![]const u8 { From fe0e1f5ee80692ce7521298f733f257840bb4727 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Aug 2023 09:24:44 -0700 Subject: [PATCH 10/12] input: key must provide unshifted codepoint --- src/apprt/embedded.zig | 37 +++++++++++++++++++++++++------------ src/apprt/gtk.zig | 26 ++++++++++++++++++++++++++ src/input/key.zig | 4 ++++ 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 68a3c805a..e1ef1612b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -458,6 +458,26 @@ pub const Surface = struct { // then we've consumed our translate mods. const consumed_mods: input.Mods = if (result.text.len > 0) translate_mods else .{}; + // We need to always do a translation with no modifiers at all in + // order to get the "unshifted_codepoint" for the key event. + const unshifted_codepoint: u21 = unshifted: { + var nomod_buf: [128]u8 = undefined; + var nomod_state: input.Keymap.State = undefined; + const nomod = try self.app.keymap.translate( + &nomod_buf, + &nomod_state, + @intCast(keycode), + .{}, + ); + + const view = std.unicode.Utf8View.init(nomod.text) catch |err| { + log.warn("cannot build utf8 view over text: {}", .{err}); + break :unshifted 0; + }; + var it = view.iterator(); + break :unshifted it.nextCodepoint() orelse 0; + }; + // log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{ // action, // keycode, @@ -489,18 +509,9 @@ pub const Surface = struct { } } - // If that doesn't work then we try to translate without - // any modifiers and convert that. - var nomod_buf: [128]u8 = undefined; - var nomod_state: input.Keymap.State = undefined; - const nomod = try self.app.keymap.translate( - &nomod_buf, - &nomod_state, - @intCast(keycode), - .{}, - ); - if (nomod.text.len == 1) { - if (input.Key.fromASCII(nomod.text[0])) |key| { + // If the above doesn't work, we use the unmodified value. + if (std.math.cast(u8, unshifted_codepoint)) |ascii| { + if (input.Key.fromASCII(ascii)) |key| { break :key key; } } @@ -517,6 +528,7 @@ pub const Surface = struct { .consumed_mods = consumed_mods, .composing = result.composing, .utf8 = result.text, + .unshifted_codepoint = unshifted_codepoint, }) catch |err| { log.err("error in key callback err={}", .{err}); return; @@ -549,6 +561,7 @@ pub const Surface = struct { .consumed_mods = .{}, .composing = false, .utf8 = buf[0..len], + .unshifted_codepoint = 0, }) catch |err| { log.err("error in key callback err={}", .{err}); return; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 1a8d497db..d2aeaac87 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1212,6 +1212,31 @@ pub const Surface = struct { const keyval_unicode = c.gdk_keyval_to_unicode(keyval); const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); + // Get the unshifted unicode value of the keyval. This is used + // by the Kitty keyboard protocol. + const keyval_unicode_unshifted: u21 = unshifted: { + var n: c_int = undefined; + var keys: [*c]c.GdkKeymapKey = undefined; + var keyvals: [*c]c.guint = undefined; + if (c.gdk_display_map_keycode( + c.gdk_event_get_display(event), + keycode, + &keys, + &keyvals, + &n, + ) == 0) break :unshifted 0; + + defer c.g_free(keys); + defer c.g_free(keyvals); + for (keys[0..@intCast(n)], 0..) |key, i| { + if (key.group == 0 and key.level == 0) { + break :unshifted @intCast(c.gdk_keyval_to_unicode(keyvals[i])); + } + } + + break :unshifted 0; + }; + // We always reset our committed text when ending a keypress so that // future keypresses don't think we have a commit event. defer self.im_len = 0; @@ -1317,6 +1342,7 @@ pub const Surface = struct { .consumed_mods = consumed_mods, .composing = self.im_composing, .utf8 = self.im_buf[0..self.im_len], + .unshifted_codepoint = keyval_unicode_unshifted, }) catch |err| { log.err("error in key callback err={}", .{err}); return false; diff --git a/src/input/key.zig b/src/input/key.zig index 622d50556..46263bf30 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -41,6 +41,10 @@ pub const KeyEvent = struct { /// text. utf8: []const u8 = "", + /// The codepoint for this key when it is unshifted. For example, + /// shift+a is "A" in UTF-8 but unshifted would provide 'a'. + unshifted_codepoint: u21 = 0, + /// Returns the effective modifiers for this event. The effective /// modifiers are the mods that should be considered for keybindings. pub fn effectiveMods(self: KeyEvent) Mods { From 6493da0dd396c97c807bf64b4b75be308a365ce0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Aug 2023 09:48:15 -0700 Subject: [PATCH 11/12] input: Kitty encodes alternate keys --- src/input/KeyEncoder.zig | 67 ++++++++++++++++++++++++++++++++++++++-- src/input/kitty.zig | 4 +-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 4fa760314..10ccfeb41 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -51,10 +51,22 @@ fn kitty( // Find the entry for this key in the kitty table. const entry_: ?KittyEntry = entry: { + // Functional or predefined keys for (kitty_entries) |entry| { if (entry.key == self.event.key) break :entry entry; } + // Otherwise, we use our unicode codepoint from UTF8. We + // always use the unshifted value. + if (self.event.unshifted_codepoint > 0) { + break :entry .{ + .key = self.event.key, + .code = self.event.unshifted_codepoint, + .final = 'u', + .modifier = false, + }; + } + break :entry null; }; @@ -119,6 +131,16 @@ fn kitty( }; } + if (self.kitty_flags.report_alternates) alternates: { + const view = try std.unicode.Utf8View.init(self.event.utf8); + var it = view.iterator(); + const cp = it.nextCodepoint() orelse break :alternates; + if (it.nextCodepoint() != null) break :alternates; + if (cp != seq.key) { + seq.alternates = &.{cp}; + } + } + if (self.kitty_flags.report_associated) { seq.text = self.event.utf8; } @@ -521,11 +543,11 @@ const KittyMods = packed struct(u8) { /// /// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u const KittySequence = struct { - key: u16, + key: u21, final: u8, mods: KittyMods = .{}, event: Event = .none, - alternates: []const u16 = &.{}, + alternates: []const u21 = &.{}, text: []const u8 = "", /// Values for the event code (see "event-type" in above comment). @@ -777,6 +799,47 @@ test "kitty: composing with modifier" { try testing.expectEqualStrings("\x1b[57441;2u", actual); } +test "kitty: shift+a on US keyboard" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .a, + .mods = .{ .shift = true }, + .utf8 = "A", + .unshifted_codepoint = 97, // lowercase A + }, + .kitty_flags = .{ + .disambiguate = true, + .report_alternates = true, + }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[97:65;2u", actual); +} + +test "kitty: matching unshifted codepoint" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .a, + .mods = .{ .shift = true }, + .utf8 = "A", + .unshifted_codepoint = 65, + }, + .kitty_flags = .{ + .disambiguate = true, + .report_alternates = true, + }, + }; + + // WARNING: This is not a valid encoding. This is a hypothetical encoding + // just to test that our logic is correct around matching unshifted + // codepoints. + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[65;2u", actual); +} + test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ diff --git a/src/input/kitty.zig b/src/input/kitty.zig index e0120ab32..6668b7fe4 100644 --- a/src/input/kitty.zig +++ b/src/input/kitty.zig @@ -6,7 +6,7 @@ const key = @import("key.zig"); /// for a given key. pub const Entry = struct { key: key.Key, - code: u16, + code: u21, final: u8, modifier: bool, }; @@ -28,7 +28,7 @@ pub const entries: []const Entry = entries: { /// Raw entry is the tuple form of an entry for easy human management. /// This should never be used in a real program so it is not pub. For /// real programs, use `entries` which has properly typed, structured data. -const RawEntry = struct { key.Key, u16, u8, bool }; +const RawEntry = struct { key.Key, u21, u8, bool }; /// The raw data for how to map keys to Kitty data. Based on the information: /// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions From 78080f0cd6eed50e7ee07f22654e27f62ddc06b0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Aug 2023 09:52:44 -0700 Subject: [PATCH 12/12] input: proper optional entry handling --- src/input/KeyEncoder.zig | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 10ccfeb41..c7f2842b2 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -111,15 +111,11 @@ fn kitty( } } - const final_entry = entry_ orelse { - // TODO: we need to look it up - return ""; - }; - + const entry = entry_ orelse return ""; const seq: KittySequence = seq: { var seq: KittySequence = .{ - .key = final_entry.code, - .final = final_entry.final, + .key = entry.code, + .final = entry.final, .mods = KittyMods.fromInput(all_mods), };