- CGEvent cghidEventTap 全局监听 Fn 键 - Apple Speech 流式语音识别(zh-CN 默认) - 实时音频 RMS 电平驱动波形动画 - NSPanel 胶囊浮动窗口 - 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换 - OpenAI 兼容 LLM 纠错(DeepSeek 预设) - LSUIElement 菜单栏运行 - SPM + Makefile 构建
131 lines
4.2 KiB
Swift
131 lines
4.2 KiB
Swift
import Cocoa
|
||
import CoreGraphics
|
||
|
||
/// 全局 Fn / Globe 键监听器
|
||
final class KeyMonitor {
|
||
|
||
// Fn 键标志:CGEventFlags.maskSecondaryFn = 0x00800000
|
||
private static let fnMask: UInt64 = 0x00800000
|
||
|
||
private var eventTap: CFMachPort?
|
||
private var runLoopSource: CFRunLoopSource?
|
||
private var isFnPressed = false
|
||
|
||
var onFnDown: (() -> Void)?
|
||
var onFnUp: (() -> Void)?
|
||
|
||
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)
|
||
| (1 << CGEventType.keyDown.rawValue)
|
||
| (1 << CGEventType.keyUp.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)
|
||
}
|
||
|
||
// 尝试 HID 级 tap(能捕获被系统拦截的 Fn 键)
|
||
var tap = CGEvent.tapCreate(
|
||
tap: .cghidEventTap,
|
||
place: .headInsertEventTap,
|
||
options: .defaultTap,
|
||
eventsOfInterest: CGEventMask(eventMask),
|
||
callback: callback,
|
||
userInfo: Unmanaged.passUnretained(self).toOpaque()
|
||
)
|
||
|
||
if tap == nil {
|
||
// 回退到会话级 tap
|
||
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>? {
|
||
let flags = event.flags.rawValue
|
||
let fnInFlags = (flags & Self.fnMask) != 0
|
||
|
||
switch type {
|
||
case .flagsChanged:
|
||
if fnInFlags && !isFnPressed {
|
||
print("[KeyMonitor] ⚡️ Fn DOWN (flags=0x\(String(flags, radix: 16)))")
|
||
return handleFnDown()
|
||
}
|
||
if !fnInFlags && isFnPressed {
|
||
print("[KeyMonitor] ⚡️ Fn UP (flags=0x\(String(flags, radix: 16)))")
|
||
return handleFnUp()
|
||
}
|
||
return Unmanaged.passUnretained(event)
|
||
|
||
case .keyDown, .keyUp:
|
||
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
|
||
if keyCode == 63 && fnInFlags {
|
||
return handleFnDown()
|
||
}
|
||
return Unmanaged.passUnretained(event)
|
||
|
||
default:
|
||
return Unmanaged.passUnretained(event)
|
||
}
|
||
}
|
||
|
||
private func handleFnDown() -> Unmanaged<CGEvent>? {
|
||
if !isFnPressed {
|
||
isFnPressed = true
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.onFnDown?()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func handleFnUp() -> Unmanaged<CGEvent>? {
|
||
if isFnPressed {
|
||
isFnPressed = false
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.onFnUp?()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
}
|