From 1e1ad7deb95fcf244a4b42de136e4fe1c0faa25e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 10 Aug 2023 15:04:15 -0700 Subject: [PATCH] macos: use the new self-hosted translation --- macos/Sources/Ghostty/SurfaceView.swift | 31 +++----- src/apprt/embedded.zig | 100 +++++++++++++++++++----- src/input.zig | 1 + src/input/key.zig | 63 +++++++++++++++ 4 files changed, 154 insertions(+), 41 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 9cadf7e69..517fcf9b7 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -349,7 +349,15 @@ extension Ghostty { let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS keyAction(action, event: event) - self.interpretKeyEvents([event]) + // We specifically DO NOT call interpretKeyEvents because ghostty_surface_key + // automatically handles all key translation, and we don't handle any commands + // currently. + // + // It is possible that in the future we'll have to modify ghostty_surface_key + // and the embedding API so that we can call this because macOS needs to do + // some things with certain keys. I'm not sure. For now this works. + // + // self.interpretKeyEvents([event]) } override func keyUp(with event: NSEvent) { @@ -359,28 +367,7 @@ extension Ghostty { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) - let unmapped_key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID - ghostty_surface_key2(surface, action, UInt32(event.keyCode), mods) - - // 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: Menu Handlers diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 3c8ba0ff3..cb32a7ccf 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -395,6 +395,81 @@ pub const Surface = struct { }; } + pub fn key2Callback( + self: *Surface, + action: input.Action, + keycode: u32, + mods: input.Mods, + ) !void { + if (action != .press) return; + + // Translate our key using the keymap for our localized keyboard layout. + var buf: [128]u8 = undefined; + const result = try self.app.keymap.translate( + &buf, + &self.keymap_state, + @intCast(keycode), + mods, + ); + + log.warn("TRANSLATE: action={} keycode={x} dead={} key={any} key_str={s} mods={}", .{ + action, + keycode, + result.composing, + result.text, + result.text, + mods, + }); + + // If this is a dead key, then we're composing a character and + // we end processing here. We don't process keybinds for dead keys. + if (result.composing) { + // TODO: we ultimately want to update some surface state so that + // we can show the user that we're in dead key mode and the + // precomposed character. For now, we can just ignore and that + // is not incorrect behavior. + log.warn("dead key mode, currently composing", .{}); + return; + } + + // We want to get the physical unmapped key to process keybinds. + const physical_key = keycode: for (input.keycodes.entries) |entry| { + if (entry.native == keycode) break :keycode entry.key; + } else .invalid; + + // If the resulting text has length 1 then we can take its key + // and attempt to translate it to a key enum and call the key callback. + // If the length is greater than 1 then we're going to call the + // charCallback. + const key = if (result.text.len == 1) key: { + // A completed key. If the length of the key is one then we can + // attempt to translate it to a key enum and call the key callback. + break :key input.Key.fromASCII(result.text[0]) orelse physical_key; + } else .invalid; + + // If both keys are invalid then we won't call the key callback. But + // if either one is valid, we want to give it a chance. + if (key != .invalid or physical_key != .invalid) { + self.core_surface.keyCallback(action, key, physical_key, mods) catch |err| { + log.err("error in key callback err={}", .{err}); + return; + }; + } + + // Next, we want to call the char callback with each codepoint. + const view = std.unicode.Utf8View.init(result.text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + return; + }; + var it = view.iterator(); + while (it.nextCodepoint()) |cp| { + self.core_surface.charCallback(cp) catch |err| { + log.err("error in char callback err={}", .{err}); + return; + }; + } + } + pub fn charCallback(self: *Surface, cp_: u32) void { const cp = std.math.cast(u21, cp_) orelse return; self.core_surface.charCallback(cp) catch |err| { @@ -573,27 +648,14 @@ pub const CAPI = struct { keycode: u32, c_mods: c_int, ) void { - if (action != .press) return; - - var buf: [128]u8 = undefined; - const mods: input.Mods = @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(c_mods))))); - const result = surface.app.keymap.translate( - &buf, - &surface.keymap_state, - @intCast(keycode), - mods, - ) catch |err| { - log.err("TRANSLATE error translating key err={}", .{err}); - return; - }; - - log.warn("TRANSLATE: action={} keycode={x} dead={} key={any} key_str={s}", .{ + surface.key2Callback( action, keycode, - result.composing, - result.text, - result.text, - }); + @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(c_mods))))), + ) catch |err| { + log.err("error processing key event err={}", .{err}); + return; + }; } /// Tell the surface that it needs to schedule a render diff --git a/src/input.zig b/src/input.zig index d65131054..18eafdeec 100644 --- a/src/input.zig +++ b/src/input.zig @@ -2,6 +2,7 @@ const std = @import("std"); pub usingnamespace @import("input/mouse.zig"); pub usingnamespace @import("input/key.zig"); +pub const keycodes = @import("input/keycodes.zig"); pub const Binding = @import("input/Binding.zig"); pub const Keymap = @import("input/Keymap.zig"); pub const SplitDirection = Binding.Action.SplitDirection; diff --git a/src/input/key.zig b/src/input/key.zig index 21261de65..52ccc5ffd 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -180,4 +180,67 @@ pub const Key = enum(c_int) { // To support more keys (there are obviously more!) add them here // and ensure the mapping is up to date in the Window key handler. + + /// Converts an ASCII character to a key, if possible. This returns + /// null if the character is unknown. + /// + /// Note that this can't distinguish between physical keys, i.e. '0' + /// may be from the number row or the keypad, but it always maps + /// to '.zero'. + /// + /// This is what we want, we awnt people to create keybindings that + /// 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, + }; + } };