# How OpenLogi Implements Auto-Switching Per-Application Profile Overlays

> OpenLogi auto-switching per-application profile overlays detect foreground apps using platform hooks and update bindings dynamically for seamless control. Learn how it works.

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

---

**OpenLogi automatically switches per-application profile overlays by detecting the foreground application through platform-specific hooks, transmitting the identifier via IPC to the GUI, and dynamically regenerating the Actions Ring bindings whenever the active window changes.**

OpenLogi is an open-source input management framework that enables context-aware control schemes through per-application profile overlays. The auto-switching mechanism relies on a real-time monitoring pipeline that tracks window focus changes across macOS, Linux, and Windows. This article examines the implementation details based on the AprilNEA/OpenLogi source code, covering the Agent-Hook-GUI architecture and the binding overlay system.

## Three-Tier Architecture for Profile Detection

OpenLogi implements per-application profile overlays through a distributed architecture consisting of a background agent, a platform-specific hook, and a GUI client. Each tier handles a distinct phase of the foreground application detection and profile switching pipeline.

### Agent (Background Server)

The **Agent** serves as the central coordinator that maintains the system state and publishes foreground application updates to the GUI. In [`openlogi-core/src/app.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/app.rs), the `ForegroundApp` type defines the structure that carries the application identifier used for profile matching:

```rust
pub struct ForegroundApp {
    pub id: String,
    pub name: String,
}

```

The agent stores the current foreground application in its internal state and pushes updates through the IPC channel defined in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs). The `ForegroundApps` DTO (Data Transfer Object) contains both the current application and a history of recent apps:

```rust
pub struct ForegroundApps {
    pub current: Option<ForegroundApp>,
    pub recent: Vec<ForegroundApp>,
}

```

### Hook (Input-Capture Layer)

The **Hook** operates at the platform level to extract the foreground application identifier using OS-specific APIs. According to [`openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hook/src/lib.rs), the hook monitors the window server through platform-specific mechanisms such as macOS `CGEventTap`, Wayland `wlroots`, or X11 `WM_CLASS` properties.

The hook constructs a `ForegroundApp` instance where the `id` field represents a stable, platform-specific identifier. On macOS, this is the `bundle_id`; on Linux Wayland, the `app_id`; and on X11, the `WM_CLASS` or lower-cased executable path. This identifier serves as the exact string key that the per-application profile lookup compares against.

### GUI (Client)

The **GUI** receives foreground application updates via the `Agent::observe` RPC and manages the visual presentation of the Actions Ring. The desktop client implementation in [`openlogi-desktop/src/state.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-desktop/src/state.rs) provides the `active_profile_name()` method, which resolves the appropriate per-application profile based on the current `ForegroundApp.id`:

```rust
pub fn active_profile_name(&self) -> Option<&str> {
    // Resolves profile key from current foreground app ID
}

```

When a new application enters focus, the GUI triggers a binding regeneration process that overlays per-application configurations onto the global defaults.

## The Auto-Switching Flow

The complete auto-switching cycle occurs in five distinct steps:

1. **Hook Detection** – Platform-specific code in the hook layer detects window focus changes and constructs a `ForegroundApp` containing the application identifier and display name.

2. **Agent Broadcasting** – The agent stores the foreground app in `ForegroundApps::current` and pushes an update through the `observe` RPC stream to all connected clients.

3. **Profile Resolution** – Upon receiving the `observe` event, the desktop process calls `AppState::active_profile_name()` to lookup the per-application profile keyed by the current `ForegroundApp.id`.

4. **Binding Overlay** – The binding builder in [`openlogi-core/src/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/bindings.rs) merges global device bindings with the per-application overlay. The `effective_bindings()` function applies overrides while falling back to global settings when no per-app entry exists.

