Files
focus-fixer/FocusFixer/PermissionManager.swift
sttlab-tech 561a547b70 Initial FocusFixer implementation
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>
2026-08-10 12:02:29 +02:00

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
}
}