How OpenLogi's IPC Architecture Separates GUI, Agent, and Overlay Processes
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.
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 (lines 102-106) enables the agent to enforce role-specific policies:
// 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:
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:
// 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:
// 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 (lines 30-41):
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:
- Pushing
observe_action_ringupdates to the overlay - Receiving
OverlayCommandmessages from the overlay
// 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 (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, abstracting platform differences:
- Unix:
UnixListeneron filesystem socket - Windows:
NamedPipefor inter-process communication
Both transport types present identical tarpc service contracts to higher layers.
Complete Connection Flow
// === 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 (
observevsobserve_action_ring) right-sizing data transfer OverlayCommandenum 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, with implementations for both client and server sides. Transport uses platform-native async sockets via 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 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.
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 →