import Cocoa import CoreGraphics /// 全局热键监听器:只监听 Fn 键(支持双击切换模式) final class KeyMonitor { // Fn 键标志 private static let fnMask: UInt64 = 0x00800000 // 双击检测 private static let doubleClickWindow: TimeInterval = 0.40 // 两次点击的最大间隔 private static let longPressThreshold: TimeInterval = 0.20 // 长按判定阈值 private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? private var isFnPressed = false // 时间戳追踪 private var lastFnUpTime: TimeInterval = 0 private var fnDownTime: TimeInterval = 0 private var longPressTimer: Timer? var onFnDown: (() -> Void)? var onFnUp: (() -> Void)? var onFnDoubleClick: (() -> Void)? // 双击 Fn 切换模式 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 if fnActive && !isFnPressed { isFnPressed = true fnDownTime = ProcessInfo.processInfo.systemUptime // 检测双击:上次松开距现在 < 400ms 且之前有单击 let elapsed = fnDownTime - lastFnUpTime if elapsed > 0 && elapsed < Self.doubleClickWindow { // 双击!切换模式,取消长按 timer longPressTimer?.invalidate() longPressTimer = nil print("[KeyMonitor] ⚡️ Fn DOUBLE-CLICK → switch mode") DispatchQueue.main.async { [weak self] in self?.onFnDoubleClick?() } return nil } // 延迟 200ms 判定是否为长按 print("[KeyMonitor] ⚡️ Fn DOWN (waiting for long-press/double-click)") 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 lastFnUpTime = ProcessInfo.processInfo.systemUptime // 取消长按 timer(说明是短按/单击) longPressTimer?.invalidate() longPressTimer = nil print("[KeyMonitor] ⚡️ Fn UP (was pressed for \(String(format: "%.0f", (lastFnUpTime - fnDownTime) * 1000))ms)") // 如果长按 timer 已被触发(已开始录音),正常触发 onFnUp // 否则这是短按(单击或双击的第一下),不触发录音 if lastFnUpTime - fnDownTime >= Self.longPressThreshold { // 长按结束,正常触发 onFnUp DispatchQueue.main.async { [weak self] in self?.onFnUp?() } } return nil } return Unmanaged.passUnretained(event) } }