The Capstone

Everything, combined and working at once:

PTY process execution · terminal parsing · screen state · graphical rendering · multiple sessions · a multiplexer server and client.

The capstone is not new features. It is the demonstration that the pieces you built separately hold together, plus the write-up that proves you can explain every layer without relying on an abstraction you do not understand.


The Deliverable

A single demonstration, run end to end, in which:

   1. `mini-mux server` starts as a daemon and owns every PTY master.

   2. `mini-term --mux` — your GRAPHICAL frontend from Section 3 — attaches to
      it as a mux client. Real window, real fonts, real GPU-or-CPU rendering.

   3. Inside, three panes in one window:
        pane 0: vim, editing a real file
        pane 1: top, updating
        pane 2: a shell, running a build

   4. A second window, `mini-mux attach` from a TERMINAL client (not the GUI),
      attached to the SAME session at a different size.

   5. You kill -9 the graphical client. The shells keep running. You reattach
      and everything is exactly where it was.

   6. Throughout: the debugger can show you every layer on demand.

If that runs, you have built a terminal emulator and a terminal multiplexer, and you understand every byte in between.


The Steps

StepWhatArtifact
1Integration: wire the GUI as a mux clientThe --mux flag works
2The demonstration scriptdemo.sh, runnable, reproducible
3The full-stack traceOne keystroke, all 14 steps, from your own logs
4The compatibility matrixEleven programs, tested, documented
5The architecture documentBoundaries, defended, with known tensions
6The test suiteUnit, golden, integration, differential, fuzz
7The engineering write-up3,000–5,000 words
8Self-assessmentAgainst the rubric

Step 1: Integration

The one piece of genuinely new work: your GUI must be able to act as a mux client.

   BEFORE (Section 3):        AFTER (capstone):

   terminal-gui               terminal-gui
     └─ owns a Pty              ├─ --local: owns a Pty         (as before)
     └─ owns a Terminal         └─ --mux:   owns a socket
                                             renders server snapshots
                                             forwards input
#![allow(unused)]
fn main() {
/// The GUI's backend: either a local PTY or a mux connection. Both produce
/// something renderable and consume input; the renderer does not care which.
enum Backend {
    Local { pty: Pty, terminal: Terminal },
    Mux { conn: MuxConnection, screen: ScreenSnapshot, modes: ModeFlags },
}

impl Backend {
    fn snapshot(&self) -> RenderSnapshot {
        match self {
            // Local: build a snapshot from our own Terminal.
            Backend::Local { terminal, .. } =>
                RenderSnapshot::from_terminal(terminal, &THEME, self.selection()),
            // Mux: the server already composited; convert its wire format.
            Backend::Mux { screen, .. } => RenderSnapshot::from_wire(screen),
        }
    }

    fn send_input(&mut self, key: Key, mods: Modifiers) {
        // Note that the ENCODING is identical in both cases — terminal-input is
        // the same crate, driven by the same mode flags. Only the destination
        // differs. That symmetry is the boundary paying off.
        let modes = self.modes();
        if let Some(bytes) = encode_key(key, mods, modes) {
            match self {
                Backend::Local { pty, .. } => { let _ = pty.write_all(&bytes); }
                Backend::Mux { conn, .. } => conn.send(Request::Input { bytes: bytes.into() }),
            }
        }
    }
}
}

What this integration proves: the render model and the input encoder are genuinely backend-independent. If either needed changing, the boundary was wrong.


Step 2: The Demonstration Script

#!/usr/bin/env bash
# demo.sh — the capstone demonstration. Reproducible, narrated, and it either
# works or it does not.
set -euo pipefail

echo "=== 1. Start the multiplexer server (daemonized) ==="
cargo run --release -p terminal-mux --bin mini-mux -- server
sleep 0.5
SERVER=$(pgrep -f 'mini-mux server')
echo "server pid=$SERVER"
ps -o pid,ppid,sid,tty,comm -p "$SERVER"
echo "  ^ TTY must be '?': the server has NO controlling terminal."

