// Polling cursor timeout for macOS kiosk sessions. No event tap or key capture. // Background hiding uses a private WindowServer hook; check after OS updates. import AppKit import CoreGraphics import Darwin import Foundation typealias MainConnectionFn = @convention(c) () -> Int32 typealias SetPropertyFn = @convention(c) (Int32, Int32, CFString, CFTypeRef) -> Int32 let skyLight = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY | RTLD_LOCAL) func symbol(_ names: [String]) -> UnsafeMutableRawPointer? { for name in names { if let library = skyLight, let pointer = dlsym(library, name) { return pointer } if let pointer = dlsym(UnsafeMutableRawPointer(bitPattern: -2), name) { return pointer } } return nil } guard let getPointer = symbol(["CGSMainConnectionID", "SLSMainConnectionID", "_CGSDefaultConnection"]), let setPointer = symbol(["CGSSetConnectionProperty", "SLSSetConnectionProperty"]) else { fputs("Background cursor API unavailable. Check this macOS version.\n", stderr) exit(2) } if CommandLine.arguments.dropFirst().first == "--check" { print("Cursor compatibility symbols found (cursor visibility was not changed).") exit(0) } let argument = CommandLine.arguments.dropFirst().first ?? "10" guard let timeout = Double(argument), timeout.isFinite, timeout >= 1, timeout <= 3600 else { fputs("Usage: kiosk-cursor [idle-seconds: 1..3600] | --check\n", stderr) exit(1) } let app = NSApplication.shared app.setActivationPolicy(.prohibited) let getConnection = unsafeBitCast(getPointer, to: MainConnectionFn.self) let setProperty = unsafeBitCast(setPointer, to: SetPropertyFn.self) let connection = getConnection() guard connection != 0, setProperty(connection, connection, "SetsCursorInBackground" as CFString, kCFBooleanTrue) == 0 else { fputs("Could not enable background cursor control.\n", stderr) exit(2) } var hidden = false func showOwnedCursor() { if hidden && CGDisplayShowCursor(CGMainDisplayID()) == .success { hidden = false } } atexit { showOwnedCursor() } var signals: [DispatchSourceSignal] = [] for signum in [SIGINT, SIGTERM, SIGHUP] { signal(signum, SIG_IGN) let source = DispatchSource.makeSignalSource(signal: signum, queue: .main) source.setEventHandler { showOwnedCursor(); exit(0) } source.resume() signals.append(source) } let motionTypes: [CGEventType] = [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged] let started = ProcessInfo.processInfo.systemUptime let timer = DispatchSource.makeTimerSource(queue: .main) timer.schedule(deadline: .now(), repeating: .milliseconds(200), leeway: .milliseconds(20)) timer.setEventHandler { let motionIdle = motionTypes.map { CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: $0) }.min() ?? 0 let idle = min(motionIdle, ProcessInfo.processInfo.systemUptime - started) if idle >= timeout && !hidden { if CGDisplayHideCursor(CGMainDisplayID()) == .success { hidden = true } else { fputs("CGDisplayHideCursor failed.\n", stderr); exit(3) } } else if idle < timeout { showOwnedCursor() } } timer.resume() print("Cursor helper started; timeout \(timeout) seconds.") fflush(stdout) app.run()