Files
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

61 lines
2.5 KiB
Swift

import AppKit
import ApplicationServices
import os
/// Activates the app that owns a window and reasserts keyboard focus on
/// that specific window via the Accessibility API.
final class FocusEnforcer {
private let logger = Logger(subsystem: "com.local.focusfixer", category: "FocusEnforcer")
private let preferences: Preferences
init(preferences: Preferences = .shared) {
self.preferences = preferences
}
/// Activates the owning app and raises/focuses its window, unless it is
/// this app, already frontmost, or user-excluded.
func enforceFocus(forWindowOwnedBy pid: pid_t) {
guard pid != ProcessInfo.processInfo.processIdentifier else {
return
}
guard let app = NSRunningApplication(processIdentifier: pid) else {
logger.debug("No running application for pid \(pid, privacy: .public)")
return
}
if let bundleID = app.bundleIdentifier, preferences.isExcluded(bundleID: bundleID) {
return
}
guard !app.isActive else {
return
}
if !app.activate(options: []) {
logger.debug("Activation returned false for pid \(pid, privacy: .public)")
}
raiseFocusedWindow(pid: pid)
}
private func raiseFocusedWindow(pid: pid_t) {
let axApp = AXUIElementCreateApplication(pid)
var windowRef: CFTypeRef?
let copyResult = AXUIElementCopyAttributeValue(axApp, kAXFocusedWindowAttribute as CFString, &windowRef)
guard copyResult == .success, let windowRef else {
logger.debug("Could not resolve focused window via AX for pid \(pid, privacy: .public): \(copyResult.rawValue, privacy: .public)")
return
}
// AXUIElementCopyAttributeValue guarantees an AXUIElement on success; the
// Swift compiler can't express that constraint, hence the forced cast.
let axWindow = windowRef as! AXUIElement
let raiseResult = AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
if raiseResult != .success {
logger.debug("Raise action failed for pid \(pid, privacy: .public): \(raiseResult.rawValue, privacy: .public)")
}
let focusResult = AXUIElementSetAttributeValue(axApp, kAXFocusedWindowAttribute as CFString, axWindow)
if focusResult != .success {
logger.debug("Setting focused window failed for pid \(pid, privacy: .public): \(focusResult.rawValue, privacy: .public)")
}
}
}