VoiceInput/Sources/Core/KeyMonitor.swift
JOJO deae1fbc06 feat: RecordingPanel 宽度自适应 + 打包 DMG
- updateText 移除文本宽度硬编码下限 160,真正根据内容自适应
- showRefining/showThinking 简化为调用 updateText,复用自适应逻辑
- 面板最小宽度由 minPanelWidth(180pt) 兜底
- 打包 VoiceInput-20260425.dmg
2026-04-25 16:34:33 +08:00

167 lines
5.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<CGEvent>? in
let monitor = Unmanaged<KeyMonitor>.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<CGEvent>? {
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)
}
}