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>
80 lines
2.9 KiB
Swift
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
|
|
}
|
|
}
|