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

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:

/// 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, 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.

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

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

#[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:

// 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 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.

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 →