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

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/master/crates/openlogi-core/src/config/device.rs#L197-L210):

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

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:

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

Fetching the active profile name from desktop state:

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 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) 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 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.

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 →