About RMUX
A local client/server terminal multiplexer written from scratch in Rust. One daemon and one wire protocol behind three public surfaces: a tmux-compatible CLI, a typed Rust SDK, and a ratatui widget.


Native, no WSL
Native ConPTY with named pipes on Windows. Unix PTYs with Unix-domain sockets on Linux and macOS. The daemon never opens a network listener.
Programmable from Rust
Typed handles for Session, Window,
Pane. Take structured snapshots, send keys, wait
for output, subscribe to pane events. No TTY scraping.
Drop-in tmux
The full tmux 4.0.1 command surface — 90 commands, persistent sessions, your existing scripts and key bindings keep working.
Installation
Use a prebuilt RMUX binary, keep Cargo as a source-build fallback, and add the SDK to your Cargo project.
CLI
curl -fsSL https://rmux.io/install.sh | shirm https://rmux.io/install.ps1 | iexcargo install rmux --lockedSDK
[dependencies]
rmux-sdk = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Configuration
rmux reads config at startup. RMUX config paths win first; if none load, a filtered tmux.conf can be used as a migration fallback.
Linux / macOS
- /etc/rmux.conf
- ~/.rmux.conf
- $XDG_CONFIG_HOME/rmux/rmux.conf
- ~/.config/rmux/rmux.conf
Windows
- %XDG_CONFIG_HOME%\rmux\rmux.conf
- %USERPROFILE%\.rmux.conf
- %APPDATA%\rmux\rmux.conf
- %RMUX_CONFIG_FILE%
tmux.conf fallback
If no RMUX config file is loaded during the default startup search,
RMUX can import a filtered tmux.conf as a migration
fallback. Explicit config loading with -f does not use
this fallback.
Linux / macOS fallback
- /etc/tmux.conf
- ~/.tmux.conf
- $XDG_CONFIG_HOME/tmux/tmux.conf
- ~/.config/tmux/tmux.conf
Windows fallback
- %XDG_CONFIG_HOME%\tmux\tmux.conf
- %USERPROFILE%\.tmux.conf
- %APPDATA%\tmux\tmux.conf
#(cmd), recursive source-file entries, unsupported options, non-regular files, and files larger than 1 MiB. Set RMUX_DISABLE_TMUX_FALLBACK=1 to disable it.Example
# Prefix key
set -g prefix C-a
unbind C-b
bind C-a send-prefix
# Splits
bind | split-window -h
bind - split-window -v
# Mouse
set -g mouse onset, bind, unbind, source-file. Create a real .rmux.conf when you want deterministic startup; tmux.conf is only a filtered migration fallback.Architecture
rmux runs as a local daemon. Three public surfaces — CLI, SDK, ratatui widget — share one wire protocol.
rmux runs as a local daemon that owns sessions, windows, panes,
and the PTY processes inside them. Three public surfaces — a
rmux CLI, a rmux-sdk Rust crate, and a
ratatui-rmux widget — talk to the daemon over the
same wire protocol, so anything one surface can do, the others
can do too.
Runtime shape
Public surfaces
rmux— tmux-compatible CLI, including tmux-style commands such aswait-for.rmux-sdk— public Rust SDK over the daemon. Typed handles, async, no exposed wire-format types.ratatui-rmux— ratatui integration consuming the SDK; embedding a live pane is a widget rather than a custom driver.
Automation model
Locatorqueries visible terminal snapshots. It is terminal-native text matching, not a DOM or CSS selector model.PaneId,pane_by_id, and retained output make pane identity stable across index recompression during a daemon lifetime.ProcessCommandSpec::ArgvandProcessCommandSpec::Shellmake launch mode explicit;command()remains legacy shell-compatible.- Keyboard, mouse, fill, capture, screenshot, quiet-state waits, and JSONL tracing are SDK automation layers over the existing daemon protocol.
OwnedSessioncleanup policies and daemon-side leases cover app-owned workspaces, includingKillOnOwnerExit.- Capability negotiation is cached by the SDK; unsupported daemon features surface as typed
RmuxError::Unsupporteddiagnostics.
Workspace crates
Eleven workspace crates are published on crates.io;
rmux-render-core remains an internal rendering building
block. Dependencies flow one way — lower crates never depend on
rmux-server or rmux.
| Crate | Role | Status |
|---|---|---|
rmux-types | Shared low-level value types | crates.io |
rmux-proto | Protocol DTOs, framing, wire-safe errors | crates.io |
rmux-os | OS-boundary helpers | crates.io |
rmux-ipc | Local IPC transport (Unix sockets, Windows named pipes) | crates.io |
rmux-sdk | Public daemon-backed Rust SDK | crates.io |
ratatui-rmux | Public ratatui integration widget | crates.io |
rmux-render-core | Shared rendering primitives | workspace-only |
rmux-pty | PTY allocation, resize, child-process control | crates.io |
rmux-core | In-memory sessions, panes, layouts, hooks | crates.io |
rmux-server | Tokio daemon, lifecycle, request dispatch | crates.io |
rmux-client | Blocking local IPC client | crates.io |
rmux | Public CLI and hidden daemon entrypoint | crates.io |
Protocol v1
Every frame on the wire follows the same envelope, defined in rmux-proto:
magic byte 0x52
wire version varint (LEB128)
payload length little-endian u32
payload bincode v1 DTO
The supported wire revisions are tracked in V1_FRAME_LEDGER;
breaking changes bump the varint and add a new entry rather than
mutating the existing frame.
Platform model
- Linux / macOS — Unix-domain sockets for IPC, native Unix PTYs.
- Windows — named pipes for IPC, native ConPTY.
- Local-only. The daemon never opens a network listener.
From the shell
Create a detached session, send a command, synchronize with a tmux-style wait-for channel, capture the pane, then attach for interactive use.
rmux new-session -d -s demo
rmux split-window -h -t demo
rmux send-keys -t demo "printf 'test result: ok\n'; rmux wait-for -S demo-done" Enter
rmux wait-for demo-done
rmux capture-pane -p -t demo
rmux attach-session -t demo
From Rust
Start or connect to the daemon, ensure one session, write text, wait for it, then capture terminal state.
let pane = session.pane(0, 0);
pane.send_text("echo hello\n").await?;
pane.wait_for_text("hello").await?;
Terminal automation
Drive a real terminal pane with visible-text locators, keyboard actions, quiet-state waits, and visible assertions.
let pane = rmux
.find_panes()
.title("agent:claude")
.one()
.await?;
pane.get_by_text("Ready").wait_for().await?;
pane.keyboard().type_text("printf 'multiplexer ready\n'").await?;
pane.keyboard().press("Enter").await?;
pane.wait_for_load_state(TerminalLoadState::Quiet).await?;
pane.expect_visible_text().to_contain("multiplexer").await?;
PaneSet orchestration
Collect agent panes into a PaneSet, broadcast one prompt, wait across all or any pane, and inspect partial results.
let discovered = rmux
.find_panes()
.title_prefix("agent:")
.running()
.all()
.await?;
let agents = PaneSet::new(discovered.into_iter().map(|pane| pane.pane));
agents
.keyboard()
.type_text("Explain rmux in one sentence.")
.await?;
agents.keyboard().press("Enter").await?;
agents.expect_all().visible_text_contains("rmux").await;
let snapshots = agents.snapshot_all().await;
Run detached
Start a long-running process inside a detached RMUX session and return immediately.
let session = rmux
.ensure_session(
EnsureSession::try_named("web")?
.create_or_reuse()
.detached(true)
.shell("python3 -m http.server 8000"),
)
.await?;
Owned session cleanup
Create a session owned by your app, choose KillOnDrop, KillOnOwnerExit, or Preserve, and make cleanup explicit.
let mut owned = rmux
.owned_session(SessionName::new("agent-run")?)
.replace_existing(true)
.cleanup_policy(CleanupPolicy::KillOnOwnerExit)
.await?;
let _signals = owned.install_default_signal_handlers()?;
let session = owned.session();
session.pane(0, 0).keyboard().type_text("cargo test\n").await?;
owned.cleanup().await?;