diff --git a/include/ghostty.h b/include/ghostty.h index 890e5f50e..1254f71bd 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -330,10 +330,22 @@ typedef struct { bool composing; } ghostty_input_key_s; +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 { - ghostty_input_key_e key; + ghostty_input_trigger_tag_e tag; + ghostty_input_trigger_key_u key; ghostty_input_mods_e mods; - bool physical; } ghostty_input_trigger_s; typedef enum { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index a444c1d9a..2f455e578 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -115,7 +115,28 @@ extension Ghostty { guard let cfg = self.config else { return nil } let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) - guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else { return nil } + let equiv: String + switch (trigger.tag) { + case GHOSTTY_TRIGGER_TRANSLATED: + if let v = Ghostty.keyEquivalent(key: trigger.key.translated) { + equiv = v + } else { + return nil + } + + case GHOSTTY_TRIGGER_PHYSICAL: + if let v = Ghostty.keyEquivalent(key: trigger.key.physical) { + equiv = v + } else { + return nil + } + + case GHOSTTY_TRIGGER_UNICODE: + equiv = String(trigger.key.unicode) + + default: + return nil + } return KeyEquivalent( key: equiv, diff --git a/src/Surface.zig b/src/Surface.zig index c2ac81240..2839f908c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1188,7 +1188,7 @@ pub fn keyCallback( const binding_mods = event.mods.binding(); var trigger: input.Binding.Trigger = .{ .mods = binding_mods, - .key = event.key, + .key = .{ .translated = event.key }, }; const set = self.config.keybind.set; @@ -1198,14 +1198,22 @@ pub fn keyCallback( set.getConsumed(trigger), }; - trigger.key = event.physical_key; - trigger.physical = true; + trigger.key = .{ .physical = event.physical_key }; if (set.get(trigger)) |v| break :action .{ v, trigger, 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/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 70abd18ca..c2b001738 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -14,8 +14,14 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u if (trigger.mods.super) try writer.writeAll(""); // Write our key - const keyval = keyvalFromKey(trigger.key) orelse return null; - try writer.writeAll(std.mem.sliceTo(c.gdk_keyval_name(keyval), 0)); + switch (trigger.key) { + .physical, .translated => |k| { + const keyval = keyvalFromKey(k) orelse return null; + try writer.writeAll(std.mem.sliceTo(c.gdk_keyval_name(keyval), 0)); + }, + + .unicode => |cp| try writer.print("{u}", .{cp}), + } // We need to make the string null terminated. try writer.writeByte(0); diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig index cff6a4ea3..ef50c4e2b 100644 --- a/src/config/CAPI.zig +++ b/src/config/CAPI.zig @@ -96,7 +96,7 @@ export fn ghostty_config_trigger( self: *Config, str: [*]const u8, len: usize, -) inputpkg.Binding.Trigger { +) inputpkg.Binding.Trigger.C { return config_trigger_(self, str[0..len]) catch |err| err: { log.err("error finding trigger err={}", .{err}); break :err .{}; @@ -106,9 +106,10 @@ export fn ghostty_config_trigger( fn config_trigger_( self: *Config, str: []const u8, -) !inputpkg.Binding.Trigger { +) !inputpkg.Binding.Trigger.C { const action = try inputpkg.Binding.Action.parse(str); - return self.keybind.set.getTrigger(action) orelse .{}; + const trigger: inputpkg.Binding.Trigger = self.keybind.set.getTrigger(action) orelse .{}; + return trigger.cval(); } export fn ghostty_config_errors_count(self: *Config) u32 { diff --git a/src/config/Config.zig b/src/config/Config.zig index eb2cf0915..c17d1e42d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1100,12 +1100,12 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // keybinds for opening and reloading config try result.keybind.set.put( alloc, - .{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .{ .translated = .comma }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .reload_config = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .translated = .comma }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .open_config = {} }, ); @@ -1119,12 +1119,12 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { try result.keybind.set.put( alloc, - .{ .key = .c, .mods = mods }, + .{ .key = .{ .translated = .c }, .mods = mods }, .{ .copy_to_clipboard = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .v, .mods = mods }, + .{ .key = .{ .translated = .v }, .mods = mods }, .{ .paste_from_clipboard = {} }, ); } @@ -1132,29 +1132,29 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // Fonts try result.keybind.set.put( alloc, - .{ .key = .equal, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .translated = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); // Increase font size mapping for keyboards with dedicated plus keys (like german) try result.keybind.set.put( alloc, - .{ .key = .plus, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .translated = .plus }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try result.keybind.set.put( alloc, - .{ .key = .minus, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .translated = .minus }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .decrease_font_size = 1 }, ); try result.keybind.set.put( alloc, - .{ .key = .zero, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .translated = .zero }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .reset_font_size = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .j, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .write_scrollback_file = {} }, ); @@ -1162,169 +1162,169 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { if (comptime !builtin.target.isDarwin()) { try result.keybind.set.put( alloc, - .{ .key = .n, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .n }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .w }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_surface = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .q, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .q }, .mods = .{ .ctrl = true, .shift = true } }, .{ .quit = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .f4, .mods = .{ .alt = true } }, + .{ .key = .{ .translated = .f4 }, .mods = .{ .alt = true } }, .{ .close_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .t, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .t }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .next_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .page_up, .mods = .{ .ctrl = true } }, + .{ .key = .{ .translated = .page_up }, .mods = .{ .ctrl = true } }, .{ .previous_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .page_down, .mods = .{ .ctrl = true } }, + .{ .key = .{ .translated = .page_down }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .o, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .o }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .right }, ); try result.keybind.set.put( alloc, - .{ .key = .e, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .e }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .down }, ); try result.keybind.set.put( alloc, - .{ .key = .left_bracket, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .translated = .left_bracket }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, ); try result.keybind.set.put( alloc, - .{ .key = .right_bracket, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .translated = .right_bracket }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, ); try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .translated = .up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .top }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .translated = .down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .bottom }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .left }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .translated = .right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .right }, ); // Resizing splits try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .up, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .down, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .left, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .right, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .equal, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .translated = .equal }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .equalize_splits = {} }, ); // Viewport scrolling try result.keybind.set.put( alloc, - .{ .key = .home, .mods = .{ .shift = true } }, + .{ .key = .{ .translated = .home }, .mods = .{ .shift = true } }, .{ .scroll_to_top = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .end, .mods = .{ .shift = true } }, + .{ .key = .{ .translated = .end }, .mods = .{ .shift = true } }, .{ .scroll_to_bottom = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .page_up, .mods = .{ .shift = true } }, + .{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } }, .{ .scroll_page_up = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .page_down, .mods = .{ .shift = true } }, + .{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } }, .{ .scroll_page_down = {} }, ); // Semantic prompts try result.keybind.set.put( alloc, - .{ .key = .page_up, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = -1 }, ); try result.keybind.set.put( alloc, - .{ .key = .page_down, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = 1 }, ); // Inspector, matching Chromium try result.keybind.set.put( alloc, - .{ .key = .i, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .translated = .i }, .mods = .{ .shift = true, .ctrl = true } }, .{ .inspector = .toggle }, ); // Terminal try result.keybind.set.put( alloc, - .{ .key = .a, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .translated = .a }, .mods = .{ .shift = true, .ctrl = true } }, .{ .select_all = {} }, ); // Selection clipboard paste try result.keybind.set.put( alloc, - .{ .key = .insert, .mods = .{ .shift = true } }, + .{ .key = .{ .translated = .insert }, .mods = .{ .shift = true } }, .{ .paste_from_selection = {} }, ); } @@ -1344,15 +1344,17 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { try result.keybind.set.put( alloc, .{ - .key = @enumFromInt(i), - .mods = mods, - // On macOS, we use the physical key for tab changing so // that this works across all keyboard layouts. This may // want to be true on other platforms as well but this // is definitely true on macOS so we just do it here for // now (#817) - .physical = builtin.target.isDarwin(), + .key = if (comptime builtin.target.isDarwin()) + .{ .physical = @enumFromInt(i) } + else + .{ .translated = @enumFromInt(i) }, + + .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, ); @@ -1362,14 +1364,14 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // Toggle fullscreen try result.keybind.set.put( alloc, - .{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .translated = .enter }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .toggle_fullscreen = {} }, ); // Toggle zoom a split try result.keybind.set.put( alloc, - .{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .{ .translated = .enter }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .toggle_split_zoom = {} }, ); @@ -1377,167 +1379,167 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { if (comptime builtin.target.isDarwin()) { try result.keybind.set.put( alloc, - .{ .key = .q, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .q }, .mods = .{ .super = true } }, .{ .quit = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .k, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .k }, .mods = .{ .super = true } }, .{ .clear_screen = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .a, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .a }, .mods = .{ .super = true } }, .{ .select_all = {} }, ); // Viewport scrolling try result.keybind.set.put( alloc, - .{ .key = .home, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .home }, .mods = .{ .super = true } }, .{ .scroll_to_top = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .end, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .end }, .mods = .{ .super = true } }, .{ .scroll_to_bottom = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .page_up, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .page_up }, .mods = .{ .super = true } }, .{ .scroll_page_up = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .page_down, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .page_down }, .mods = .{ .super = true } }, .{ .scroll_page_down = {} }, ); // Semantic prompts try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .shift = true } }, .{ .jump_to_prompt = -1 }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .shift = true } }, .{ .jump_to_prompt = 1 }, ); // Mac windowing try result.keybind.set.put( alloc, - .{ .key = .n, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .n }, .mods = .{ .super = true } }, .{ .new_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .w }, .mods = .{ .super = true } }, .{ .close_surface = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true } }, .{ .close_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .super = true, .shift = true, .alt = true } }, + .{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true, .alt = true } }, .{ .close_all_windows = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .t, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .t }, .mods = .{ .super = true } }, .{ .new_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .left_bracket, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .translated = .left_bracket }, .mods = .{ .super = true, .shift = true } }, .{ .previous_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .right_bracket, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true, .shift = true } }, .{ .next_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .d, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .d }, .mods = .{ .super = true } }, .{ .new_split = .right }, ); try result.keybind.set.put( alloc, - .{ .key = .d, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .translated = .d }, .mods = .{ .super = true, .shift = true } }, .{ .new_split = .down }, ); try result.keybind.set.put( alloc, - .{ .key = .left_bracket, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .left_bracket }, .mods = .{ .super = true } }, .{ .goto_split = .previous }, ); try result.keybind.set.put( alloc, - .{ .key = .right_bracket, .mods = .{ .super = true } }, + .{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true } }, .{ .goto_split = .next }, ); try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .top }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .bottom }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .translated = .left }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .left }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .translated = .right }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .right }, ); try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .up, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .down, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .translated = .left }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .left, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .translated = .right }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .right, 10 } }, ); try result.keybind.set.put( alloc, - .{ .key = .equal, .mods = .{ .shift = true, .alt = true } }, + .{ .key = .{ .translated = .equal }, .mods = .{ .shift = true, .alt = true } }, .{ .equalize_splits = {} }, ); // Inspector, matching Chromium try result.keybind.set.put( alloc, - .{ .key = .i, .mods = .{ .alt = true, .super = true } }, + .{ .key = .{ .translated = .i }, .mods = .{ .alt = true, .super = true } }, .{ .inspector = .toggle }, ); // Alternate keybind, common to Mac programs try result.keybind.set.put( alloc, - .{ .key = .f, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .translated = .f }, .mods = .{ .super = true, .ctrl = true } }, .{ .toggle_fullscreen = {} }, ); } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 200fd6d58..5640843d9 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -79,11 +79,9 @@ pub fn parse(raw_input: []const u8) !Binding { } // If the key starts with "physical" then this is an physical key. - const physical = "physical:"; - const key_part = if (std.mem.startsWith(u8, part, physical)) key_part: { - result.physical = true; - break :key_part part[physical.len..]; - } else part; + const physical_prefix = "physical:"; + const physical = std.mem.startsWith(u8, part, physical_prefix); + const key_part = if (physical) part[physical_prefix.len..] else part; // Check if its a key const keysInfo = @typeInfo(key.Key).Enum; @@ -91,14 +89,33 @@ 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 != .invalid) return Error.InvalidFormat; + if (!result.isKeyUnset()) return Error.InvalidFormat; - result.key = @field(key.Key, field.name); + const keyval = @field(key.Key, field.name); + result.key = if (physical) + .{ .physical = keyval } + else + .{ .translated = keyval }; continue :loop; } } } + // 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; } @@ -499,28 +516,80 @@ pub const Key = enum(c_int) { /// This is an extern struct because this is also used in the C API. /// /// This must be kept in sync with include/ghostty.h ghostty_input_trigger_s -pub const Trigger = extern struct { +pub const Trigger = struct { /// The key that has to be pressed for a binding to take action. - key: key.Key = .invalid, + key: Trigger.Key = .{ .translated = .invalid }, /// The key modifiers that must be active for this to match. mods: key.Mods = .{}, - /// key is the "physical" version. This is the same as mapped for - /// standard US keyboard layouts. For non-US keyboard layouts, this - /// is used to bind to a physical key location rather than a translated - /// key. - physical: bool = false, + pub const Key = union(C.Tag) { + /// key is the translated version of a key. This is the key that + /// a logical keyboard layout at the OS level would translate the + /// physical key to. For example if you use a US hardware keyboard + /// but have a Dvorak layout, the key would be the Dvorak key. + translated: key.Key, + + /// key is the "physical" version. This is the same as mapped for + /// standard US keyboard layouts. For non-US keyboard layouts, this + /// 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. + pub const C = extern struct { + tag: Tag = .translated, + key: C.Key = .{ .translated = .invalid }, + mods: key.Mods = .{}, + + 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); std.hash.autoHash(&hasher, self.key); std.hash.autoHash(&hasher, self.mods.binding()); - std.hash.autoHash(&hasher, self.physical); return hasher.final(); } + /// Convert the trigger to a C API compatible trigger. + pub fn cval(self: Trigger) C { + return .{ + .tag = self.key, + .key = switch (self.key) { + .translated => |v| .{ .translated = v }, + .physical => |v| .{ .physical = v }, + .unicode => |v| .{ .unicode = @intCast(v) }, + }, + .mods = self.mods, + }; + } + /// Format implementation for fmt package. pub fn format( self: Trigger, @@ -538,8 +607,11 @@ pub const Trigger = extern struct { if (self.mods.shift) try writer.writeAll("shift+"); // Key - if (self.physical) try writer.writeAll("physical:"); - try writer.print("{s}", .{@tagName(self.key)}); + 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}), + } } }; @@ -720,7 +792,7 @@ test "parse: triggers" { // single character try testing.expectEqual( Binding{ - .trigger = .{ .key = .a }, + .trigger = .{ .key = .{ .translated = .a } }, .action = .{ .ignore = {} }, }, try parse("a=ignore"), @@ -730,14 +802,14 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("shift+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .ctrl = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("ctrl+a=ignore")); @@ -746,7 +818,7 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true, .ctrl = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("shift+ctrl+a=ignore")); @@ -755,7 +827,7 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("a+shift=ignore")); @@ -764,17 +836,25 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .a, - .physical = true, + .key = .{ .physical = .a }, }, .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 = .{ .mods = .{ .shift = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, .consumed = false, @@ -784,8 +864,7 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .a, - .physical = true, + .key = .{ .physical = .a }, }, .action = .{ .ignore = {} }, .consumed = false, @@ -807,14 +886,14 @@ test "parse: modifier aliases" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .super = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("cmd+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .super = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("command+a=ignore")); @@ -822,14 +901,14 @@ test "parse: modifier aliases" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .alt = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("opt+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .alt = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("option+a=ignore")); @@ -837,7 +916,7 @@ test "parse: modifier aliases" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .ctrl = true }, - .key = .a, + .key = .{ .translated = .a }, }, .action = .{ .ignore = {} }, }, try parse("control+a=ignore")); @@ -855,7 +934,10 @@ test "parse: action no parameters" { // no parameters try testing.expectEqual( - Binding{ .trigger = .{ .key = .a }, .action = .{ .ignore = {} } }, + Binding{ + .trigger = .{ .key = .{ .translated = .a } }, + .action = .{ .ignore = {} }, + }, try parse("a=ignore"), ); try testing.expectError(Error.InvalidFormat, parse("a=ignore:A")); @@ -949,24 +1031,24 @@ test "set: maintains reverse mapping" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .a }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key == .a); + try testing.expect(trigger.key.translated == .a); } // should be most recent - try s.put(alloc, .{ .key = .b }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .translated = .b } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key == .b); + try testing.expect(trigger.key.translated == .b); } // removal should replace - s.remove(.{ .key = .b }); + s.remove(.{ .key = .{ .translated = .b } }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key == .a); + try testing.expect(trigger.key.translated == .a); } } @@ -977,14 +1059,14 @@ test "set: overriding a mapping updates reverse" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .a }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key == .a); + try testing.expect(trigger.key.translated == .a); } // should be most recent - try s.put(alloc, .{ .key = .a }, .{ .new_tab = {} }); + try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_tab = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }); try testing.expect(trigger == null); @@ -998,12 +1080,12 @@ test "set: consumed state" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .a }, .{ .new_window = {} }); - try testing.expect(s.getConsumed(.{ .key = .a })); + try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); + try testing.expect(s.getConsumed(.{ .key = .{ .translated = .a } })); - try s.putUnconsumed(alloc, .{ .key = .a }, .{ .new_window = {} }); - try testing.expect(!s.getConsumed(.{ .key = .a })); + try s.putUnconsumed(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); + try testing.expect(!s.getConsumed(.{ .key = .{ .translated = .a } })); - try s.put(alloc, .{ .key = .a }, .{ .new_window = {} }); - try testing.expect(s.getConsumed(.{ .key = .a })); + try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); + try testing.expect(s.getConsumed(.{ .key = .{ .translated = .a } })); }