macOS: update default behaviour of KeyboardLayout.character(for:modifiers:) (#14009)

Follow up for #13888, and prepare for #13205.

The comments are copied from the history commit.

## AI Disclosure

The tests are updated by Claude, I cherrypicked them.
This commit is contained in:
Mitchell Hashimoto
2026-08-25 05:41:01 -07:00
committed by GitHub
3 changed files with 11 additions and 17 deletions

View File

@@ -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.

View File

@@ -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 }

View File

@@ -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())
}
}