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

> Discover how OpenLogi detects the foreground application using system focus events and stable identifiers for seamless per-application profile auto-switching. Learn more.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-30

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/app.rs). This lightweight data carrier contains two fields:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs), the callback extracts the bundle identifier and localized name from `NSRunningApplication`:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/agent.rs) matches the `ForegroundApp.id` against configured per-application profiles:

```rust
// 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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.