EyeFocus
macOS menu bar app that automatically focuses windows when you scroll or look at a different monitor.
The Problem
On macOS with multiple monitors, scrolling in a window does not move keyboard focus to it. You can scroll through a document on your second monitor, but when you start typing, keystrokes go to the previously focused app on the other monitor.
Features
Scroll to Focus
When you scroll in a window, that window gains keyboard focus. Works across monitors and across different windows of the same app.
Eye Tracking Focus
Uses your webcam to detect which monitor you're looking at and focuses the topmost window there. Face landmark detection via Apple's Vision framework, combining head yaw and pupil position for gaze direction.
Feedback
Optionally play a sound or show a system notification when focus changes. Both can be enabled independently.
Requirements
- macOS 13.0 (Ventura) or later
- A webcam (for eye tracking)
- Swift toolchain (comes with Xcode Command Line Tools)
Building
xcode-select --install # if needed
./bundle.shBuilds a release binary via SPM, creates EyeFocus.app, and code-signs it.
Running
Runs as a menu bar app (eye icon). Click the icon to open the popover.
You must launch via open EyeFocus.app (not the binary directly) for macOS permissions to work correctly.
Permissions
EyeFocus needs three macOS permissions. On first use, enabling each feature opens System Settings to the relevant pane:
| Permission | Purpose | Required For |
|---|---|---|
| Input Monitoring | Intercept scroll wheel events | Scroll to Focus |
| Accessibility | Activate and raise windows | Both features |
| Camera | Webcam frames for gaze detection | Eye Tracking |
After granting a permission, the feature auto-enables.
Rebuilding the app invalidates permissions (ad-hoc signing changes the binary hash). After each rebuild, remove and re-add EyeFocus.app in System Settings.
Calibration
Eye tracking needs calibration to account for your camera's position (e.g. mounted on one monitor, off to the side).
- Enable Eye Tracking Focus
- Click Calibrate
- A sound plays: look at your LEFT monitor for 3 seconds
- Another sound: look at your RIGHT monitor for 3 seconds
- Completion sound: calibration is saved
Calibration persists across restarts. Re-calibrate any time by clicking Calibrate again.
How It Works
Scroll to Focus
CGEventTap(listen-only) intercepts scroll wheel events- Cursor position finds the window under it via
CGWindowListCopyWindowInfo - The owning app is activated via
NSRunningApplication.activate - The specific window is raised via
AXUIElement(kAXRaiseAction) - Events are debounced (150ms) to avoid rapid switching
Eye Tracking
AVCaptureSessioncaptures low-res webcam frames- Every 3rd frame is processed by
VNDetectFaceLandmarksRequest - Head yaw (70%) and pupil offset (30%) are combined into a gaze score
- 4 consecutive same-direction readings are required before switching
- Target monitor's topmost window is activated
Project Structure
EyeFocus/
App/
EyeFocusApp.swift # @main SwiftUI app with MenuBarExtra
AppDelegate.swift # NSApplicationDelegate
FeatureManager.swift # Coordinates both features
PopoverView.swift # Menu bar popover UI
ScrollFocus/
ScrollEventTap.swift # CGEventTap for scroll events
WindowUnderCursor.swift # Find window at screen coordinates
WindowActivator.swift # Activate apps and raise windows
WindowInfo.swift # Window metadata struct
EyeFocusError.swift # Error types
EyeTracker/
CameraCapture.swift # AVCaptureSession wrapper
GazeEstimator.swift # Vision framework gaze detection
MonitorMapper.swift # Map gaze direction to screens
EyeTrackingController.swift # Eye tracking pipeline
Utilities/
Debouncer.swift # Debounce helper
PermissionsManager.swift # Permission checks and requests
Log.swift # Dual stdout + file logger
Resources/
Info.plist # App config (LSUIElement, camera usage)
EyeFocus.entitlements # Camera entitlement
Package.swift # SPM build configuration
bundle.sh # Build + bundle + codesign script
Logs
tail -f /tmp/eyefocus.log