import AppKit import Carbon.HIToolbox import Foundation /// User-chosen capture hotkey. `modifiers` are Carbon bits (`cmdKey`, `optionKey`, /// `shiftKey`, `controlKey`), matching `HotkeyCenter.register`. struct HotkeyPreference: Codable, Equatable, Sendable { var keyCode: UInt32 var modifiers: UInt32 static let defaultsKey = "ai.flowmaster.shotdeck.hotkey" /// ⌥⇧2 — kVK_ANSI_2 (19) with optionKey|shiftKey. static let `default` = HotkeyPreference( keyCode: 19, modifiers: UInt32(optionKey) | UInt32(shiftKey) ) static func load(defaults: UserDefaults = .standard) -> HotkeyPreference { guard let data = defaults.data(forKey: defaultsKey), let decoded = try? JSONDecoder().decode(HotkeyPreference.self, from: data), decoded.modifiers != 0 else { return .default } return decoded } func save(defaults: UserDefaults = .standard) { guard let data = try? JSONEncoder().encode(self) else { return } defaults.set(data, forKey: Self.defaultsKey) } /// ⌘⌥⇧⌃ in that order, then a name for common keycodes. var displayString: String { var s = "" if modifiers & UInt32(cmdKey) != 0 { s += "⌘" } if modifiers & UInt32(optionKey) != 0 { s += "⌥" } if modifiers & UInt32(shiftKey) != 0 { s += "⇧" } if modifiers & UInt32(controlKey) != 0 { s += "⌃" } s += Self.keyName(for: keyCode) return s } /// `nil` when the event is modifier-only or has no ⌘⌥⇧⌃ flags. static func fromKeyEvent(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) -> HotkeyPreference? { switch Int(keyCode) { case kVK_Shift, kVK_RightShift, kVK_Command, kVK_RightCommand, kVK_Option, kVK_RightOption, kVK_Control, kVK_RightControl, kVK_CapsLock, kVK_Function: return nil default: break } var carbon: UInt32 = 0 if modifierFlags.contains(.command) { carbon |= UInt32(cmdKey) } if modifierFlags.contains(.option) { carbon |= UInt32(optionKey) } if modifierFlags.contains(.shift) { carbon |= UInt32(shiftKey) } if modifierFlags.contains(.control) { carbon |= UInt32(controlKey) } guard carbon != 0 else { return nil } return HotkeyPreference(keyCode: UInt32(keyCode), modifiers: carbon) } private static func keyName(for keyCode: UInt32) -> String { switch Int(keyCode) { case kVK_ANSI_A: return "A" case kVK_ANSI_B: return "B" case kVK_ANSI_C: return "C" case kVK_ANSI_D: return "D" case kVK_ANSI_E: return "E" case kVK_ANSI_F: return "F" case kVK_ANSI_G: return "G" case kVK_ANSI_H: return "H" case kVK_ANSI_I: return "I" case kVK_ANSI_J: return "J" case kVK_ANSI_K: return "K" case kVK_ANSI_L: return "L" case kVK_ANSI_M: return "M" case kVK_ANSI_N: return "N" case kVK_ANSI_O: return "O" case kVK_ANSI_P: return "P" case kVK_ANSI_Q: return "Q" case kVK_ANSI_R: return "R" case kVK_ANSI_S: return "S" case kVK_ANSI_T: return "T" case kVK_ANSI_U: return "U" case kVK_ANSI_V: return "V" case kVK_ANSI_W: return "W" case kVK_ANSI_X: return "X" case kVK_ANSI_Y: return "Y" case kVK_ANSI_Z: return "Z" case kVK_ANSI_0: return "0" case kVK_ANSI_1: return "1" case kVK_ANSI_2: return "2" case kVK_ANSI_3: return "3" case kVK_ANSI_4: return "4" case kVK_ANSI_5: return "5" case kVK_ANSI_6: return "6" case kVK_ANSI_7: return "7" case kVK_ANSI_8: return "8" case kVK_ANSI_9: return "9" case kVK_ANSI_Equal: return "=" case kVK_ANSI_Minus: return "-" case kVK_ANSI_RightBracket: return "]" case kVK_ANSI_LeftBracket: return "[" case kVK_ANSI_Quote: return "'" case kVK_ANSI_Semicolon: return ";" case kVK_ANSI_Backslash: return "\\" case kVK_ANSI_Comma: return "," case kVK_ANSI_Slash: return "/" case kVK_ANSI_Period: return "." case kVK_ANSI_Grave: return "`" case kVK_Space: return "Space" case kVK_Return: return "Return" case kVK_Tab: return "Tab" case kVK_Delete: return "Delete" case kVK_ForwardDelete: return "Fwd Delete" case kVK_Escape: return "Esc" case kVK_Home: return "Home" case kVK_End: return "End" case kVK_PageUp: return "Page Up" case kVK_PageDown: return "Page Down" case kVK_Help: return "Help" case kVK_LeftArrow: return "←" case kVK_RightArrow: return "→" case kVK_DownArrow: return "↓" case kVK_UpArrow: return "↑" case kVK_F1: return "F1" case kVK_F2: return "F2" case kVK_F3: return "F3" case kVK_F4: return "F4" case kVK_F5: return "F5" case kVK_F6: return "F6" case kVK_F7: return "F7" case kVK_F8: return "F8" case kVK_F9: return "F9" case kVK_F10: return "F10" case kVK_F11: return "F11" case kVK_F12: return "F12" case kVK_F13: return "F13" case kVK_F14: return "F14" case kVK_F15: return "F15" case kVK_F16: return "F16" case kVK_F17: return "F17" case kVK_F18: return "F18" case kVK_F19: return "F19" case kVK_F20: return "F20" default: return "Key \(keyCode)" } } }