a73x

1b15df83

docs: add marketing documentation

a73x   2026-09-09 11:16

Commit message
docs: add marketing documentation

README.md
Old New
@@ -4,8 +4,11 @@ Terminal multiplexer where detach/reattach is **state sync, not replay**:
4 the terminal engine (ghostty-vt) runs authoritatively in a daemon (`mux d`), 4 the terminal engine (ghostty-vt) runs authoritatively in a daemon (`mux d`),
5 and the wire carries its grid as cells that the client (`mux`) copies into a 5 and the wire carries its grid as cells that the client (`mux`) copies into a
6 grid of its own. Attach costs one snapshot; everything after is row deltas; 6 grid of its own. Attach costs one snapshot; everything after is row deltas;
7 a dropped connection is a non-event. Linux and Apple-silicon macOS, prototype 7 a dropped connection is a non-event. Mux publicly supports Linux x86_64 only.
8 (see [Status](#status)). 8
9 - [Ethos](docs/Ethos.md)
10 - [Architecture](docs/Architecture.md)
11 - [Features](docs/Features.md)
9 12
10 ## Build 13 ## Build
11 14
@@ -69,8 +72,8 @@ are an ordered list of installed monospace `font-family` values and a
69 `font-size` from 1–192 points. The first family supplies grid metrics; each 72 `font-size` from 1–192 points. The first family supplies grid metrics; each
70 later family is tried when an entire cell grapheme is unavailable in the 73 later family is tried when an entire cell grapheme is unavailable in the
71 families before it. 74 families before it.
72 Fractional sizes are preserved until rasterization: points × 96/72 on Linux 75 Fractional sizes are preserved until rasterization: points × 96/72 on Linux,
73 (points × 1 on macOS), then display scale, rounded to a pixel. Blank lines and 76 then display scale, rounded to a pixel. Blank lines and
74 full-line `#` comments are accepted; a family may be double quoted. Duplicate 77 full-line `#` comments are accepted; a family may be double quoted. Duplicate
75 singleton keys, unknown mux config keys, malformed values and unavailable 78 singleton keys, unknown mux config keys, malformed values and unavailable
76 families produce diagnostics. Per-style families, escapes and inline comments 79 families produce diagnostics. Per-style families, escapes and inline comments
@@ -93,9 +96,9 @@ That choice remains fixed until release, even if Shift changes or the pointer
93 crosses another pane. Ctrl+Shift+C copies the current local selection. 96 crosses another pane. Ctrl+Shift+C copies the current local selection.
94 Application OSC 52 writes update the desktop clipboard (`c`) or primary 97 Application OSC 52 writes update the desktop clipboard (`c`) or primary
95 selection (`p`/`s`); clipboard queries remain refused. Ctrl+Shift+V pastes 98 selection (`p`/`s`); clipboard queries remain refused. Ctrl+Shift+V pastes
96 into the focused pane or an editable picker field; Cmd+V does the same on 99 into the focused pane or an editable picker field. A terminal that enables
97 macOS. A terminal that enables bracketed paste receives one bracket pair 100 bracketed paste receives one bracket pair around the complete clipboard text,
98 around the complete clipboard text, including when the payload is chunked. 101 including when the payload is chunked.
99 102
100 Choose a Ghostty theme file by name in `$XDG_CONFIG_HOME/mux/themes/` (or 103 Choose a Ghostty theme file by name in `$XDG_CONFIG_HOME/mux/themes/` (or
101 `~/.config/mux/themes/`), or by absolute path: 104 `~/.config/mux/themes/`), or by absolute path:
@@ -218,39 +221,31 @@ Binaries land in `zig-out/bin/`. For remote machines, build a static binary
218 that runs on any x86_64 Linux: 221 that runs on any x86_64 Linux:
219 222
220 ```sh 223 ```sh
221 ~/Downloads/zig-x86_64-linux-0.15.2/zig build -Dtarget=x86_64-linux-musl 224 mise exec -- zig build -Dtarget=x86_64-linux-musl
222 ``` 225 ```
223 226
224 `make release` builds both `mux` and `muxg` in `ReleaseSafe`, strips them, 227 `make release` builds both `mux` and `muxg` in `ReleaseSafe`, strips them,
225 checks both against `build.zig`'s version, and packages them in `dist/`. 228 checks both against `build.zig`'s version, and packages them in `dist/`. On
226 On Linux, `mux` remains static musl; `muxg` uses the build host's native target 229 Linux, `mux` remains static musl; `muxg` uses the build host's native target
227 and needs compatible CPU, libc, SDL3, FreeType, fontconfig, and HarfBuzz libraries 230 and needs compatible CPU, libc, SDL3, FreeType, fontconfig, and HarfBuzz
228 on the destination, plus an OpenGL-capable display. These libraries are not 231 libraries on the destination, plus an OpenGL-capable display. These libraries
229 bundled. The tarball is what to hand another machine, because it 232 are not bundled. The tarball carries the executable bit for installation on
230 carries the exec bit that a bare downloaded binary does not: 233 another machine:
231 234
232 ```sh 235 ```sh
233 make release 236 make release
234 ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSION-x86_64-linux-musl.tar.gz 237 ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSION-x86_64-linux-musl.tar.gz
235 ``` 238 ```
236 239
237 A release is one tarball per OS, retaining the daemon target's name: 240 Linux x86_64 releases are published as
238 `mux-vVERSION-x86_64-linux-musl.tar.gz` and 241 `mux-vVERSION-x86_64-linux-musl.tar.gz` under
239 `mux-vVERSION-aarch64-macos.tar.gz`. Each is cut on that OS — `make release` 242 `https://git.a73x.sh/mux/releases/vVERSION/`. Install a published release:
240 builds for the host it runs on — and both are published under
241 `https://git.a73x.sh/mux/releases/vVERSION/`, so installing a published
242 build is one line:
243 243
244 ```sh 244 ```sh
245 mkdir -p ~/.local/bin 245 mkdir -p ~/.local/bin
246 curl -fsSL https://git.a73x.sh/mux/releases/vVERSION/mux-vVERSION-x86_64-linux-musl.tar.gz | tar xzf - -C ~/.local/bin 246 curl -fsSL https://git.a73x.sh/mux/releases/vVERSION/mux-vVERSION-x86_64-linux-musl.tar.gz | tar xzf - -C ~/.local/bin
247 curl -fsSL https://git.a73x.sh/mux/releases/vVERSION/mux-vVERSION-aarch64-macos.tar.gz | tar xzf - -C ~/.local/bin
248 ``` 247 ```
249 248
250 On an Apple-silicon Mac the same build and install commands work, with the
251 toolchain and native dependencies installed first — see
252 [macOS](#macos).
253
254 ## Quick start, local 249 ## Quick start, local
255 250
256 ```sh 251 ```sh
@@ -907,26 +902,9 @@ dropped connection can never truncate a binary, then runs that box's own
907 running just gets the install (exit 0, and it says so); a box with no mux 902 running just gets the install (exit 0, and it says so); a box with no mux
908 at all is refused — a push replaces an install, it does not invent one. 903 at all is refused — a push replaces an install, it does not invent one.
909 904
910 Coming from v0.0.1-15 or older is the one upgrade `mux d upgrade` cannot 905 Older daemons may not support in-place upgrade or newer session-discovery
911 do: that daemon probes a candidate for `muxd <version>` and this binary 906 requests. Stop and start those daemons with the current binary instead.
912 prints `mux <version>`, so it refuses. `mux d stop` then `mux d start -d`, 907
913 once per box. A v0.0.1-16 daemon is the same story for the same reason: it
914 execs its candidate as `mux d run`, and the verb is `start` now. `make install` deletes `muxd`, `muxa` and `muxweb` from the
915 install directory on its way in — they are modes of `mux` now, and a
916 `muxd` left on PATH is a second daemon waiting for someone to start it.
917
918 One more mixed-version caveat runs the other way, daemon newer than client.
919 A daemon now answers `sessions_req` with a `# holds NAME N` line per session
920 beside the names, which an older client skips as it skips the version line —
921 but a client built before those lines existed sized its receive buffer for
922 what it expected and reads anything longer as a transport error. Released
923 v0.0.1-16 sized it for the names alone, 1056 bytes; a development build off
924 main after the version line and before the holds lines sized it at 1101.
925 Either way a new daemon holding roughly fourteen or more sessions with
926 32-character names reads `unreachable` on that wall until the wall is
927 upgraded. Upgrading the client end is the fix; there is nothing to do on the
928 daemon. (Say it by what the client was BUILT from, not by a version number:
929 this branch stamps 0.0.1-17 itself, and no v0.0.1-17 tag exists yet.)
930 908
931 Copy and paste work through the session: a mux drag and an application's own 909 Copy and paste work through the session: a mux drag and an application's own
932 OSC 52 write both reach your terminal's clipboard (including from a remote box over QUIC, 910 OSC 52 write both reach your terminal's clipboard (including from a remote box over QUIC,
@@ -936,103 +914,6 @@ bell rings. The clipboard READ direction (`OSC 52` query) is refused
936 deliberately — answering it would let anything in any session read 914 deliberately — answering it would let anything in any session read
937 whatever you last copied. 915 whatever you last copied.
938 916
939 ## macOS
940
941 mux builds and runs on an Apple-silicon Mac — `aarch64` only. There is no
942 Intel build, no universal binary, and no way to cross-compile the Mac
943 binary from Linux, so a Mac binary is made on a Mac.
944
945 You need Xcode's Command Line Tools for the SDK and `cmake` for the QUIC
946 dependencies, and the pinned toolchain in the same place a Linux checkout
947 puts it: unpack the aarch64 Zig 0.15.2 tarball so that the binary is at
948 `deps/zig/zig`. The Makefile spells that path and nothing else, so a
949 system `zig` is neither used nor enough.
950
951 ```sh
952 xcode-select --install
953 brew install cmake
954 make build # first build fetches + compiles the QUIC deps, as on Linux
955 make install # mux + muxg in ~/.local/bin; also needs the native libraries listed above
956 ```
957
958 The Mac tarball of a release is cut by `make release-mac`, which runs
959 `make release` on the Mac named by `MAC_BUILDER` — the same box, and the
960 same variable, the macOS gates build on — and publishes it from there.
961
962 `make build` runs `deps/mac-sdk.sh` first, which may build a shadow SDK
963 under `deps/mac-sdk/`. Xcode 26.4 and later ship a `libSystem` stub that
964 lists only the `arm64e` slice, and the pinned Zig 0.15.2 matches the bare
965 `arm64` one, so against that SDK every system symbol comes out undefined
966 and the build cannot start. The shadow is the real SDK by symlink
967 everywhere except that one stub, where Zig's own copy stands in, plus an
968 `xcrun` on PATH that points at it. It costs a few seconds once, retires
969 itself when a future Xcode lists `arm64` again, and needs nothing of you.
970
971 One runtime difference worth knowing. The daemon's default socket lives in
972 `$XDG_RUNTIME_DIR` when that is set, and macOS does not set it, so mux
973 falls back to `/tmp/mux-<uid>` — created 0700 and re-checked for owner and
974 mode on every use, the way tmux uses `/tmp/tmux-UID`. The macOS `$TMPDIR`
975 is not used for this: the longest socket name mux creates would not fit in
976 the 103 bytes a unix socket path gets.
977
978 `make test` passes on a Mac. `make e2e` has two groups that do not, both
979 deliberately and both for the fallback above. A leg in the select group
980 requires `mux d stats` and a bare `mux` to REFUSE when
981 `$XDG_RUNTIME_DIR` is unset, and a leg in the askpass group requires a
982 wall in that state to start no prompt socket at all. On macOS there is
983 always a runtime directory, so mux answers instead of refusing and the
984 wall arms its socket, which is the fallback working. The suite is linear,
985 so a `make e2e` on a Mac stops at the first of the two. The other gates —
986 `make agent`, `make soak`, `make throughput`, `make xversion` — have not
987 been run on a Mac at all.
988
989 The gate for macOS is `make mac`, the macOS half of what `make vm` is for
990 Linux. Both macOS gates take the machines they use BY NAME, as ssh targets
991 in the environment, and neither has a default for one:
992
993 | Name | What it is |
994 |---|---|
995 | `MAC_BOX` | the macOS box under test, with no developer tooling on it. Scrubbed to a mux-less state at the start of a run and again at the end, so make it a VM. |
996 | `MAC_BUILDER` | a Mac holding this repo with zig and the shadow SDK. It builds, and nothing else: no mux is installed there and nothing is reset there. Defaults to `MAC_BOX`. |
997 | `LINUX_BOX` | `make xos` only: an x86_64 Linux VM that both this box and `MAC_BOX` can reach. Scrubbed the same way. |
998
999 `make mac` pushes this commit to the builder, builds and runs the unit suite
1000 there, streams the binary onto `MAC_BOX`, and then runs the user journeys —
1001 so what is graded is the product on a bare Mac rather than on the machine
1002 that compiled it. A run looks like this:
1003
1004 ```sh
1005 eval "$(test/provision-mac.sh)" # prints: export MAC_BOX=admin@192.168.0.x
1006 MAC_BUILDER=squirtle make mac
1007 test/provision-mac.sh --down # when you are done with the guest
1008 ```
1009
1010 `test/provision-mac.sh` is where the virtual machine's life is spelled: it
1011 clones a pristine base image on a Mac that has Cirrus Labs `tart`, boots the
1012 clone bridged onto the LAN so this box can ssh to it directly, and installs
1013 this box's public key on it. The base image itself is made once, by hand, on
1014 that Mac:
1015
1016 ```sh
1017 brew trust cirruslabs/cli && brew install cirruslabs/cli/tart
1018 tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest mux-mac-base
1019 tart run --no-graphics mux-mac-base & # user admin, password admin
1020 tart exec mux-mac-base sh -c 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "PUBKEY" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
1021 tart stop mux-mac-base
1022 ```
1023
1024 `PUBKEY` is that Mac's own `~/.ssh/id_ed25519.pub`. Nothing stops you
1025 pointing `MAC_BOX` at some other spare Mac instead, as long as you mean the
1026 word scrubbed.
1027
1028 `make mac` and `make vm` each run one operating system on both ends of every
1029 connection. `make xos` is the gate that does not: it attaches a macOS client
1030 to a Linux daemon and a Linux client to a macOS daemon, over the ssh handoff
1031 and over QUIC, and checks that a cross-architecture `mux d upgrade` is
1032 refused in both directions before any of the image is streamed. It uses all
1033 three names above, and pairs the two boxes' ssh keys itself, because each
1034 one dials the other from the inside.
1035
1036 ## How it works 917 ## How it works
1037 918
1038 One real terminal engine runs, and it is the daemon's: shell output mutates 919 One real terminal engine runs, and it is the daemon's: shell output mutates
@@ -1053,23 +934,5 @@ issue list` (`docs/roadmap.md` is the older ranked queue).
1053 934
1054 ## Status 935 ## Status
1055 936
1056 Prototype, eighteen milestones in, current cut tagged `v0.0.1-15`. Linux 937 Prototype. Mux publicly supports Linux x86_64 only. See the feature and
1057 and Apple-silicon macOS; every gate below was measured on Linux. All 938 architecture documentation above for the current product scope.
1058 founding kill criteria cleared, measured on real networks rather than
1059 loopback: reattach into a live full-screen `nvim` in ~5ms from one
1060 snapshot; steady-state delta traffic ~65 bytes per keystroke, and a cold
1061 attach to a fresh session 86 bytes of cells where it was 5644 of VT; the
1062 protocol adds ~4ms to a raw ssh round trip (constant, not a multiple of
1063 RTT); ten consecutive real network tears resumed hands-off, all
1064 delta-served, on both ssh and QUIC transports; QUIC cold attach beats
1065 ssh-via (6.9ms vs 9.8ms clean, 234.1ms vs 235.7ms with 75ms emulated
1066 delay on a real path); and a predicted keystroke paints in **0.1ms** on a
1067 150ms path where the same keystroke's authoritative echo takes **154ms** —
1068 typing stops waiting for the round trip, and the two numbers come from one
1069 keystroke on one connection so the gap is not an artefact of comparing
1070 runs. Auto-start is here for local unix-socket attaches — `mux` and `mux
1071 --sock PATH` start a daemon when nothing is serving that path — and the wall
1072 starts the one on the default socket for the same reason. A remote daemon
1073 is started only by the dial a user ASKED for — `mux HOST`, a picker Enter —
1074 and never by a dial nobody asked for. Not yet here: reconnect backoff
1075 tuning, prediction for backspace and for multi-byte input.
RETRO.md
Old New
@@ -1584,3 +1584,5 @@ and stopped localhost demo server PID 257482; port 18778 is closed. Its composit
1584 and app fixtures had already stopped. Retained the page, 25-second recording and 1584 and app fixtures had already stopped. Retained the page, 25-second recording and
1585 check evidence, preserved unrelated services, and left the known NVIDIA frame 1585 check evidence, preserved unrelated services, and left the known NVIDIA frame
1586 budget debt open. No next sprint was started. 1586 budget debt open. No next sprint was started.
1587
1588 Documentation update — 2026-09-06: Identifying the public support boundary required removing stale platform and release copy; concise source-backed architecture and feature pages made the current scope clear.
docs/Architecture.md
Old New
@@ -0,0 +1,39 @@
1 # Architecture
2
3 Mux separates terminal execution from terminal presentation. The daemon owns sessions, PTYs, and the Ghostty VT engine. Each client holds a replica of the terminal grid: it receives a cell snapshot when needed, then row deltas as the authoritative grid changes.
4
5 ```mermaid
6 flowchart LR
7 B[Browser client replica]
8 W[`mux web` hub]
9 S[Unix socket]
10 H[SSH handoff]
11 Q[QUIC]
12 V[`--via` command]
13
14 subgraph Daemon
15 D[mux daemon]
16 VT[Daemon-owned Ghostty VT state]
17 D --> VT
18 end
19
20 B -->|local HTTP/WebSocket: input and commands| W
21 W -->|input and commands| S --> D
22 W -->|input and commands| H --> D
23 W -->|input and commands| Q --> D
24 W -->|input and commands| V --> D
25 VT -->|snapshots and row deltas| S --> W
26 VT -->|snapshots and row deltas| H --> W
27 VT -->|snapshots and row deltas| Q --> W
28 VT -->|snapshots and row deltas| V --> W
29 W -->|local HTTP/WebSocket: snapshots and row deltas| B
30 ```
31
32 The browser reaches a local `mux web` hub over HTTP and WebSocket; it does not
33 speak directly to a Unix socket. The hub uses the same client services to reach
34 daemon sessions through a local Unix socket, SSH handoff, QUIC, or `--via`.
35 Those transports carry bytes and do not interpret terminal frames.
36
37 The daemon owns VT parsing and terminal state. It receives client input and
38 commands, then sends snapshots and row deltas for each client replica to apply
39 before rendering.
docs/Ethos.md
Old New
@@ -0,0 +1,19 @@
1 # Ethos
2
3 Mux keeps terminal sessions alive and makes their current state available wherever you attach. It is a state-sync terminal multiplexer: the daemon owns the terminal state, while clients render replicas of it.
4
5 ## Keep the terminal authoritative
6
7 The daemon runs the Ghostty VT engine. Clients do not replay escape sequences; they receive terminal cells, starting with a snapshot and continuing with row deltas. Reconnecting restores the current screen rather than replaying a terminal transcript.
8
9 ## Keep clients flexible
10
11 Use the terminal/TUI client, a browser wall, the native `muxg` client, or the `mux a` automation interface against the same daemon sessions. Clients can connect locally or remotely without changing the terminal-state model.
12
13 ## Keep control explicit
14
15 Sessions outlive client connections. Detaching is separate from ending a session or stopping a daemon. A read-only check does not start a daemon. SSH-agent forwarding is opt-in through `mux -A`; `mux a` does not offer an agent.
16
17 ## Platform support
18
19 Mux publicly supports Linux x86_64 only.
docs/Features.md
Old New
@@ -0,0 +1,31 @@
1 # Features
2
3 ## State-synced sessions
4
5 Detach and reattach without replaying terminal output. The daemon keeps the authoritative Ghostty VT state; clients receive a snapshot and then row deltas.
6
7 ## Multiple clients
8
9 Use the terminal/TUI client (`mux`), browser wall (`mux web`), native client (`muxg`), or `mux a` automation interface with daemon sessions. Terminal and native clients can attach locally, through SSH handoff, over QUIC, or through a `--via` command.
10
11 ## Session control
12
13 Create, browse, and arrange sessions in terminal and native client layouts. Detaching leaves sessions alive. Ending a session and stopping a daemon are explicit actions.
14
15 ## Agent interface
16
17 `mux a` inspects and drives an existing session through JSON replies. It attaches at 0×0, so it does not claim session geometry, and it never offers SSH-agent forwarding.
18
19 ## SSH-agent forwarding
20
21 `mux -A` is opt-in terminal-client behavior. It forwards that terminal client's SSH agent into its attached session; `mux a` does not offer an agent.
22
23 ## muxg-only TCP loopback forwarding
24
25 TCP loopback forwarding is available only in `muxg`. `muxg --forward LOCAL_PORT:REMOTE_PORT TARGET` binds `127.0.0.1:LOCAL_PORT` and forwards TCP to `127.0.0.1:REMOTE_PORT` on the target host. It supports repeated rules, but not hostname destinations, wildcard binds, reverse forwarding, or UDP.
26
27 The native picker does not configure forwarding yet. Picker-based forwarding is future work.
28
29 ## Platform support
30
31 Mux publicly supports Linux x86_64 only.