107 lines
3.3 KiB
Swift
107 lines
3.3 KiB
Swift
import Cocoa
|
||
import CoreGraphics
|
||
|
||
/// 全局热键监听器:只监听 Fn 键
|
||
final class KeyMonitor {
|
||
|
||
// Fn 键标志
|
||
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)
|
||
|
||
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
|
||
|
||
if fnActive && !isFnPressed {
|
||
isFnPressed = true
|
||
print("[KeyMonitor] ⚡️ Fn DOWN")
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.onFnDown?()
|
||
}
|
||
return nil
|
||
}
|
||
if !fnActive && isFnPressed {
|
||
isFnPressed = false
|
||
print("[KeyMonitor] ⚡️ Fn UP")
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.onFnUp?()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
return Unmanaged.passUnretained(event)
|
||
}
|
||
}
|