shux — terminal multiplexer with a JSON-RPC API + pixel snapshotter
Install
sh
npx skills add indrasvat/shux --global --yes
shux is a Rust terminal multiplexer (sessions / windows / panes, like tmux)
that also exposes a length-prefixed JSON-RPC surface over UDS and TCP,
atomic declarative templates, optimistic concurrency on every entity, a
sealed event bus, and a built-in rasterizer that returns PNG bytes for any
pane — no terminal emulator in the loop.
When to reach for it
Pick shux instead of the alternatives when any of these apply:
- You need to drive a TUI from outside (agent, CI, script) without a human at the keyboard.
- You want a PNG of what a terminal looks like right now, headless, no display server.
- You're running scripted CLI/REPL interactions that need typed keystrokes and known wait points.
- You're doing visual regression on a TUI you built (Bubbletea, Charm, ratatui, anything).
- You want declarative workspace templates that apply atomically.
If you're a human at a keyboard and tmux works for you, keep using tmux.
When a human does attach to shux, the normal interactions should still feel
modern: click panes to focus, drag borders to resize, drag visible text to copy
via OSC 52, and right-click a visible selection for the inline Copy / Clear
menu. Reserve prefix copy mode for scrollback, search, and keyboard-only
selection.
Where shux artifacts live:
Run
once per project. It creates a top-level
dir:
.shux/
├── templates/ # spec.toml files you commit (committed)
├── scripts/ # automation scripts you commit (committed)
├── goldens/ # reference PNGs for visual regression (committed)
├── out/ # snapshots, diffs, logs, anything ephemeral (gitignored)
└── .gitignore # ignores `out/`
When you write code that produces shux artifacts:
- Put templates under (apply with
shux state apply .shux/templates/<name>.toml
).
- Put driver scripts under .
- Write snapshots, diffs, debug logs into (gitignored by default).
- Commit golden images to so visual-regression diffs have a ground truth.
Never pollute
,
, or the project root with shux output.
80% quickstart (three RPCs)
bash
# 1. Spawn a session running any command (or shell). Capture the
# pane_id from the response so the next calls can target it. CLI
# session creation starts in the caller's current directory unless
# you pass --cwd.
RESP=$(shux --format json session create demo -d --title demo -- lazygit)
PID=$(echo "$RESP" | jq -r .pane_id)
# 2. Drive it.
shux pane set-size -s demo --cols 200 --rows 60
shux pane send-keys -s demo --text 'j' # text input
shux pane send-keys -s demo --data 'Gw==' # base64 control (here: Esc)
# 3. Get a PNG back.
shux --format json pane snapshot -s demo \
| jq -r .png_base64 | base64 -d > frame.png
# Tear down when done.
shux session kill demo
Every CLI verb maps 1:1 to an RPC method (RPC dots become CLI spaces —
↔
). Drop to the raw form with
shux rpc call <method> --params @file
whenever you'd rather write
the payload in JSON directly.
reads from stdin.
For declarative multi-pane workspaces, use a template:
toml
# spec.toml
[session]
name = "review"
[[windows]]
title = "vivecaka"
[[windows.panes]]
command = ["vivecaka", "--repo", "cli/cli"]
bash
shux state apply spec.toml # atomic — all or nothing
Tools shux replaces
| If you'd reach for | For this job | Use this shux primitive |
|---|
| · · | Multiplex sessions / windows / panes | shux state apply spec.toml
· |
| iTerm2 (Python SDK / AppleScript) | Drive a terminal app from outside | + |
| · · | Scripted CLI / REPL interaction | → → |
| iTerm2 / | Block until screen contains (or stops containing) a needle | (text · regex · ) |
| · | Record a terminal session | (sealed data-plane stream) |
| · · | Generate TUI demo GIFs / WebPs | loop → |
| · | Still PNG of a terminal frame | or |
| iTerm2 broadcast input | Send keystrokes to many panes at once | fan-out (one RPC per pane) |
| · | Replay a recorded session | Re-feed VT bytes through a fresh pane → |
| GNU parallel mode | Run N tasks in N panes, watch in one place | Template with N panes + RPC orchestrator |
| Custom Bubbletea / ratatui test harness | Visual regression for your TUI | + golden-image diff (SSIM or raw RGBA) |
The common RPC surface
Every CLI verb maps 1:1 to an RPC method — RPC dots become CLI spaces
(
↔
,
↔
). All RPCs accept JSON in, return JSON out, on
stdin/stdout.
lists the full request/response
shape per method.
| Category | Methods |
|---|
| Session | · · · · |
| Window | · · · · |
| Pane I/O | · · · · |
| Pane mgmt | · · · · · |
| Window snap | · (composed multi-pane PNG) |
| State | (atomic batch) · · |
Every entity carries a
field. Pass
on
mutating RPCs to get optimistic-concurrency rejection (error code
) on stale writes — useful when multiple agents collaborate.
Common control bytes for
The
field sends raw text. For control characters, use
(base64).
| Key | Bytes | Base64 |
|---|
| Enter | | |
| Escape | | |
| Tab | | |
| Backspace | | |
| Ctrl+C | | |
| Ctrl+L | | |
| Up arrow | | |
| Down arrow | | |
Decide which method to use
Need to spawn something? → session.create (shux session create --title <label> -- <cmd>)
Need a multi-pane workspace? → state.apply (shux state apply spec.toml)
Need to type into a TUI? → pane.send_keys (shux pane send-keys --text|--data)
Need pixel feedback of one pane? → pane.snapshot (shux pane snapshot)
Need a snapshot of the whole
window (borders, titles, status)? → window.snapshot (shux window snapshot)
Need a snapshot of the session's
active window? → session.snapshot (shux session snapshot)
Need plain text of the screen? → pane.capture (shux pane capture)
Need to block until text appears? → pane.wait_for (shux pane wait-for --text|--regex)
Need a stream of PTY output? → pane.output.watch (event-bus, sealed)
Want raw RPC for a new method? → shux rpc call <method> --params @file
Extend shux with a process plugin
shux has a process-plugin host. A plugin is any executable that speaks
shux's line-delimited JSON-RPC dialect on stdin/stdout — bash, python,
node, anything. It:
- Subscribes to bus events listed in its manifest.
- Calls any registered shux RPC method (,
, , …) to react.
- Publishes its own events via . The daemon
namespaces them under
plugin.<plugin_id>.<type>
so other
plugins (or shux events watch --filter plugin.<id>.
) can
subscribe to them cleanly — see references/plugins.md.
- Persists its own state across hot reload via
plugin.state.get/set/delete
. The CLI pins it to the calling
project's root at install time (walks up from cwd for ,
anchors there) — so a daemon shared across checkouts keeps each
project's state isolated. Atomic writes, 256 KiB cap, per-plugin
isolation. Path: <project-root>/.shux/plugins/<name>/state.json
.
bash
shux plugin install ./my-plugin.sh # spawn, handshake, register. Hot reload ON
# by default — saves respawn the plugin
# in <500ms. Use `--no-watch` to opt out.
shux plugin list # name · version · pid · subscribes · watching
shux plugin reload <name> # manual hot-reload tick (kill + respawn)
shux plugin kill <name> # graceful shutdown (2s) → SIGKILL
shux plugin grant <name> <method> # opt the plugin in to a sensitive RPC.
# Default-deny model: every plugin RPC
# passes through a permission check
# before reaching the daemon router
# (v0.19+).
shux plugin grant <name> <method> --target <id> # scoped to one entity
shux plugin grant <name> <filter> --subscribe # widen manifest subscribes
shux plugin revoke <name> <method> # mirror of grant
shux plugin grants <name> # show the allow-set
shux plugin audit <name> --tail 50 # tail NDJSON audit log
Reference plugins:
examples/plugins/hello/plugin.sh
— smallest working example (~50 lines): handshake + PTY output + state mutation.
examples/plugins/watcher/plugin.sh
— subscribes to , emits a namespaced plugin.watcher.command_exit
via for downstream plugins.
examples/plugins/conductor/plugin.sh
— VT-poll watchdog + settle-snapshot archive ⭐ + window-aggregation OS notifications for coding-agent panes (claude / codex / opencode / gemini). Identifies the agent on , polls every 2 s, classifies state, updates the pane border title (), auto-dismisses trust prompts via . On every transition, calls and saves the resulting PNG to .shux/conductor/snapshots/
with a rolling — a feature literally impossible in any tool that doesn't own its own rasterizer. Tracks per-window in-flight counts and fires ONE / notification when a window's last in-flight agent goes idle.
Full protocol — handshake, event payload shape, RPC-out direction,
, shutdown grace, UUID vs name rule, what's not in v0 — lives in
references/plugins.md.
Deep dives
| Topic | Where |
|---|
| Full RPC inventory + JSON request/response shapes | references/api.md |
| Apply-template TOML shape, lowering rules, multi-window workspaces | references/templates.md |
| Scenario-driver patterns (send/wait/snap loops, golden-image diff) | references/scenarios.md |
| Process plugins — protocol, manifest, event/RPC shapes, gotchas | references/plugins.md |
Worked examples
- examples/headless-tui-test.md — drive a TUI in CI, snapshot at every step, diff against checked-in goldens.
- examples/vision-llm-feedback.md — agent builds a Bubbletea app, snapshots its own UI, feeds PNG to a vision model, self-corrects.
- examples/replace-tmux-workflow.md — common
tmux new-session / send-keys / capture-pane
patterns translated to shux.
Gotchas
- is synchronous (oneshot ack). The next sees the new dims. No sleep needed between them.
- caps the output at 16M pixels (~4000×4000). Resize first if you'd exceed.
- targets the session's active pane (often the last-spawned). In multi-pane templates pass (from or 's ) — otherwise the wait will silently watch the wrong pane and time out.
- is JSON-quoted text. For raw control bytes (Esc/Enter/Tab/Ctrl+letter), use with base64.
shux state apply foo.toml
atomically commits the graph but PTY spawn outcomes are reported per-pane in . A spawn failure does not roll back the graph.
- The first pane of the first template window is folded into the session's auto-created initial window — there is no phantom default-shell pane.
- The bundled font (JetBrains Mono Regular, OFL-1.1) is monochrome. Color emoji and CJK glyphs render as tofu in the current rasterizer (P2 roadmap).