# How OpenLogi Handles Per-App Profiles for Logitech Devices: Configuration Architecture Explained

> Discover how OpenLogi handles per-app profiles for Logitech devices using application-specific overrides that activate automatically. Learn about the configuration architecture.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: architecture
- Published: 2026-08-29

---

**OpenLogi implements per-app profiles for Logitech devices by maintaining a global device configuration augmented with application-specific overrides that automatically activate when the foreground window changes.**

OpenLogi is an open-source input device manager designed to replace proprietary Logitech software on macOS and Linux. The application achieves sophisticated **per-app profiles for Logitech devices** through a hierarchical TOML-based configuration system that layers binding overrides on top of default mappings, enabling context-aware button remapping without manual profile switching.

## Configuration Data Model for Per-App Bindings

At the core of OpenLogi's profile system lies the `DeviceConfig` struct, which stores both global defaults and application-specific exceptions in a nested map structure.

### The per_app_bindings Structure

Each device configuration contains a `per_app_bindings` field defined in [[`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config/device.rs#L197-L210):

```rust
pub per_app_bindings: BTreeMap<String, BTreeMap<ButtonId, Action>>,

```

The **outer key** represents the application identifier (e.g., `"com.apple.Safari"` or `"com.microsoft.VSCode"`), while the **inner map** associates specific physical buttons (`ButtonId`) with logical actions. This `BTreeMap` structure ensures deterministic ordering and efficient lookups during runtime resolution.

## Managing Per-App Overrides via the Config API

The `Config` struct exposes high-level helpers for programmatically managing these overlays without manually editing TOML files. According to [[`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config.rs#L549-L572), the primary interface is:

- **`set_per_app_binding(device_key, app_id, button, Some(action))`** – Adds or updates an override for a specific application and button combination.
- **`set_per_app_binding(device_key, app_id, button, None)`** – Removes a specific override and automatically prunes empty application entries to keep the configuration clean.

These methods handle persistence automatically, ensuring the TOML file reflects changes immediately while maintaining structural integrity.

## Runtime Profile Resolution and Merging

When the agent service needs to determine which actions to execute for a button press, it performs a real-time merge of global and per-app configurations.

### Foreground Application Detection

The system relies on the hook subsystem implemented in [[`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs#L516-L522) to monitor the operating system's accessibility APIs. This component continuously reports the `ForegroundApp::id`, which serves as the lookup key for profile selection.

As documented in the IPC contract at [[`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs#L60-L66), the agent matches this identifier against the keys stored in `per_app_bindings` to determine which configuration layer to apply.

### The Overlay Mechanism

The actual merging logic resides in the `app_overlay` helper method within [[`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config.rs#L537-L552). This function implements the precedence rules:

1. If the current foreground application exists as a key in `per_app_bindings` and contains non-empty mappings, those bindings supersede the global profile.
2. If no matching application entry exists, or if the specific button lacks an override, the system falls back to the global device configuration.

This overlay approach ensures that users can override only specific buttons for specific applications while inheriting global defaults for all other controls.

## Desktop UI State Management

The graphical client bridges the configuration backend with user interaction through the `ActionRing` state structure. Located in [[`crates/openlogi-desktop/src/state/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/action_ring.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-desktop/src/state/action_ring.rs#L83-L107), this component maintains a `per_app` map that mirrors the core configuration structure.

When users switch between applications, the UI loads the corresponding per-app layout for editing. If the current application lacks custom bindings, the interface transparently displays the default global profile, providing visual feedback about which configuration layer is active. The `active_profile_name()` method returns the current context, enabling the interface to indicate whether the user is viewing global settings or application-specific overrides.

## Code Examples for Per-App Profile Management

The following examples demonstrate interacting with the per-app profile system using the OpenLogi configuration API:

**Adding a per-app binding:**

```rust
let mut cfg = Config::load_from_path("config.toml")?;
cfg.set_per_app_binding(
    "2b042",                     // device serial
    "com.apple.Safari",          // app identifier
    ButtonId::Back,              // button to remap
    Some(Action::Undo),          // new action
);
cfg.save()?;                     // persists to TOML

```

**Removing a per-app binding:**

```rust
cfg.set_per_app_binding(
    "2b042",
    "com.apple.Safari",
    ButtonId::Back,
    None,                         // None prunes the entry
);

```

**Fetching the active profile name from desktop state:**

```rust
let active_name = state.active_profile_name(); // Option<String>
println!("Current profile: {:?}", active_name);

```

## Summary

- OpenLogi stores **per-app profiles** as nested `BTreeMap` structures within the `DeviceConfig` struct, mapping application identifiers to button-specific action overrides.
- The `set_per_app_binding` API in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) provides type-safe methods for adding, updating, and removing overrides while automatically cleaning empty entries.
- **Runtime resolution** occurs through the `app_overlay` mechanism, which layers per-app bindings on top of global configurations when the hook subsystem detects a matching foreground application.
- The desktop UI maintains parity with the backend through the `ActionRing` state, enabling visual editing of per-app configurations with automatic fallback to global defaults.

## Frequently Asked Questions

### What configuration file format does OpenLogi use for per-app profiles?

OpenLogi persists all device configurations, including per-app bindings, to **TOML files** on disk. The `per_app_bindings` map serializes as nested tables where application identifiers serve as keys, making the structure both human-readable and machine-parseable.

### How does OpenLogi detect which application is currently active?

The system utilizes a dedicated **hook subsystem** ([`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs)) that interfaces with platform-specific accessibility APIs to monitor foreground window changes. This component reports the `ForegroundApp::id` to the agent, which then performs lookups against the `per_app_bindings` keys to determine which profile layer to activate.

### Do per-app profiles completely replace the global device configuration?

No, per-app profiles operate as **overlays** rather than replacements. When the agent resolves button actions, it first checks for application-specific overrides using the `app_overlay` helper. If a button lacks a per-app mapping, the system falls back to the global bindings defined in the base `DeviceConfig`, ensuring consistency across applications.

### How does the desktop UI handle switching between applications?

The `ActionRing` state manager in [`crates/openlogi-desktop/src/state/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/action_ring.rs) tracks the current foreground application and loads the corresponding per-app layout for display. When users switch to an application without custom bindings, the UI automatically displays the default global profile, while `active_profile_name()` indicates whether the current view represents global or application-specific settings.