# How Per‑Application Profiles Are Configured in Open Logi

> Learn how to configure per application profiles in OpenLogi. Discover how override tables in config.toml and UI APIs enable profile management for each device.

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

---

**Open Logi stores **per‑application profiles** as override tables inside each device's [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml), with the desktop UI providing selectors to create, edit, and delete these profiles through the `AppState` and `ProfileScopeActions` APIs.**

Open Logi, an open‑source Logitech HID++ device manager, lets you bind different actions to the same physical controls depending on which application is currently active. This per‑application profile system is implemented across three layers: a **TOML‑based configuration** in `openlogi‑core`, a **reactive state layer** in the desktop app, and a **UI feature module** that renders the profile selector. This article explains exactly how each layer works, with source‑level references from the AprilNEA/OpenLogi repository.

## Data Model: How Profiles Are Stored

Every Logitech device with a stable unit identifier gets its own persistent [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml). Inside this file, per‑application overrides live in a dedicated table that maps **application identifiers** to **action‑override maps**.

### Reading Existing Profiles

In [`crates/openlogi-desktop/src/state.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state.rs), the `AppState::app_profiles` method surfaces these entries to the UI:

```rust
// Extract from state.rs lines 94-100
pub fn app_profiles(&self) -> impl Iterator<Item = (&str, usize)> + '_ {
    self.config
        .app_profiles(&self.current_device_key)
        .map(|id| {
            let count = self.config.per_app_overrides(&self.current_device_key, id)
                .map(|m| m.len())
                .unwrap_or(0);
            (id, count)
        })
}

```

Each tuple contains the **profile identifier** (e.g., `com.google.Chrome` on macOS or `exe:C:\Tools\Zed.exe` on Windows) and the **number of overridden actions** for that app. The `Config::app_profiles` and `Config::per_app_overrides` methods in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) handle the actual TOML deserialization.

### Friendly Names for Unknown Apps

When the agent encounters an application it has never seen, `friendly_app_name` in [`crates/openlogi-desktop/src/features/profiles.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/profiles.rs) generates a human‑readable label:

```rust
// Simplified from profiles.rs lines 66-82
fn friendly_app_name(identifier: &str) -> String {
    if let Some(path) = identifier.strip_prefix("exe:") {
        Path::new(path).file_stem()
            .map(|s| s.to_string_lossy().into_owned())
            .unwrap_or_else(|| identifier.into())
    } else {
        // macOS bundle ID: com.company.AppName → "AppName"
        identifier.split('.').last()
            .map(|s| s.to_string())
            .unwrap_or_else(|| identifier.into())
    }
}

```

## Configuration Storage in openlogi‑core

The `Config` struct in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) provides the low‑level interface for per‑application profile persistence.

| Method | Purpose | Source Location |
|--------|---------|-----------------|
| `Config::app_profiles(device_key)` | Returns all profile identifiers for a device | Lines 665-672 |
| `Config::per_app_overrides(key, app)` | Returns the `HashMap<Action, Target>` for a specific app | Referenced in state.rs lines 101-108 |

These methods abstract the TOML structure so the desktop UI never parses configuration directly. Changes flow **downward**: UI actions mutate `AppState`, which delegates to `Config` methods, which rewrite the [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) atomically.

## UI Representation: The Profile Selector

The desktop application builds a **profile scope bar** that appears in both the button‑layout editor and the action‑ring editor. Two helper functions construct this UI:

- `button_profile_scope_bar` — for the main button configuration view
- `action_ring_profile_scope_bar` — for the radial action‑ring editor

