From 8e7d109a2d45cdc54dbab6c573b3a60f900b85fd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 2 Jun 2024 10:32:03 -0700 Subject: [PATCH] input: add unicode bindings --- include/ghostty.h | 2 + macos/Sources/Ghostty/Ghostty.Config.swift | 3 ++ src/Surface.zig | 9 +++++ src/input/Binding.zig | 47 +++++++++++++++++++--- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 3b20f2b03..1254f71bd 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -333,11 +333,13 @@ typedef struct { typedef enum { GHOSTTY_TRIGGER_TRANSLATED, GHOSTTY_TRIGGER_PHYSICAL, + GHOSTTY_TRIGGER_UNICODE, } ghostty_input_trigger_tag_e; typedef union { ghostty_input_key_e translated; ghostty_input_key_e physical; + uint32_t unicode; } ghostty_input_trigger_key_u; typedef struct { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index a43038c1d..2f455e578 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -131,6 +131,9 @@ extension Ghostty { return nil } + case GHOSTTY_TRIGGER_UNICODE: + equiv = String(trigger.key.unicode) + default: return nil } diff --git a/src/Surface.zig b/src/Surface.zig index 348a05726..2839f908c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1205,6 +1205,15 @@ pub fn keyCallback( set.getConsumed(trigger), }; + if (event.unshifted_codepoint > 0) { + trigger.key = .{ .unicode = event.unshifted_codepoint }; + if (set.get(trigger)) |v| break :action .{ + v, + trigger, + set.getConsumed(trigger), + }; + } + break :binding; }; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b5243ab8f..5640843d9 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -89,11 +89,7 @@ pub fn parse(raw_input: []const u8) !Binding { if (!std.mem.eql(u8, field.name, "invalid")) { if (std.mem.eql(u8, key_part, field.name)) { // Repeat not allowed - if (result.key != .translated or - result.key.translated != .invalid) - { - return Error.InvalidFormat; - } + if (!result.isKeyUnset()) return Error.InvalidFormat; const keyval = @field(key.Key, field.name); result.key = if (physical) @@ -105,6 +101,21 @@ pub fn parse(raw_input: []const u8) !Binding { } } + // If we're still unset and we have exactly one unicode + // character then we can use that as a key. + if (result.isKeyUnset()) unicode: { + // Invalid UTF8 drops to invalid format + const view = std.unicode.Utf8View.init(key_part) catch break :unicode; + var it = view.iterator(); + + // No codepoints or multiple codepoints drops to invalid format + const cp = it.nextCodepoint() orelse break :unicode; + if (it.nextCodepoint() != null) break :unicode; + + result.key = .{ .unicode = cp }; + continue :loop; + } + // We didn't recognize this value return Error.InvalidFormat; } @@ -524,6 +535,11 @@ pub const Trigger = struct { /// is used to bind to a physical key location rather than a translated /// key. physical: key.Key, + + /// This is used for binding to keys that produce a certain unicode + /// codepoint. This is useful for binding to keys that don't have a + /// registered keycode with Ghostty. + unicode: u21, }; /// The extern struct used for triggers in the C API. @@ -535,14 +551,24 @@ pub const Trigger = struct { pub const Tag = enum(c_int) { translated, physical, + unicode, }; pub const Key = extern union { translated: key.Key, physical: key.Key, + unicode: u32, }; }; + /// Returns true if this trigger has no key set. + pub fn isKeyUnset(self: Trigger) bool { + return switch (self.key) { + .translated => |v| v == .invalid, + else => false, + }; + } + /// Returns a hash code that can be used to uniquely identify this trigger. pub fn hash(self: Trigger) u64 { var hasher = std.hash.Wyhash.init(0); @@ -558,6 +584,7 @@ pub const Trigger = struct { .key = switch (self.key) { .translated => |v| .{ .translated = v }, .physical => |v| .{ .physical = v }, + .unicode => |v| .{ .unicode = @intCast(v) }, }, .mods = self.mods, }; @@ -583,6 +610,7 @@ pub const Trigger = struct { switch (self.key) { .translated => |k| try writer.print("{s}", .{@tagName(k)}), .physical => |k| try writer.print("physical:{s}", .{@tagName(k)}), + .unicode => |c| try writer.print("{u}", .{c}), } } }; @@ -813,6 +841,15 @@ test "parse: triggers" { .action = .{ .ignore = {} }, }, try parse("shift+physical:a=ignore")); + // unicode keys + try testing.expectEqual(Binding{ + .trigger = .{ + .mods = .{ .shift = true }, + .key = .{ .unicode = 'ö' }, + }, + .action = .{ .ignore = {} }, + }, try parse("shift+ö=ignore")); + // unconsumed keys try testing.expectEqual(Binding{ .trigger = .{