How OpenLogi Detects the Foreground Application for Per-Application Profile Auto-Switching

OpenLogi detects the foreground application through platform-specific observers in the openlogi-hook crate that monitor system focus events, extracting stable application identifiers to trigger automatic profile switching via IPC.

OpenLogi is an open-source input device manager that automatically switches configuration profiles based on the active window. According to the AprilNEA/OpenLogi source code, this per-application profile auto-switching relies on a multi-layered architecture that spans platform-specific system hooks, structured IPC communication, and deterministic identifier matching.

The ForegroundApp Data Structure

At the core of the detection system is the ForegroundApp struct defined in crates/openlogi-core/src/app.rs. This lightweight data carrier contains two fields:

pub struct ForegroundApp {
    pub id: String,           // Stable identifier (bundle ID, executable path, or desktop file)
    pub display_name: String, // Human-readable application name
}

The id field serves as the canonical key for profile lookups, while display_name provides UI labels. This abstraction allows the rest of the system to remain platform-agnostic after the initial detection phase.

Platform-Specific Detection Mechanisms

The agent process (crate openlogi-hook) implements distinct observers for each operating system that register callbacks with native windowing APIs. These observers run continuously and publish ForegroundApp instances whenever the user switches windows.

macOS Implementation

On macOS, the observer registers with NSWorkspace.sharedNotificationCenter for NSWorkspaceDidActivateApplicationNotification events. According to crates/openlogi-hook/src/macos.rs, the callback extracts the bundle identifier and localized name from NSRunningApplication:

// crates/openlogi-hook/src/macos.rs
fn foreground_app_from_running_application(
    app: &NSRunningApplication, 
    pool: &mut AutoreleasePool
) -> Option<ForegroundApp> {
    let bundle_id = app.bundleIdentifier()?.to_string();
    let display_name = app.localizedName()?.to_string();
    Some(ForegroundApp {
        id: bundle_id,        // e.g., "com.apple.Safari"
        display_name,
    })
}

Windows Implementation

The Windows observer in crates/openlogi-hook/src/windows/foreground.rs utilizes SetWinEventHook to listen for EVENT_OBJECT_FOCUS and EVENT_SYSTEM_FOREGROUND events. The hook procedure retrieves the foreground window's process ID, queries the executable path, and normalizes it for stability:

// crates/openlogi-hook/src/windows/foreground.rs
unsafe extern "system" fn foreground_event_proc(
    _: HWINEVENTHOOK, 
    _: DWORD, 
    window: HWND, 
    _: LONG, 
    _: LONG, 
    _: DWORD, 
    _: DWORD
) {
    if let Some(pid) = get_window_process_id(window) {
        let exe_path = query_process_image_name(pid);
        let id = exe_path.to_ascii_lowercase();  // Normalize to lowercase
        let app = ForegroundApp { 
            id, 
            display_name: exe_path 
        };
        callback(Some(app));
    }
}

The system calls GetWindowThreadProcessId followed by OpenProcess and QueryFullProcessImageNameW to resolve the path, ensuring the identifier remains consistent across window instances.

Linux Implementation

For Linux, crates/openlogi-hook/src/linux.rs implements multiple backends depending on the display server:

  1. Wayland: Listens to wlroots foreign-toplevel events to capture the application ID (typically the desktop file name)
  2. GNOME/X11: Subscribes to D-Bus org.gnome.Shell signals or monitors X11 _NET_ACTIVE_WINDOW property changes

Both backends resolve to a desktop file identifier:

// crates/openlogi-hook/src/linux/wlr_foreign_toplevel.rs
fn handle_foreign_toplevel_event(event: ForeignToplevelEvent) -> Option<ForegroundApp> {
    let app_id = event.app_id?.to_string(); // e.g., "firefox.desktop"
    Some(ForegroundApp { 
        id: app_id, 
        display_name: app_id 
    })
}

IPC Communication and Profile Matching

Once detected, the foreground application information flows through a structured pipeline to the desktop UI.

Publishing via AgentSnapshot

The agent constructs an AgentSnapshot (defined in crates/openlogi-ipc/src/ipc.rs) containing a foreground field of type ForegroundApps. This struct holds both the current application and a history of recent apps:

// crates/openlogi-ipc/src/ipc.rs
pub struct AgentSnapshot {
    pub foreground:ForegroundApps,
    // ... other fields
}

pub struct ForegroundApps {
    pub current: Option<ForegroundApp>,
    pub history: Vec<ForegroundApp>,
}

The snapshot transmits over a tarpc socket to the desktop process (openlogi-desktop), ensuring the UI remains responsive by decoupling system hooking from interface rendering.