Both create a `ProfileScopeShell` that drives the selector, as seen in [`crates/openlogi-desktop/src/features/profiles.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/profiles.rs) lines 74-110.

### ProfileScopeModel Structure

The selector's model, defined at lines 38-42 and populated at lines 71-84, contains three categories of applications:

```rust
// Conceptual structure from profiles.rs
pub struct ProfileScopeModel {
    pub current_app: Option<String>,      // Currently editing this profile
    pub persisted_profiles: Vec<AppProfile>, // Already have overrides stored
    pub recent_apps: Vec<AppEntry>,       // Running apps not yet persisted
    pub catalogue_apps: Vec<AppEntry>,    // Known applications from system scan
}

```

The **persisted profiles** list comes directly from `AppState::app_profiles`. **Recent apps** and **catalogue entries** let users create new profiles without manually typing identifiers.

## Creating and Editing Per‑Application Profiles

When a user selects an application from the profile selector, `ProfileScopeActions::select` updates the editing state:

```rust
// From profiles.rs lines 100-106
impl ProfileScopeActions {
    pub fn select(&mut self, app_id: String, cx: &mut AppContext) {
        AppState::update_bindings(cx, |state| {
            state.set_editing_app(Some(app_id));
        });
    }
}

```

The UI then displays bindings for that specific application. Any modifications trigger `AppState::update_bindings`, which ultimately persists changes through `Config::set_per_app_override` in the core crate.

### Practical Example: Adding a Chrome‑Specific Binding

```rust
// Start editing the Chrome profile
AppState::update_bindings(cx, |state| {
    state.set_editing_app(Some("com.google.Chrome".to_string()));
});

// Add a custom scroll-to-top binding
AppState::update_bindings(cx, |state| {
    state.bindings_mut().insert(
        openlogi_core::binding::Action::ScrollUp,
        openlogi_core::binding::Target::KeyCombo("Cmd+Home".into()),
    );
});

```

## Removing Per‑Application Profiles

Profile deletion requires confirmation through a dialog. The UI provides separate confirmation flows for buttons and action rings:

| Dialog Function | Removal Handler |
|-----------------|-----------------|
| `open_button_remove_confirmation` | `AppState::remove_editing_app_profile` |
| `open_action_ring_remove_confirmation` | `AppState::remove_editing_action_ring_profile` |

Both handlers clear the edit selection and delete the entry from [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml), as implemented in [`profiles.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/profiles.rs) lines 99-115 and 121-128.

```rust
// Simplified removal flow
AppState::update_bindings(cx, |state| {
    state.remove_editing_app_profile(); // Clears editing_app + deletes config entry
});

```

## Device Persistence Requirements

Not all connected devices support per‑application profiles. The `current_device_is_persistent` guard in [`crates/openlogi-desktop/src/state.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state.rs) (lines 85-92) ensures the profile UI only appears for devices with stable unit identifiers—typically Logitech's Unifying receivers or Bolt receivers in persistent mode. Transient BLE probes skip profile configuration entirely.

```rust
// From state.rs lines 85-92
pub fn current_device_is_persistent(&self) -> bool {
    self.current_device
        .as_ref()
        .map(|d| d.unit_id().is_some())
        .unwrap_or(false)
}

```

## Summary

- **Storage**: Per‑application profiles live as override tables in each device's [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml), managed by `Config::app_profiles` and `Config::per_app_overrides` in `openlogi‑core`.
- **State**: `AppState` in `openlogi‑desktop` exposes these profiles through `app_profiles()` and tracks the currently editing application via `editing_app` / `editing_action_ring_app`.
- **UI**: The `ProfileScopeShell` in [`features/profiles.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/features/profiles.rs) renders a selector with persisted profiles, recent apps, and catalogue entries; `ProfileScopeActions::select` initiates editing.
- **Lifecycle**: Create profiles by selecting apps and updating bindings; remove them through confirmation dialogs that call `remove_editing_app_profile`.
- **Requirements**: Profiles only apply to **persistent devices** with stable unit identifiers, not transient BLE connections.

## Frequently Asked Questions

### What identifier format does Open Logi use for per‑application profiles?

Open Logi uses **platform‑specific binary identifiers**: macOS applications use their bundle ID (e.g., `com.google.Chrome`), while Windows applications use the `exe:` prefix followed by the full executable path (e.g., `exe:C:\Tools\Zed.exe`). The `friendly_app_name` function in [`profiles.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/profiles.rs) derives human‑readable labels from these identifiers when displaying them in the UI.

### Can I configure per‑application profiles for any connected Logitech device?

No. Per‑application profile configuration is only available for **persistent devices**—those with a stable unit identifier that can store a [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml). The `current_device_is_persistent` guard checks `device.unit_id().is_some()`; transient BLE probes or unpaired devices bypass the profile UI entirely.

### How does Open Logi handle applications that have never been profiled before?

The profile selector shows **recently active applications** and **catalogue entries** alongside already‑persisted profiles. When you select a new app, `ProfileScopeActions::select` stores it in `AppState::editing_app`, and the first override you create automatically persists the profile to [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) via `Config::set_per_app_override`.

### Where are per‑application binding overrides actually written to disk?

All overrides are stored in the **per‑device TOML configuration file** managed by `openlogi‑core`. The `Config` struct in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) handles atomic read‑modify‑write cycles; desktop UI code never touches the filesystem directly, instead calling through `AppState::update_bindings` which delegates to core configuration methods.