The Teaching Method

This chapter describes the shape of every phase and every implementation step in this curriculum. Read it once so the structure of the following chapters is not a surprise, and come back to it when you are designing your own experiments.


The Phase Template

Every phase — every section, every lab — follows this order. The order is not decorative; each step exists because skipping it produces a specific failure.

#StepWhy it is hereWhat goes wrong if you skip it
1Explain the mental model before writing codeYou cannot debug a system whose shape you do not know.You write code that works by coincidence and cannot fix it when it stops.
2Draw an ASCII architecture diagramForces you to commit to which component owns what.Responsibilities blur; you end up with a Terminal that owns a socket.
3Show the flow of bytes, events, processes, and signalsTerminals are a flow problem, not a data-structure problem.You debug by adding print statements at random.
4Identify user space vs. kernel spaceHalf of all terminal confusion is a misplaced boundary.You try to fix an echo bug in your renderer.
5Implement the smallest observable versionSmall enough to hold in your head, big enough to prove something.You build a plausible-looking layer you have never actually run.
6Add instrumentationYou cannot verify what you cannot see.Every future bug costs an hour of re-instrumenting.
7Design experimentsAn experiment is a claim plus a way to be wrong.You confirm your assumptions instead of testing them.
8Add focused testsTests are the only thing that survives a refactor.Milestone 7 breaks Milestone 5 and you find out in week three.
9Explain common mistakesMost bugs in this domain are the same bugs.You rediscover setsid-before-TIOCSCTTY the hard way.
10Refactor only after the behavior is understoodA refactor before understanding is a bet.You "clean up" the pending-wrap flag because it looked redundant.

The Implementation Step Template

Each implementation step in this curriculum contains these eleven parts. When a lab omits one, it is because the step genuinely has none — not as a shortcut.

┌──────────────────────────────────────────────────────────────────┐
│ 1.  The concept being learned         "what is this for?"        │
│ 2.  The smallest goal                 one sentence               │
│ 3.  The expected observable behavior  what you will SEE          │
│ 4.  The Rust code                     small enough to read       │
│ 5.  Line-by-line explanation          of the parts that matter   │
│ 6.  Commands to run                   copy-pasteable             │
│ 7.  Expected output                   so you know if it worked   │
│ 8.  Debugging steps                   for when it did not        │
│ 9.  One experiment                    a claim you could disprove │
│ 10. One test                          that survives refactoring  │
│ 11. One challenge extension           to go past the lesson      │
│ 12. A checkpoint question             to verify understanding    │
└──────────────────────────────────────────────────────────────────┘

Avoid large unexplained code dumps. If a code block in this curriculum is longer than about 80 lines, it is followed by a line-by-line walkthrough of the parts that carry meaning. If you find yourself copying a block you cannot annotate, stop and annotate it — the annotation is the exercise.


The Predict-First Protocol

Throughout the curriculum you will be asked to predict behavior before revealing the result. This is not a gimmick. Prediction converts a passive read into a test of your model, and the gap between prediction and observation is where learning happens. A confirmed prediction teaches you almost nothing; a wrong one teaches you exactly which belief was false.

The protocol:

  1. Read the question.
  2. Write your prediction down — in a file, in a comment, on paper. Writing is required. A prediction you kept in your head will silently rewrite itself when you see the answer. This is hindsight bias and you are not immune to it.
  3. Include your confidence (high / medium / guessing).
  4. Run the experiment.
  5. If you were wrong, write one sentence naming the false belief. Not "I forgot" — the actual belief.

Keep these in predictions.md in your workspace. At the capstone you will review it.

Example questions you will be asked

  • What do you expect will happen if the child process is connected using pipes instead of a PTY?
  • Which process should receive Ctrl+C?
  • What happens if the PTY window size is never updated?
  • Why does Vim behave differently when stdout is redirected?
  • Who owns the terminal screen: the shell, the kernel, or the emulator?
  • If you write 81 characters into an 80-column terminal, where is the cursor?
  • If you close the master fd while the child is in the middle of a write(), what does the child see?
  • Two clients of different sizes attach to one session. What size is the pane?

Warning: The four questions most people get wrong on first encounter are: who echoes, where ^C becomes a signal, what pending wrap does, and why the mux server needs its own emulator. If you predict all four correctly, you may move faster through Sections 1 and 4.


What "Instrumentation" Means Here

Instrumentation is not println! scattered in a loop. It is a deliberate, switchable view of one layer. Every layer you build gets one:

LayerInstrumentationEnabled by
Raw transportHex dump of every byte read/written, with direction and timestamp--debug-bytes
UTF-8 decodingEvery decode event: bytes consumed → codepoint, or an error--debug-utf8
ParserState transitions: (state, byte) → state, action--debug-parser
ActionsThe parsed action stream in human-readable form--debug-actions
CursorEvery cursor movement with its cause--debug-cursor
ScreenDirty rows per frame; a full snapshot on demand--debug-damage, SIGUSR1 → snapshot
ModesEvery mode set/reset with its DEC number and name--debug-modes
ProcessEvery spawn, signal, and exit--debug-proc

