fux
fux is a minimal persistent terminal multiplexer. Workspaces group related work, tabs switch
layouts, and splits show terminals together. One session server per user keeps pane processes
and their bounded history alive while viewers attach, detach and reconnect. The authoritative
model lives in a standalone bevy_ecs World; the
viewer is a small terminal compositor. fux builds, installs and runs with no other program
present, opens no network listeners and needs no keys.
Use
A fresh fux starts the session server on demand, creates the workspace default with the tab
main and one pane running your shell, and attaches. Detaching or closing the terminal leaves the
server and its panes running. Persistence means surviving detach: nothing is resurrected after
the server or the machine restarts.
fuxattaches to the workspace most recently attached by any viewer; on a fresh server it createsdefault.fux NAMEopens or creates a named workspace. There is no startup picker.fux workspace list,fux workspace new [NAME],fux workspace kill NAME.fux bindingsprints the configured prefix and bindings by group.fux serve --name NAMEruns the server in the foreground (SIGINT/SIGTERM shut it down).fux attach --socket PATHattaches to an explicit private attachment socket, for example one a koh gateway exposes.fux [NAME] new|split|focus|kill|resize|send-keys|capture|list|tab|subscribe …andfux [NAME] ctl JSONdrive the workspace's control socket from scripts.fux --helplists them.
Configuration is $XDG_CONFIG_HOME/fux/config.toml (default ~/.config/fux/config.toml); a
missing file means defaults. Every key is optional:
= "C-a" # one byte: a printable key, C-x, Esc, Space, DEL, 0xHH
= { = ["/bin/zsh", "-l"] } # default: $SHELL -l, else /bin/sh -l
= "disabled" # or "write-only": OSC 52 copies reach the terminal
[] # key = action, merged over the defaults
= "split-side"
[]
= 10000 # per pane, 1-100000
[]
= 128 # per workspace
= 32
= 64
[]
= 60000 # final records of fux's own panes, 1-14400000; automation sets split.final_retain_ms
[] # sixteen ANSI names, "default" (terminal foreground) or "none" (keep the cell's colour)
= "white" # workspace name, inactive tabs, pane id: title
= "bright-black" # background of the bar row
= "default" # current tab (drawn reversed)
= "bright-black" # separators away from the focused pane
= "default" # separators touching the focused pane (bold)
= "yellow" # transient notices; errors are always red
Private sockets live under $XDG_RUNTIME_DIR/fux (macOS fallback ~/Library/Caches/fux-runtime);
daemon diagnostics go to $XDG_STATE_HOME/fux/daemon.log (default ~/.local/state/fux).
The protocols are not versioned: every fux, koh and zor build is expected to match the tree it was
built from. A server older than the viewer shows up as a rejected frame or reply, reported as an
error that names the session server; save your work in it and restart it (fux workspace kill
per workspace, or SIGTERM to its pid from the descriptor). fux never stops a server on its own.
Keys
Ordinary keys are byte-exact pane input. The prefix (Ctrl-A by default) enters command mode and
immediately shows the command column: a box in the bottom-right corner, directly above the bar,
one row per binding under its group heading, only as wide as its widest line and only as tall as
its content. Commands run at once; a burst such as prefix-| is applied before the next repaint,
so nothing flashes. Prefix twice sends one literal prefix. Unknown keys stay in command mode and
keep the column open; Esc leaves without sending anything. Dim rows are unavailable in the current
context and say why when pressed. Keys are matched without Shift: x and X, | and \, -
and _ are the same key, so no two bindings may differ only by Shift. When the terminal is too
short for every row the column scrolls one row per ↑/↓ and a screenful per PgUp/PgDn,
with ▲ n more / ▼ n more rows marking what is hidden (on one or two rows it scrolls without
them). The tab and workspace choosers, the rename and new-workspace prompts and the
close confirmations use the same corner box. There is no command-mode timeout.
| Group | Keys | Action |
|---|---|---|
| Panes | ` | -` |
x |
close the focused pane after confirming with y; the target is the pane you pressed on |
|
r |
resize mode: arrows or h j k l adjust repeatedly, Enter finishes, changes are kept |
|
[ |
history and copy mode for the focused pane | |
| Focus | h j k l |
move focus by direction |
| Tabs | t n p |
new tab, next, previous |
w , c |
choose tab, rename the current tab, close the current tab (confirmed) | |
| Workspaces | s a |
choose a workspace, add one (optionally named) and switch to it |
| Session | d |
detach |
The bottom row is always the bar, on its own background: the workspace name, the tabs with the
current one reversed, and the focused pane as id: title on the right (or its exit status once
it has exited). Transient
notices such as copy results, errors and workspace switches appear in that right zone for two
seconds or until the next key. Panes have no frame: adjacent panes share one thin separator, drawn
bold next to the focused pane, and a single pane fills everything above the bar. Colours are muted
by default and configurable through [style]. Pane sizes are negotiated over the smallest viewer
showing the tab, so two viewers with different terminals see the same pane contents; the larger
viewer leaves unused margins. On a tiny terminal the column first scrolls and then truncates labels with …; with a single row only
the bar is shown. A pane that exits while unfocused keeps a dim
exit N marker in its last row; the focused pane's status is in the bar.
Copy mode ([) browses the pane's private history: arrows or h j k l move, u/d and
PgUp/PgDn scroll, Space starts a selection, y or Enter copies it and returns to live output, g
jumps back to live output, q leaves, Esc clears the selection first and then backs out. New
output never moves another viewer's viewport or changes a selection silently: if eviction or a
resize invalidates the selected rows, the selection is cleared with a visible notice.
Mouse: when the application under the pointer does not request mouse input, the wheel browses
that pane's history and dragging selects text inside the pane. When it does, events reach the
application with pane-relative coordinates. Hold Shift to force fux's own history/selection
handling; the keyboard path above always works for terminals that reserve gestures. Copies use
the configured clipboard policy (disabled by default; write-only emits one bounded OSC 52
sequence of at most 1 MiB encoded) and report success or the reason for failure.
Escape is both a key and the start of many sequences. A lone Esc is forwarded after a short disambiguation window (35 ms) unless more bytes arrive; sequences split across reads are reassembled, and a cancelled mode keeps ownership of an unfinished paste until it drains.
Panes and history
Every pane keeps up to scrollback-lines rows of history in the server while hidden, on another
tab, or detached; switching tabs or workspaces and reattaching never discards it. Older rows are
evicted first. Closing a pane, closing a tab or killing a workspace frees the history. There is no
merged output log.
When the only pane of the only tab exits by itself the workspace retires with that exit status:
attached viewers see the final screen, then exit with the code; the workspace finalizes once the
viewers have seen it (or after five seconds). The manager retains bounded final records after
pane retirement, even after the workspace sockets disappear, for the duration the pane's
launcher chose (split.final_retain_ms, at most four hours; [final] retain-ms, 60 seconds
by default, for panes fux creates itself). Other natural exits close the pane, and an emptied
tab closes. Confirmed close and kill send SIGHUP to the pane's process group, SIGKILL after one
second, and reap it. Workspace kill and server shutdown do the same for every pane.
Headless and agent use
Everything works without a terminal. fux workspace new NAME starts the server and a workspace;
fux [NAME] <command> drives it over the control protocol (new, split, focus, kill,
resize, send-keys, capture, list, tab, subscribe, info). Panes carry a
monotonic output sequence reported by list, capture and pane.output events;
fux [NAME] capture PANE --cells returns the visible grid cell by cell with the same coherent
metadata as the text form. fux [NAME] new --env K=V --rows R --columns C -- CMD sets a pane's environment and headless size, and fux [NAME] send-keys PANE --keys "C-c Enter" sends named keys. fux [NAME] subscribe streams every event of the
workspace as JSON lines.
fux is the minimal layer: it owns the PTYs, the retained grids, the exit evidence and the
event log, and exposes them as primitives. Workflows built on those primitives (waiting for a
condition, running one command to completion, agent and task policy) live in zor. The
one-shot convenience is zor run -- COMMAND: it creates an ephemeral workspace (starting a
session server when none is running), runs the command in a pane of a given size and
environment, waits for retained final evidence, prints the final screen and exits with the
command's status; it never takes over or cleans up a pre-existing workspace.
Working with koh and zor
fux composes with the koh and zor programs through process protocols pinned by the fixtures in
crates/fux/tests/verify/fixtures/; it never links, spawns or supervises them. zor lives in
this repository as the separate crate crates/zor (its own binary, tests, lints and ownership
boundary: agent, task, check and artifact policy stay there), so a protocol change is one PR and
one gate. koh remains an independent repository, pinned at an exact commit in
tools/xtask/companions.json and never patched locally: a needed koh change goes upstream
first, then the pin moves.
Remote access is koh's job. On the machine running fux:
and on the viewer machine:
koh authenticates the peer before it opens the local socket, carries the opaque attachment stream, and resumes it across transient link loss without repeating input. Stopping either side leaves the panes and local attachments untouched.
Observation is zor's job. Point it at a pane's workspace control socket:
zor negotiates the control preface, samples list and capture, and runs its own rules. A
missing, stalled, crashed or malformed observer cannot block or change a pane. The socket paths,
identities and events zor consumes are documented in the control protocol.
Protocol fixtures
crates/fux/tests/verify/fixtures/ holds one golden JSON frame per schema and direction (control requests,
replies and events, manager frames, attachment messages). cargo test -p fux --test fixtures round-trips
each through its Rust type with deny_unknown_fields, so a schema change is a visible diff. These
files are the shape koh and zor consume; the real-binary integration suites remain the final check.
Verification
Deterministic ECS tests inject events and time (crates/fux/tests/ecs/, including randomized
command sequences); real-process scenarios use disposable HOME/XDG directories and owned
processes only (crates/fux/tests/local_cli.rs, the fixture-child suite). The real zor
integration (cargo test -p fux --test automation_integration) builds crates/zor from this
checkout; ZOR_BIN overrides it with another build, and FUX_REQUIRE_ZOR_BIN=1 makes a
missing binary a failure. The optional cross-repository job and
cargo run --locked --manifest-path tools/xtask/Cargo.toml -- dependencies verify --build
check out koh at its pinned commit (dependencies apply clones it; verify requires the exact
commit with no local changes) and run the required real koh integration with explicit binary
paths (FUX_BIN, KOH_REQUIRE_FUX_BIN=1), so it can never silently skip.
Documents
- docs/design.md: architecture, entity model, system order, lifecycle.
- docs/verification.md: what was accepted when, and where the evidence lives.
- docs/local-attachment-protocol.md and docs/local-control-protocol.md.
- docs/security.md, docs/release-readiness.md, CHANGELOG.md, HANDOFF.md.
- Earlier architectures (the koh-hosted wrapper, the standalone host) and the prompts that drove each rewrite live in git history; docs/design.md keeps a short history.
Licensed under MIT. Terminal handling reused from earlier fux releases and the koh/zor projects retains attribution in LICENSES.