How Mouse Navigation Implements Focus Follows Mouse in vorssaint-utils

Focus follows mouse in vorssaint-utils works by monitoring global mouse movement via NSEvent, waiting for the pointer to settle using a configurable delay timer, then activating the underlying window through macOS Accessibility APIs when specific guard conditions are met.

The vorssaint-utils repository provides a robust macOS implementation of focus-follows-mouse (FFM), allowing windows to activate automatically when the cursor pauses over them. Unlike traditional click-to-focus models, this Swift-based utility uses a polling-based architecture with thread-safe state management and safety guards to prevent accidental focus stealing during gaming or drag operations. The implementation lives in Sources/Vorssaint/Services/FocusFollowsMouse and integrates deeply with the macOS Accessibility and WindowServer frameworks.

Architectural Overview of the Focus Follows Mouse Service

The implementation splits responsibilities between two core files in Sources/Vorssaint/Services/FocusFollowsMouse:

  • FocusFollowsMouseService.swift – The runtime service that installs global mouse monitors, manages the evaluation timer, and orchestrates window activation.
  • FocusFollowsMouseSupport.swift – Pure logic helpers for delay calculations, window querying on background queues, and determining whether activation should occur.

The service follows a preference-driven lifecycle. When syncWithPreferences() is called—typically from SettingsView.swift or during session changes—the service reads focusFollowsMouseEnabled and focusFollowsMouseDelay from UserDefaults. If the feature is enabled, the session is active, and the app holds Accessibility privileges (AXIsProcessTrusted), the service starts; otherwise it stops immediately.

Step-by-Step Implementation Flow

1. Global Mouse Monitoring and State Recording

When active, the service installs a global event monitor using NSEvent.addGlobalMonitorForEvents:

// From FocusFollowsMouseService.swift
NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .mouseDragged]) { event in
    self.recordMovement(to: event.locationInWindow)
}

Every movement triggers recordMovement(to:), which updates a shared FocusFollowsMouseState instance. This state object stores the latest pointer location, the current systemUptime timestamp, and a generation counter. Each new movement invalidates any pending evaluation by incrementing the generation, ensuring that continuous motion never triggers focus changes.

2. Delay-Based Settled Pointer Detection

After the first movement, recordMovement creates a Timer firing every 0.05 seconds (50ms). This timer repeatedly calls evaluateIfSettled() until the pointer stabilizes:

// Timer initialization pattern from FocusFollowsMouseService.swift
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
    self.evaluateIfSettled()
}

The pointer is considered settled when the elapsed time since the last recorded movement exceeds the configured delay (default 250ms, customizable via focusFollowsMouseDelay). This logic resides in FocusFollowsMouseState.nextEvaluation, which compares the current uptime against the last movement timestamp.

3. Safety Guards Before Activation

Before querying windows, evaluateIfSettled() enforces strict guard conditions defined in FocusFollowsMouseService.swift:

  • Accessibility Permission: Verifies AXIsProcessTrusted() returns true.
  • No Input Held: Checks nothingIsHeldDown to ensure no mouse buttons or modifier keys are pressed.
  • No Exclusions: Validates the target is not blocked by MouseAppExceptions.shared.excludesPointerTarget.

If any guard fails, the evaluation aborts immediately, preventing focus theft during gaming sessions or drag-and-drop operations.

4. Window Querying and Accessibility Hit-Testing

Once the pointer settles and guards pass, FocusFollowsMouseSupport.queryWindow executes on a dedicated serial DispatchQueue named queryQueue to avoid blocking the main thread:

// Background window enumeration
queryQueue.async {
    let windows = WindowServerSupport.onScreenWindowInfo()
    // Find window containing point, filter by layer and ownership...
}

The query iterates through WindowServerSupport.onScreenWindowInfo() to find the window containing the cursor point. It filters out the app's own click-through windows and verifies the window belongs to a layer that participates in FFM (e.g., normal application windows, not menus or overlays).

The closure target(at:processID:) then performs a precise Accessibility hit-test:

  1. Creates an application reference via AXUIElementCreateApplication
  2. Calls AXUIElementCopyElementAtPosition to get the AX element at the cursor coordinates
  3. Extracts the top-level window, verifies its role, and resolves its CG Window ID using AXWindowResolver

5. Activation Decision Logic