echo
echo "=== 2. Create a session with three panes ==="
cargo run --release -p terminal-mux --bin mini-mux -- new-session -d -s capstone
cargo run --release -p terminal-mux --bin mini-mux -- split -h
cargo run --release -p terminal-mux --bin mini-mux -- split -v
cargo run --release -p terminal-mux --bin mini-mux -- send -t 0 'vim README.md' Enter
cargo run --release -p terminal-mux --bin mini-mux -- send -t 1 'top' Enter
cargo run --release -p terminal-mux --bin mini-mux -- send -t 2 'cargo build' Enter

echo
echo "=== 3. Who owns what ==="
lsof -p "$SERVER" 2>/dev/null | grep -E 'ptmx|sock' | head
echo "  ^ the SERVER holds every PTY master and the listening socket."
for pid in $(pgrep -P "$SERVER"); do
  ps -o pid,ppid,pgid,sid,tty,comm -p "$pid"
done
echo "  ^ each pane child: its OWN session, its OWN pts."

echo
echo "=== 4. Attach the GRAPHICAL client ==="
cargo run --release -p terminal-gui -- --mux &
GUI=$!
sleep 3

echo
echo "=== 5. Attach a SECOND, terminal client at a different size ==="
echo "    (run in another terminal: mini-mux attach -t capstone)"
read -r -p "    Press Enter when the second client is attached... "

echo
echo "=== 6. THE TEST: kill the graphical client HARD ==="
kill -9 "$GUI"
sleep 1
echo "pane children after SIGKILL of the GUI client:"
pgrep -P "$SERVER" | while read -r p; do ps -o pid,comm -p "$p" --no-headers; done
echo "  ^ STILL RUNNING. No PTY master was closed, so no SIGHUP was generated."

echo
echo "=== 7. Reattach; everything is where it was ==="
cargo run --release -p terminal-gui -- --mux &
sleep 3
echo "  ^ vim is on the same line; top shows CURRENT data, not a stale frame."

echo
echo "=== 8. The debugger, on demand ==="
cargo run --release -p terminal-debugger -- decode \
  <(cargo run -q -p terminal-mux --bin mini-mux -- capture -t 0) | head -20

Record it (asciinema rec capstone.cast, or a screen recording for the GUI parts) and check the recording in.


Step 3: The Full-Stack Trace

Reproduce Trace a Keystroke with your own debug output, through the mux path — which is one layer deeper than Section 3's.

MINI_TERM_DEBUG=bytes,utf8,parser,actions,cursor,damage,proto \
MINI_TERM_DEBUG_LOG=/tmp/capstone-trace.log \
  cargo run --release -p terminal-gui -- --mux
# Press exactly one key: `a`. Then quit.

Annotate the log, line by line, mapping each entry to a layer:

[input]  KeyEvent physical=KeyA logical=Character("a") text="a"    ← GUI, winit
[encode] text path → [0x61]                                        ← terminal-input
[proto]  → Request::Input { bytes: [0x61] }                        ← mux protocol
                                                                     ┄ socket ┄
[proto]  ← Request::Input on client 1                              ← mux server
[pty-w]  write(pane0.master, 1): 61                                ← terminal-pty
                                                                     ┄ KERNEL ┄
                                                                     line discipline: ECHO
[pty-r]  read(pane0.master, 1): 61                                 ← terminal-pty
[utf8]   0x61 → Char('a')                                          ← terminal-protocol
[parser] Ground + 0x61 → print                                     ← terminal-protocol
[action] PRINT 'a' U+0061                                          ← terminal-protocol
[cursor] (0,12) → (0,13) cause=print                               ← terminal-core
[damage] mark row 0                                                ← terminal-core
[proto]  → Event::PaneRows { pane: 0, rows: [(0, ...)] }           ← mux server
                                                                     ┄ socket ┄
[render] dirty=[0] atlas=HIT frame=0.29ms                          ← terminal-gui

Fifteen layers, one keystroke, all from your own instrumentation. That log is the single best artifact of the capstone.


Steps 4–6: The Documentation and Test Artifacts

