# How OpenLogi's IPC Architecture Separates GUI, Agent, and Overlay Processes

> Explore OpenLogi's IPC architecture. Discover how it cleanly separates GUI, Agent, and Overlay processes using tarpc, client declarations, and observable streams without shared mutable state.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: architecture
- Published: 2026-09-06

---

**OpenLogi uses a single tarpc-based IPC channel with client kind declarations and role-specific observable streams to cleanly isolate the GUI, Agent, and Overlay processes while avoiding shared mutable state.**

OpenLogi is an open-source input device configuration tool that splits functionality across three distinct processes. Understanding its IPC architecture reveals how modern Rust applications achieve clean separation of concerns without sacrificing performance. The design centers on the `Agent` process as the single source of truth, with the **GUI** (`openlogi-desktop`) and **Overlay** (`openlogi-overlay`) acting as specialized consumers connected through [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs).

## The Three Process Roles

Each process in OpenLogi's architecture has a narrowly defined responsibility:

| Process | Binary | Core Purpose | Connection Model |
|---------|--------|--------------|----------------|
| **Agent** | `openlogi-agent` | HID input hooks, device enumeration, DPI/lighting writes, canonical state storage | Listens on Unix/Windows socket via `openlogi-ipc::transport` |
| **GUI** | `openlogi-desktop` | Configuration UI, full state polling, user-visible settings | tarpc client calling `Agent::protocol_version`, `Agent::status`, `Agent::snapshot` |
| **Overlay** | `openlogi-overlay` | Actions Ring display, hover/activate/cancel event reporting | tarpc client with `ClientKind::Overlay` using `observe_action_ring` and `OverlayCommand` |

## Client Kind Declaration: The Role Verification Mechanism

After version handshake, every client must explicitly declare its role through `declare_client(kind)`. This declaration in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) (lines 102-106) enables the agent to enforce role-specific policies:

```rust
// Overlay declaration
client.declare_client(context::current(), ClientKind::Overlay).await.ok()?;

// GUI declaration  
client.declare_client(context::current(), ClientKind::Gui).await.ok()?;

```

The `ClientKind` enum distinguishes between `Gui` and `Overlay` variants, allowing the agent to apply different behaviors—for example, the macOS dormancy gate only arms for `ClientKind::Gui`.

## Stream Separation: Right-Sized Data for Each Client

OpenLogi's IPC architecture prevents unnecessary data transfer through **dedicated observable streams** defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs):

### Full State Stream for GUI

The GUI receives complete system state via `observe(since)` (lines 550-557), a level-triggered channel returning the full `AgentSnapshot`:

```rust
// GUI side polling loop
let mut gen = 0;
loop {
    let obs = client.observe(context::current(), gen).await?;
    gen = obs.generation;
    // obs.snapshot contains status, inventory, pairing, etc.
    update_ui(obs.snapshot);
}

```

### Minimal Ring Stream for Overlay

The overlay uses `observe_action_ring(since)` (lines 554-558), receiving only an optional `ActionRingInvocation`:

```rust
// Overlay side observation loop
let mut gen = 0;
loop {
    let ring_obs = client.observe_action_ring(context::current(), gen).await?;
    gen = ring_obs.generation;
    match ring_obs.invocation {
        Some(inv) => draw_ring(inv),
        None => hide_ring(),
    }
}

```

This **payload isolation** keeps the overlay lightweight—critical for a helper that must render UI overlays with minimal latency.

## Bidirectional Overlay Communication

The overlay sends user interactions back to the agent through `OverlayCommand`, defined in [`crates/openlogi-overlay/src/agent.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/agent.rs) (lines 30-41):

```rust
pub enum OverlayCommand {
    Hover { session_id: Uuid, slot: u8 },
    Activate { session_id: Uuid, slot: u8 },
    Cancel { session_id: Uuid },
}

```

The agent's `spawn_ipc` routine handles this with two concurrent async tasks:

1. Pushing `observe_action_ring` updates to the overlay
2. Receiving `OverlayCommand` messages from the overlay

```rust
// Sending actions from overlay to agent
commands.send(OverlayCommand::Hover { session_id, slot }).ok();
commands.send(OverlayCommand::Activate { session_id, slot }).ok();
commands.send(OverlayCommand::Cancel { session_id }).ok();

```

## Process Supervision and Lifecycle Management

The agent enforces single-overlay semantics through [`crates/openlogi-agent/src/overlay.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/overlay.rs) (lines 23-34). A **run token** (`OPENLOGI_RUN`) passed via environment variable identifies the current agent session:

- New agent starts → generates fresh run token
- Overlay supervisor launches overlay with token
- Stale overlays from previous runs self-terminate when token mismatch detected

This enables **independent restarts**—the overlay can crash and restart without disturbing the GUI or agent state.

## Transport Layer Implementation

The low-level socket handling resides in [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs), abstracting platform differences:

- **Unix**: `UnixListener` on filesystem socket
- **Windows**: `NamedPipe` for inter-process communication

Both transport types present identical tarpc service contracts to higher layers.

## Complete Connection Flow

```rust
// === Agent initialization ===
openlogi_agent::overlay::spawn(); // Start supervisor thread

// === GUI connection sequence ===
let client = openlogi_ipc::client::connect().await?;
client.declare_client(context::current(), ClientKind::Gui).await?;
// Begin polling observe(since)

// === Overlay connection sequence ===
let client = openlogi_ipc::client::connect().await?;
client.declare_client(context::current(), ClientKind::Overlay).await?;
// Begin polling observe_action_ring(since) + sending OverlayCommands

```

## Key Architectural Benefits

- **No shared mutable state** between processes—only message passing via tarpc
- **Independent failure domains**—overlay restart doesn't affect GUI or agent
- **Bandwidth optimization**—each client receives precisely the data it needs
- **Policy enforcement**—agent can apply role-specific rules based on `ClientKind`

## Summary

OpenLogi's IPC architecture achieves clean process separation through:

- A **single tarpc socket** multiplexing all client connections
- **Mandatory `declare_client(kind)`** calls that establish role identity
- **Separate observable streams** (`observe` vs `observe_action_ring`) right-sizing data transfer
- **`OverlayCommand` enum** for lightweight bidirectional overlay communication
- **Run token supervision** ensuring overlay lifecycle alignment with agent sessions

The implementation in `AprilNEA/OpenLogi` demonstrates how Rust's type system and async runtimes enable sophisticated multi-process architectures without runtime overhead.

## Frequently Asked Questions

### What IPC library does OpenLogi use for process communication?

OpenLogi uses **tarpc**, a Rust RPC framework built on Tokio. The service contract is defined as a trait in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs), with implementations for both client and server sides. Transport uses platform-native async sockets via [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs).

### Why does the overlay use a separate stream instead of the full state?

The `observe_action_ring` stream minimizes payload size and parsing overhead. The overlay only needs ring visibility state, not the full device inventory, DPI settings, and pairing information contained in `AgentSnapshot`. This keeps the overlay responsive as a UI helper.

### Can multiple GUI instances connect to the same agent simultaneously?

According to the source code, the architecture supports multiple concurrent connections. However, role-specific policies may apply—some operations gated by `ClientKind::Gui` could create contention. The design prioritizes single-GUI operation with overlay as secondary helper.

### How does the agent prevent overlay processes from accumulating across restarts?

The overlay supervisor in [`crates/openlogi-agent/src/overlay.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/overlay.rs) passes an `OPENLOGI_RUN` environment variable to each spawned overlay. When a new agent starts with a fresh token, existing overlays detect the mismatch and self-terminate, ensuring at most one overlay runs per agent session.