import Foundation import KeyboardShortcuts import Carbon import AppKit extension KeyboardShortcuts.Name { static let toggleMiniRecorder = Self("toggleMiniRecorder") static let escapeRecorder = Self("escapeRecorder") static let toggleEnhancement = Self("toggleEnhancement") // Prompt selection shortcuts static let selectPrompt1 = Self("selectPrompt1") static let selectPrompt2 = Self("selectPrompt2") static let selectPrompt3 = Self("selectPrompt3") static let selectPrompt4 = Self("selectPrompt4") static let selectPrompt5 = Self("selectPrompt5") static let selectPrompt6 = Self("selectPrompt6") static let selectPrompt7 = Self("selectPrompt7") static let selectPrompt8 = Self("selectPrompt8") static let selectPrompt9 = Self("selectPrompt9") } @MainActor class HotkeyManager: ObservableObject { @Published var isListening = false @Published var isShortcutConfigured = false @Published var isPushToTalkEnabled: Bool { didSet { UserDefaults.standard.set(isPushToTalkEnabled, forKey: "isPushToTalkEnabled") if !isPushToTalkEnabled { isRightOptionKeyPressed = false isFnKeyPressed = false isRightCommandKeyPressed = false } setupKeyMonitors() } } @Published var pushToTalkKey: PushToTalkKey { didSet { UserDefaults.standard.set(pushToTalkKey.rawValue, forKey: "pushToTalkKey") isRightOptionKeyPressed = false isFnKeyPressed = false isRightCommandKeyPressed = false } } private var whisperState: WhisperState private var isRightOptionKeyPressed = false private var isFnKeyPressed = false private var isRightCommandKeyPressed = false private var localKeyMonitor: Any? private var globalKeyMonitor: Any? private var visibilityTask: Task? // Add cooldown management private var lastShortcutTriggerTime: Date? private let shortcutCooldownInterval: TimeInterval = 0.5 // 500ms cooldown enum PushToTalkKey: String, CaseIterable { case rightOption = "rightOption" case fn = "fn" case rightCommand = "rightCommand" var displayName: String { switch self { case .rightOption: return "Right Option (⌥)" case .fn: return "Fn" case .rightCommand: return "Right Command (⌘)" } } } init(whisperState: WhisperState) { self.isPushToTalkEnabled = UserDefaults.standard.bool(forKey: "isPushToTalkEnabled") self.pushToTalkKey = PushToTalkKey(rawValue: UserDefaults.standard.string(forKey: "pushToTalkKey") ?? "") ?? .rightCommand self.whisperState = whisperState updateShortcutStatus() setupEnhancementShortcut() // Start observing mini recorder visibility setupVisibilityObserver() } private func setupVisibilityObserver() { visibilityTask = Task { @MainActor in for await isVisible in whisperState.$isMiniRecorderVisible.values { if isVisible { setupEscapeShortcut() // Set Command+E shortcut when visible KeyboardShortcuts.setShortcut(.init(.e, modifiers: .command), for: .toggleEnhancement) setupPromptShortcuts() } else { removeEscapeShortcut() // Remove Command+E shortcut when not visible KeyboardShortcuts.setShortcut(nil, for: .toggleEnhancement) removePromptShortcuts() } } } } private func setupEscapeShortcut() { // Set ESC as the shortcut using KeyboardShortcuts native approach KeyboardShortcuts.setShortcut(.init(.escape), for: .escapeRecorder) // Setup handler KeyboardShortcuts.onKeyDown(for: .escapeRecorder) { [weak self] in Task { @MainActor in guard let self = self, await self.whisperState.isMiniRecorderVisible else { return } await self.whisperState.dismissMiniRecorder() } } } private func removeEscapeShortcut() { KeyboardShortcuts.setShortcut(nil, for: .escapeRecorder) } private func setupEnhancementShortcut() { // Only setup the handler, don't set the shortcut here // The shortcut will be set/removed based on visibility KeyboardShortcuts.onKeyDown(for: .toggleEnhancement) { [weak self] in Task { @MainActor in guard let self = self, await self.whisperState.isMiniRecorderVisible, let enhancementService = await self.whisperState.getEnhancementService() else { return } enhancementService.isEnhancementEnabled.toggle() } } } private func removeEnhancementShortcut() { KeyboardShortcuts.setShortcut(nil, for: .toggleEnhancement) } private func setupPromptShortcuts() { // Set up Command+1 through Command+9 shortcuts with proper key definitions KeyboardShortcuts.setShortcut(.init(.one, modifiers: .command), for: .selectPrompt1) KeyboardShortcuts.setShortcut(.init(.two, modifiers: .command), for: .selectPrompt2) KeyboardShortcuts.setShortcut(.init(.three, modifiers: .command), for: .selectPrompt3) KeyboardShortcuts.setShortcut(.init(.four, modifiers: .command), for: .selectPrompt4) KeyboardShortcuts.setShortcut(.init(.five, modifiers: .command), for: .selectPrompt5) KeyboardShortcuts.setShortcut(.init(.six, modifiers: .command), for: .selectPrompt6) KeyboardShortcuts.setShortcut(.init(.seven, modifiers: .command), for: .selectPrompt7) KeyboardShortcuts.setShortcut(.init(.eight, modifiers: .command), for: .selectPrompt8) KeyboardShortcuts.setShortcut(.init(.nine, modifiers: .command), for: .selectPrompt9) // Setup handlers for each shortcut setupPromptHandler(for: .selectPrompt1, index: 0) setupPromptHandler(for: .selectPrompt2, index: 1) setupPromptHandler(for: .selectPrompt3, index: 2) setupPromptHandler(for: .selectPrompt4, index: 3) setupPromptHandler(for: .selectPrompt5, index: 4) setupPromptHandler(for: .selectPrompt6, index: 5) setupPromptHandler(for: .selectPrompt7, index: 6) setupPromptHandler(for: .selectPrompt8, index: 7) setupPromptHandler(for: .selectPrompt9, index: 8) } private func setupPromptHandler(for shortcutName: KeyboardShortcuts.Name, index: Int) { KeyboardShortcuts.onKeyDown(for: shortcutName) { [weak self] in Task { @MainActor in guard let self = self, await self.whisperState.isMiniRecorderVisible, let enhancementService = await self.whisperState.getEnhancementService() else { return } let prompts = enhancementService.allPrompts if index < prompts.count { // Enable AI enhancement if it's not already enabled if !enhancementService.isEnhancementEnabled { enhancementService.isEnhancementEnabled = true } // Switch to the selected prompt enhancementService.setActivePrompt(prompts[index]) } } } } private func removePromptShortcuts() { // Remove Command+1 through Command+9 shortcuts KeyboardShortcuts.setShortcut(nil, for: .selectPrompt1) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt2) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt3) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt4) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt5) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt6) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt7) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt8) KeyboardShortcuts.setShortcut(nil, for: .selectPrompt9) } func updateShortcutStatus() { isShortcutConfigured = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil if isShortcutConfigured { setupShortcutHandler() setupKeyMonitors() } else { removeKeyMonitors() } } private func setupShortcutHandler() { KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder) { [weak self] in Task { @MainActor in await self?.handleShortcutTriggered() } } } private func handleShortcutTriggered() async { // Check cooldown if let lastTrigger = lastShortcutTriggerTime, Date().timeIntervalSince(lastTrigger) < shortcutCooldownInterval { return // Still in cooldown period } // Update last trigger time lastShortcutTriggerTime = Date() // Handle the shortcut await whisperState.handleToggleMiniRecorder() } private func removeKeyMonitors() { if let monitor = localKeyMonitor { NSEvent.removeMonitor(monitor) localKeyMonitor = nil } if let monitor = globalKeyMonitor { NSEvent.removeMonitor(monitor) globalKeyMonitor = nil } } private func setupKeyMonitors() { guard isPushToTalkEnabled else { removeKeyMonitors() return } // Remove existing monitors first removeKeyMonitors() // Local monitor for when app is in foreground localKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in Task { @MainActor in await self?.handlePushToTalkKey(event) } return event } // Global monitor for when app is in background globalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in Task { @MainActor in await self?.handlePushToTalkKey(event) } } } private func handlePushToTalkKey(_ event: NSEvent) async { // Only handle push-to-talk if enabled and configured guard isPushToTalkEnabled && isShortcutConfigured else { return } let keyState: Bool switch pushToTalkKey { case .rightOption: keyState = event.modifierFlags.contains(.option) && event.keyCode == 0x3D guard keyState != isRightOptionKeyPressed else { return } isRightOptionKeyPressed = keyState case .fn: keyState = event.modifierFlags.contains(.function) guard keyState != isFnKeyPressed else { return } isFnKeyPressed = keyState case .rightCommand: keyState = event.modifierFlags.contains(.command) && event.keyCode == 0x36 guard keyState != isRightCommandKeyPressed else { return } isRightCommandKeyPressed = keyState } // Toggle recording based on key state if whisperState.isMiniRecorderVisible != keyState { await whisperState.handleToggleMiniRecorder() } } deinit { visibilityTask?.cancel() Task { @MainActor in removeKeyMonitors() removeEscapeShortcut() removeEnhancementShortcut() } } }