561a547b70
Menu-bar utility that fixes cross-display keyboard focus loss: a listen-only CGEvent tap locates the window under the cursor and reasserts app activation + AX focus on it, with debounce, tap auto-recovery, and an Accessibility permission gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
48 lines
1.6 KiB
Swift
48 lines
1.6 KiB
Swift
import ApplicationServices
|
|
import Foundation
|
|
import os
|
|
|
|
/// Requests and polls for the Accessibility (TCC) grant FocusFixer needs
|
|
/// to observe events and drive the AX API.
|
|
final class PermissionManager {
|
|
private let logger = Logger(subsystem: "com.local.focusfixer", category: "PermissionManager")
|
|
private var pollTimer: Timer?
|
|
|
|
var isTrusted: Bool {
|
|
AXIsProcessTrusted()
|
|
}
|
|
|
|
/// Shows the system Accessibility prompt (if not already trusted) and
|
|
/// polls until the grant appears, then calls `onGranted` on the main
|
|
/// queue. Safe to call repeatedly; a prior poll is cancelled first.
|
|
func waitUntilTrusted(pollInterval: TimeInterval = 1.0, onGranted: @escaping () -> Void) {
|
|
stopPolling()
|
|
|
|
if isTrusted {
|
|
onGranted()
|
|
return
|
|
}
|
|
|
|
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
|
|
_ = AXIsProcessTrustedWithOptions(options)
|
|
logger.info("Accessibility permission not granted yet; prompting and polling")
|
|
|
|
pollTimer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] timer in
|
|
guard let self else {
|
|
timer.invalidate()
|
|
return
|
|
}
|
|
guard self.isTrusted else { return }
|
|
timer.invalidate()
|
|
self.pollTimer = nil
|
|
self.logger.info("Accessibility permission granted")
|
|
onGranted()
|
|
}
|
|
}
|
|
|
|
func stopPolling() {
|
|
pollTimer?.invalidate()
|
|
pollTimer = nil
|
|
}
|
|
}
|