The interminai daemon communicates with client commands via a Unix domain socket using a simple JSON-based request-response protocol.
Note on encoding: JSON strings automatically escape special characters:
- Escape sequences like
\x1b(ESC) become\u001bin JSON - Newlines
\nbecome\\n - Rust's
serde_jsonhandles encoding/decoding automatically - Application code works with raw bytes/strings, JSON library handles escaping
Alternatives considered:
- Binary protocol with length prefixes (more efficient but harder to debug)
- Base64 encoding (adds 33% overhead, less human-readable)
- JSON chosen for debuggability and simplicity
- Client connects to Unix socket
- Client sends one JSON request (newline-terminated)
- Daemon sends one JSON response (newline-terminated)
- Connection closes after response (except for WAIT which may block)
All requests are JSON objects with a type field:
{
"type": "COMMAND_NAME",
... additional fields ...
}All responses are JSON objects:
{
"status": "ok" | "error",
"data": { ... },
"error": "error message if status is error"
}Request:
{
"type": "INPUT",
"data": "keys to send (may contain escape sequences)"
}Response:
{
"status": "ok"
}Errors:
- Process not running
- Failed to write to PTY
Request:
{
"type": "OUTPUT",
"format": "ascii" | "ansi",
"from": 0,
"to": null
}Line numbering: Negative = scrollback, positive = screen, 0 = boundary.
Line -1 is the scrollback line closest to the screen, -N is N lines back.
Line 1 is the first screen line, line rows is the last.
Line 0 does not exist -- it is the boundary between scrollback and screen.
Request fields:
format:"ascii"(default) or"ansi"for color outputfrom: First line to include (inclusive). Default/null = 0 (boundary = screen line 1). Use negative values for scrollback (e.g., -100 for last 100 scrollback lines). Use"-"(string) to start from the beginning of the scrollback buffer.to: Last line to include (inclusive). Default/null = last screen line. Use 0 for boundary (= scrollback only, no screen). Use negative for scrollback subset (e.g., -1 = up to the last scrollback line).
Response:
{
"status": "ok",
"data": {
"screen": "Plain text representation of screen\nwith newlines...",
"cursor": {
"row": 5,
"col": 10
},
"size": {
"rows": 24,
"cols": 80
},
"from": -100,
"to": 24,
"scrollback_available": 150,
"scrollback_capacity": 10000
}
}Response fields:
screen: The requested line range. Withansiformat, includes ANSI color codes. Whenfromis negative, scrollback lines are prepended before screen lines.cursor: Cursor position relative to the visible screen (0-indexed).size: Terminal dimensions (rows x cols).from,to: The effective line range returned (clamped to available bounds).scrollback_available: Lines currently in the scrollback buffer.scrollback_capacity: Maximum buffer size (set bystart --scrollback).
Request:
{
"type": "STATUS",
"activity": false
}The activity field is optional (default: false).
Response (normal mode, activity=false):
{
"status": "ok",
"data": {
"running": true
}
}Response (process finished):
{
"status": "ok",
"data": {
"running": false,
"exit_code": 0
}
}Response (activity mode, activity=true):
{
"status": "ok",
"data": {
"running": true,
"activity": true
}
}Fields (activity mode):
activity: true if PTY output was received since last STATUS/WAIT with activity mode
The activity flag is cleared after reading.
Request:
{
"type": "WAIT",
"activity": false
}The activity field is optional (default: false).
Response (normal mode, activity=false):
{
"status": "ok",
"data": {
"exit_code": 0
}
}Response (activity mode, activity=true):
{
"status": "ok",
"data": {
"activity": true,
"exited": false
}
}Fields (activity mode):
activity: true if PTY output was received (application printed something)exited: true if the child process has exited
Notes:
- Normal mode: blocks until the process exits
- Activity mode: returns as soon as PTY output is received OR process exits
- Connection stays open while waiting
- Returns immediately if condition already met (process exited, or activity pending)
- In activity mode, the activity flag is cleared after reading (subsequent calls block until new activity)
Request:
{
"type": "KILL",
"signal": "SIGTERM" | "SIGKILL" | "SIGINT" | "9" | "15" | "2" | ...
}Response:
{
"status": "ok",
"data": {
"signal_sent": "SIGTERM"
}
}Errors:
- Invalid signal name/number
- Process already dead
- Failed to send signal
Request:
{
"type": "STOP"
}Response:
{
"status": "ok",
"data": {
"message": "Shutting down"
}
}Notes:
- Daemon will kill child process (if running)
- Daemon will close socket
- Daemon will exit after sending response
- If socket was auto-generated, daemon unlinks it before exit
Returns unhandled escape sequences and terminal (termios) settings. Useful for debugging rendering issues, identifying escape sequences an application uses, and understanding terminal mode configuration.
Request:
{
"type": "DEBUG",
"data": {
"clear": false
}
}The data field is optional. If omitted or clear is false, the unhandled
sequences buffer is returned without modification. If clear is true, the
buffer is atomically returned and then cleared.
Response:
{
"status": "ok",
"data": {
"unhandled": [
{"sequence": "\\e[?25l", "raw_hex": "1b5b3f32356c"},
{"sequence": "\\e7", "raw_hex": "1b37"}
],
"dropped": 5,
"termios": {
"mode": "cooked",
"flags": ["ECHO", "ISIG", "ICRNL", "IXON", "OPOST", "ONLCR"],
"hex": {
"iflag": "0x0500",
"oflag": "0x0005",
"lflag": "0x8a3b",
"cflag": "0xf00bf"
},
"c_cc": {
"VINTR": "^C",
"VEOF": "^D",
"VERASE": "^?",
"VKILL": "^U",
"VSUSP": "^Z",
"VQUIT": "^\\"
}
}
}
}Fields:
unhandled: Array of unhandled escape sequences in FIFO order (oldest first)sequence: Human-readable escape sequence (e.g.,\e[?25l)raw_hex: Raw bytes in hexadecimal
dropped: Number of sequences dropped from the buffer due to overflowtermios: Terminal settings (fromtcgetattr())mode: "cooked" (canonical) or "raw" (non-canonical)flags: Active termios flags (ECHO, ISIG, ICRNL, IXON, OPOST, ONLCR, etc.)hex: Hex values for c_iflag, c_oflag, c_lflag, c_cflagc_cc: Control characters in^Xnotation (e.g.,^C= 0x03)
Notes:
- Buffer size is configurable via
--debug-bufferflag tostart(default: 10) - Intentionally ignored sequences (like SGR/colors) are not recorded
- The
modefield reflects ICANON: "cooked" means line-buffered input with editing (backspace works), "raw" means each keystroke is passed immediately - Common flags: ECHO (echo input), ISIG (Ctrl+C sends SIGINT), ICRNL (CR鈫扤L translation), OPOST/ONLCR (output processing)
If a request cannot be parsed as JSON or is missing required fields:
{
"status": "error",
"error": "Invalid request: missing 'type' field"
}If the type field contains an unknown command:
{
"status": "error",
"error": "Unknown command: INVALID_COMMAND"
}If a command fails to execute:
{
"status": "error",
"error": "Failed to send input: Broken pipe"
}Important: The daemon must not crash on errors. It should:
- Send an error response
- Close the connection
- Continue serving other requests
If a client disconnects before reading the full response:
- Daemon detects broken pipe / connection reset
- Daemon discards remaining response data
- Daemon logs the event (if debugging enabled)
- Daemon continues serving other clients
The daemon must be resilient to clients dying mid-response.
Commands are processed sequentially, one at a time. The daemon does not process commands in parallel. This guarantees that if you send command A then command B, A will complete before B starts.
This means:
- No race conditions between commands
- Predictable ordering
- WAIT will block all other commands until the process exits
If you need to send input while a WAIT is pending, don't use WAIT - poll with STATUS instead.
Client sends (pressing 'i', typing 'hello', ESC, then :wq):
{"type":"INPUT","data":"ihello\u001b:wq\n"}\n
Daemon responds:
{"status":"ok"}\n
Client sends (getting screen output):
{"type":"OUTPUT","format":"ascii"}\n
Daemon responds (screen contains escape sequences in output):
{"status":"ok","data":{"screen":" File Edit View\n~\n~\n","cursor":{"row":1,"col":0},"size":{"rows":24,"cols":80}}}\n
Notes:
- Each message is a single line of JSON terminated by
\n - Binary data (like ESC =
\x1b) is escaped as\u001bin JSON - JSON libraries handle escaping automatically - app code uses raw strings
- Maximum message size: 10MB (reasonable limit for screen output)
- The
\nat the end of each JSON message is the message delimiter, not part of the JSON
Named signals to numbers (POSIX standard):
| Name | Number |
|---|---|
| SIGHUP | 1 |
| SIGINT | 2 |
| SIGQUIT | 3 |
| SIGKILL | 9 |
| SIGTERM | 15 |
| SIGUSR1 | 10 |
| SIGUSR2 | 12 |
Both formats are accepted. Daemon normalizes to signal number internally.