70 lines
2.5 KiB
Swift
70 lines
2.5 KiB
Swift
import ApplicationServices
|
|
import CoreGraphics
|
|
|
|
public final class HotkeyMonitor {
|
|
public enum HotkeyError: Error, LocalizedError {
|
|
case tapUnavailable
|
|
|
|
public var errorDescription: String? {
|
|
"Kan de Fn-toets niet volgen: geef Input Monitoring-rechten in Systeeminstellingen en probeer opnieuw (soms is een herstart van de app nodig)."
|
|
}
|
|
}
|
|
|
|
public var onEvent: ((HotkeyEvent) -> Void)?
|
|
|
|
private var tracker = FnKeyTracker()
|
|
private var tap: CFMachPort?
|
|
private var source: CFRunLoopSource?
|
|
|
|
public init() {}
|
|
|
|
public func start() throws {
|
|
guard tap == nil else { return }
|
|
let mask = CGEventMask(1 << CGEventType.flagsChanged.rawValue)
|
|
let callback: CGEventTapCallBack = { _, type, event, refcon in
|
|
if let refcon {
|
|
let monitor = Unmanaged<HotkeyMonitor>.fromOpaque(refcon).takeUnretainedValue()
|
|
monitor.handle(type: type, event: event)
|
|
}
|
|
return Unmanaged.passUnretained(event)
|
|
}
|
|
guard let tap = CGEvent.tapCreate(
|
|
tap: .cgSessionEventTap,
|
|
place: .headInsertEventTap,
|
|
options: .listenOnly,
|
|
eventsOfInterest: mask,
|
|
callback: callback,
|
|
userInfo: Unmanaged.passUnretained(self).toOpaque()
|
|
) else { throw HotkeyError.tapUnavailable }
|
|
|
|
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
|
|
CGEvent.tapEnable(tap: tap, enable: true)
|
|
self.tap = tap
|
|
self.source = source
|
|
}
|
|
|
|
public func stop() {
|
|
if let source { CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes) }
|
|
if let tap { CGEvent.tapEnable(tap: tap, enable: false) }
|
|
tap = nil
|
|
source = nil
|
|
}
|
|
|
|
private func handle(type: CGEventType, event: CGEvent) {
|
|
switch type {
|
|
case .tapDisabledByTimeout, .tapDisabledByUserInput:
|
|
if let tap { CGEvent.tapEnable(tap: tap, enable: true) }
|
|
case .flagsChanged:
|
|
// .maskSecondaryFn is also set while arrows/F-keys are held; only react to the Fn key itself.
|
|
guard event.getIntegerValueField(.keyboardEventKeycode) == 63 else { return }
|
|
let fnDown = event.flags.contains(.maskSecondaryFn)
|
|
if let hotkeyEvent = tracker.process(fnDown: fnDown) {
|
|
onEvent?(hotkeyEvent)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|