# How Herdr Headless Server Mode Works: Architecture and Event Loop Implementation

> Discover how Herdr headless server mode operates. Learn about its architecture and event loop, streaming UI frames and handling API requests without a local terminal.

- Repository: [Can Celik/herdr](https://github.com/ogulcancelik/herdr)
- Tags: architecture
- Published: 2026-05-31

---

**Herdr’s headless server mode runs the full application state machine without a local terminal, streaming rendered UI frames to thin clients over a Unix-domain socket while processing API requests and maintaining PTY runtimes.**

Herdr is a terminal workspace manager that implements a detached server architecture for persistent sessions. The **herdr headless server mode**—accessed via the `herdr server` command—eliminates direct terminal dependencies by replacing the standard TUI event loop with an asynchronous Tokio runtime. This allows the server to continue managing workspaces after client disconnections, handle JSON API requests, and support live handoff updates without dropping PTY state.

## Architecture Overview

The headless implementation centers on the **`HeadlessServer`** struct defined in [`src/server/headless.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/headless.rs). Key components include:

- **`run_server()`** – The entry point in [`src/main.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/main.rs) (lines 46–48) that initializes logging, raises file-descriptor limits, loads configuration, and spawns the async runtime before delegating to the headless implementation.
- **`HeadlessServer`** – A struct holding the `App` state, a Unix-domain listener on `herdr-client.sock`, client connection maps, foreground-client tracking, and shutdown flags (lines 104–122).
- **Event Loop** – The **`HeadlessServer::run`** method (lines 173–229) that replaces `App::run()` when no TUI is present, driving state updates, input routing, and frame streaming.
- **Protocol Layer** – Binary client protocol definitions in [`src/protocol/mod.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/protocol/mod.rs) handling `ServerMessage` enums, frame encoding, and size limits.
- **Client Management** – Connection acceptance logic in [`src/server/client_accept.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/client_accept.rs) and state structures in [`src/server/clients.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/clients.rs).

### Startup Sequence (`herdr server`)

When you execute `herdr server`, the following initialization occurs:

```rust
// src/main.rs → server::headless::run_server()
pub fn run_server() {
    init_logging();
    raise_file_limit();
    let config = load_config();
    start_json_api_socket(&config);  // herdr.sock
    let rt = tokio::runtime::Runtime::new().unwrap();
    let app = App::new(config);
    // Disable local sound/terminal notifications
    let server = HeadlessServer::new(app);
    server.run();
}

```

The server binds a **client socket** at `herdr-client.sock` (path resolution handled in [`socket_paths.rs`](https://github.com/ogulcancelik/herdr/blob/main/socket_paths.rs)) with strict permissions. If the socket is already bound, `run_server()` aborts immediately with a clear error (lines 43–49).

## The Headless Event Loop

The **`HeadlessServer::run`** method implements a non-blocking event loop that never touches `stdin` or the host terminal. Each iteration performs these steps:

1. **Shutdown Check** – Exits if `self.shutting_down` or a SIGINT was captured.
2. **Event Draining** – Calls `drain_internal_events_with_forwarding()` to route clipboard writes, sound notifications, and state changes to the foreground client.
3. **API Processing** – Handles JSON commands from `herdr.sock` via `handle_api_request_with_shutdown_check()`.
4. **Client Acceptance** – Polls the non-blocking `UnixListener` for new connections (250 ms deadline) via `accept_client_connections`.
5. **Server Events** – Processes messages from client threads (input, scroll events) through `drain_server_events`.
6. **Scheduled Tasks** – Executes resize polls, animation timers, and autosave logic via `handle_scheduled_tasks_headless`.
7. **Conditional Render** – If `needs_render` is set and `app.can_render_now()` returns true, generates a virtual frame.

The loop uses `tokio::select!` to coordinate these operations, ensuring the server remains responsive while maintaining minimal CPU usage during idle periods.

## Client Connections and Input Routing

When a thin client connects to `herdr-client.sock`, the server creates a **`ClientConnection`** entry with a unique `client_id`. The implementation tracks a **foreground driver**—the client that dictates shared runtime size, keybindings, and theme settings.

Input handling follows this path:

- Raw keyboard and mouse events arrive as binary protocol messages.
- The server translates these into `RawInputEvent` instances.
- Events are fed to `App::route_client_events` for processing by the active workspace.
- Special events (clipboard paste, scroll buffers) route through dedicated handlers like `handle_terminal_attach_scroll`.

The foreground client receives all forwarded notifications (toast, sound alerts) via `ServerMessage::Notify` variants.

## Rendering and Frame Streaming

Unlike the TUI mode that writes directly to a terminal, the headless server renders to a **virtual buffer**:

```rust
// src/server/headless.rs → render_and_stream()
let frame = self.app.render_to_buffer(effective_area);
let data = FrameData::new(frame, encoding_config);
self.broadcast(ServerMessage::Render(data));

```

The **`render_and_stream`** function (lines 379–398) generates a `ratatui::buffer::Buffer` containing the complete UI state. This buffer is encoded into **`FrameData`** using ANSI escape sequences (with optional Kitty graphics protocol support) and broadcast to all attached clients via `ServerMessage::Render`.

Clients decode these frames and display them locally, achieving a terminal-agnostic UI that functions over SSH or containerized environments.

## Graceful Shutdown and Live Handoff

The server supports two termination modes:

**Standard Shutdown**
- Triggered by `Ctrl+C` or the `herdr server stop` command.
- Sets `should_quit` flag → calls `initiate_shutdown()` → `complete_shutdown()`.
- Pauses all PTY readers, persists session state via `app.save_session_now()`, removes Unix sockets, and exits cleanly.

**Live Handoff** (`herdr update --handoff`)
- Temporarily pauses PTY readers without killing processes.
- Exports runtime state and file descriptors via `perform_live_handoff` (lines 670–764).
- Spawns a replacement server process that imports PTY handles through `run_handoff_import_server`.
- Transfers ownership of `herdr.sock` and `herdr-client.sock` atomically.

This mechanism enables zero-downtime binary updates while preserving all active terminal sessions.

## Practical Usage Examples

### Starting a Headless Server

```bash
herdr server

```

Expected output:

```

herdr server running; you can use any herdr CLI command in another terminal.
api socket: /home/user/.local/share/herdr/herdr.sock
client socket: /home/user/.local/share/herdr/herdr-client.sock
logs: /home/user/.local/share/herdr/herdr-server.log

```

### Attaching a Client

From another terminal or machine:

```bash
herdr client

```

The client connects to `herdr-client.sock`, receives the initial full frame, and enters an interactive mode where keystrokes are forwarded to the server.

### API-Only Interaction

Create a workspace without attaching a UI:

```bash
curl --unix-socket $(herdr status --format=api-socket) \
     -X POST \
     -d '{"type":"CreateWorkspace","name":"production"}' \
     http://api/

```

The JSON API socket (`herdr.sock`) operates independently of client connections, allowing automation scripts to manage workspaces while users remain attached via thin clients.

### Live Binary Update

Update Herdr without disconnecting PTY sessions:

```bash

# Terminal 1: Existing server

herdr server stop

# Terminal 2: Immediate handoff

herdr update --handoff

```

The handoff protocol exports all pane state from [`src/server/handoff.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/handoff.rs) and imports it into the new process version.

## Summary

- **Herdr headless server mode** runs the complete `App` logic in a Tokio async runtime without terminal dependencies, located in [`src/server/headless.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/headless.rs).
- The **`HeadlessServer::run`** event loop processes internal events, API requests, and client connections concurrently, never blocking on terminal I/O.
- **Virtual rendering** via `render_and_stream` encodes `ratatui` buffers as `FrameData` and streams them to thin clients over `herdr-client.sock`.
- **Input routing** maps client keystrokes from the binary protocol to `RawInputEvent` instances processed by the standard application logic.
- **Live handoff** in `perform_live_handoff` enables zero-downtime updates by transferring PTY file descriptors and socket bindings to replacement processes.

## Frequently Asked Questions

### How does Herdr handle client reconnections in headless mode?

When a client disconnects, the server retains the workspace state in the `App` struct. Subsequent connections create new `ClientConnection` entries that receive the current frame buffer immediately. Since the server maintains the canonical PTY state—not the client—reconnections resume the session exactly where it was left, provided the server process remains running.

### What is the difference between the API socket and the client socket?

The **API socket** (`herdr.sock`) exposes a JSON-over-HTTP interface defined in [`src/api.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/api.rs) for programmatic control (creating workspaces, sending commands). The **client socket** (`herdr-client.sock`) uses a binary protocol defined in [`src/protocol/mod.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/protocol/mod.rs) for thin clients that need interactive terminal emulation, receiving rendered frames and sending keyboard input.

### Can the headless server run without Tokio?

No. The implementation relies on `tokio::select!` and Tokio’s async runtime to coordinate the event loop, handle non-blocking Unix socket accepts, and manage background tasks like autosave and animation timers. The `run_server()` function explicitly constructs a Tokio runtime before instantiating the `HeadlessServer`.

### How does the 250ms polling interval affect performance?

The 250 millisecond deadline in `tokio::select!` balances latency and CPU efficiency. It ensures the server checks for new client connections frequently enough for responsive attach/detach operations while yielding CPU during idle periods. The render loop only executes when `needs_render` is set by PTY updates, preventing unnecessary frame generation.