import ApplicationServices import CoreGraphics public final class HotkeyMonitor { public enum HotkeyError: Error, LocalizedError { case tapUnavailable public var errorDescription: String? { "Kan de sneltoets niet volgen: geef Input Monitoring-rechten in Systeeminstellingen en probeer opnieuw (soms is een herstart van de app nodig)." } } public var onEvent: ((HotkeyEvent) -> Void)? /// The key that has to be held. Changing it resets the press tracking. public var binding: HotkeyBinding = .fn { didSet { tracker = ModifierKeyTracker() } } private var tracker = ModifierKeyTracker() 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.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: // Flags are shared between left and right keys and with arrows/F-keys: only react to the bound key itself. guard event.getIntegerValueField(.keyboardEventKeycode) == Int64(binding.keyCode) else { return } let isDown = event.flags.contains(binding.flag) if let hotkeyEvent = tracker.process(isDown: isDown) { onEvent?(hotkeyEvent) } default: break } } }