Desktop Profile Resolution

Upon receiving the snapshot, the desktop state logic in crates/openlogi-desktop/src/state/agent.rs matches the ForegroundApp.id against configured per-application profiles:

// crates/openlogi-desktop/src/state/agent.rs
fn update_active_profile(&mut self, snapshot:&AgentSnapshot) {
    if let Some(app) = &snapshot.foreground.current {
        self.active_profile = self.config.per_app_profiles()
            .get(&app.id)           // Lookup by exact ID match
            .cloned()
            .unwrap_or_else(|| self.config.default_profile().clone());
    }
}

The id field maps directly to TOML configuration sections such as [profile."firefox.desktop"] or [profile."com.apple.Safari"], enabling declarative profile binding.

Identifier Normalization for Stability

To ensure reliable matching across sessions and window instances, OpenLogi normalizes identifiers at the detection layer:

  • macOS: Uses the bundle identifier (e.g., com.apple.Safari) which remains constant across versions
  • Windows: Converts executable paths to lowercase and strips drive letters to handle path variations
  • Linux: Standardizes on desktop file names (e.g., firefox.desktop) rather than process names, accommodating flatpak and snap packaging variations

This normalization prevents profile mismatches when applications launch from different working directories or mount points.

Code Example: Complete Detection Flow

The following example demonstrates how the components integrate to switch profiles when a user opens Firefox on Linux:

// 1. Hook receives Wayland event (openlogi-hook/src/linux.rs)
let app = ForegroundApp {
    id: "firefox.desktop".to_string(),
    display_name: "Firefox".to_string(),
};

// 2. Agent publishes via IPC (openlogi-ipc/src/ipc.rs)
let snapshot = AgentSnapshot {
    foreground: ForegroundApps {
        current: Some(app),
        history: vec![],
    },
    // ... device states, etc.
};
ipc_client.send(snapshot).await?;

// 3. Desktop matches profile (openlogi-desktop/src/state/agent.rs)
if let Some(profile) = config.per_app_profiles().get("firefox.desktop") {
    apply_profile(profile);  // DPI, key mappings, etc.
}

Summary

  • Platform abstraction: The ForegroundApp struct in crates/openlogi-core/src/app.rs provides a unified interface across macOS, Windows, and Linux detection mechanisms.
  • System integration: Native APIs including NSWorkspace notifications, SetWinEventHook, and wlroots foreign-toplevel events capture focus changes with minimal latency.
  • Stable identifiers: Bundle IDs, normalized executable paths, and desktop file names ensure consistent profile matching regardless of installation method.
  • Decoupled architecture: IPC via tarpc in crates/openlogi-ipc/src/ipc.rs separates privileged system hooks from the user interface.
  • Declarative configuration: The id field maps directly to TOML profile keys, enabling automatic switching without manual intervention.

Frequently Asked Questions

What fields does the ForegroundApp struct contain?

The ForegroundApp struct contains two String fields: id and display_name. The id serves as the stable canonical identifier used for profile lookups, while display_name provides human-readable text for UI presentation. Both fields populate from platform-specific sources such as macOS bundle identifiers, Windows executable paths, or Linux desktop file names.

How does OpenLogi handle foreground detection on Wayland?

On Wayland, the observer in crates/openlogi-hook/src/linux.rs connects to the wlroots foreign-toplevel management protocol. This protocol emits events when toplevel windows gain focus, providing the application ID (typically matching the desktop file name) without requiring elevated privileges or X11 polling. The implementation falls back to D-Bus or X11 _NET_ACTIVE_WINDOW for GNOME or non-wlroots compositors.

Why does Windows use lowercase executable paths as identifiers?

Windows normalizes executable paths to lowercase in crates/openlogi-hook/src/windows/foreground.rs to ensure profile stability. File system paths may vary in casing due to different installation locations or user input, but the lowercase normalization guarantees that C:\Program Files\App.exe and c:\program files\app.exe resolve to the same profile key. The system also strips drive letters to handle scenarios where applications run from different volumes.

How does the desktop process know when to switch profiles?

The desktop process receives an AgentSnapshot via the tarpc IPC channel whenever the foreground application changes. The update_active_profile method in crates/openlogi-desktop/src/state/agent.rs examines snapshot.foreground.current, extracts the id field, and performs a hashmap lookup against config.per_app_profiles(). If a matching profile exists, the system applies it immediately; otherwise, it falls back to the default profile. This reactive design ensures profiles switch in real-time as users alt-tab between applications.

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 →