OpenLogi Actions Ring Per-Application Layout System: A Complete Technical Guide
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) 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. This enum establishes eight fixed positions arranged clockwise around the cursor:
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:
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:
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 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:
#[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. 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 returns the merged view. The resolution algorithm performs the following steps:
- Retrieve the device's
ActionRingConfig - Clone the
defaultlayout as the base - Check if
per_appcontains an entry matching the current foreground app's bundle ID - If found, merge the per-application overrides (specific slots replace default entries)
- 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. The agent creates an ActionRingSession and transmits a ShowActionsRing RPC containing the device key and resolved layout:
pub struct ShowActionsRing {
pub device_key: String,
pub layout: ActionRingLayout,
}
The overlay process (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 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:
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:
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 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 mapset_action_ring_icon– Modifies the optional icon for an entryset_action_ring_enabled– Toggles the global enabled flag
These methods update the in-memory configuration, which serializes back to 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
ActionRingConfigstructures within a TOML file, supporting both default and per-application layouts. - The
ActionRingSlotenum defines eight fixed geometric positions, whileRingActionvalidation 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 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 on disk, preserving bundle identifiers and their associated slot mappings between system restarts.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →