import Foundation public struct KeyModifiers: OptionSet, Hashable, Sendable { public let rawValue: UInt8 public init(rawValue: UInt8) { self.rawValue = rawValue } public static let command = Self(rawValue: 1 << 0) public static let control = Self(rawValue: 1 << 1) public static let option = Self(rawValue: 1 << 2) public static let shift = Self(rawValue: 1 << 3) } public struct KeyChord: Hashable, Sendable { public let key: String public let modifiers: KeyModifiers public init(key: String, modifiers: KeyModifiers) { self.key = key self.modifiers = modifiers } public init(_ text: String) throws { let parts = text.lowercased().split(separator: "+", omittingEmptySubsequences: false) func invalid(_ reason: String) -> BindingError { BindingError(message: "Invalid chord '\(text)': \(reason)") } guard let last = parts.last, !last.isEmpty, parts.allSatisfy({ !$0.isEmpty }) else { throw invalid("empty key or modifier; use 'plus' for the + key.") } var modifiers: KeyModifiers = [] for part in parts.dropLast() { let modifier: KeyModifiers switch part { case "cmd": modifier = .command case "ctrl": modifier = .control case "alt", "option": modifier = .option case "shift": modifier = .shift default: throw invalid("unknown modifier '\(part)'.") } guard !modifiers.contains(modifier) else { throw invalid("duplicate modifier '\(part)'.") } modifiers.insert(modifier) } let key = String(last) let namedKeys: Set = ["space", "escape", "return", "tab", "backspace", "delete", "left", "right", "up", "down", "home", "end", "pageup", "pagedown", "plus"] let functionKey: Bool if key.hasPrefix("f"), let number = Int(key.dropFirst()) { functionKey = (1...35).contains(number) && key == "f\(number)" } else { functionKey = false } let printable = key.count == 1 && key != "+" && key.unicodeScalars.allSatisfy { !CharacterSet.controlCharacters.contains($0) && !CharacterSet.whitespacesAndNewlines.contains($0) } guard namedKeys.contains(key) || functionKey || printable else { throw invalid("unknown key '\(key)'.") } guard !(key == "tab" && modifiers.contains(.command)) else { throw invalid("unsupported OS-reserved cmd+tab shortcut.") } self.init(key: key == "plus" ? "+" : key, modifiers: modifiers) } public var description: String { var parts: [String] = [] if modifiers.contains(.control) { parts.append("ctrl") } if modifiers.contains(.option) { parts.append("alt") } if modifiers.contains(.shift) { parts.append("shift") } if modifiers.contains(.command) { parts.append("cmd") } parts.append(key == "+" ? "plus" : key) return parts.joined(separator: "+") } } public struct KeyBindings: Equatable, Sendable { private let byAction: [PlayerAction: [KeyChord]] private let byChord: [KeyChord: [PlayerAction]] private static let builtIn: [PlayerAction: [KeyChord]] = { let texts: [PlayerAction: [String]] = [ .play: ["k", "space"], .pause: ["k", "space"], .mute: ["m"], .unmute: ["m"], .volumeUp: ["up"], .volumeDown: ["down"], .seekForward5s: ["right"], .seekBackward5s: ["left"], .seekForward10s: ["l"], .seekBackward10s: ["j"], .stepFrameForward: ["."], .stepFrameBackward: [","], .enterFullscreen: ["f", "ctrl+cmd+f"], .exitFullscreen: ["f", "ctrl+cmd+f", "escape"], .enterCompact: ["p", "ctrl+cmd+p"], .exitCompact: ["p", "ctrl+cmd+p"], .enableAlwaysOnTop: ["shift+p"], .disableAlwaysOnTop: ["shift+p"], .showQueue: ["cmd+shift+l"], .hideQueue: ["cmd+shift+l"], .openFile: ["o", "cmd+o"], .openStream: ["cmd+shift+o"], .closeFile: ["cmd+shift+w"], .quit: ["q", "cmd+q"] ] return texts.mapValues { $0.map { try! KeyChord($0) } } }() private static let defaults = try! KeyBindings(overrides: [:]) public init() { self = Self.defaults } public init(overrides: [String: [String]]) throws { var bindings = Self.builtIn for name in overrides.keys.sorted() { guard let action = PlayerAction(name: name) else { throw BindingError(message: "Unknown action 'keys.\(name)'.") } var chords: [KeyChord] = [] var seen: Set = [] for text in overrides[name]! { let chord: KeyChord do { chord = try KeyChord(text) } catch { throw BindingError(message: "keys.\(name): \(error.localizedDescription)") } guard seen.insert(chord).inserted else { throw BindingError(message: "keys.\(name): duplicate chord '\(text)' (\(chord.description)).") } chords.append(chord) } if chords.isEmpty { bindings.removeValue(forKey: action) } else { bindings[action] = chords } } var lookup: [KeyChord: [PlayerAction]] = [:] for action in bindings.keys.sorted(by: { $0.name < $1.name }) { for chord in bindings[action]! { if let existing = lookup[chord]?.first, existing.inverse != action { throw BindingError(message: "Chord '\(chord.description)' conflicts between keys.\(existing.name) and keys.\(action.name).") } lookup[chord, default: []].append(action) } } byAction = bindings byChord = lookup } public func shortcuts(for action: PlayerAction) -> [KeyChord] { byAction[action] ?? [] } public func actions(for chord: KeyChord) -> [PlayerAction] { byChord[chord] ?? [] } } struct BindingError: LocalizedError { let message: String var errorDescription: String? { message } }