import AppKit
|
import CoreGraphics
|
import Darwin
|
import Foundation
|
import AlignerCore
|
|
final class CGEventTapTriggerProbe {
|
enum ProbeError: Error, CustomStringConvertible {
|
case eventTapUnavailable
|
|
var description: String {
|
switch self {
|
case .eventTapUnavailable:
|
return "CGEventTapCreate returned nil. Accessibility may be missing, or macOS may require a different trigger route."
|
}
|
}
|
}
|
|
private let shortcut: KeyboardShortcut
|
private var eventTap: CFMachPort?
|
private var runLoopSource: CFRunLoopSource?
|
private(set) var capturedQuickSwitchCount = 0
|
|
init(shortcut: KeyboardShortcut = TriggerDefaults.quickSwitchShortcut) {
|
self.shortcut = shortcut
|
}
|
|
func start() throws {
|
guard eventTap == nil else { return }
|
|
let eventMask = CGEventMask(1 << CGEventType.keyDown.rawValue)
|
guard let tap = CGEvent.tapCreate(
|
tap: .cgSessionEventTap,
|
place: .headInsertEventTap,
|
options: .defaultTap,
|
eventsOfInterest: eventMask,
|
callback: Self.eventCallback,
|
userInfo: Unmanaged.passUnretained(self).toOpaque()
|
) else {
|
throw ProbeError.eventTapUnavailable
|
}
|
|
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
|
CGEvent.tapEnable(tap: tap, enable: true)
|
|
eventTap = tap
|
runLoopSource = source
|
}
|
|
func stop() {
|
if let eventTap {
|
CGEvent.tapEnable(tap: eventTap, enable: false)
|
}
|
if let runLoopSource {
|
CFRunLoopRemoveSource(CFRunLoopGetMain(), runLoopSource, .commonModes)
|
}
|
|
runLoopSource = nil
|
eventTap = nil
|
}
|
|
deinit {
|
stop()
|
}
|
|
private func handle(event: CGEvent, type: CGEventType) -> Unmanaged<CGEvent>? {
|
guard type == .keyDown else {
|
return Unmanaged.passUnretained(event)
|
}
|
|
let receivedShortcut = KeyboardShortcut(
|
keyCode: UInt16(event.getIntegerValueField(.keyboardEventKeycode)),
|
modifiers: Self.shortcutModifiers(from: event.flags)
|
)
|
let decision = TriggerEventPolicy.decision(
|
for: receivedShortcut,
|
configuredQuickSwitch: shortcut,
|
overlayVisible: false,
|
consumeInitialQuickSwitch: true
|
)
|
|
switch decision {
|
case .quickSwitch(let consumeEvent):
|
capturedQuickSwitchCount += 1
|
print("Round01 trigger probe captured Alt+Tab")
|
fflush(stdout)
|
return consumeEvent ? nil : Unmanaged.passUnretained(event)
|
case .passThrough:
|
return Unmanaged.passUnretained(event)
|
}
|
}
|
|
private static let eventCallback: CGEventTapCallBack = { _, type, event, userInfo in
|
guard let userInfo else {
|
return Unmanaged.passUnretained(event)
|
}
|
|
let probe = Unmanaged<CGEventTapTriggerProbe>.fromOpaque(userInfo).takeUnretainedValue()
|
return probe.handle(event: event, type: type)
|
}
|
|
private static func shortcutModifiers(from flags: CGEventFlags) -> ShortcutModifiers {
|
var modifiers = ShortcutModifiers()
|
|
if flags.contains(.maskAlternate) {
|
modifiers.insert(.option)
|
}
|
if flags.contains(.maskCommand) {
|
modifiers.insert(.command)
|
}
|
if flags.contains(.maskShift) {
|
modifiers.insert(.shift)
|
}
|
if flags.contains(.maskControl) {
|
modifiers.insert(.control)
|
}
|
|
return modifiers
|
}
|
}
|
|
struct Round1TriggerProbeOptions {
|
let enabled: Bool
|
let quitAfter: TimeInterval?
|
|
static func parse(arguments: [String]) -> Round1TriggerProbeOptions {
|
Round1TriggerProbeOptions(
|
enabled: arguments.contains("--round01-trigger-probe"),
|
quitAfter: timeInterval(for: "--round01-trigger-probe-quit-after", in: arguments)
|
)
|
}
|
|
private static func timeInterval(for key: String, in arguments: [String]) -> TimeInterval? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return TimeInterval(argument.dropFirst(prefix.count))
|
}
|
}
|