macos: simplify menu shortcut identity

Separate menu shortcut presentation from lookup identity. Store either a
normalized key equivalent or a physical keycode in a private hashable enum,
allowing Swift to synthesize equality and hashing instead of maintaining
parallel optional-key logic.

Assign display characters directly from KeyboardShortcut and remove unused
NSMenuItem and SwiftUI conversion helpers.
This commit is contained in:
Jon Parise
2026-08-18 11:49:46 -04:00
parent 569ff3307c
commit 761696c349
2 changed files with 22 additions and 66 deletions

View File

@@ -103,7 +103,7 @@ private extension Ghostty.MenuShortcutManager {
return false
}
menu.keyEquivalent = key.keyEquivalent
menu.keyEquivalent = shortcut.key.character.description
menu.keyEquivalentModifierMask = key.modifierFlags
// The key equivalent was already localized from the physical keycode.
menu.allowsAutomaticKeyEquivalentLocalization = !isPhysical
@@ -118,10 +118,14 @@ private extension Ghostty.MenuShortcutManager {
extension Ghostty.MenuShortcutManager {
/// Hashable key for a menu shortcut match, normalized for quick lookup.
struct MenuShortcutKey: Hashable {
private enum Identity: Hashable {
case keyEquivalent(String)
case physicalKeyCode(UInt16)
}
private static let shortcutModifiers: NSEvent.ModifierFlags = [.shift, .control, .option, .command]
let keyEquivalent: String
private let physicalKeyCode: UInt16?
private let identity: Identity
private let modifiersRawValue: UInt
var modifierFlags: NSEvent.ModifierFlags {
@@ -139,18 +143,12 @@ extension Ghostty.MenuShortcutManager {
// it's originally uppercased, then we need to add `shift` to the modifiers
mods.insert(.shift)
}
self.keyEquivalent = normalized
self.physicalKeyCode = nil
self.identity = .keyEquivalent(normalized)
self.modifiersRawValue = mods.rawValue
}
init(
keyEquivalent: String = "",
physicalKeyCode: UInt16,
modifiers: NSEvent.ModifierFlags
) {
self.keyEquivalent = keyEquivalent
self.physicalKeyCode = physicalKeyCode
init(physicalKeyCode: UInt16, modifiers: NSEvent.ModifierFlags) {
self.identity = .physicalKeyCode(physicalKeyCode)
self.modifiersRawValue = modifiers.intersection(Self.shortcutModifiers).rawValue
}
@@ -159,55 +157,16 @@ extension Ghostty.MenuShortcutManager {
self.init(keyEquivalent: keyEquivalent, modifiers: event.modifierFlags)
}
/// Create from a `NSMenuItem`
///
/// - Important: This will check whether the `keyEquivalent` is uppercased by `.shift` modifier.
init?(_ menuItem: NSMenuItem) {
self.init(
keyEquivalent: menuItem.keyEquivalent,
modifiers: menuItem.keyEquivalentModifierMask,
)
}
/// Create from a swiftUI `KeyboardShortcut`
/// Create from a SwiftUI `KeyboardShortcut`.
init?(_ shortcut: KeyboardShortcut, physicalKeyCode: UInt16? = nil) {
// Ghostty configured shortcuts are already normalized
// in `Ghostty.keyboardShortcut(for:)`, see also gh-#12039
let keyEquivalent = shortcut.key.character.description
let modifierMask = NSEvent.ModifierFlags(swiftUIFlags: shortcut.modifiers)
let modifiers = NSEvent.ModifierFlags(swiftUIFlags: shortcut.modifiers)
if let physicalKeyCode {
self.init(
keyEquivalent: keyEquivalent,
physicalKeyCode: physicalKeyCode,
modifiers: modifierMask)
self.init(physicalKeyCode: physicalKeyCode, modifiers: modifiers)
} else {
self.init(keyEquivalent: keyEquivalent, modifiers: modifierMask)
self.init(
keyEquivalent: shortcut.key.character.description,
modifiers: modifiers)
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
guard lhs.modifiersRawValue == rhs.modifiersRawValue else { return false }
return switch (lhs.physicalKeyCode, rhs.physicalKeyCode) {
case let (.some(lhs), .some(rhs)): lhs == rhs
case (nil, nil): lhs.keyEquivalent == rhs.keyEquivalent
default: false
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(modifiersRawValue)
hasher.combine(physicalKeyCode)
if physicalKeyCode == nil {
hasher.combine(keyEquivalent)
}
}
var swiftUIShortcut: KeyboardShortcut? {
guard let character = keyEquivalent.first else { return nil }
return KeyboardShortcut(
KeyEquivalent(character),
modifiers: .init(nsFlags: modifierFlags)
)
}
}
}

View File

@@ -13,11 +13,6 @@ struct NormalizedMenuShortcutKeyTests {
#expect(key == nil)
}
@Test func lowercasesKeyEquivalent() {
let key = Key(keyEquivalent: "A", modifiers: .command)
#expect(key?.keyEquivalent == "a")
}
@Test func stripsNonShortcutModifiers() {
// .capsLock and .function should be stripped
let key = Key(keyEquivalent: "c", modifiers: [.command, .capsLock, .function])
@@ -75,12 +70,14 @@ struct NormalizedMenuShortcutKeyTests {
}
@Test func physicalKeysUseKeyCodeIdentity() {
let configured = Key(keyEquivalent: "`", physicalKeyCode: 0x32, modifiers: .command)
let event = Key(physicalKeyCode: 0x32, modifiers: .command)
let physical = Key(physicalKeyCode: 0x32, modifiers: .command)
let same = Key(physicalKeyCode: 0x32, modifiers: .command)
let different = Key(physicalKeyCode: 0x31, modifiers: .command)
let unicode = Key(keyEquivalent: "`", modifiers: .command)
#expect(configured == event)
#expect(configured != unicode)
#expect(physical == same)
#expect(physical != different)
#expect(physical != unicode)
}
@Test func differentModifiersAreNotEqual() {