diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index 5c13003b3..754bb7a3a 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -6,7 +6,13 @@ extension NSEvent { /// /// This will not set the "text" or "composing" fields since these can't safely be set /// with the information or lifetimes given. - func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s { + /// + /// The translationMods should be set to the modifiers used for actual character + /// translation if available. + func ghosttyKeyEvent( + _ action: ghostty_input_action_e, + translationMods: NSEvent.ModifierFlags? = nil + ) -> ghostty_input_key_s { var key_ev: ghostty_input_key_s = .init() key_ev.action = action key_ev.keycode = UInt32(keyCode) @@ -22,15 +28,19 @@ extension NSEvent { // so far: control and command never contribute to the translation of text, // assume everything else did. key_ev.mods = Ghostty.ghosttyMods(modifierFlags) - key_ev.consumed_mods = Ghostty.ghosttyMods(modifierFlags.subtracting([.control, .command])) + key_ev.consumed_mods = Ghostty.ghosttyMods( + (translationMods ?? modifierFlags) + .subtracting([.control, .command])) // Our unshifted codepoint is the codepoint with no modifiers. We // ignore multi-codepoint values. key_ev.unshifted_codepoint = 0 - if let charactersIgnoringModifiers, - let codepoint = charactersIgnoringModifiers.unicodeScalars.first - { - key_ev.unshifted_codepoint = codepoint.value + if type == .keyDown || type == .keyUp { + if let charactersIgnoringModifiers, + let codepoint = charactersIgnoringModifiers.unicodeScalars.first + { + key_ev.unshifted_codepoint = codepoint.value + } } return key_ev diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 52314f534..574c88044 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -961,13 +961,19 @@ extension Ghostty { // These never have "composing" set to true because these are the // result of a composition. for text in list { - _ = keyAction(action, event: translationEvent, text: text) + _ = keyAction( + action, + event: event, + translationEvent: translationEvent, + text: text + ) } } else { // We have no accumulated text so this is a normal key event. _ = keyAction( action, - event: translationEvent, + event: event, + translationEvent: translationEvent, text: translationEvent.ghosttyCharacters, composing: markedText.length > 0 ) @@ -1040,16 +1046,6 @@ extension Ghostty { let equivalent: String switch (event.charactersIgnoringModifiers) { - case "/": - // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep - // sound and we don't like the beep sound. - if (!event.modifierFlags.contains(.control) || - !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { - return false - } - - equivalent = "_" - case "\r": // Pass C- through verbatim // (prevent the default context menu equivalent) @@ -1165,12 +1161,13 @@ extension Ghostty { private func keyAction( _ action: ghostty_input_action_e, event: NSEvent, + translationEvent: NSEvent? = nil, text: String? = nil, composing: Bool = false ) -> Bool { guard let surface = self.surface else { return false } - var key_ev = event.ghosttyKeyEvent(action) + var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing if let text { return text.withCString { ptr in diff --git a/src/input/key.zig b/src/input/key.zig index f9db4a04a..ec65170f2 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -150,21 +150,25 @@ pub const Mods = packed struct(Mods.Backing) { /// like macos-option-as-alt. The translation mods should be used for /// translation but never sent back in for the key callback. pub fn translation(self: Mods, option_as_alt: config.OptionAsAlt) Mods { - // We currently only process macos-option-as-alt so other - // platforms don't need to do anything. - if (comptime !builtin.target.os.tag.isDarwin()) return self; + var result = self; - // Alt has to be set only on the correct side - switch (option_as_alt) { - .false => return self, - .true => {}, - .left => if (self.sides.alt == .right) return self, - .right => if (self.sides.alt == .left) return self, + // Control is never used for translation. + result.ctrl = false; + + // macos-option-as-alt for darwin + if (comptime builtin.target.os.tag.isDarwin()) alt: { + // Alt has to be set only on the correct side + switch (option_as_alt) { + .false => break :alt, + .true => {}, + .left => if (self.sides.alt == .right) break :alt, + .right => if (self.sides.alt == .left) break :alt, + } + + // Unset alt + result.alt = false; } - // Unset alt - var result = self; - result.alt = false; return result; } @@ -186,6 +190,14 @@ pub const Mods = packed struct(Mods.Backing) { ); } + test "translation removes control" { + const testing = std.testing; + + const mods: Mods = .{ .ctrl = true }; + const result = mods.translation(.true); + try testing.expectEqual(Mods{}, result); + } + test "translation macos-option-as-alt" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;