How OpenLogi Identifies Applications for Per-App Profiles on macOS

OpenLogi uses the macOS bundle identifier extracted from NSRunningApplication via NSWorkspace notifications to uniquely identify foreground applications and match them against per-app configuration profiles.

OpenLogi, an open-source input remapping tool available in the AprilNEA/OpenLogi repository, implements macOS per-app profiles by tracking the foreground application through system-level Cocoa APIs. The application identification system relies on stable bundle identifiers rather than volatile process names or window titles to ensure reliable profile switching. This article examines the Rust source code to explain how the software detects active applications and maps them to user-defined configurations.

Monitoring Application Activation with NSWorkspace Notifications

The detection pipeline begins in crates/openlogi-hook/src/macos.rs, where OpenLogi registers a system watcher for NSWorkspaceDidActivateApplicationNotification. This Cocoa notification fires whenever the user switches to a different application, providing immediate updates about the new foreground process without requiring polling loops.

When the notification triggers, the backend extracts the NSRunningApplication object representing the newly active app. The foreground_app_from_running_application function handles the conversion from Objective-C objects to Rust data structures, isolating the bundle identifier through the bundleIdentifier() method.

Extracting the Bundle Identifier

The core identification logic resides in the foreground_app_from_running_application function. It retrieves the bundle identifier and localized display name from the NSRunningApplication instance, converting Objective-C strings to owned Rust String values within an autorelease pool:

// crates/openlogi-hook/src/macos.rs
fn foreground_app_from_running_application(
    app: &NSRunningApplication,
    pool: objc2::rc::AutoreleasePool<'_>,
) -> Option<ForegroundApp> {
    let bundle_id = app.bundleIdentifier()?;
    let name = app.localizedName();
    let (id, name) = unsafe {
        (
            bundle_id.to_str(pool).to_owned(),
            name.as_ref().map(|n| n.to_str(pool).to_owned()),
        )
    };
    let display_name = name.unwrap_or_else(|| id.clone());
    Some(ForegroundApp { id, display_name })
}

The id field contains the bundle identifier (e.g., com.apple.Safari), which serves as the canonical key for profile matching throughout the application. This value remains constant even if the user moves the application bundle to a different directory or updates the software.

Structuring Application Data with ForegroundApp

The ForegroundApp struct represents the active application within OpenLogi's internal systems. It contains two fields: id stores the bundle identifier used for programmatic matching, while display_name holds the human-readable application name for UI presentation in the desktop interface.

The HookBackend::frontmost_app() method exposes this structure to the rest of the program, allowing the IPC layer in crates/openlogi-ipc/src/ipc.rs to transport application state from the privileged hook process to the desktop GUI. This separation ensures that the UI can display the active application name while the agent performs profile lookups using the stable bundle ID.

Mapping Bundle IDs to Per-App Profiles

Profile resolution occurs through a lookup system that compares the current foreground application's bundle ID against keys in the user's TOML configuration file. The ApplicationTarget type in crates/openlogi-core/src/binding/application_target.rs formalizes this relationship.

The ApplicationTarget Configuration Model

The core data structure stores the bundle identifier in its path field, supporting bundle IDs, file paths, or URLs as application targets:

// crates/openlogi-core/src/binding/application_target.rs
pub struct ApplicationTarget {
    path: String,          // e.g. "com.apple.Safari"
    display_name: String, // e.g. "Safari"
}

When defining per-app profiles, users specify the bundle identifier as the profile key in the configuration file:


# ~/.config/openlogi/profiles.toml

[profile."com.apple.Safari"]
button_1 = "Command+L"
button_2 = "Space"

Profile Lookup and Matching Logic

The desktop UI's state management logic performs the actual profile resolution by comparing the received id field from the IPC message against the keys of loaded per-app profiles. When identifiers match, the GUI highlights the corresponding profile and the agent applies those remapping bindings to the active input device. The display name is used solely for UI rendering, while the bundle ID drives all programmatic matching decisions.

Practical Implementation Example

To retrieve the current foreground application programmatically using OpenLogi's hook backend:

use openlogi_hook::HookBackend;

let front_app = HookBackend::frontmost_app();
if let Some(app) = front_app {
    println!("Active app bundle ID: {}", app.id);
    println!("Display name: {}", app.display_name);
}

The profile matching implementation follows this pattern:

let current_id = HookBackend::frontmost_app()
    .map(|app| app.id)
    .unwrap_or_default();

if let Some(profile) = config.profiles.get(&current_id) {
    // Apply `profile` bindings to the current input context
}

Summary

  • OpenLogi monitors NSWorkspaceDidActivateApplicationNotification in crates/openlogi-hook/src/macos.rs to detect foreground application changes.
  • The foreground_app_from_running_application function extracts the bundle identifier from NSRunningApplication as the stable unique key.
  • The ForegroundApp struct transports both the bundle ID (for matching) and display name (for UI) through the IPC layer.
  • The ApplicationTarget type stores bundle identifiers in the path field, enabling TOML configuration profiles keyed by bundle ID.
  • Profile resolution occurs in the desktop state logic by comparing the current foreground app's bundle ID against configured per-app profile keys.

Frequently Asked Questions

What identifier does OpenLogi use to distinguish applications?

OpenLogi uses the macOS bundle identifier (e.g., com.apple.Safari) extracted from the NSRunningApplication object via the bundleIdentifier() method. This identifier remains stable across application updates and relocations, unlike process names or executable paths that may change.

How does OpenLogi detect when the active application changes?

The software registers an observer for NSWorkspaceDidActivateApplicationNotification in the hook backend (crates/openlogi-hook/src/macos.rs). When the user switches applications, macOS triggers this notification, allowing OpenLogi to immediately capture the new foreground app's metadata through the foreground_app_from_running_application function.

Where are per-app profiles stored in OpenLogi?

Per-app profiles reside in the TOML configuration file, typically located at ~/.config/openlogi/profiles.toml. Each profile section uses the bundle identifier as its key (e.g., [profile."com.apple.Safari"]), containing button-to-keybinding mappings specific to that application.

Can OpenLogi distinguish between different instances of the same app?

OpenLogi identifies applications solely by their bundle identifier, not by process ID or window-specific attributes. Consequently, it treats all instances of the same application (e.g., multiple Safari windows) as a single profile target, applying identical remapping rules to every instance of that application.

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 →