How ogulcancelik/herdr Server Client Architecture Works: A Deep Dive
Herdr runs as a headless server process that owns all application state and communicates with thin terminal clients via two independent Unix-domain sockets—one for JSON-RPC control commands and another for high-throughput binary frame streaming.
The ogulcancelik/herdr project implements a clean separation between UI rendering and application logic through its distinct server-client model. Unlike traditional terminal applications that bundle state management with the interface, herdr maintains a persistent headless server process while allowing multiple lightweight clients to attach and detach dynamically. This architecture enables remote control, session persistence, and rendering optimizations impossible in monolithic designs.
The Dual-Socket Communication Model
Herdr utilizes two separate communication channels to isolate control-plane traffic from data-plane streaming. This separation ensures that API commands remain responsive even when the system streams large render payloads.
The JSON-RPC API Socket (herdr.sock)
Defined in src/api/server.rs and src/api/client.rs, this socket handles newline-delimited JSON requests for external tooling, CLI commands, and the auto-detect subsystem. It exposes control-plane operations, allowing scripts to query state or trigger actions without connecting to the high-throughput binary stream.
The Binary Client Socket (herdr-client.sock)
The primary data plane operates over herdr-client.sock, implemented in src/server/client_transport.rs and src/client/mod.rs. This channel uses length-prefixed bincode messages defined in src/protocol/wire.rs to stream rendered frames, input events, clipboard data, and notifications with minimal overhead.
Connection Establishment and Protocol Handshake
When a client initiates a connection, it opens the client socket path returned by crate::server::socket_paths::client_socket_path() and transmits a ClientMessage::Hello payload containing critical metadata.
The handshake includes the protocol version, terminal geometry (columns and rows), cell dimensions in pixels, requested render encoding, key-binding profile, and launch mode:
// src/client/mod.rs
let hello = ClientMessage::Hello {
version: PROTOCOL_VERSION,
cols,
rows,
cell_width_px,
cell_height_px,
requested_encoding,
keybindings: requested_keybindings(),
launch_mode: if direct_attach_requested {
ClientLaunchMode::TerminalAttach
} else {
ClientLaunchMode::App
},
};
protocol::write_message(stream, &hello)?;
The server validates the version using protocol::check_client_version() and inspects custom keybindings before responding with ServerMessage::Welcome. If validation fails, the server sends an error string in the welcome message and immediately closes the connection:
// src/server/client_transport.rs
match protocol::check_client_version(version) {
VersionCheck::Compatible => {}
VersionCheck::Incompatible(reason) => {
let welcome = ServerMessage::Welcome {
version: PROTOCOL_VERSION,
encoding: RenderEncoding::SemanticFrame,
error: Some(reason),
};
protocol::write_message(&mut stream, &welcome)?;
return Ok(());
}
}
Bidirectional Message Flow and Threading
After a successful handshake, the server spawns two dedicated threads per client to handle concurrent bidirectional traffic without blocking the main event loop.
Server-to-Client Writer Thread
The writer thread manages the ClientWriter struct containing separate channels for control messages and render frames. It serializes data as length-prefixed binary blobs, prioritizing control traffic over render data:
// src/server/client_transport.rs
loop {
// Priority: control > render
if let Ok(ctrl) = control_rx.try_recv() {
write_framed_bytes(&mut stream, &ctrl);
continue;
}
if let Ok(rend) = render_rx.try_recv() {
write_framed_bytes(&mut stream, &rend);
let _ = server_event_tx.blocking_send(ServerEvent::ClientWriterDrained { client_id });
continue;
}
}
Control messages flow through an unbounded std::sync::mpsc::Sender, while render frames move via a single-slot SyncSender to prevent memory buildup during slow client consumption.
Client-to-Server Reader Thread
The reader thread continuously parses incoming messages using the framing helpers from src/protocol/wire.rs. It decodes ClientMessage variants (input, resize, clipboard, attach requests) and forwards them as ServerEvent variants on a Tokio mpsc::Sender for the central event loop to process.
Render Encoding Negotiation
Herdr supports two render modes negotiated during the handshake and stored in ClientState.render_encoding:
- SemanticFrame: The server transmits full
FrameDatastructures including cell attributes, cursor positions, and optional Kitty graphics. The client applies differential updates usingrender_ansi::BlitEncoderto minimize terminal traffic. - TerminalAnsi: The server sends raw ANSI escape sequences via
TerminalFramestructures, offering maximum bandwidth efficiency for terminals that natively parse ANSI streams.
Socket Path Resolution and Session Management
The system resolves socket paths through the logic in src/server/socket_paths.rs, supporting multiple named sessions and environment overrides:
- HERDR_SOCKET_PATH: Overrides the API socket path and automatically derives the client socket name by appending
-client.sock. - HERDR_CLIENT_SOCKET_PATH: Serves as a legacy fallback for direct client socket configuration.
- Default paths: If no environment variables are set, sockets reside in the session's configuration directory.
// src/server/socket_paths.rs
pub fn client_socket_path() -> PathBuf {
if crate::session::explicit_session_requested() {
return crate::session::client_socket_path_for(crate::session::active_name().as_deref());
}
client_socket_path_from_overrides(
std::env::var(crate::api::SOCKET_PATH_ENV_VAR).ok().as_deref(),
std::env::var(CLIENT_SOCKET_PATH_ENV_VAR).ok().as_deref(),
)
}
This design allows multiple isolated herdr sessions to coexist on the same machine without socket conflicts.
Error Handling and Graceful Shutdown
The protocol implements clean disconnection semantics for both parties. Clients may send ClientMessage::Detach to request a controlled exit, while the server can broadcast ServerMessage::ServerShutdown with an optional reason string to force client termination.
All framing errors—including oversized frames, unexpected EOFs, or bincode deserialization failures—are wrapped in protocol::FramingError and trigger ServerEvent::ClientDisconnected, ensuring the server promptly cleans up resources for disconnected clients.
Summary
- ogulcancelik/herdr implements a headless server architecture where all state lives in a persistent process separate from the terminal UI.
- Communication flows over two Unix-domain sockets: a JSON-RPC control socket (
herdr.sock) and a binary data socket (herdr-client.sock). - The handshake protocol in
src/protocol/wire.rsnegotiates version compatibility, render encodings (SemanticFrameorTerminalAnsi), and terminal geometry before admitting clients. - Each client connection spawns dedicated reader and writer threads on the server, with prioritized control channels and backpressure-aware render queues.
- Socket paths resolve through environment variables (
HERDR_SOCKET_PATH,HERDR_CLIENT_SOCKET_PATH) or session-specific directories, enabling multi-session workflows.
Frequently Asked Questions
How does herdr handle multiple clients connecting to the same server?
The server maintains independent reader and writer thread pairs for each connected client, as implemented in src/server/client_transport.rs. Each client receives its own render stream and input channel, allowing multiple terminals to view or control the same session simultaneously, though the server state remains singular and shared.
What happens if the client and server versions are incompatible?
During the initial handshake, the server calls protocol::check_client_version() to validate the client's protocol version. If versions mismatch, the server sends a ServerMessage::Welcome containing an error description and immediately closes the connection before entering the main message loop, preventing protocol confusion.
Can external scripts control herdr without using the terminal client?
Yes. External tools connect to the API socket (herdr.sock) using the JSON-RPC protocol defined in src/api/. By constructing Request objects and utilizing ApiClient::local() (which respects the HERDR_SOCKET_PATH environment variable), scripts can execute commands, query state, and manage sessions without attaching a thin client.
Which render encoding should I choose for remote connections?
For high-latency or bandwidth-constrained links, request TerminalAnsi encoding during the handshake, which transmits raw ANSI escape sequences rather than full frame structures. For local connections or terminals supporting advanced features like Kitty graphics, SemanticFrame provides richer metadata and allows the client to optimize differential updates.
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 →