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

ChapterCovers
Why a Server ProcessSIGHUP, PTY master ownership, why detach works, why pipes are insufficient
The Client/Server ProtocolFraming, the message catalog, JSON first then binary, versioning
Panes, Windows, and LayoutThe layout tree, geometry, compositing, resize propagation
Input Routing and Copy ModeThe prefix key, command mode, copy mode, why the mux parses input first

The Labs

LabMilestoneBuild
Lab 15M9Multiple PTY-backed sessions in one process
Lab 16M10The layout tree, compositing, focus
Lab 17M11The daemon, the socket, the protocol
Lab 18M12Detach, 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:

RequirementPipePTY
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 top and 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 -9 on the client leaves the shells running — proven with ps.
  • 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 prefix sends the raw byte).
  • cargo tree -p terminal-mux | grep -E 'winit|wgpu|softbuffer' prints nothing.

Common Mistakes in This Section

MistakeSymptomFix
The client owns the PTY mastersClosing the client kills everything — you built a tabbed terminal, not a muxThe server owns them
The server does not setsid()The server dies with the terminal that launched itDaemonize properly
No terminal emulator in the serverReattach shows a blank screen or replays gigabytesOne headless Terminal per pane
Replaying raw bytes on attachWrong screen whenever the pane used the alt screen or scroll regionsRepaint from state
Forwarding all input to the paneThe prefix key never worksParse the prefix in the client, before forwarding
Not draining hidden panesThe child blocks in write() and appears hungAlways read every master, always parse
One size for all clientsA big client sees a small pane, or a small client sees corruptionAn explicit policy: smallest-wins or per-client
Socket in /tmp with mode 0666Any local user can attach to your shellsA per-user directory, mode 0700, and check the owner
Unbounded per-client output bufferA slow client makes the server OOMBound it; drop the client if it will not drain
Blocking write to a client socketOne stalled client freezes every paneNon-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

CapabilityEvidence
Explain why tmux needs a serverSIGHUP + master ownership, in one sentence
Explain why a mux contains an emulatorThe detached-top argument
Design a client/server protocolThe message catalog, with a versioning story
Reason about resize with multiple observersYour documented policy and its trade-offs
Route input correctlyThe prefix parser, with a literal escape
Debug "my session died"Who owned the master? Did the server daemonize?

Next: Why a Server Process.