VoiceInput/Sources/Core/KeyMonitor.swift

133 lines
4.4 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 AI
final class KeyMonitor {
// Fn NX_SECONDARYFNMASK
private static let fnMask: UInt64 = 0x00800000
// Control NX_CONTROLMASK
private static let controlMask: UInt64 = 0x00001000
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var isFnPressed = false
private var isControlPressed = false
var onFnDown: (() -> Void)?
var onFnUp: (() -> Void)?
var onControlDown: (() -> Void)?
var onControlUp: (() -> 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)
}
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 (Fn + Control)")
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
_ = event.getIntegerValueField(.keyboardEventKeycode)
let fnInFlags = (flags & Self.fnMask) != 0
let controlInFlags = (flags & Self.controlMask) != 0
switch type {
case .flagsChanged:
// Fn
if fnInFlags && !isFnPressed {
isFnPressed = true
DispatchQueue.main.async { [weak self] in
self?.onFnDown?()
}
return nil
}
if !fnInFlags && isFnPressed {
isFnPressed = false
DispatchQueue.main.async { [weak self] in
self?.onFnUp?()
}
return nil
}
// Control
if controlInFlags && !isControlPressed {
isControlPressed = true
DispatchQueue.main.async { [weak self] in
self?.onControlDown?()
}
return nil
}
if !controlInFlags && isControlPressed {
isControlPressed = false
DispatchQueue.main.async { [weak self] in
self?.onControlUp?()
}
return nil
}
return Unmanaged.passUnretained(event)
default:
return Unmanaged.passUnretained(event)
}
}
}