# OpenLogi Actions Ring Per-Application Layout System: A Complete Technical Guide

> Explore OpenLogi Actions Ring per-application layout system. Discover how to define custom layouts that override defaults for specific foreground applications. Get the technical guide.

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

---

**OpenLogi’s Actions Ring uses a hierarchical configuration system where per-application layouts defined in `action_ring.per_app` override the default eight-slot ring layout when specific foreground applications are active.**

The **OpenLogi** project (available at `AprilNEA/OpenLogi`) implements a sophisticated input customization layer for Logitech devices that supports context-aware control schemes. At the heart of this system lies the **Actions Ring per-application layout system**, which allows users to define distinct eight-position radial menus that automatically activate based on the current foreground application.

## Core Data Model and Configuration Schema

All configuration resides in a TOML file ([`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml)) parsed by the `openlogi-core` crate. The per-application layout system relies on a strictly typed hierarchy that ensures type safety across the desktop UI, background agent, and overlay renderer.

### ActionRingSlot Enumeration

The physical geometry of the ring is defined by the `ActionRingSlot` enum in [`crates/openlogi-core/src/binding/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action_ring.rs). This enum establishes eight fixed positions arranged clockwise around the cursor:

```rust
pub enum ActionRingSlot {
    Top, TopRight, Right, BottomRight,
    Bottom, BottomLeft, Left, TopLeft,
}

```

Each variant includes geometry helpers for positioning that the overlay uses to render slots at the correct screen coordinates.

### RingAction Validation

To prevent invalid states, the system employs the `validate_ring_action` function. This validation prohibits empty actions (`Action::None`) and recursive triggers (`Action::ShowActionsRing`) that would create infinite loops:

```rust
fn validate_ring_action(action: &Action) -> Result<(), RingActionError> {
    match action {
        Action::None => Err(RingActionError::EmptyAction),
        Action::ShowActionsRing => Err(RingActionError::RecursiveTrigger),
        _ => Ok(()),
    }
}

```

### ActionRingEntry Structure

Each slot contains an `ActionRingEntry` struct that wraps the executable action with optional presentation metadata:

```rust
pub struct ActionRingEntry {
    action: RingAction,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    icon: Option<ActionRingIcon>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    label: Option<String>,
}

```

### ActionRingConfig Container

The configuration schema in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) defines the `ActionRingConfig` struct as the primary container. It stores the global toggle, haptic feedback settings, default layout, and the critical `per_app` override map:

```rust
#[derive(Default, Serialize, Deserialize)]
pub struct ActionRingConfig {
    pub enabled: bool,
    pub haptics: bool,
    pub default: BTreeMap<ActionRingSlot, ActionRingEntry>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub per_app: BTreeMap<String, BTreeMap<ActionRingSlot, ActionRingEntry>>,
}

```

The `per_app` field uses the application bundle identifier (e.g., `com.apple.Safari`) as the key, allowing complete layout overrides for specific software.

## Runtime Resolution Flow

When a user activates the Actions Ring, the system executes a multi-stage resolution pipeline that determines which layout to display.

### Foreground Application Detection

