import Cocoa import CoreGraphics /// 全局热键监听器:监听 Fn 键(长按录音)和 Control 键(双击切换模式) final class KeyMonitor { // Fn 键标志 private static let fnMask: UInt64 = 0x00800000 // Control 键标志 private static let controlMask: UInt64 = 0x00040000 // 双击检测 private static let doubleClickWindow: TimeInterval = 0.40 // 两次点击的最大间隔 private static let longPressThreshold: TimeInterval = 0.20 // 长按判定阈值 private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? // Fn 键状态 private var isFnPressed = false private var fnDownTime: TimeInterval = 0 private var longPressTimer: Timer? // Control 键双击检测 private var isControlPressed = false private var lastControlUpTime: TimeInterval = 0 // Fn 回调 var onFnDown: (() -> Void)? var onFnUp: (() -> Void)? // Control 回调 var onControlDoubleClick: (() -> Void)? // 双击 Control 切换模式 func start() -> Bool { guard eventTap == nil else { return true } guard CGPreflightListenEventAccess() else { CGRequestListenEventAccess() print("[KeyMonitor] ❌ Accessibility permission not granted") return false } let eventMask = (1 << CGEventType.flagsChanged.rawValue) let callback: CGEventTapCallBack = { (proxy, type, event, refcon) -> Unmanaged? in let monitor = Unmanaged.fromOpaque(refcon!).takeUnretainedValue() return monitor.handleEvent(proxy: proxy, type: type, event: event) } var tap = CGEvent.tapCreate( tap: .cghidEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), callback: callback, userInfo: Unmanaged.passUnretained(self).toOpaque() ) if tap == nil { print("[KeyMonitor] ⚠️ HID tap failed, falling back to session tap") tap = CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), callback: callback, userInfo: Unmanaged.passUnretained(self).toOpaque() ) } guard let tap = tap else { print("[KeyMonitor] ❌ Failed to create event tap") return false } eventTap = tap runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: tap, enable: true) print("[KeyMonitor] ✅ Started") return true } func stop() { guard let tap = eventTap else { return } CGEvent.tapEnable(tap: tap, enable: false) if let source = runLoopSource { CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .commonModes) } eventTap = nil runLoopSource = nil print("[KeyMonitor] Stopped") } private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged? { guard type == .flagsChanged else { return Unmanaged.passUnretained(event) } let flags = event.flags.rawValue let fnActive = (flags & Self.fnMask) != 0 let controlActive = (flags & Self.controlMask) != 0 // MARK: - Fn 键:长按录音 if fnActive && !isFnPressed { isFnPressed = true fnDownTime = ProcessInfo.processInfo.systemUptime // 延迟 200ms 判定是否为长按 print("[KeyMonitor] ⚡️ Fn DOWN (waiting for long-press)") longPressTimer?.invalidate() longPressTimer = Timer.scheduledTimer(withTimeInterval: Self.longPressThreshold, repeats: false) { [weak self] _ in guard let self = self, self.isFnPressed else { return } print("[KeyMonitor] ⚡️ Fn LONG-PRESS → start recording") self.longPressTimer = nil DispatchQueue.main.async { self.onFnDown?() } } return nil } if !fnActive && isFnPressed { isFnPressed = false let fnUpTime = ProcessInfo.processInfo.systemUptime // 取消长按 timer(说明是短按,不触发录音) longPressTimer?.invalidate() longPressTimer = nil print("[KeyMonitor] ⚡️ Fn UP (was pressed for \(String(format: "%.0f", (fnUpTime - fnDownTime) * 1000))ms)") // 如果长按 timer 已被触发(已开始录音),正常触发 onFnUp if fnUpTime - fnDownTime >= Self.longPressThreshold { DispatchQueue.main.async { [weak self] in self?.onFnUp?() } } return nil } // MARK: - Control 键:双击切换模式 if controlActive && !isControlPressed { isControlPressed = true let controlDownTime = ProcessInfo.processInfo.systemUptime // 检测双击:上次松开距现在 < 400ms let elapsed = controlDownTime - lastControlUpTime if elapsed > 0 && elapsed < Self.doubleClickWindow { print("[KeyMonitor] ⚡️ Control DOUBLE-CLICK → switch mode") DispatchQueue.main.async { [weak self] in self?.onControlDoubleClick?() } } return nil } if !controlActive && isControlPressed { isControlPressed = false lastControlUpTime = ProcessInfo.processInfo.systemUptime return nil } return Unmanaged.passUnretained(event) } }