GitHub - AquiGorka/eye-focus

3 min read Original article ↗

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.sh

Builds 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).

  1. Enable Eye Tracking Focus
  2. Click Calibrate
  3. A sound plays: look at your LEFT monitor for 3 seconds
  4. Another sound: look at your RIGHT monitor for 3 seconds
  5. Completion sound: calibration is saved

Calibration persists across restarts. Re-calibrate any time by clicking Calibrate again.

How It Works

Scroll to Focus

  1. CGEventTap (listen-only) intercepts scroll wheel events
  2. Cursor position finds the window under it via CGWindowListCopyWindowInfo
  3. The owning app is activated via NSRunningApplication.activate
  4. The specific window is raised via AXUIElement (kAXRaiseAction)
  5. Events are debounced (150ms) to avoid rapid switching

Eye Tracking

  1. AVCaptureSession captures low-res webcam frames
  2. Every 3rd frame is processed by VNDetectFaceLandmarksRequest
  3. Head yaw (70%) and pupil offset (30%) are combined into a gaze score
  4. 4 consecutive same-direction readings are required before switching
  5. 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