The `openlogi-agent-core` crate contains platform-specific watchers in [`openlogi-agent-core/src/watchers/foreground_app.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-agent-core/src/watchers/foreground_app.rs). These detect the current foreground application on macOS, Linux, or Windows and propagate the bundle identifier to the orchestrator.

### Layout Resolution Logic

The `Config::action_ring(&self, device_key: &str)` method in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) returns the merged view. The resolution algorithm performs the following steps:

1. Retrieve the device's `ActionRingConfig`
2. Clone the `default` layout as the base
3. Check if `per_app` contains an entry matching the current foreground app's bundle ID
4. If found, merge the per-application overrides (specific slots replace default entries)
5. Return the final eight-slot layout

This merge strategy ensures that per-application configurations only override explicitly defined slots, preserving default actions for unspecified positions.

### IPC Transmission to Overlay

Once resolved, the layout travels across process boundaries via the IPC layer defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs). The agent creates an `ActionRingSession` and transmits a `ShowActionsRing` RPC containing the device key and resolved layout:

```rust
pub struct ShowActionsRing {
    pub device_key: String,
    pub layout: ActionRingLayout,
}

```

The overlay process ([`openlogi-overlay/src/main.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-overlay/src/main.rs)) receives this structure, uses `ActionRingSlot::placement` for positioning, and renders the icons and labels accordingly. Haptic feedback triggers through the coalescing worker in [`openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-agent/src/server.rs) if `haptics` is enabled.

## Per-Application Configuration Examples

### Loading a Device’s Layout with Per-App Override

To programmatically resolve the effective layout for the current foreground application:

```rust
use openlogi_core::Config;
use openlogi_core::binding::ActionRingSlot;

fn resolve_ring_layout(cfg: &Config, device_key: &str, foreground_app: &str) {
    // 1️⃣ Global config
    let mut layout = cfg.action_ring(device_key).default.clone();

    // 2️⃣ Per‑app override (if any)
    if let Some(app_overrides) = cfg
        .devices
        .get(device_key)
        .and_then(|dev| dev.action_ring.per_app.get(foreground_app))
    {
        // Merge: replace only the slots that the app explicitly defines.
        for (slot, entry) in app_overrides {
            layout.insert(*slot, entry.clone());
        }
    }

    // `layout` now contains the eight `ActionRingEntry`s that the overlay will draw.
    for slot in ActionRingSlot::ALL {
        let entry = layout.get(&slot).expect("slot must exist");
        println!("Slot {:?}: {:?}", slot, entry.action());
    }
}

```

### Updating a Slot for a Specific Application

To modify the per-application layout programmatically:

```rust
use openlogi_core::{Config, binding::{ActionRingSlot, RingAction, Action}};
use std::convert::TryFrom;

fn set_safari_top_right(cfg: &mut Config, device_key: &str) {
    // Build a RingAction – here we bind “Back” to the slot.
    let back_action = RingAction::new(Action::Back).unwrap();

    // Update the per‑app map.
    cfg.devices
        .entry(device_key.to_string())
        .or_default()
        .action_ring
        .per_app
        .entry("com.apple.Safari".to_string())
        .or_default()
        .insert(ActionRingSlot::TopRight, ActionRingEntry {
            action: back_action,
            icon: None,
            label: Some("Back".into()),
        });
}

```

## UI Editor and Persistence

### Visual Editor Implementation

The desktop UI in [`crates/openlogi-desktop/src/features/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/action_ring.rs) provides a graphical editor for the per-application layout system. This interface displays both the **global** and **per-application** tabs, allowing users to drag actions onto specific slots and customize icons and labels.

The editor uses the same `ActionRingSlot` geometry helpers as the overlay, ensuring that the preview exactly matches the runtime appearance.

### Configuration Mutators

When users save changes, the UI invokes mutators on the `Config` struct:

- `set_action_ring_slot` – Updates a specific slot in either the default or per-app map
- `set_action_ring_icon` – Modifies the optional icon for an entry
- `set_action_ring_enabled` – Toggles the global enabled flag

These methods update the in-memory configuration, which serializes back to [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) on disk. The `ActionRingConfig` derives `Serialize` and `Deserialize`, ensuring that the `per_app` map persists across application restarts.

## Summary

- **OpenLogi** stores Actions Ring configurations in `ActionRingConfig` structures within a TOML file, supporting both default and per-application layouts.
- The `ActionRingSlot` enum defines eight fixed geometric positions, while `RingAction` validation prevents recursive triggers and empty actions.
- At runtime, the system merges the default layout with per-application overrides based on the foreground app detected by platform-specific watchers.
- The IPC layer (`openlogi-ipc`) transports resolved layouts to the overlay process, which renders the ring using shared geometry calculations.
- The desktop UI provides visual editing capabilities for per-application configurations, persisting changes through type-safe mutators in `openlogi-core`.

## Frequently Asked Questions

### How does OpenLogi resolve conflicts between default and per-application layouts?

OpenLogi uses a merge-based strategy rather than replacement. The system starts with the `default` layout as the base, then overlays only the specific slots defined in the `per_app` map for the current foreground application. This approach allows per-application configurations to inherit unspecified slots from the global default, minimizing redundant configuration.

### What validation prevents invalid actions in the Actions Ring?

The `validate_ring_action` function in [`crates/openlogi-core/src/binding/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action_ring.rs) enforces two critical constraints: it rejects `Action::None` (which would create empty, non-functional slots) and `Action::ShowActionsRing` (which would cause recursive loops by triggering the ring from within itself). These checks occur during `RingAction` construction, ensuring invalid states never reach the configuration file.

### Which components handle the rendering of the per-application layout?

Three primary components collaborate on rendering. The **agent core** (`openlogi-agent-core`) resolves the effective layout and creates an `ActionRingSession`. The **IPC layer** (`openlogi-ipc`) serializes and transmits the layout via the `ShowActionsRing` RPC. Finally, the **overlay process** (`openlogi-overlay`) receives the layout, positions slots using `ActionRingSlot` geometry data, and draws the visual elements at the cursor location.

### How is the per-application configuration persisted across sessions?

The system persists configurations through serde serialization of the `ActionRingConfig` struct, which includes the `per_app: BTreeMap<String, BTreeMap<ActionRingSlot, ActionRingEntry>>` field. When users modify layouts through the desktop UI or programmatic APIs, mutators like `set_action_ring_slot` update the in-memory `Config` object. The entire configuration tree serializes to [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) on disk, preserving bundle identifiers and their associated slot mappings between system restarts.