The FocusFollowsMouseSupport.shouldActivate method implements smart activation rules to minimize disruption:

  • If the target app is already frontmost, activation only occurs when the target window differs from the currently focused window. This prevents games that capture the pointer from losing focus continuously.
  • The service checks against MouseAppExceptionSupport definitions to exclude specific UI layers or applications (e.g., fullscreen video players).

6. Window Activation

When activation is approved, the service dispatches to the main thread and calls WindowActivator.activate, which sends an AXRaise request via the Accessibility API to bring the target window forward and make its application the active frontmost app.

Key Design Patterns in vorssaint-utils

Thread Discipline

Mouse events arrive on the main thread, but the expensive window enumeration and Accessibility queries run on the serial queryQueue. This prevents the polling timer from stalling the UI during rapid mouse movements.

Preference-Driven Lifecycle

The syncWithPreferences() method reacts to changes from the Settings UI (SettingsView.swift) and SessionActivity notifications. When the user disables the feature or the display sleeps, resetMovement() and stop() clear the state and invalidate the timer.

Configurable Delay System

The default 250ms delay prevents "jitter" from rapid focus switching. Users customize this via DefaultsKey.focusFollowsMouseDelay, and the FocusFollowsMouseState sanitizes timing calculations using systemUptime for monotonic clock accuracy.

Enabling Focus Follows Mouse Programmatically

Below is a complete example for enabling the feature in your own client code:

import Vorssaint

// Enable in UserDefaults (normally controlled via SettingsView.swift UI)
UserDefaults.standard.set(true, forKey: DefaultsKey.focusFollowsMouseEnabled)
UserDefaults.standard.set(300, forKey: DefaultsKey.focusFollowsMouseDelay) // 300ms delay

// Verify Accessibility permission
guard AXIsProcessTrusted() else {
    print("Accessibility permission required")
    return
}

// Synchronize preferences to start the service
FocusFollowsMouseService.shared.syncWithPreferences()

Result: The service now monitors global mouse position. Moving the pointer over any window and pausing for 300ms will activate that window, provided it is not excluded and no mouse buttons are held. The service automatically stops if Accessibility permissions are revoked or if the session becomes inactive.

Summary

  • Focus follows mouse in vorssaint-utils uses NSEvent.addGlobalMonitorForEvents to track global mouse movement and a 50ms polling timer to detect settled pointer states.
  • Two-file architecture separates concerns: FocusFollowsMouseService.swift handles lifecycle and monitoring, while FocusFollowsMouseSupport.swift manages window queries and activation logic.
  • Configurable delay (default 250ms) prevents rapid focus switching; users customize this via focusFollowsMouseDelay in UserDefaults.
  • Strict safety guards require Accessibility permissions, no held modifiers/buttons, and exclusion list validation before activating any window.
  • Thread-safe design runs window queries on a dedicated queryQueue while keeping event monitoring on the main thread.
  • Smart activation avoids stealing focus from already-frontmost applications unless the specific window changes, preventing disruption to games and fullscreen apps.

Frequently Asked Questions

How does vorssaint-utils prevent focus stealing during gaming?

The implementation checks nothingIsHeldDown to ensure no mouse buttons or modifier keys are pressed before activation. Additionally, MouseAppExceptions.shared.excludesPointerTarget allows specific applications (like games) to be excluded from focus-follows-mouse entirely. If the frontmost application is already active, the service only switches focus when the window changes, preventing pointer-captured games from losing focus during camera movements.

What is the default delay for focus activation, and can it be changed?

The default delay is 250 milliseconds, defined in the FocusFollowsMouseState logic. Users can adjust this via the focusFollowsMouseDelay UserDefaults key, typically through the Settings UI in SettingsView.swift. The syncWithPreferences() method reads this value and applies it immediately without requiring an app restart.

Why does the service require Accessibility permissions?

Activating windows belonging to other applications requires the Accessibility API, specifically AXUIElementCreateApplication and AXUIElementCopyElementAtPosition for hit-testing, plus AXRaise for raising windows. The evaluateIfSettled() function explicitly checks AXIsProcessTrusted(); if permissions are missing, the service stops immediately to avoid console spam and failed assertions.

Where is the window querying logic performed to avoid UI lag?

The expensive window enumeration occurs on a dedicated serial DispatchQueue named queryQueue inside FocusFollowsMouseSupport.queryWindow. This prevents the 50ms polling timer on the main thread from blocking the UI during the WindowServerSupport.onScreenWindowInfo() iteration and Accessibility element resolution.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →