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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# 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
|
||||
|
||||
1. Open a text editor window on display A and a terminal window on display B.
|
||||
2. Click into the terminal on display B, then click the text editor on
|
||||
display A.
|
||||
3. Type immediately after the click. Confirm keystrokes land in the text
|
||||
editor, not the terminal.
|
||||
4. Repeat in the opposite direction (A → B).
|
||||
|
||||
## DisplayLink virtual display
|
||||
|
||||
1. Connect a DisplayLink virtual display (or enable one if already paired).
|
||||
2. Repeat the "Basic cross-display focus" steps with one of the two windows
|
||||
on the virtual display.
|
||||
3. Confirm focus follows clicks with no perceptible lag and no dropped
|
||||
keystrokes.
|
||||
|
||||
## No focus thrashing on the active app
|
||||
|
||||
1. Click repeatedly inside the already-frontmost window (same app, same
|
||||
display).
|
||||
2. Confirm no visible activation flicker and no window re-raise animation —
|
||||
`NSRunningApplication.isActive` should short-circuit these clicks.
|
||||
|
||||
## Debounce
|
||||
|
||||
1. Rapidly double-click and drag-select across two different windows.
|
||||
2. Confirm only one focus reassertion happens per interaction, not one per
|
||||
raw mouse-down.
|
||||
|
||||
## Excluded apps
|
||||
|
||||
1. Add an app's bundle ID to the excluded list (`excludedBundleIDs` in
|
||||
`UserDefaults` for `com.local.focusfixer`).
|
||||
2. Click into that app's window from another display.
|
||||
3. Confirm FocusFixer does not activate or raise it.
|
||||
|
||||
## System UI is left alone
|
||||
|
||||
1. Click the Dock, a Spotlight search result, and Control Center.
|
||||
2. Confirm none of these trigger FocusFixer activation/raise logic (check
|
||||
logs — see below — for absence of unexpected activation entries).
|
||||
|
||||
## Self-activation guard
|
||||
|
||||
1. Click the FocusFixer menu-bar icon.
|
||||
2. 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
|
||||
|
||||
1. Trigger sustained system load (e.g. a busy `yes > /dev/null` loop or a
|
||||
heavy build) to risk `kCGEventTapDisabledByTimeout`.
|
||||
2. Continue clicking across displays during the load.
|
||||
3. Confirm focus reassertion keeps working — tail the logs (below) for a
|
||||
"re-enabling" message, which indicates recovery kicked in.
|
||||
|
||||
## Permission revoked mid-session
|
||||
|
||||
1. While FocusFixer is running, run:
|
||||
`tccutil reset Accessibility com.local.focusfixer`
|
||||
2. Click across displays; confirm FocusFixer degrades silently (no dialog,
|
||||
no crash) and logs the AX failures.
|
||||
3. 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
|
||||
|
||||
1. Use the menu-bar "Enabled" checkbox to disable FocusFixer.
|
||||
2. Confirm clicks across displays no longer reassert focus.
|
||||
3. Re-enable and confirm behaviour resumes.
|
||||
4. Toggle "Launch at Login", quit and relog (or use
|
||||
`sfltool` / System Settings → General → Login Items) to confirm the
|
||||
registration took effect.
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
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`.
|
||||
Reference in New Issue
Block a user