diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 570043832..7a7371b89 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -29,9 +29,11 @@ extension Ghostty { Self.writingSystemKeyRange.contains(physical.rawValue), let inputKey = Input.Key(cKey: physical), let keyCode = inputKey.keyCode, + // Command can select a distinct layout table. Other modifiers remain + // separate in the menu's modifier mask and must not affect this character. let character = KeyboardLayout.character( for: keyCode, - modifiers: modifierFlags) + modifiers: modifierFlags.intersection(.command)) else { return nil } // Printable physical keys must be translated through the current layout. diff --git a/macos/Sources/Helpers/KeyboardLayout.swift b/macos/Sources/Helpers/KeyboardLayout.swift index f8dd5b30f..7dd20599f 100644 --- a/macos/Sources/Helpers/KeyboardLayout.swift +++ b/macos/Sources/Helpers/KeyboardLayout.swift @@ -16,6 +16,8 @@ class KeyboardLayout { /// Translate a physical keycode for use as a menu key equivalent. /// /// AppKit retranslates against the current input source without changing its dead key state. + /// + /// - Important: Must be called on the main thread because underlying Text Input Sources APIs are not thread-safe. @MainActor static func character( for keyCode: UInt16, modifiers: NSEvent.ModifierFlags @@ -32,7 +34,7 @@ class KeyboardLayout { charactersIgnoringModifiers: "", isARepeat: false, keyCode: keyCode), - let result = event.characters(byApplyingModifiers: modifiers.intersection(.command)), + let result = event.characters(byApplyingModifiers: modifiers), result.count == 1 else { return nil } diff --git a/macos/Tests/Helpers/KeyboardLayoutTests.swift b/macos/Tests/Helpers/KeyboardLayoutTests.swift index 15c58928b..56229503b 100644 --- a/macos/Tests/Helpers/KeyboardLayoutTests.swift +++ b/macos/Tests/Helpers/KeyboardLayoutTests.swift @@ -17,21 +17,11 @@ struct KeyboardLayoutTests { #expect(KeyboardLayout.character(for: UInt16.max, modifiers: []) == nil) } - @Test(arguments: [ - ([.shift, .control, .option], []), - ([.command, .shift, .control, .option], .command), - ] as [(NSEvent.ModifierFlags, NSEvent.ModifierFlags)]) - func characterUsesOnlyCommandModifier( - modifiers: NSEvent.ModifierFlags, - effectiveModifiers: NSEvent.ModifierFlags - ) throws { + @Test func characterAppliesModifiers() throws { let keyCode: UInt16 = 0x00 // W3C KeyA - let expected = try #require(KeyboardLayout.character( - for: keyCode, - modifiers: effectiveModifiers)) - let actual = try #require(KeyboardLayout.character( - for: keyCode, - modifiers: modifiers)) - #expect(actual == expected) + let unmodified = try #require(KeyboardLayout.character(for: keyCode, modifiers: [])) + let shifted = try #require(KeyboardLayout.character(for: keyCode, modifiers: .shift)) + #expect(shifted != unmodified) + #expect(String(shifted).lowercased() == String(unmodified).lowercased()) } }