Design rules for instrumentation, learned the hard way:

  1. It must not go to the same terminal you are debugging. Write it to a file, or to fd 2 when fd 2 is redirected, or to a separate socket. A debug log that scribbles on the screen you are debugging is worse than nothing.
  2. It must be switchable at runtime, not by recompiling. Use an env var or a flag.
  3. It must be cheap when off. A if self.debug check, not a formatted string that is discarded.
  4. It must be replayable. Anything you can record, you can replay without a shell. That is what the debugger is for.

What "One Experiment" Means Here

An experiment has four parts, and it is not an experiment without all four:

CLAIM        A falsifiable statement about the system.
             "Ctrl+C is delivered to the foreground process group, not the session leader."

METHOD       The exact commands, in order, that would show it.
             "Run `sleep 100`, note TPGID from `ps`, press ^C, observe which process died."

PREDICTION   What you expect to observe, written before you run it.

RESULT       What you observed, and — if it differs — which belief was wrong.

Every experiment in this book is written in that shape. When you invent your own, keep it. The habit transfers: this is how you will debug a terminal bug in production five years from now.


What "One Test" Means Here

A test in this curriculum is focused: it asserts one behavior, it names the escape sequence or syscall it is about, and it fails informatively.

#![allow(unused)]
fn main() {
#[test]
fn cursor_forward_stops_at_last_column() {
    // CSI 999 C  — Cursor Forward by 999 in an 80-column terminal.
    // Per ECMA-48 the cursor clamps at the last column; it must NOT wrap.
    let mut term = Terminal::new(24, 80);
    term.advance(b"\x1b[999C");
    assert_eq!(term.cursor().col, 79, "CUF must clamp to the last column");
}
}

The four properties every test here has:

  1. It names the sequence in a comment, with the raw bytes. Six months later \x1b[999C is unreadable; CSI 999 C — Cursor Forward is not.
  2. It asserts one thing. A test that checks the cursor and the cell contents and the style tells you nothing when it fails.
  3. Its failure message says what the rule is, not just what the numbers were.
  4. It survives a refactor because it tests behavior through the public API, not internals.

For every escape sequence you implement, the book asks for six things:

  1. The raw bytes.
  2. How the parser recognizes it (which state, which transition).
  3. Which state transition occurs in the terminal.
  4. The screen before and after.
  5. A focused unit test.
  6. A shell command that generates the sequence, so you can see a real terminal do it.

That sixth item is the one people skip, and it is the one that catches misunderstandings. If you cannot produce a sequence with a shell command, you may not understand when a real program emits it.


What "One Challenge Extension" Means Here

Each lab ends with something past the lesson: a feature the lab did not need, a performance problem the naïve version has, or a correctness edge the happy path avoided. Challenges are optional but they are where the depth compounds. A representative sample:

  • Make the parser zero-allocation on the hot path, and prove it with a benchmark.
  • Handle a PTY master that becomes writable-blocked because the child is not reading.
  • Implement DECRQM so a program can query which modes you support.
  • Make resize reflow wrapped lines instead of truncating them. (This is genuinely hard. That is the point.)

Common Failure Modes of Learners (Not of Code)

Failure modeSymptomCorrection
Reaching for a crate too earlyportable-pty in Cargo.toml at Milestone 2You skipped the whole point of Section 1. Delete it, write the raw version, then add it back and diff.
Building three layers before running any"It compiles" as a status reportNothing is done until you have observed it. Run the smallest version first.
Debugging by guessingRandom edits, recompilesAdd instrumentation to the layer you suspect. If you cannot name the layer, that is the real problem.
Skipping the experiments"I understood it from the text"You did not. The text is the hypothesis; the experiment is the evidence.
Treating the emulator and the mux as the same thingA Pane that owns a fontRe-read the mental model. Draw the boundary again.
Testing only the happy pathWorks with echo, breaks with vimEvery escape-sequence test needs a boundary case: zero parameters, huge parameters, at the edge of the screen.
Reading Section 5 firstBeautiful crate names, no understandingThe boundaries are conclusions, not premises.

How to Ask Yourself a Debugging Question

When something is wrong, ask in this order. This ordering is the single most valuable transferable skill in the curriculum.

1. WHICH LAYER?
   Is the wrong thing in the bytes, or in the interpretation of the bytes?
   → Hex-dump the transport. If the bytes are wrong, the bug is upstream of the parser.

2. WHICH SIDE OF THE KERNEL?
   Is this the line discipline or my code?
   → `stty -a` on the relevant tty. Compare with a known-good terminal.

3. WHICH PROCESS?
   Who is in the foreground process group? Who has the controlling terminal?
   → `ps -o pid,pgid,sid,tpgid,stat,tty,comm`

4. WHICH DIRECTION?
   Input path or output path? They fail differently and have different owners.

5. IS IT MY BUG OR A MISSING FEATURE?
   → Run the same thing in xterm/Ghostty/tmux. If it works there, you are missing a capability.
     Name the capability before you write code.

Validation / Self-check

  1. Name the ten steps of the phase template, and give a failure that results from skipping each.
  2. What are the four parts of an experiment, and which one do people skip?
  3. Why must a prediction be written rather than held in your head?
  4. Name the six things this book asks for with every escape sequence you implement.
  5. What are the four design rules for instrumentation, and which one is violated by a println! in your render loop?
  6. Give the five-question debugging order, and apply it to this bug: "my colors are wrong in ls but right in vim."

Next: Section 1 — The PTY Laboratory.