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

> Discover how OpenLogi identifies applications for per-app profiles on macOS by leveraging NSRunningApplication and NSWorkspace notifications for precise profile matching.

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

---

**OpenLogi identifies the active application for per-app profiles by extracting the unique bundle identifier from macOS's `NSRunningApplication` via the `NSWorkspace` notification system, then matching that identifier against configured profile keys.**

OpenLogi is an open-source Rust application that enables per-application input customization for Logitech devices. On macOS, the system identifies foreground applications using platform-specific APIs rather than process names or window titles, ensuring accurate profile matching even when multiple app instances run simultaneously. The implementation spans the hook backend, core data models, and IPC communication layer to reliably track and respond to application switches.

## Extracting the Foreground Application via NSWorkspace

The detection logic resides in the macOS-specific hook backend, which interfaces directly with Apple's `AppKit` frameworks to receive system-level activation events.

### Monitoring Activation Notifications

In [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs), the backend registers a watcher on `NSWorkspaceDidActivateApplicationNotification`. When macOS broadcasts that the user has switched to a different application, the handler extracts the `NSRunningApplication` object representing the newly active foreground process.

The core extraction function `foreground_app_from_running_application` (lines 48–56) converts the Objective-C application object into Rust's `ForegroundApp` struct:

```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();
    // Convert the Objective-C strings to owned Rust Strings.
    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 })
}

```

This function relies on `app.bundleIdentifier()` to retrieve the canonical bundle ID (e.g., `com.apple.Safari`), which serves as the immutable **primary key** for profile lookups.

### Exposing to the Core System

The `HookBackend::frontmost_app()` method calls `foreground_app_from_running_application` to expose the current foreground application to the rest of the OpenLogi system. This abstraction ensures that the platform-specific macOS code remains encapsulated within the hook crate while providing a generic `ForegroundApp` interface to the cross-platform logic.

## Storing Application Identifiers in the Profile Model

Once extracted, the bundle identifier must be stored in a format that the configuration system can reference and match against user-defined profiles.

### The ApplicationTarget Struct

In [`crates/openlogi-core/src/binding/application_target.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/application_target.rs), the `ApplicationTarget` type defines how OpenLogi represents target applications. The struct stores the bundle identifier in its `path` field, while `display_name` holds a human-readable string for UI rendering:

```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"
}

```

According to the OpenLogi source code, the `path` field accommodates bundle identifiers, file system paths, or URLs depending on the platform and configuration format, but on macOS it consistently stores the **bundle identifier** as the unique application key.

### Bundle ID as the Primary Key

The configuration system uses the bundle identifier as the sole lookup key for per-app profiles. When a user creates a profile for an application, OpenLogi persists the bundle identifier (e.g., `com.apple.Safari`) rather than the localized display name (e.g., "Safari"). This approach prevents profile mismatches when users rename applications or run localized versions with different display names.

## Matching Active Apps to Configuration Profiles

With the bundle identifier extracted and stored, the system must compare the current foreground application against configured profiles to activate the correct input bindings.

### IPC Transport of Foreground Data

The IPC layer defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) serializes the `ForegroundApp` struct (containing both `id` and `display_name`) and transmits it from the background agent to the desktop GUI. This separation of concerns allows the agent to handle low-level input hooking while the GUI manages profile selection and user feedback.

### Profile Lookup Logic

The desktop UI's state management logic (located in `crates/openlogi-desktop/src/state/*.rs`) receives the current `ForegroundApp` and performs a dictionary lookup against the profile map loaded from the TOML configuration:

```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
}

```

When the `current_id` (bundle identifier) matches a key in the profile configuration, OpenLogi immediately applies the associated button remappings and sensitivity settings. The display name is used only for UI presentation in the status bar or configuration window, while the bundle identifier drives the actual logic.

## Code Implementation Examples

The following examples demonstrate how to interact with OpenLogi's application identification system programmatically.

**Retrieving the foreground application from the 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);
}

```

**Defining a per-app profile in TOML configuration:**

```toml

# ~/.config/openlogi/profiles.toml

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

[profile."com.microsoft.VSCode"]
button_1 = "Command+Shift+F"

```

**Matching logic for profile selection:**

```rust
// Pseudo-code based on crates/openlogi-desktop/src/state logic
fn get_active_profile(config: &Config) -> Option<&Profile> {
    let current_id = HookBackend::frontmost_app()
        .map(|app| app.id)
        .unwrap_or_default();
    
    config.profiles.get(&current_id)
}

```

## Summary

- OpenLogi identifies macOS applications using **bundle identifiers** extracted via `NSWorkspaceDidActivateApplicationNotification` in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs).
- The `foreground_app_from_running_application` function converts `NSRunningApplication` objects into Rust structs containing the bundle ID and localized display name.
- The `ApplicationTarget` struct in [`crates/openlogi-core/src/binding/application_target.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/application_target.rs) stores the bundle identifier in its `path` field as the canonical profile key.
- Profile matching occurs in the desktop state layer by comparing the current foreground app's bundle ID against keys in the TOML configuration.
- The IPC system transports both the identifier (for logic) and display name (for UI) between the background agent and frontend interface.

## Frequently Asked Questions

### What identifier does OpenLogi use for profile matching on macOS?

OpenLogi uses the **bundle identifier** (e.g., `com.apple.Safari`) as the unique key for matching applications to profiles. This identifier is extracted from the `NSRunningApplication` object via the `bundleIdentifier()` method in the macOS hook backend, providing a stable identifier that persists across application renames or localization changes.

### How does OpenLogi detect when the user switches applications?

The system registers a watcher on `NSWorkspaceDidActivateApplicationNotification` within the [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs) module. When macOS posts this notification upon a foreground application change, the hook backend extracts the new application's metadata and propagates it through the IPC layer to trigger profile updates.

### Can OpenLogi distinguish between different instances of the same app?

Yes, because OpenLogi relies on **bundle identifiers** rather than process IDs or window titles. All instances of the same macOS application share the same bundle identifier (e.g., `com.apple.Safari`), so they trigger the same per-app profile. This design intentionally groups all windows of an application under a single profile configuration.

### Where is the per-app profile configuration stored?

Per-app profiles are stored in a TOML configuration file, typically located at `~/.config/openlogi/profiles.toml`. Each profile section uses the application's bundle identifier as the key (e.g., `[profile."com.apple.Safari"]`), followed by key-value pairs defining button mappings and sensitivity settings specific to that application.