# How OpenLogi Identifies Applications for Per-App Profiles on macOS

> Discover how OpenLogi identifies macOS applications for per-app profiles using bundle identifiers and NSWorkspace notifications. Streamline your configurations effortlessly.

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

---

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

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

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

```toml

# ~/.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:

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

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