From 65cd31dc79e8dcb3746424d0ca5cb0bbcdb39dee Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:04:55 +0200 Subject: [PATCH] macOS: add NormalizedMenuShortcutKeyTests --- macos/Sources/App/macOS/AppDelegate.swift | 17 +++- .../NormalizedMenuShortcutKeyTests.swift | 93 +++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index b02337e4b..e116690e6 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1288,18 +1288,25 @@ extension AppDelegate { } /// Hashable key for a menu shortcut match, normalized for quick lookup. - private struct MenuShortcutKey: Hashable { + struct MenuShortcutKey: Hashable { private static let shortcutModifiers: NSEvent.ModifierFlags = [.shift, .control, .option, .command] - private let keyEquivalent: String - private let modifiersRawValue: UInt + let keyEquivalent: String + let modifiersRawValue: UInt init?(keyEquivalent: String, modifiers: NSEvent.ModifierFlags) { let normalized = keyEquivalent.lowercased() guard !normalized.isEmpty else { return nil } - + var mods = modifiers.intersection(Self.shortcutModifiers) + if + keyEquivalent.lowercased() != keyEquivalent.uppercased(), + normalized.uppercased() == keyEquivalent { + // If key equivalent is case sensitive and + // it's originally uppercased, then we need to add `shift` to the modifiers + mods.insert(.shift) + } self.keyEquivalent = normalized - self.modifiersRawValue = modifiers.intersection(Self.shortcutModifiers).rawValue + self.modifiersRawValue = mods.rawValue } init?(event: NSEvent) { diff --git a/macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift b/macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift new file mode 100644 index 000000000..bb61857f9 --- /dev/null +++ b/macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift @@ -0,0 +1,93 @@ +import AppKit +import Testing +@testable import Ghostty + +@Suite +struct NormalizedMenuShortcutKeyTests { + typealias Key = AppDelegate.MenuShortcutKey + + // MARK: - Init from keyEquivalent + modifiers + + @Test func returnsNilForEmptyKeyEquivalent() { + let key = Key(keyEquivalent: "", modifiers: .command) + #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]) + let expected = Key(keyEquivalent: "c", modifiers: .command) + #expect(key == expected) + } + + @Test func preservesShortcutModifiers() { + let key = Key(keyEquivalent: "c", modifiers: [.shift, .control, .option, .command]) + let allMods: NSEvent.ModifierFlags = [.shift, .control, .option, .command] + #expect(key?.modifiersRawValue == allMods.rawValue) + } + + @Test func uppercaseLetterInsertsShift() { + // "A" is uppercase and case-sensitive, so .shift should be added + let key = Key(keyEquivalent: "A", modifiers: .command) + let expected = NSEvent.ModifierFlags([.command, .shift]).rawValue + #expect(key?.modifiersRawValue == expected) + } + + @Test func lowercaseLetterDoesNotInsertShift() { + let key = Key(keyEquivalent: "a", modifiers: .command) + let expected = NSEvent.ModifierFlags.command.rawValue + #expect(key?.modifiersRawValue == expected) + } + + @Test func nonCaseSensitiveCharacterDoesNotInsertShift() { + // "1" is not case-sensitive (uppercased == lowercased is false for digits, + // but "1".uppercased() == "1".lowercased() == "1" so isCaseSensitive is false) + let key = Key(keyEquivalent: "1", modifiers: .command) + let expected = NSEvent.ModifierFlags.command.rawValue + #expect(key?.modifiersRawValue == expected) + } + + // MARK: - Equality / Hashing + + @Test func sameKeyAndModsAreEqual() { + let a = Key(keyEquivalent: "c", modifiers: .command) + let b = Key(keyEquivalent: "c", modifiers: .command) + #expect(a == b) + } + + @Test func uppercaseAndLowercaseWithShiftAreEqual() { + // "C" with .command should equal "c" with [.command, .shift] + // because the uppercase init auto-inserts .shift + let fromUpper = Key(keyEquivalent: "C", modifiers: .command) + let fromLowerWithShift = Key(keyEquivalent: "c", modifiers: [.command, .shift]) + #expect(fromUpper == fromLowerWithShift) + } + + @Test func differentKeysAreNotEqual() { + let a = Key(keyEquivalent: "a", modifiers: .command) + let b = Key(keyEquivalent: "b", modifiers: .command) + #expect(a != b) + } + + @Test func differentModifiersAreNotEqual() { + let a = Key(keyEquivalent: "c", modifiers: .command) + let b = Key(keyEquivalent: "c", modifiers: .option) + #expect(a != b) + } + + @Test func canBeUsedAsDictionaryKey() { + let key = Key(keyEquivalent: "c", modifiers: .command)! + var dict: [Key: String] = [:] + dict[key] = "copy" + #expect(dict[key] == "copy") + + // Same key created separately should find the same entry + let key2 = Key(keyEquivalent: "c", modifiers: .command)! + #expect(dict[key2] == "copy") + } +}