5. **Ring Update** – The overlay process ([`openlogi-overlay/src/ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-overlay/src/ring.rs)) renders the Actions Ring with the updated icon layout. If the user clicks outside the ring, `spawn_click_away_dismissal` in [`openlogi-overlay/src/session.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-overlay/src/session.rs) handles safe dismissal of the previous session.

## Profile Configuration and Key Matching

Per-application overlays are stored in the same TOML configuration format as global bindings but reside in a separate section keyed by the application identifier. The configuration schema in [`openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/config.rs) supports nested profiles that override specific actions while inheriting base bindings.

The **profile key** uses platform-specific stable identifiers to prevent collisions:

- **macOS**: Application `bundle_id` (e.g., `com.apple.Safari`)
- **Wayland**: `app_id` from the Wayland protocol
- **X11**: `WM_CLASS` string or lower-cased executable path

This guarantees that a profile created for a specific application matches only that application, avoiding accidental cross-platform or cross-instance collisions.

## Code Implementation Example

The following Rust implementation demonstrates the complete flow from IPC observation to effective binding generation:

```rust
// 1. Receive foreground application update via IPC
let apps: ForegroundApps = agent.observe().await?;
let current_app_id = apps.current.as_ref().map(|a| &a.id);

// 2. Resolve the per-application profile name from configuration
let profile_name = state.active_profile_name(); // Returns Option<&str>

// 3. Build effective bindings by overlaying per-app config onto globals
let effective_bindings = openlogi_core::bindings::effective_bindings(
    &global_bindings,
    per_app_overlays.get(profile_name),
);

// 4. Update the Actions Ring with the new layout
let ring_view = RingView::new(effective_bindings);
cx.update(|cx| cx.add_window(ring_view, window_geometry));

```

The `effective_bindings` function in [`openlogi-core/src/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/bindings.rs) handles the merge logic, ensuring that per-application actions take precedence while unspecified actions retain their global defaults. The UI-level binding logic in [`openlogi-desktop/src/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-desktop/src/bindings.rs) then processes these effective bindings to generate the final input map.

## Summary

- **Three-tier architecture**: The Hook captures OS-level focus changes, the Agent distributes state via IPC, and the GUI applies visual overlays.
- **Stable identifiers**: Profile matching relies on platform-specific IDs (`bundle_id`, `app_id`, `WM_CLASS`) to ensure accurate per-application targeting.
- **Dynamic binding overlay**: The system merges global and per-application bindings through `effective_bindings()`, with per-app configurations taking precedence.
- **Real-time updates**: The `Agent::observe` RPC stream enables instantaneous profile switching without manual intervention when the foreground window changes.

## Frequently Asked Questions

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

OpenLogi uses platform-specific hooks that interface directly with the window server. On macOS, it utilizes `CGEventTap`; on Linux Wayland, it monitors `wlroots` or reads the `app_id`; and on X11, it examines the `WM_CLASS` property or executable path. These hooks run in the background and publish the foreground application identifier to the Agent via the `ForegroundApp` struct.

### What happens if no per-application profile exists for the current app?

When `active_profile_name()` returns `None` or the application ID is not found in the per-application configuration, the system falls back to the global bindings. The `effective_bindings()` function in [`openlogi-core/src/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/bindings.rs) handles this merge transparently, ensuring that users retain their base configuration while specific overrides apply only to registered applications.

### Can multiple devices have different per-application profiles simultaneously?

Yes. The binding overlay system operates on a per-device basis within the global configuration. Each device maintains its own binding map, and the profile switching logic in [`openlogi-desktop/src/state.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-desktop/src/state.rs) tracks the active profile independently for each connected device. When the foreground application changes, the GUI regenerates the binding map specific to the currently active device while respecting its unique per-application overrides.

### How does the Actions Ring handle rapid application switching?

The overlay process implements session management through `spawn_click_away_dismissal` in [`openlogi-overlay/src/session.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-overlay/src/session.rs). When rapid switching occurs, the system safely dismisses the existing ring instance before rendering the new profile's Actions Ring. The IPC stream via `Agent::observe` ensures that the GUI receives updates in real-time, allowing the ring to reflect the new application's binding configuration immediately upon focus change.