diff --git a/include/ghostty.h b/include/ghostty.h index 96f7de272..572a0eaf5 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -261,7 +261,7 @@ void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); -void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e); +void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_key_e, ghostty_input_mods_e); void ghostty_surface_char(ghostty_surface_t, uint32_t); void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e); void ghostty_surface_mouse_pos(ghostty_surface_t, double, double); diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index ea0b7fd50..535fa1e67 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -75,3 +75,6 @@ extension Ghostty.Notification { static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit") static let SplitDirectionKey = ghosttyFocusSplit.rawValue } + +// Make the input enum hashable. +extension ghostty_input_key_e : Hashable {} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 7afc826b6..1bbf15fe4 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -302,20 +302,39 @@ extension Ghostty { } override func keyDown(with event: NSEvent) { - guard let surface = self.surface else { return } - let key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID - let mods = Self.translateFlags(event.modifierFlags) let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS - ghostty_surface_key(surface, action, key, mods) + keyAction(action, event: event) self.interpretKeyEvents([event]) } override func keyUp(with event: NSEvent) { + keyAction(GHOSTTY_ACTION_RELEASE, event: event) + } + + private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let surface = self.surface else { return } - let key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID let mods = Self.translateFlags(event.modifierFlags) - ghostty_surface_key(surface, GHOSTTY_ACTION_RELEASE, key, mods) + let unmapped_key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID + + // We translate the key to the localized keyboard layout. However, we only support + // ASCII characters to make our translation easier across platforms. This is something + // we want to make a lot more robust in the future, so this will hopefully change. + // For now, this makes most keyboard layouts work, and for those that don't, they can + // use physical keycode mappings. + let key = { + if let str = event.characters(byApplyingModifiers: .init(rawValue: 0)) { + if str.utf8.count == 1, let firstByte = str.utf8.first { + if let translatedKey = Self.ascii[firstByte] { + return translatedKey + } + } + } + + return unmapped_key + }() + + ghostty_surface_key(surface, action, key, unmapped_key, mods) } // MARK: NSTextInputClient @@ -536,6 +555,89 @@ extension Ghostty { 0x43: GHOSTTY_KEY_KP_MULTIPLY, 0x4E: GHOSTTY_KEY_KP_SUBTRACT, ]; + + static let ascii: [UInt8 : ghostty_input_key_e] = [ + // 0-9 + 0x30: GHOSTTY_KEY_ZERO, + 0x31: GHOSTTY_KEY_ONE, + 0x32: GHOSTTY_KEY_TWO, + 0x33: GHOSTTY_KEY_THREE, + 0x34: GHOSTTY_KEY_FOUR, + 0x35: GHOSTTY_KEY_FIVE, + 0x36: GHOSTTY_KEY_SIX, + 0x37: GHOSTTY_KEY_SEVEN, + 0x38: GHOSTTY_KEY_EIGHT, + 0x39: GHOSTTY_KEY_NINE, + + // A-Z + 0x41: GHOSTTY_KEY_A, + 0x42: GHOSTTY_KEY_B, + 0x43: GHOSTTY_KEY_C, + 0x44: GHOSTTY_KEY_D, + 0x45: GHOSTTY_KEY_E, + 0x46: GHOSTTY_KEY_F, + 0x47: GHOSTTY_KEY_G, + 0x48: GHOSTTY_KEY_H, + 0x49: GHOSTTY_KEY_I, + 0x4A: GHOSTTY_KEY_J, + 0x4B: GHOSTTY_KEY_K, + 0x4C: GHOSTTY_KEY_L, + 0x4D: GHOSTTY_KEY_M, + 0x4E: GHOSTTY_KEY_N, + 0x4F: GHOSTTY_KEY_O, + 0x50: GHOSTTY_KEY_P, + 0x51: GHOSTTY_KEY_Q, + 0x52: GHOSTTY_KEY_R, + 0x53: GHOSTTY_KEY_S, + 0x54: GHOSTTY_KEY_T, + 0x55: GHOSTTY_KEY_U, + 0x56: GHOSTTY_KEY_V, + 0x57: GHOSTTY_KEY_W, + 0x58: GHOSTTY_KEY_X, + 0x59: GHOSTTY_KEY_Y, + 0x5A: GHOSTTY_KEY_Z, + + // a-z + 0x61: GHOSTTY_KEY_A, + 0x62: GHOSTTY_KEY_B, + 0x63: GHOSTTY_KEY_C, + 0x64: GHOSTTY_KEY_D, + 0x65: GHOSTTY_KEY_E, + 0x66: GHOSTTY_KEY_F, + 0x67: GHOSTTY_KEY_G, + 0x68: GHOSTTY_KEY_H, + 0x69: GHOSTTY_KEY_I, + 0x6A: GHOSTTY_KEY_J, + 0x6B: GHOSTTY_KEY_K, + 0x6C: GHOSTTY_KEY_L, + 0x6D: GHOSTTY_KEY_M, + 0x6E: GHOSTTY_KEY_N, + 0x6F: GHOSTTY_KEY_O, + 0x70: GHOSTTY_KEY_P, + 0x71: GHOSTTY_KEY_Q, + 0x72: GHOSTTY_KEY_R, + 0x73: GHOSTTY_KEY_S, + 0x74: GHOSTTY_KEY_T, + 0x75: GHOSTTY_KEY_U, + 0x76: GHOSTTY_KEY_V, + 0x77: GHOSTTY_KEY_W, + 0x78: GHOSTTY_KEY_X, + 0x79: GHOSTTY_KEY_Y, + 0x7A: GHOSTTY_KEY_Z, + + // Symbols + 0x27: GHOSTTY_KEY_APOSTROPHE, + 0x5C: GHOSTTY_KEY_BACKSLASH, + 0x2C: GHOSTTY_KEY_COMMA, + 0x3D: GHOSTTY_KEY_EQUAL, + 0x60: GHOSTTY_KEY_GRAVE_ACCENT, + 0x5B: GHOSTTY_KEY_LEFT_BRACKET, + 0x2D: GHOSTTY_KEY_MINUS, + 0x2E: GHOSTTY_KEY_PERIOD, + 0x5D: GHOSTTY_KEY_RIGHT_BRACKET, + 0x3B: GHOSTTY_KEY_SEMICOLON, + 0x2F: GHOSTTY_KEY_SLASH, + ] } } diff --git a/src/Surface.zig b/src/Surface.zig index 80fd87ac6..50fe9f970 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -850,6 +850,7 @@ pub fn keyCallback( self: *Surface, action: input.Action, key: input.Key, + unmapped_key: input.Key, mods: input.Mods, ) !void { const tracy = trace(@src()); @@ -870,13 +871,24 @@ pub fn keyCallback( self.ignore_char = false; if (action == .press or action == .repeat) { - const trigger: input.Binding.Trigger = .{ - .mods = mods, - .key = key, + const binding_action_: ?input.Binding.Action = action: { + var trigger: input.Binding.Trigger = .{ + .mods = mods, + .key = key, + }; + //log.warn("BINDING TRIGGER={}", .{trigger}); + + const set = self.config.keybind.set; + if (set.get(trigger)) |v| break :action v; + + trigger.key = unmapped_key; + trigger.unmapped = true; + if (set.get(trigger)) |v| break :action v; + + break :action null; }; - //log.warn("BINDING TRIGGER={}", .{trigger}); - if (self.config.keybind.set.get(trigger)) |binding_action| { + if (binding_action_) |binding_action| { //log.warn("BINDING ACTION={}", .{binding_action}); switch (binding_action) { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 08fd04a5d..40c159c89 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -324,10 +324,11 @@ pub const Surface = struct { self: *Surface, action: input.Action, key: input.Key, + unmapped_key: input.Key, mods: input.Mods, ) void { // log.warn("key action={} key={} mods={}", .{ action, key, mods }); - self.core_surface.keyCallback(action, key, mods) catch |err| { + self.core_surface.keyCallback(action, key, unmapped_key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; @@ -460,11 +461,13 @@ pub const CAPI = struct { surface: *Surface, action: input.Action, key: input.Key, + unmapped_key: input.Key, mods: c_int, ) void { surface.keyCallback( action, key, + unmapped_key, @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), ); } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index dfe758bf9..a71399cd9 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -683,8 +683,10 @@ pub const Surface = struct { => .invalid, }; + // TODO: we need to do mapped keybindings + const core_win = window.getUserPointer(CoreSurface) orelse return; - core_win.keyCallback(action, key, mods) catch |err| { + core_win.keyCallback(action, key, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 5d6937aa7..0c88c62f9 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -935,7 +935,7 @@ pub const Surface = struct { const key = translateKey(keyval); const mods = translateMods(state); log.debug("key-press code={} key={} mods={}", .{ keycode, key, mods }); - self.core_surface.keyCallback(.press, key, mods) catch |err| { + self.core_surface.keyCallback(.press, key, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return 0; }; @@ -965,7 +965,7 @@ pub const Surface = struct { const key = translateKey(keyval); const mods = translateMods(state); const self = userdataSelf(ud.?); - self.core_surface.keyCallback(.release, key, mods) catch |err| { + self.core_surface.keyCallback(.release, key, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return 0; }; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 746ccbff7..9e4341f56 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -53,11 +53,18 @@ pub fn parse(input: []const u8) !Binding { } } + // If the key starts with "unmapped" then this is an unmapped key. + const unmapped_prefix = "unmapped:"; + const key_part = if (std.mem.startsWith(u8, part, unmapped_prefix)) key_part: { + result.unmapped = true; + break :key_part part[unmapped_prefix.len..]; + } else part; + // Check if its a key const keysInfo = @typeInfo(key.Key).Enum; inline for (keysInfo.fields) |field| { if (!std.mem.eql(u8, field.name, "invalid")) { - if (std.mem.eql(u8, part, field.name)) { + if (std.mem.eql(u8, key_part, field.name)) { // Repeat not allowed if (result.key != .invalid) return Error.InvalidFormat; @@ -237,11 +244,18 @@ pub const Trigger = struct { /// The key modifiers that must be active for this to match. mods: key.Mods = .{}, + /// key is the "unmapped" 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. + unmapped: bool = false, + /// Returns a hash code that can be used to uniquely identify this trigger. pub fn hash(self: Binding) u64 { var hasher = std.hash.Wyhash.init(0); std.hash.autoHash(&hasher, self.key); std.hash.autoHash(&hasher, self.mods); + std.hash.autoHash(&hasher, self.unmapped); return hasher.final(); } }; @@ -326,6 +340,16 @@ test "parse: triggers" { .action = .{ .ignore = {} }, }, try parse("a+shift=ignore")); + // unmapped keys + try testing.expectEqual(Binding{ + .trigger = .{ + .mods = .{ .shift = true }, + .key = .a, + .unmapped = true, + }, + .action = .{ .ignore = {} }, + }, try parse("shift+unmapped:a=ignore")); + // invalid key try testing.expectError(Error.InvalidFormat, parse("foo=ignore"));