mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-30 19:59:38 +02:00
111 lines
4.6 KiB
Swift
111 lines
4.6 KiB
Swift
// Watches the macOS fn/Globe key globally and prints "d"/"u" per press/release. Exists because
|
|
// libuiohook maps keycode 63 to VC_UNDEFINED, so no JS-side tap can ever see fn. Listen-only CGEvent
|
|
// tap: needs the same Input Monitoring grant the app already requests, never swallows anything.
|
|
import CoreGraphics
|
|
import Foundation
|
|
import IOKit.hid
|
|
|
|
// Permission is REPORTED, never inferred. A live process used to be the only evidence anyone had
|
|
// that the tap worked, and that read is simply wrong: tapCreate can hand back a port that never
|
|
// delivers an event, which is how a dead fn key looked identical to a key nobody pressed.
|
|
let isProbe = CommandLine.arguments.contains("--no-prompt")
|
|
var hidGranted = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted
|
|
// The probe stays silent (ENG-341 keeps the install flow prompt-free), but a user who is actually
|
|
// trying to dictate has earned the one prompt that can fix it. Without this the grant could only
|
|
// ever be discovered, never requested, so a denied machine stayed denied forever.
|
|
if !hidGranted && !isProbe {
|
|
hidGranted = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent)
|
|
}
|
|
print(hidGranted ? "p granted" : "p denied")
|
|
fflush(stdout)
|
|
|
|
// Denied is REPORTED and then we keep going on purpose. Exiting here looks tidier, but a running
|
|
// tap is the only thing that makes macOS list the app under Input Monitoring, so quitting removes
|
|
// the very switch the user is being sent to flip (caught the hard way: "it just takes me to
|
|
// Settings and nothing else"). The ambiguity that started all this is already gone, because the
|
|
// parent trusts the reported permission and never the fact that this process is alive.
|
|
if !hidGranted {
|
|
print("e no-permission")
|
|
fflush(stdout)
|
|
}
|
|
|
|
var fnDown = false
|
|
// Proof the tap is on the wire at all. Without it, "no fn events" is ambiguous between a deaf tap
|
|
// and an untouched key, and that ambiguity is what made this unfixable from a log.
|
|
var wireAlive = false
|
|
var tapRef: CFMachPort?
|
|
var srcRef: CFRunLoopSource?
|
|
|
|
let callback: CGEventTapCallBack = { _, type, event, _ in
|
|
if type == .flagsChanged {
|
|
if !wireAlive {
|
|
wireAlive = true
|
|
print("w")
|
|
fflush(stdout)
|
|
}
|
|
let keycode = event.getIntegerValueField(.keyboardEventKeycode)
|
|
if keycode == 63 {
|
|
let down = event.flags.contains(.maskSecondaryFn)
|
|
if down != fnDown {
|
|
fnDown = down
|
|
print(down ? "d" : "u")
|
|
fflush(stdout)
|
|
}
|
|
}
|
|
} else if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
|
|
// macOS pauses slow taps; re-enable or fn goes silently dead until relaunch.
|
|
if let tap = tapRef { CGEvent.tapEnable(tap: tap, enable: true) }
|
|
}
|
|
return Unmanaged.passUnretained(event)
|
|
}
|
|
|
|
let mask = (CGEventMask(1) << CGEventType.flagsChanged.rawValue)
|
|
|
|
// New tap first, old tap down after: a failed re-arm keeps the working tap instead of going deaf.
|
|
// The fnDown de-dupe above makes the brief two-tap overlap print nothing twice.
|
|
func armTap() -> Bool {
|
|
guard let tap = CGEvent.tapCreate(
|
|
tap: .cgSessionEventTap, place: .headInsertEventTap, options: .listenOnly,
|
|
eventsOfInterest: mask, callback: callback, userInfo: nil
|
|
) else { return false }
|
|
let src = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
CFRunLoopAddSource(CFRunLoopGetMain(), src, .commonModes)
|
|
CGEvent.tapEnable(tap: tap, enable: true)
|
|
if let old = tapRef { CGEvent.tapEnable(tap: old, enable: false); CFMachPortInvalidate(old) }
|
|
if let oldSrc = srcRef { CFRunLoopRemoveSource(CFRunLoopGetMain(), oldSrc, .commonModes) }
|
|
tapRef = tap
|
|
srcRef = src
|
|
return true
|
|
}
|
|
|
|
guard armTap() else {
|
|
print("e tap-failed")
|
|
fflush(stdout)
|
|
exit(1)
|
|
}
|
|
print("t ok")
|
|
fflush(stdout)
|
|
|
|
// The parent pokes "r\n" when the app gains focus: another app's tap registered after ours sits
|
|
// AHEAD of ours (head-insert) and can eat fn before we see it, with no disable event to catch, so
|
|
// re-arming is the only way to win the key back (ENG-317). stdin EOF means the parent is gone;
|
|
// exiting then stops a crashed OpenSwarm from stranding a process holding a global keyboard tap.
|
|
DispatchQueue.global().async {
|
|
while let line = readLine(strippingNewline: true) {
|
|
if line == "r" {
|
|
CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) {
|
|
if !armTap() {
|
|
print("e rearm-failed")
|
|
fflush(stdout)
|
|
}
|
|
}
|
|
CFRunLoopWakeUp(CFRunLoopGetMain())
|
|
}
|
|
}
|
|
exit(0)
|
|
}
|
|
|
|
print("r")
|
|
fflush(stdout)
|
|
CFRunLoopRun()
|