Files
focus-fixer/FocusFixer/AppDelegate.swift
T
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

80 lines
2.9 KiB
Swift

import AppKit
import ServiceManagement
import os
/// App lifecycle, the Accessibility permission gate, and the menu-bar item.
/// Wires WindowLocator + FocusEnforcer to the events EventTapController
/// reports; owns no event-tap or AX logic itself.
final class AppDelegate: NSObject, NSApplicationDelegate {
private let logger = Logger(subsystem: "com.local.focusfixer", category: "AppDelegate")
private let preferences = Preferences.shared
private let permissionManager = PermissionManager()
private let eventTapController = EventTapController()
private let focusEnforcer = FocusEnforcer()
private var statusItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
setUpStatusItem()
permissionManager.waitUntilTrusted { [weak self] in
self?.startEventTap()
}
}
private func startEventTap() {
eventTapController.start { [weak self] point in
self?.handleMouseDown(at: point)
}
}
private func handleMouseDown(at point: CGPoint) {
guard preferences.isEnabled else { return }
guard let pid = WindowLocator.ownerPID(at: point) else { return }
focusEnforcer.enforceFocus(forWindowOwnedBy: pid)
}
// MARK: - Menu bar
private func setUpStatusItem() {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
item.button?.image = NSImage(systemSymbolName: "scope", accessibilityDescription: "FocusFixer")
let menu = NSMenu()
let enabledItem = NSMenuItem(title: "Enabled", action: #selector(toggleEnabled(_:)), keyEquivalent: "")
enabledItem.target = self
enabledItem.state = preferences.isEnabled ? .on : .off
menu.addItem(enabledItem)
let loginItem = NSMenuItem(title: "Launch at Login", action: #selector(toggleLaunchAtLogin(_:)), keyEquivalent: "")
loginItem.target = self
loginItem.state = SMAppService.mainApp.status == .enabled ? .on : .off
menu.addItem(loginItem)
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Quit FocusFixer", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
item.menu = menu
statusItem = item
}
@objc private func toggleEnabled(_ sender: NSMenuItem) {
preferences.isEnabled.toggle()
sender.state = preferences.isEnabled ? .on : .off
}
@objc private func toggleLaunchAtLogin(_ sender: NSMenuItem) {
do {
if SMAppService.mainApp.status == .enabled {
try SMAppService.mainApp.unregister()
} else {
try SMAppService.mainApp.register()
}
} catch {
logger.error("Failed to toggle login item: \(error.localizedDescription, privacy: .public)")
}
sender.state = SMAppService.mainApp.status == .enabled ? .on : .off
}
}