import AppKit import Carbon import Foundation public enum ShortcutScope { case global case whilePluginActive } public struct ShortcutModifiers: OptionSet, Hashable, Codable, Sendable { public let rawValue: UInt8 public static let command = ShortcutModifiers(rawValue: 1 << 0) public static let control = ShortcutModifiers(rawValue: 1 << 1) public static let option = ShortcutModifiers(rawValue: 1 << 2) public static let shift = ShortcutModifiers(rawValue: 1 << 3) public init(rawValue: UInt8) { self.rawValue = rawValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.init(rawValue: try container.decode(UInt8.self)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(rawValue) } public var carbonFlags: UInt32 { var flags: UInt32 = 0 if contains(.command) { flags |= UInt32(cmdKey) } if contains(.control) { flags |= UInt32(controlKey) } if contains(.option) { flags |= UInt32(optionKey) } if contains(.shift) { flags |= UInt32(shiftKey) } return flags } public var symbolString: String { var output = "" if contains(.control) { output += "⌃" } if contains(.option) { output += "⌥" } if contains(.shift) { output += "⇧" } if contains(.command) { output += "⌘" } return output } } public struct ShortcutBinding: Hashable, Codable, Sendable { public let keyCode: UInt16 public let modifiers: ShortcutModifiers public init(keyCode: UInt16, modifiers: ShortcutModifiers) { self.keyCode = keyCode self.modifiers = modifiers } public var isValid: Bool { !modifiers.isEmpty && !ShortcutKeyCode.isModifier(keyCode) } } public enum ShortcutCustomization: Equatable, Codable { case inheritDefault case custom(ShortcutBinding) case cleared private enum CodingKeys: String, CodingKey { case kind case binding } private enum Kind: String, Codable { case inheritDefault case custom case cleared } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let kind = try container.decode(Kind.self, forKey: .kind) switch kind { case .inheritDefault: self = .inheritDefault case .custom: self = .custom(try container.decode(ShortcutBinding.self, forKey: .binding)) case .cleared: self = .cleared } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .inheritDefault: try container.encode(Kind.inheritDefault, forKey: .kind) case let .custom(binding): try container.encode(Kind.custom, forKey: .kind) try container.encode(binding, forKey: .binding) case .cleared: try container.encode(Kind.cleared, forKey: .kind) } } } public struct PluginShortcutDefinition: Identifiable { public let id: String public let title: String public let description: String public let actionID: String public let scope: ShortcutScope public let defaultBinding: ShortcutBinding? public let isRequired: Bool public let sharedBindingGroupID: String? public let settingsGroupID: String? public let settingsGroupTitle: String? public let settingsGroupDescription: String? public let settingsControlTitle: String? public let settingsControlSystemImage: String? public init( id: String, title: String, description: String, actionID: String, scope: ShortcutScope, defaultBinding: ShortcutBinding?, isRequired: Bool, sharedBindingGroupID: String? = nil, settingsGroupID: String? = nil, settingsGroupTitle: String? = nil, settingsGroupDescription: String? = nil, settingsControlTitle: String? = nil, settingsControlSystemImage: String? = nil ) { self.id = id self.title = title self.description = description self.actionID = actionID self.scope = scope self.defaultBinding = defaultBinding self.isRequired = isRequired self.sharedBindingGroupID = sharedBindingGroupID self.settingsGroupID = settingsGroupID self.settingsGroupTitle = settingsGroupTitle self.settingsGroupDescription = settingsGroupDescription self.settingsControlTitle = settingsControlTitle self.settingsControlSystemImage = settingsControlSystemImage } } public struct ShortcutSettingsItem: Identifiable { public let id: String public let pluginID: String public let pluginTitle: String public let title: String public let description: String public let bindingText: String public let isRequired: Bool public let canClear: Bool public let usesDefaultValue: Bool public let errorMessage: String? public let settingsGroupID: String? public let settingsGroupTitle: String? public let settingsGroupDescription: String? public let settingsControlTitle: String? public let settingsControlSystemImage: String? public init( id: String, pluginID: String, pluginTitle: String, title: String, description: String, bindingText: String, isRequired: Bool, canClear: Bool, usesDefaultValue: Bool, errorMessage: String?, settingsGroupID: String? = nil, settingsGroupTitle: String? = nil, settingsGroupDescription: String? = nil, settingsControlTitle: String? = nil, settingsControlSystemImage: String? = nil ) { self.id = id self.pluginID = pluginID self.pluginTitle = pluginTitle self.title = title self.description = description self.bindingText = bindingText self.isRequired = isRequired self.canClear = canClear self.usesDefaultValue = usesDefaultValue self.errorMessage = errorMessage self.settingsGroupID = settingsGroupID self.settingsGroupTitle = settingsGroupTitle self.settingsGroupDescription = settingsGroupDescription self.settingsControlTitle = settingsControlTitle self.settingsControlSystemImage = settingsControlSystemImage } } public enum ShortcutValidationError: LocalizedError { case missingModifier case modifierOnly case requiredShortcut case duplicate(ownerDescription: String) public var errorDescription: String? { switch self { case .missingModifier: return PluginKitLocalization.shortcutValidationMissingModifier case .modifierOnly: return PluginKitLocalization.shortcutValidationModifierOnly case .requiredShortcut: return PluginKitLocalization.shortcutValidationRequiredShortcut case let .duplicate(ownerDescription): return PluginKitLocalization.shortcutValidationDuplicate(ownerDescription: ownerDescription) } } } public enum ShortcutFormatter { public static func displayString(for binding: ShortcutBinding?) -> String { guard let binding else { return "None" } return displayTokens(for: binding).joined(separator: " + ") } public static func displayTokens(for binding: ShortcutBinding) -> [String] { var tokens: [String] = [] if binding.modifiers.contains(.control) { tokens.append("⌃") } if binding.modifiers.contains(.option) { tokens.append("⌥") } if binding.modifiers.contains(.shift) { tokens.append("⇧") } if binding.modifiers.contains(.command) { tokens.append("⌘") } tokens.append(keyDisplayName(for: binding.keyCode)) return tokens } public static func keyDisplayName(for keyCode: UInt16) -> String { switch keyCode { case UInt16(kVK_ANSI_A): return "A" case UInt16(kVK_ANSI_B): return "B" case UInt16(kVK_ANSI_C): return "C" case UInt16(kVK_ANSI_D): return "D" case UInt16(kVK_ANSI_E): return "E" case UInt16(kVK_ANSI_F): return "F" case UInt16(kVK_ANSI_G): return "G" case UInt16(kVK_ANSI_H): return "H" case UInt16(kVK_ANSI_I): return "I" case UInt16(kVK_ANSI_J): return "J" case UInt16(kVK_ANSI_K): return "K" case UInt16(kVK_ANSI_L): return "L" case UInt16(kVK_ANSI_M): return "M" case UInt16(kVK_ANSI_N): return "N" case UInt16(kVK_ANSI_O): return "O" case UInt16(kVK_ANSI_P): return "P" case UInt16(kVK_ANSI_Q): return "Q" case UInt16(kVK_ANSI_R): return "R" case UInt16(kVK_ANSI_S): return "S" case UInt16(kVK_ANSI_T): return "T" case UInt16(kVK_ANSI_U): return "U" case UInt16(kVK_ANSI_V): return "V" case UInt16(kVK_ANSI_W): return "W" case UInt16(kVK_ANSI_X): return "X" case UInt16(kVK_ANSI_Y): return "Y" case UInt16(kVK_ANSI_Z): return "Z" case UInt16(kVK_ANSI_0): return "0" case UInt16(kVK_ANSI_1): return "1" case UInt16(kVK_ANSI_2): return "2" case UInt16(kVK_ANSI_3): return "3" case UInt16(kVK_ANSI_4): return "4" case UInt16(kVK_ANSI_5): return "5" case UInt16(kVK_ANSI_6): return "6" case UInt16(kVK_ANSI_7): return "7" case UInt16(kVK_ANSI_8): return "8" case UInt16(kVK_ANSI_9): return "9" case UInt16(kVK_ANSI_Minus): return "-" case UInt16(kVK_ANSI_Equal): return "=" case UInt16(kVK_ANSI_LeftBracket): return "[" case UInt16(kVK_ANSI_RightBracket): return "]" case UInt16(kVK_ANSI_Backslash): return "\\" case UInt16(kVK_ANSI_Semicolon): return ";" case UInt16(kVK_ANSI_Quote): return "'" case UInt16(kVK_ANSI_Comma): return "," case UInt16(kVK_ANSI_Period): return "." case UInt16(kVK_ANSI_Slash): return "/" case UInt16(kVK_ANSI_Grave): return "`" case UInt16(kVK_Return): return "↩" case UInt16(kVK_Tab): return "⇥" case UInt16(kVK_Space): return "␣" case UInt16(kVK_Delete): return "⌫" case UInt16(kVK_ForwardDelete): return "⌦" case UInt16(kVK_Escape): return "ESC" case UInt16(kVK_LeftArrow): return "←" case UInt16(kVK_RightArrow): return "→" case UInt16(kVK_UpArrow): return "↑" case UInt16(kVK_DownArrow): return "↓" case UInt16(kVK_Home): return "↖" case UInt16(kVK_End): return "↘" case UInt16(kVK_PageUp): return "⇞" case UInt16(kVK_PageDown): return "⇟" case UInt16(kVK_Help): return "Help" case UInt16(kVK_F1): return "F1" case UInt16(kVK_F2): return "F2" case UInt16(kVK_F3): return "F3" case UInt16(kVK_F4): return "F4" case UInt16(kVK_F5): return "F5" case UInt16(kVK_F6): return "F6" case UInt16(kVK_F7): return "F7" case UInt16(kVK_F8): return "F8" case UInt16(kVK_F9): return "F9" case UInt16(kVK_F10): return "F10" case UInt16(kVK_F11): return "F11" case UInt16(kVK_F12): return "F12" case UInt16(kVK_F13): return "F13" case UInt16(kVK_F14): return "F14" case UInt16(kVK_F15): return "F15" case UInt16(kVK_F16): return "F16" case UInt16(kVK_F17): return "F17" case UInt16(kVK_F18): return "F18" case UInt16(kVK_F19): return "F19" case UInt16(kVK_F20): return "F20" default: return "Key \(keyCode)" } } } public enum ShortcutKeyCode { public static let escape = UInt16(kVK_Escape) private static let modifierKeyCodes: Set = [ UInt16(kVK_Command), UInt16(kVK_RightCommand), UInt16(kVK_Shift), UInt16(kVK_RightShift), UInt16(kVK_Option), UInt16(kVK_RightOption), UInt16(kVK_Control), UInt16(kVK_RightControl), UInt16(kVK_CapsLock), 63 ] public static func isModifier(_ keyCode: UInt16) -> Bool { modifierKeyCodes.contains(keyCode) } } public extension ShortcutModifiers { static func from(_ flags: NSEvent.ModifierFlags) -> ShortcutModifiers { var modifiers: ShortcutModifiers = [] let normalizedFlags = flags.intersection(.deviceIndependentFlagsMask) if normalizedFlags.contains(.command) { modifiers.insert(.command) } if normalizedFlags.contains(.control) { modifiers.insert(.control) } if normalizedFlags.contains(.option) { modifiers.insert(.option) } if normalizedFlags.contains(.shift) { modifiers.insert(.shift) } return modifiers } static func from(_ flags: CGEventFlags) -> ShortcutModifiers { var modifiers: ShortcutModifiers = [] if flags.contains(.maskCommand) { modifiers.insert(.command) } if flags.contains(.maskControl) { modifiers.insert(.control) } if flags.contains(.maskAlternate) { modifiers.insert(.option) } if flags.contains(.maskShift) { modifiers.insert(.shift) } return modifiers } } public extension NSEvent { var shortcutBindingCandidate: ShortcutBinding? { ShortcutBinding( keyCode: keyCode, modifiers: ShortcutModifiers.from(modifierFlags) ) } }