ArtifactFrom
Compatibility matrixInteractive Compatibility
Architecture documentLab 20, including "known tensions"
Test suiteUnit + golden + integration + differential + fuzz, all green, with timings
Boundary auditPassing, in CI
predictions.mdYour predictions from every experiment, annotated with what was wrong
experiments.mdAll seven Section 1 experiments plus the later ones
answers-m0.mdThe twelve questions, answered at the start and re-answered now, with a diff

That last one is worth doing properly. The diff between your first answers and your last is the most direct measure of what you learned.


Step 7: The Engineering Write-Up

3,000–5,000 words. Not a tutorial and not a README — an engineering document of the kind you would write for colleagues who need to maintain this.

Required sections

1. What I Built (300 words). Scope, honestly. What works, what does not, what you deliberately did not implement.

2. The Architecture (800 words). The crate graph with the dependency rules. For each boundary: what it protects and the concrete capability lost if it broke. Include the "known tensions."

3. The Hardest Bug (600 words). One bug, in depth: the symptom, what you thought it was, how you found it, what it actually was, and what the fix taught you. This section reveals more about your understanding than any other — pick a bug where your model was wrong, not one where you made a typo.

4. Design Decisions and Their Alternatives (700 words). At least four decisions where you chose between real options: the resize policy, JSON versus binary, Cell representation, ambiguous width, state versus replay on attach. For each: the options, the criterion, the choice, and what it cost.

5. What I Would Do Differently (400 words). With the benefit of having finished. Be specific. "Better tests" is not an answer; "the Cell type should have been char + an overflow table from the start, because I rewrote three call sites when I changed it" is.

6. Measurements (400 words). Real numbers: parse throughput, frame time at three grid sizes, memory per session, socket bytes/sec, test suite duration, atlas hit rate. Numbers you measured, with the method.

7. What I Still Do Not Understand (300 words). The most valuable section, and the one that distinguishes an engineer from someone performing competence. Every real system has edges you did not reach. Name them.

The standard

The write-up should let a competent engineer who has never seen your code:

  • Understand the architecture without reading it.
  • Predict where a given bug would live.
  • Know which trade-offs are settled and which are open.
  • Know what you are unsure about, so they do not trust the wrong thing.

Step 8: Self-Assessment

Score yourself against the evaluation rubric. Be honest — the rubric is for you, not for anyone else, and a generous self-assessment costs you the only thing it could have given.


The Final Questions

The twelve from the introduction, plus these, which you could not have answered before:

  1. Why does a multiplexer server need a terminal emulator inside it? Give the argument in three sentences.
  2. Why is repainting from state correct where replaying bytes is not?
  3. Explain pending wrap to someone who has never heard of it, and give the visible bug.
  4. Where does UTF-8 decoding belong relative to the escape-sequence parser, and why?
  5. Why can terminal-core compile to WebAssembly, and what did that constraint buy you?
  6. Trace one keystroke through all fifteen layers of the mux path.
  7. Why is the same arrow key different bytes in bash and vim?
  8. What breaks when a program crashes with mouse reporting enabled, and whose job is it to fix?
  9. Name three deliberate divergences from xterm in your implementation and defend each.
  10. Which crate would change for a Windows port, and how do you know?
  11. What is the most important measurement you took, and what decision did it drive?
  12. Who owns the terminal screen — the shell, the kernel, or the emulator? Compare your answer with the one you wrote at Milestone 0.

Beyond the Capstone

Eight larger, self-directed projects are laid out in the capstone portfolio — reflow on resize, a GPU renderer, a ConPTY backend, graphics protocols, and more. Several are plausible upstream contributions.

The shorter list, in rough order of value:

DirectionWhy
Contribute to a real terminalAlacritty, WezTerm, Ghostty, foot, zellij all take contributions. You now have the context to be useful on day one.
Implement sixel or the kitty graphics protocolImages in a terminal; a large, well-specified, satisfying project
Implement reflow on resize properlyGenuinely hard, with open bugs in every major terminal
A GPU renderer with ligaturesShaping plus instanced rendering, done right
A ConPTY backendThe ultimate boundary test
Full kitty keyboard protocolFixes the Escape ambiguity permanently
A terminal for a different mediumWeb (WASM), mobile, or an embedded display
Write the width-comparison toolThe ecosystem genuinely lacks a current one

Next: The Evaluation Rubric.