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>
3.6 KiB
3.6 KiB
Manual test checklist — focus behaviour
The event tap and Accessibility side-effects can't be unit tested. Run this
checklist by hand after any change to EventTapController, WindowLocator,
or FocusEnforcer, and whenever behaviour changes update it alongside the
code.
Prerequisites: FocusFixer built and running as a .app bundle (not via
swift run), Accessibility permission granted in System Settings →
Privacy & Security → Accessibility.
Basic cross-display focus
- Open a text editor window on display A and a terminal window on display B.
- Click into the terminal on display B, then click the text editor on display A.
- Type immediately after the click. Confirm keystrokes land in the text editor, not the terminal.
- Repeat in the opposite direction (A → B).
DisplayLink virtual display
- Connect a DisplayLink virtual display (or enable one if already paired).
- Repeat the "Basic cross-display focus" steps with one of the two windows on the virtual display.
- Confirm focus follows clicks with no perceptible lag and no dropped keystrokes.
No focus thrashing on the active app
- Click repeatedly inside the already-frontmost window (same app, same display).
- Confirm no visible activation flicker and no window re-raise animation —
NSRunningApplication.isActiveshould short-circuit these clicks.
Debounce
- Rapidly double-click and drag-select across two different windows.
- Confirm only one focus reassertion happens per interaction, not one per raw mouse-down.
Excluded apps
- Add an app's bundle ID to the excluded list (
excludedBundleIDsinUserDefaultsforcom.local.focusfixer). - Click into that app's window from another display.
- Confirm FocusFixer does not activate or raise it.
System UI is left alone
- Click the Dock, a Spotlight search result, and Control Center.
- Confirm none of these trigger FocusFixer activation/raise logic (check logs — see below — for absence of unexpected activation entries).
Self-activation guard
- Click the FocusFixer menu-bar icon.
- Confirm FocusFixer never tries to activate/raise itself (it has no normal windows, but verify no AX errors logged for its own PID).
Tap recovery
- Trigger sustained system load (e.g. a busy
yes > /dev/nullloop or a heavy build) to riskkCGEventTapDisabledByTimeout. - Continue clicking across displays during the load.
- Confirm focus reassertion keeps working — tail the logs (below) for a "re-enabling" message, which indicates recovery kicked in.
Permission revoked mid-session
- While FocusFixer is running, run:
tccutil reset Accessibility com.local.focusfixer - Click across displays; confirm FocusFixer degrades silently (no dialog, no crash) and logs the AX failures.
- Re-grant Accessibility permission in System Settings and confirm FocusFixer resumes without a relaunch (permission polling should pick it back up if the app is still waiting; otherwise relaunch and confirm the prompt reappears correctly).
Toggling from the menu
- Use the menu-bar "Enabled" checkbox to disable FocusFixer.
- Confirm clicks across displays no longer reassert focus.
- Re-enable and confirm behaviour resumes.
- Toggle "Launch at Login", quit and relog (or use
sfltool/ System Settings → General → Login Items) to confirm the registration took effect.
Logs
log stream --predicate 'subsystem == "com.local.focusfixer"' --level debug
Watch for repeated tapDisabledByTimeout/tapDisabledByUserInput messages
(indicates the tap callback is doing too much work) and AX error codes
logged by FocusEnforcer.