Section 4: The Minimal tmux-Style Multiplexer
The multiplexer must not be confused with the terminal emulator. That sentence is the whole section, and if you finish able to explain it without hesitation, you have got what matters.
A terminal emulator owns pixels and one PTY. A multiplexer owns many PTYs, runs a headless terminal emulator per pane, composites them into one logical screen, and — critically — outlives its own UI. That last property is not a feature; it is the constraint that forces the entire architecture.
What You Build
M9 Multiple sessions in one process — N PTYs, N Terminals, one event loop
M10 Panes and layout — a layout tree, compositing, input routing
M11 A server process — daemonized, Unix socket, framed protocol
M12 Detach and attach — sessions survive the client; multiple clients
Then, as extensions: session names, prefix commands, copy mode, and multi-client resize negotiation.
The Architecture
terminal-mux-server (a daemon; no controlling terminal)
├── Unix socket listener /tmp/mini-mux-<uid>/default mode 0700
├── Session manager
│ └── Session "work"
│ ├── Window 0 "editor"
│ │ ├── layout: Split{Horizontal, 0.6, [Leaf(p0), Leaf(p1)]}
│ │ ├── Pane 0 ── PTY master ── child (vim) ── Terminal (headless)
│ │ └── Pane 1 ── PTY master ── child (bash) ── Terminal (headless)
│ └── Window 1 "logs"
│ └── Pane 2 ── PTY master ── child (tail) ── Terminal (headless)
├── Event loop (poll/epoll/kqueue over: listener, client sockets, pane masters, signals)
└── Client protocol (length-prefixed frames)
terminal-mux-client (short-lived; owns nothing durable)
├── the user's terminal in RAW mode (or your GUI from Section 3)
├── keyboard input ──▶ prefix parser ──▶ command | forward to the server
├── rendering: server screen updates ──▶ the user's terminal
└── socket connection
The Chapters
| Chapter | Covers |
|---|---|
| Why a Server Process | SIGHUP, PTY master ownership, why detach works, why pipes are insufficient |
| The Client/Server Protocol | Framing, the message catalog, JSON first then binary, versioning |
| Panes, Windows, and Layout | The layout tree, geometry, compositing, resize propagation |
| Input Routing and Copy Mode | The prefix key, command mode, copy mode, why the mux parses input first |
The Labs
| Lab | Milestone | Build |
|---|---|---|
| Lab 15 | M9 | Multiple PTY-backed sessions in one process |
| Lab 16 | M10 | The layout tree, compositing, focus |
| Lab 17 | M11 | The daemon, the socket, the protocol |
| Lab 18 | M12 | Detach, attach, multiple clients, resize policy |
| Lab 19 | — | Copy mode: scrollback navigation, search, selection, the two clipboards |
The Central Insight, Stated Up Front
A multiplexer keeps your shells alive because the PTY master is owned by a process that did not die.
Everything follows:
WITHOUT a mux:
your terminal emulator owns the PTY master
you close the window → the emulator exits → the master fd closes
→ the KERNEL sends SIGHUP to the session leader (your shell)
→ the shell HUPs its jobs and exits
→ everything dies
WITH a mux:
the mux SERVER owns the PTY masters
you close the window → the CLIENT exits → the client's socket closes
→ the server notices a client left. That is all that happens.
→ the pane PTYs are untouched. No fd closed. No SIGHUP. Nothing dies.
The corollary that surprises people: the server must contain a terminal emulator per pane. While
detached, top keeps producing output. Someone must parse it, or on reattach you would have nothing
to draw. That someone is a headless terminal-core — the exact crate you built in Section 2, with no
display, which is why that boundary mattered.
Why Pipes Are Insufficient (Again, But For a New Reason)
Section 1 covered why an emulator needs a PTY rather than pipes. A multiplexer has the same requirement, plus one more:
| Requirement | Pipe | PTY |
|---|---|---|
| Interactive programs work | ❌ | ✅ |
| Job control in each pane | ❌ | ✅ |
| Per-pane window size | ❌ | ✅ (each pane has its own winsize) |
SIGWINCH when a pane resizes | ❌ | ✅ |
| A pane can be resized independently of every other pane | ❌ | ✅ |
| Each pane is its own session with its own controlling terminal | ❌ | ✅ |
The last two are what make panes possible at all. A pane is 40 columns wide because its PTY says so; the program inside believes it is on a 40-column terminal and has no idea it is sharing a screen.
Deliverables
-
Multiple PTY-backed sessions, each with its own
Terminal, in one event loop. -
Sessions keep running while hidden — proven by switching away from
topand back. - Panes with a layout tree, compositing, borders, and focus.
- Per-pane resize propagation.
-
A daemonized server with a Unix socket in a per-user directory, mode
0700. - A framed protocol with a documented message catalog and round-trip tests.
-
kill -9on the client leaves the shells running — proven withps. - Detach and reattach, with a full repaint from state (not a byte replay).
- Two clients attached simultaneously, both rendering and typing.
- A documented multi-client resize policy.
-
A prefix key with a literal escape (
prefix prefixsends the raw byte). -
cargo tree -p terminal-mux | grep -E 'winit|wgpu|softbuffer'prints nothing.
Common Mistakes in This Section
| Mistake | Symptom | Fix |
|---|---|---|
| The client owns the PTY masters | Closing the client kills everything — you built a tabbed terminal, not a mux | The server owns them |
The server does not setsid() | The server dies with the terminal that launched it | Daemonize properly |
| No terminal emulator in the server | Reattach shows a blank screen or replays gigabytes | One headless Terminal per pane |
| Replaying raw bytes on attach | Wrong screen whenever the pane used the alt screen or scroll regions | Repaint from state |
| Forwarding all input to the pane | The prefix key never works | Parse the prefix in the client, before forwarding |
| Not draining hidden panes | The child blocks in write() and appears hung | Always read every master, always parse |
| One size for all clients | A big client sees a small pane, or a small client sees corruption | An explicit policy: smallest-wins or per-client |
Socket in /tmp with mode 0666 | Any local user can attach to your shells | A per-user directory, mode 0700, and check the owner |
| Unbounded per-client output buffer | A slow client makes the server OOM | Bound it; drop the client if it will not drain |
| Blocking write to a client socket | One stalled client freezes every pane | Non-blocking, with an output buffer per client |
How to Verify Success
# 1. Start the server and attach.
mini-mux server &
mini-mux attach
# 2. Inside: create panes, run things.
# <prefix> % split vertically
# <prefix> " split horizontally
# <prefix> o focus the next pane
# run `vim` in one, `top` in another
# 3. The critical test — kill the CLIENT, not the server:
# from another terminal:
pkill -9 -f 'mini-mux attach'
ps -o pid,ppid,sid,tty,comm | grep -E 'vim|top' # STILL RUNNING
# 4. Reattach and see the current screen.
mini-mux attach
# top is still updating; vim is where you left it
# 5. Two clients at once, from two terminals:
mini-mux attach # terminal A
mini-mux attach # terminal B
# typing in either appears in both
# 6. Detach cleanly:
# <prefix> d
Section Profile: What a Section 4 Graduate Can Do
| Capability | Evidence |
|---|---|
| Explain why tmux needs a server | SIGHUP + master ownership, in one sentence |
| Explain why a mux contains an emulator | The detached-top argument |
| Design a client/server protocol | The message catalog, with a versioning story |
| Reason about resize with multiple observers | Your documented policy and its trade-offs |
| Route input correctly | The prefix parser, with a literal escape |
| Debug "my session died" | Who owned the master? Did the server daemonize? |
Next: Why a Server Process.