import AppKit import SwiftUI // MARK: - Shortcut Action /// All configurable keyboard shortcut actions in MixBoard. enum ShortcutAction: String, CaseIterable, Identifiable, Codable { // Playback case playPause = "Play / Pause" case stop = "Stop" case nextTrack = "Next Track" case previousTrack = "Previous Track" case skipForward = "Skip Forward 10s" case skipBackward = "Skip Backward 10s" case toggleShuffle = "Toggle Shuffle" case toggleRepeat = "Toggle Repeat" // Mix case addToMix1 = "Add to Mix 1" case addToMix2 = "Add to Mix 2" case addToMix3 = "Add to Mix 3" // General case nowPlaying = "Now Playing" case search = "Search All Playlists" case newPlaylist = "New Playlist" case exportToDAW = "Export to DAW" case importFromiPhone = "Import from iPhone" var id: String { rawValue } var group: ShortcutGroup { switch self { case .playPause, .stop, .nextTrack, .previousTrack, .skipForward, .skipBackward, .toggleShuffle, .toggleRepeat: return .playback case .addToMix1, .addToMix2, .addToMix3: return .mix case .nowPlaying, .search, .newPlaylist, .exportToDAW, .importFromiPhone: return .general } } } /// Grouping for display in the Shortcuts settings tab. enum ShortcutGroup: String, CaseIterable { case playback = "Playback" case mix = "Mix" case general = "General" var actions: [ShortcutAction] { ShortcutAction.allCases.filter { $0.group == self } } } // MARK: - Shortcut Binding /// A stored keyboard shortcut: key + modifier flags. struct ShortcutBinding: Codable, Equatable { var key: String // Character or special key: "space", "right", "left", etc. var command: Bool = false var shift: Bool = false var option: Bool = false var control: Bool = false /// Convert to SwiftUI `KeyEquivalent`. var keyEquivalent: KeyEquivalent { switch key.lowercased() { case "space": return .space case "right": return .rightArrow case "left": return .leftArrow case "up": return .upArrow case "down": return .downArrow case "return": return .return case "tab": return .tab case "delete": return .delete case "escape": return .escape default: if let char = key.first { return KeyEquivalent(char) } return KeyEquivalent("?") } } /// Convert to SwiftUI `EventModifiers`. var eventModifiers: EventModifiers { var mods: EventModifiers = [] if command { mods.insert(.command) } if shift { mods.insert(.shift) } if option { mods.insert(.option) } if control { mods.insert(.control) } return mods } /// Human-readable display string, e.g. "⇧⌘1". var displayString: String { var parts: [String] = [] if control { parts.append("⌃") } if option { parts.append("⌥") } if shift { parts.append("⇧") } if command { parts.append("⌘") } let keyDisplay: String switch key.lowercased() { case "space": keyDisplay = "Space" case "right": keyDisplay = "→" case "left": keyDisplay = "←" case "up": keyDisplay = "↑" case "down": keyDisplay = "↓" case "return": keyDisplay = "↩" case "tab": keyDisplay = "⇥" case "delete": keyDisplay = "⌫" case "escape": keyDisplay = "⎋" default: keyDisplay = key.uppercased() } parts.append(keyDisplay) return parts.joined() } /// Build a `ShortcutBinding` from an `NSEvent`. static func from(_ event: NSEvent) -> ShortcutBinding { let key: String switch event.keyCode { case 49: key = "space" case 124: key = "right" case 123: key = "left" case 126: key = "up" case 125: key = "down" case 36: key = "return" case 48: key = "tab" case 51: key = "delete" default: key = event.charactersIgnoringModifiers?.lowercased() ?? "?" } let mods = event.modifierFlags return ShortcutBinding( key: key, command: mods.contains(.command), shift: mods.contains(.shift), option: mods.contains(.option), control: mods.contains(.control) ) } } // MARK: - Config Singleton /// Manages all keyboard shortcut bindings, persisted to UserDefaults. final class KeyboardShortcutConfig: ObservableObject { static let shared = KeyboardShortcutConfig() @Published var shortcuts: [ShortcutAction: ShortcutBinding] { didSet { save() } } /// Factory defaults. static let defaultShortcuts: [ShortcutAction: ShortcutBinding] = [ .playPause: ShortcutBinding(key: "space"), .stop: ShortcutBinding(key: ".", command: true), .nextTrack: ShortcutBinding(key: "right", command: true), .previousTrack: ShortcutBinding(key: "left", command: true), .skipForward: ShortcutBinding(key: "right", command: true, shift: true), .skipBackward: ShortcutBinding(key: "left", command: true, shift: true), .toggleShuffle: ShortcutBinding(key: "s", command: true, shift: true), .toggleRepeat: ShortcutBinding(key: "r", command: true, shift: true), .addToMix1: ShortcutBinding(key: "1", command: true), .addToMix2: ShortcutBinding(key: "2", command: true), .addToMix3: ShortcutBinding(key: "3", command: true), .nowPlaying: ShortcutBinding(key: "p", command: true, shift: true), .search: ShortcutBinding(key: "f", command: true), .newPlaylist: ShortcutBinding(key: "n", command: true, shift: true), .exportToDAW: ShortcutBinding(key: "e", command: true), .importFromiPhone: ShortcutBinding(key: "i", command: true, shift: true), ] init() { shortcuts = Self.load() } /// Get the binding for an action (with fallback to default). func binding(for action: ShortcutAction) -> ShortcutBinding { shortcuts[action] ?? Self.defaultShortcuts[action]! } func resetToDefaults() { shortcuts = Self.defaultShortcuts } // MARK: - Persistence private func save() { let stringKeyed = Dictionary(uniqueKeysWithValues: shortcuts.map { ($0.key.rawValue, $0.value) }) if let data = try? JSONEncoder().encode(stringKeyed) { UserDefaults.standard.set(data, forKey: "keyboardShortcuts") } } private static func load() -> [ShortcutAction: ShortcutBinding] { guard let data = UserDefaults.standard.data(forKey: "keyboardShortcuts"), let stringKeyed = try? JSONDecoder().decode([String: ShortcutBinding].self, from: data) else { return defaultShortcuts } // Merge with defaults so new actions added in updates get their fallback var result = defaultShortcuts for (key, value) in stringKeyed { if let action = ShortcutAction(rawValue: key) { result[action] = value } } return result } }