How OpenLogi Identifies Applications for Per-App Profiles on macOS

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

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

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

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:

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:


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

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

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 →