How Per-Application Profile Overlays Auto-Switch on App Focus in OpenLogi

OpenLogi implements per-application profile overlays by reading the identifier of the frontmost application and overlaying stored per-app bindings onto the generic device configuration, using a background watcher that polls at approximately 1 Hz across Linux (X11/Wayland), macOS, and Windows.

OpenLogi is an open-source input device manager that enables context-aware control schemes through automatic profile switching. This article examines the source code implementation of how per-application profile overlays automatically switch when app focus changes, covering the cross-platform detection mechanisms and configuration merging logic found in the AprilNEA/OpenLogi repository.

Platform-Specific Front-Most Detection

OpenLogi abstracts platform differences through the FrontmostSource trait, which provides an opaque identifier string for the currently focused window. Each platform implements this trait to extract the appropriate application identifier.

Linux: X11 and Wayland Backends

In crates/openlogi-hook/src/linux.rs, the FrontmostSource trait defines the interface for retrieving the active window identifier. The implementation distinguishes between X11 and Wayland sessions using detect_session_kind() (lines 66‑81) and selects the appropriate backend via detect_frontmost_source() (lines 102‑130).

macOS Bundle Identification

On macOS, crates/openlogi-hook/src/macos.rs implements frontmost_app() using NSWorkspace.sharedWorkspace().frontmostApplication()?.bundleIdentifier() to retrieve the application's bundle ID (e.g., com.apple.Terminal).

Windows Executable Detection

The Windows implementation in crates/openlogi-hook/src/windows.rs provides frontmost_app(), which returns the active window's executable name as the identifier.

The Background Watcher Thread

The ForegroundAppWatcher in crates/openlogi-agent-core/src/watchers/foreground_app.rs runs on a dedicated background thread to monitor focus changes without blocking the main application logic.

The watcher registers a channel that monitors openlogi_hook::frontmost_bundle_id, polling the front-most source at approximately 1 Hz. When the identifier differs from the previously cached value, the watcher emits a change event through a broadcast channel, triggering the configuration system to recompute the effective bindings.

Configuration Overlay Resolution

When a new foreground-app identifier arrives, the core configuration resolves the effective binding map by merging per-application overlays into the base device profile.

In crates/openlogi-core/src/config.rs, the effective_bindings() method (lines 410‑428) calls app_overlay() (lines 800‑819) to retrieve platform-specific overrides. The overlay lookup checks overlays.get(app) for an exact match, or falls back to an exe:<lowercase-executable> key for Windows-style identifiers.

The crates/openlogi-core/src/bindings.rs file (lines 15‑86) then applies this overlay during the binding lookup, ensuring the per-app configuration takes precedence over generic device settings. The merged map is subsequently sent to the input-hook or overlay UI, causing the per-application profile to appear automatically.

Complete Auto-Switching Workflow

The end-to-end process follows these steps:

  1. Session Detection: detect_session_kind() determines whether the environment is X11, Wayland, or Unknown (lines 66‑81).
  2. Backend Selection: detect_frontmost_source() builds a candidate list ( prioritizing Wayland backends with X11 fallback) and returns the first successful connection (lines 102‑130).
  3. Identifier Reading: The selected backend's frontmost_bundle_id() returns an opaque string (WM_CLASS on X11, app_id on Wayland, bundle ID on macOS, or executable name on Windows).
  4. Change Detection: ForegroundAppWatcher compares the new identifier against its cache and posts updates to the broadcast channel only when the value changes.
  5. Binding Resolution: The agent calls Config::effective_bindings(device_key, app_bundle), which merges the per-app overlay into the base profile using app_overlay().
  6. Profile Application: The resulting binding map is applied to the device or displayed in the overlay UI, completing the automatic switch.

Implementation Examples

The following code demonstrates the core API usage for implementing auto-switching in your own integration:

// 1. Get the front-most identifier (platform-agnostic)
use openlogi_hook::frontmost_bundle_id;

if let Some(app_id) = frontmost_bundle_id() {
    println!("Frontmost app: {}", app_id);
}
// 2. Resolve the effective binding map for a device
use openlogi_core::{Config, DeviceKey};

let cfg = Config::load()?;                     // reads ~/.config/openlogi-dev/config.toml
let device_key = DeviceKey::Mouse;              // example device
let bindings = cfg.effective_bindings(device_key, &app_id);

// `bindings` now contains the base profile overlaid with any per-app entries
// 3. Background watcher (simplified)
use openlogi_agent_core::watchers::foreground_app::ForegroundAppWatcher;

let (tx, rx) = std::sync::mpsc::channel();
let watcher = ForegroundAppWatcher::new(tx);
watcher.start();                               // spawns a thread that polls every second

// In the main loop:
while let Ok(app_id) = rx.recv() {
    // react to a change in foreground app
    println!("Switching profile for {}", app_id);
}

Summary

  • Cross-platform identifiers: OpenLogi uses WM_CLASS on X11, app_id on Wayland, bundle IDs on macOS, and executable names on Windows to identify applications.
  • Polling architecture: A background thread in ForegroundAppWatcher checks the front-most application at ~1 Hz to detect focus changes without blocking the main thread.
  • Configuration merging: The effective_bindings() method in crates/openlogi-core/src/config.rs overlays per-app profiles onto base device configurations using the app_overlay() logic.
  • Namespace isolation: X11 and Wayland identifiers exist in separate namespaces, requiring distinct overlay entries for the same application under different display servers.

Frequently Asked Questions

How does OpenLogi handle the same application running on different platforms?

Because the identifier is taken exactly as reported by the platform-specific backend (WM_CLASS on X11 versus bundle ID on macOS), per-application profiles are platform-specific. You must define separate overlay entries for each platform identifier (e.g., firefox for X11 and org.mozilla.firefox for macOS) as the code does not attempt to reconcile these namespaces.

What is the polling frequency for focus changes, and can it be adjusted?

The ForegroundAppWatcher polls the front-most source at approximately 1 Hz (once per second). This frequency is hardcoded in the background thread implementation to balance responsiveness with CPU usage. The source code in crates/openlogi-agent-core/src/watchers/foreground_app.rs does not expose a configuration parameter for this interval in the current implementation.

How are binding conflicts resolved between base and overlay profiles?

According to crates/openlogi-core/src/bindings.rs (lines 15‑86), the overlay system performs a merge where per-application bindings take precedence over base device mappings. When effective_bindings() constructs the final map, values from app_overlay() overwrite any conflicting entries in the base profile, ensuring that app-specific configurations override generic settings.

What happens when the front-most detection fails or returns None?

If frontmost_bundle_id() returns None (indicating no focused window or detection failure), the configuration system falls back to the base device profile without applying any overlay. The app_overlay() method in crates/openlogi-core/src/config.rs handles missing identifiers by returning an empty overlay, leaving the base bindings unchanged until a valid identifier is detected.

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 →