diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 09aa6206a..50a88401e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -757,8 +757,8 @@ fn keyEvent( // Norwegian, and French layouts and thats what we have real users for // right now. const lower = c.gdk_keyval_to_lower(keyval); - if (std.math.cast(u21, lower)) |val| break :unshifted val; - break :unshifted 0; + const lower_unicode = c.gdk_keyval_to_unicode(lower); + break :unshifted std.math.cast(u21, lower_unicode) orelse 0; }; // We always reset our committed text when ending a keypress so that diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index bc931fed0..e18ef7312 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -143,12 +143,23 @@ 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 and !isControl(cp)) { - seq.alternates = &.{cp}; + // Break early if this is a control key + if (isControl(seq.key)) break :alternates; + + // Set the first alternate (shifted version) + { + const view = try std.unicode.Utf8View.init(self.event.utf8); + var it = view.iterator(); + // We break early if there are codepoints...there are no + // alternate key(s) to report + const shifted = it.nextCodepoint() orelse break :alternates; + // Only report the shifted key if we have a shift modifier + if (shifted != seq.key and seq.mods.shift) seq.alternates[0] = shifted; + } + + // Set the base layout key + if (self.event.key.codepoint()) |base| { + if (base != seq.key) seq.alternates[1] = base; } } @@ -553,7 +564,7 @@ const KittySequence = struct { final: u8, mods: KittyMods = .{}, event: Event = .none, - alternates: []const u21 = &.{}, + alternates: [2]?u21 = .{ null, null }, text: []const u8 = "", /// Values for the event code (see "event-type" in above comment). @@ -581,7 +592,15 @@ const KittySequence = struct { // Key section try writer.print("\x1B[{d}", .{self.key}); - for (self.alternates) |alt| try writer.print(":{d}", .{alt}); + // Write our alternates + if (self.alternates[0]) |shifted| try writer.print(":{d}", .{shifted}); + if (self.alternates[1]) |base| { + if (self.alternates[0] == null) { + try writer.print("::{d}", .{base}); + } else { + try writer.print(":{d}", .{base}); + } + } // Mods and events section const mods = self.mods.seqInt(); @@ -901,9 +920,115 @@ test "kitty: matching unshifted codepoint" { // WARNING: This is not a valid encoding. This is a hypothetical encoding // just to test that our logic is correct around matching unshifted - // codepoints. + // codepoints. We get an alternate here because the unshifted_codepoint does + // not match the base key const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[65;2u", actual); + try testing.expectEqualStrings("\x1b[65::97;2u", actual); +} + +test "kitty: report alternates with caps" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .j, + .mods = .{ .caps_lock = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, + .kitty_flags = .{ + .disambiguate = true, + .report_all = true, + .report_alternates = true, + .report_associated = true, + }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[106;65;74u", actual); +} + +test "kitty: report alternates colon (shift+';')" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .semicolon, + .mods = .{ .shift = true }, + .utf8 = ":", + .unshifted_codepoint = ';', + }, + .kitty_flags = .{ + .disambiguate = true, + .report_all = true, + .report_alternates = true, + .report_associated = true, + }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[59:58;2;58u", actual); +} + +test "kitty: report alternates with ru layout" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .semicolon, + .mods = .{}, + .utf8 = "ч", + .unshifted_codepoint = 1095, + }, + .kitty_flags = .{ + .disambiguate = true, + .report_all = true, + .report_alternates = true, + .report_associated = true, + }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[1095::59;;1095u", actual); +} + +test "kitty: report alternates with ru layout shifted" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .semicolon, + .mods = .{ .shift = true }, + .utf8 = "Ч", + .unshifted_codepoint = 1095, + }, + .kitty_flags = .{ + .disambiguate = true, + .report_all = true, + .report_alternates = true, + .report_associated = true, + }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", actual); +} + +test "kitty: report alternates with ru layout caps lock" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .semicolon, + .mods = .{ .caps_lock = true }, + .utf8 = "Ч", + .unshifted_codepoint = 1095, + }, + .kitty_flags = .{ + .disambiguate = true, + .report_all = true, + .report_alternates = true, + .report_associated = true, + }, + }; + + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("\x1b[1095::59;65;1063u", actual); } // macOS generates utf8 text for arrow keys. diff --git a/src/input/key.zig b/src/input/key.zig index 830501772..aabd3e2e3 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -298,154 +298,138 @@ pub const Key = enum(c_int) { /// are independent of the physical key. pub fn fromASCII(ch: u8) ?Key { return switch (ch) { - 'a' => .a, - 'b' => .b, - 'c' => .c, - 'd' => .d, - 'e' => .e, - 'f' => .f, - 'g' => .g, - 'h' => .h, - 'i' => .i, - 'j' => .j, - 'k' => .k, - 'l' => .l, - 'm' => .m, - 'n' => .n, - 'o' => .o, - 'p' => .p, - 'q' => .q, - 'r' => .r, - 's' => .s, - 't' => .t, - 'u' => .u, - 'v' => .v, - 'w' => .w, - 'x' => .x, - 'y' => .y, - 'z' => .z, - '0' => .zero, - '1' => .one, - '2' => .two, - '3' => .three, - '4' => .four, - '5' => .five, - '6' => .six, - '7' => .seven, - '8' => .eight, - '9' => .nine, - ';' => .semicolon, - ' ' => .space, - '\'' => .apostrophe, - ',' => .comma, - '`' => .grave_accent, - '.' => .period, - '/' => .slash, - '-' => .minus, - '=' => .equal, - '[' => .left_bracket, - ']' => .right_bracket, - '\\' => .backslash, - else => null, + inline else => |comptime_ch| { + return comptime result: { + @setEvalBranchQuota(100_000); + for (codepoint_map) |entry| { + if (entry[0] == @as(u21, @intCast(comptime_ch))) { + break :result entry[1]; + } + } + + break :result null; + }; + }, }; } /// True if this key represents a printable character. pub fn printable(self: Key) bool { return switch (self) { - .a, - .b, - .c, - .d, - .e, - .f, - .g, - .h, - .i, - .j, - .k, - .l, - .m, - .n, - .o, - .p, - .q, - .r, - .s, - .t, - .u, - .v, - .w, - .x, - .y, - .z, - .zero, - .one, - .two, - .three, - .four, - .five, - .six, - .seven, - .eight, - .nine, - .semicolon, - .space, - .apostrophe, - .comma, - .grave_accent, - .period, - .slash, - .minus, - .equal, - .left_bracket, - .right_bracket, - .backslash, - .kp_0, - .kp_1, - .kp_2, - .kp_3, - .kp_4, - .kp_5, - .kp_6, - .kp_7, - .kp_8, - .kp_9, - .kp_decimal, - .kp_divide, - .kp_multiply, - .kp_subtract, - .kp_add, - .kp_equal, - => true, + inline else => |tag| { + return comptime result: { + @setEvalBranchQuota(10_000); + for (codepoint_map) |entry| { + if (entry[1] == tag) break :result true; + } - else => false, + break :result false; + }; + }, }; } /// Returns true if this is a keypad key. pub fn keypad(self: Key) bool { return switch (self) { - .kp_0, - .kp_1, - .kp_2, - .kp_3, - .kp_4, - .kp_5, - .kp_6, - .kp_7, - .kp_8, - .kp_9, - .kp_decimal, - .kp_divide, - .kp_multiply, - .kp_subtract, - .kp_add, - .kp_enter, - .kp_equal, - => true, - - else => false, + inline else => |tag| { + const name = @tagName(tag); + const result = comptime std.mem.startsWith(u8, name, "kp_"); + return result; + }, }; } + + // Returns the codepoint representing this key, or null if the key is not + // printable + pub fn codepoint(self: Key) ?u21 { + return switch (self) { + inline else => |tag| { + return comptime result: { + @setEvalBranchQuota(10_000); + for (codepoint_map) |entry| { + if (entry[1] == tag) break :result entry[0]; + } + + break :result null; + }; + }, + }; + } + + test "keypad keys" { + const testing = std.testing; + try testing.expect(Key.kp_0.keypad()); + try testing.expect(!Key.one.keypad()); + } + + const codepoint_map: []const struct { u21, Key } = &.{ + .{ 'a', .a }, + .{ 'b', .b }, + .{ 'c', .c }, + .{ 'd', .d }, + .{ 'e', .e }, + .{ 'f', .f }, + .{ 'g', .g }, + .{ 'h', .h }, + .{ 'i', .i }, + .{ 'j', .j }, + .{ 'k', .k }, + .{ 'l', .l }, + .{ 'm', .m }, + .{ 'n', .n }, + .{ 'o', .o }, + .{ 'p', .p }, + .{ 'q', .q }, + .{ 'r', .r }, + .{ 's', .s }, + .{ 't', .t }, + .{ 'u', .u }, + .{ 'v', .v }, + .{ 'w', .w }, + .{ 'x', .x }, + .{ 'y', .y }, + .{ 'z', .z }, + .{ '0', .zero }, + .{ '1', .one }, + .{ '2', .two }, + .{ '3', .three }, + .{ '4', .four }, + .{ '5', .five }, + .{ '6', .six }, + .{ '7', .seven }, + .{ '8', .eight }, + .{ '9', .nine }, + .{ ';', .semicolon }, + .{ ' ', .space }, + .{ '\'', .apostrophe }, + .{ ',', .comma }, + .{ '`', .grave_accent }, + .{ '.', .period }, + .{ '/', .slash }, + .{ '-', .minus }, + .{ '=', .equal }, + .{ '[', .left_bracket }, + .{ ']', .right_bracket }, + .{ '\\', .backslash }, + + // Keypad entries. We just assume keypad with the kp_ prefix + // so that has some special meaning. These must also always be last. + .{ '0', .kp_0 }, + .{ '1', .kp_1 }, + .{ '2', .kp_2 }, + .{ '3', .kp_3 }, + .{ '4', .kp_4 }, + .{ '5', .kp_5 }, + .{ '6', .kp_6 }, + .{ '7', .kp_7 }, + .{ '8', .kp_8 }, + .{ '9', .kp_9 }, + .{ '.', .kp_decimal }, + .{ '/', .kp_divide }, + .{ '*', .kp_multiply }, + .{ '-', .kp_subtract }, + .{ '+', .kp_add }, + .{ '=', .kp_equal }, + }; };