# How OpenLogi Resolves Per-Application Profiles Across macOS, Linux, and Windows

> OpenLogi resolves per-application profiles across macOS Linux and Windows by normalizing foreground app identifiers and matching them against configured profile keys. Learn how.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-09-02

---

**OpenLogi resolves per-application profiles by normalizing platform-specific foreground application identifiers into a common `ForegroundApp` struct, then performing an exact-match lookup against configured profile keys in `effective_bindings`.**

OpenLogi is an open-source input remapping tool that needs to switch profiles automatically based on which application currently has focus. This article explains how the codebase detects the foreground application on each supported operating system and maps it to the correct per-application profile configuration.

## The Core Abstraction: ForegroundApp

All platform-specific detection logic converges on a single type defined in [`crates/openlogi-core/src/app.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/app.rs):

```rust
/// Platform-agnostic representation of the currently focused application
pub struct ForegroundApp {
    /// OS-specific identifier used for profile matching
    pub id: String,
    /// Human-readable label for UI display
    pub display_name: String,
}

```

The `id` field contains the raw platform identifier, while `display_name` provides a user-friendly label when the platform doesn't supply one. This separation allows OpenLogi to match profiles deterministically while still presenting readable names in the interface.

## Platform-Specific Identifier Extraction

Each operating system provides a different native identifier that becomes the `ForegroundApp.id`. The hook layer implementations handle these differences without polluting the core logic.

### macOS: Bundle Identifiers and Fallback Paths

In [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs), the function `foreground_app_from_running_application` extracts the identifier from `NSRunningApplication`:

1. **Primary method**: Read the `bundleIdentifier` property (e.g., `com.apple.Safari`)
2. **Fallback**: Use the executable path if the bundle ID is unavailable

The hook registers an observer via `watch_frontmost_application_activations`, which subscribes to `NSWorkspaceDidActivateApplicationNotification` and builds a `ForegroundApp` on each activation change.

```rust
// From macos.rs – notification callback converts NSRunningApplication
fn foreground_app_from_running_application(app: NSRunningApplication) -> Option<ForegroundApp> {
    let id = app.bundle_identifier()
        .map(|s| s.to_string())
        .unwrap_or_else(|| app.executable_url()?.path().to_string());
    
    Some(ForegroundApp {
        id,
        display_name: app.localized_name()?.to_string(),
    })
}

```

### Linux: X11 WM_CLASS and Wayland app_id

The Linux implementation in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs) uses a trait-based `FrontmostSource` abstraction to support multiple window systems:

| Backend | Identifier Source | Example |
|---------|-------------------|---------|
| **X11** | `WM_CLASS` property (class component) | `firefox` |
| **Wayland** | `app_id` via xdg-shell protocol | `org.mozilla.Firefox` |

The `FrontmostController` selects the appropriate backend at runtime and yields a `ForegroundApp` with the same structure as macOS. This allows OpenLogi to work across display server transitions without configuration changes.

### Windows: Executable Path Normalization

Windows detection in [`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs) uses `ForegroundApplicationObserver` to monitor foreground window changes. The identifier is constructed from:

- Query the foreground window handle via `GetForegroundWindow()`
- Obtain the process ID with `GetWindowThreadProcessId()`
- Retrieve the full executable path via `QueryFullProcessImageNameW()`
- **Lowercase the entire path** for consistent matching

```rust
// Conceptual flow from windows.rs
let path = get_process_image_path(pid)?;
let id = path.to_lowercase(); // "c:\program files\chrome\chrome.exe"

```

This path-based approach avoids ambiguity that could arise from process names alone, since multiple applications may share the same executable name in different directories.

## From Detection to Profile Resolution

The foreground application flows through four architectural layers before reaching the profile matching logic:

### 1. Hook Layer (Platform Detection)

Each platform's `watch_frontmost_application_activations` function accepts a callback that receives `Option<ForegroundApp>`:

```rust
// Unified API across all platforms
openlogi_hook::watch_frontmost_application_activations(|app| {
    // app: Option<ForegroundApp>
    agent.update_foreground(app);
});

```

### 2. Agent Core (State Management)

`openlogi-agent-core` maintains the latest `ForegroundApp` in its internal state, making it available for IPC serialization.

### 3. IPC Layer (Cross-Process Communication)

The `ForegroundApp` struct derives `Serialize` and `Deserialize` for tarpc-based communication. The wire format is defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs):

```rust
#[derive(Serialize, Deserialize)]
pub struct ForegroundApps {
    pub current: Option<ForegroundApp>,
}

```

### 4. Desktop UI (Profile Matching)

The UI receives `ForegroundApps` messages and delegates to `openlogi_core::config::effective_bindings` for resolution. This function performs an **exact string match** between `ForegroundApp.id` and profile configuration keys:

```rust
// From openlogi-core configuration logic
impl Config {
    pub fn effective_bindings(&self, app_id: String) -> Option<ProfileId> {
        // Exact match only — no normalization or fuzzy matching
        self.per_app_profiles.get(&app_id).cloned()
    }
}

```

If no profile matches the current `id`, the system falls back to the **"Default"** profile.

## Why Exact-Match Profile Keys Matter

The design intentionally avoids cross-platform identifier normalization. This has important implications for users:

- **macOS profile** with key `com.apple.Safari` will not match on Linux or Windows
- **Linux Wayland profile** with key `org.mozilla.Firefox` differs from **X11** key `firefox`
- **Windows profile** must use the full lowercased path: `c:\program files\mozilla firefox\firefox.exe`

Users must create separate per-application profiles for each platform where they run OpenLogi. This trade-off guarantees predictable behavior without heuristic matching that could activate wrong profiles.

## Implementation Consistency Across Platforms

The modular architecture enables future platform support. Adding a new window system requires only:

1. Implement a `FrontmostSource` trait that returns `Option<String>` for the identifier
2. Convert to `ForegroundApp` with appropriate `display_name` fallback
3. Register with the common `watch_frontmost_application_activations` entry point

All downstream layers—agent state, IPC serialization, and profile resolution—remain unchanged.

## Summary

- `ForegroundApp` in [`crates/openlogi-core/src/app.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/app.rs) provides the unified abstraction for foreground application detection
- macOS uses bundle identifiers with executable path fallback; Linux uses `WM_CLASS` or `app_id`; Windows uses lowercased executable paths
- Profile matching in `effective_bindings` requires exact string equality between `ForegroundApp.id` and configuration keys
- The pipeline flows through hook detection → agent state → IPC serialization → UI resolution with no platform-specific logic in the core matching path
- Cross-platform profile portability is intentionally sacrificed for matching determinism

## Frequently Asked Questions

### Why doesn't OpenLogi normalize identifiers across platforms?

Heuristic normalization introduces false positive risks. A macOS bundle ID like `com.google.Chrome` and a Linux `app_id` of `google-chrome` might represent the same browser, but automatic equivalence rules could incorrectly match unrelated applications. Exact matching puts control entirely in user configuration.

### Can I use the same configuration file on macOS and Linux?

Only the "Default" profile and global settings are portable. Per-application profiles must be recreated for each platform with the appropriate `id` values. The configuration format supports platform-specific sections if future versions choose to add this feature.

### What happens when a profile doesn't match any configured application?

The system transparently falls back to the "Default" profile. This ensures OpenLogi remains functional even when encountering previously unknown applications or when detection temporarily fails.

### How does OpenLogi handle applications with no detectable identifier?

Some platforms may yield `None` from detection—for example, Wayland clients that don't set `app_id` or restricted system processes. In these cases, `watch_frontmost_application_activations` passes `None` to the callback, and the UI continues using the current profile without change.