// lesson: interpret

Interpreting the Stream โ€” Terminal Semantics

You have a parser that turns bytes into events, and a screen that mutates cells. This lesson is the join: what each event means. This is where your program stops being a collection of parts and becomes a terminal emulator โ€” the component that could sit on a pty master, watch a real program's output, and know what the screen should look like.

Precision matters here more than anywhere else in the course. Every program that draws to a terminal is betting on these exact semantics; be one column off in tab handling and ls -l output leans like a tower in Pisa.

Control characters (the CTRL events)

  • CR \r โ€” cursor to column 0. Nothing else. Doesn't erase.
  • LF \n โ€” cursor down one row, column unchanged. This is the one everyone gets wrong, because the canonical-mode tty driver spent your whole life quietly turning \n into \r\n (that's ONLCR, which raw mode disabled). At the bottom row, LF scrolls โ€” but scrolling is the next lesson; for now LF clamps at the bottom.
  • BS \b โ€” cursor left one, clamped at column 0. Doesn't erase! Programs erase by printing \b \b โ€” left, space over it, left again. (This is also how progress bars rewrite themselves; that, and \r + reprint.)
  • TAB \t โ€” cursor right to the next tab stop: the next column that's a multiple of 8, clamped to the last column. Columns 0โ†’8โ†’16โ†’โ€ฆ. A tab at column 8 goes to 16, not nowhere.
  • BEL \a โ€” ring the bell. We ignore it with a clear conscience.

CSI commands

The dispatch is on the final byte; the parameters were pre-chewed by your parser. Remember the two defaulting rules: a missing parameter is 0, and for the movement/positioning commands both 0 and absent mean the default, which is 1.

  • CUP โ€” H (and its twin f): cursor to (row, col), 1-based on the wire, so subtract 1 for your 0-based grid. Out-of-range values clamp to the edges (ESC[999;999H is the standard "go to bottom-right corner" idiom โ€” real programs rely on the clamp!).
  • CUU/CUD/CUF/CUB โ€” A/B/C/D: relative moves of max(1, p) rows/columns, clamped at the edges.
  • ED โ€” J: erase in display. p=0: cursor to end of screen (inclusive); p=1: start of screen to cursor (inclusive); p=2: everything. The cursor does not move โ€” programs invariably follow ESC[2J with ESC[H, and if your emulator moves the cursor on its own, doubly-moved cursors paint chaos. Erased cells become blanks with default style.
  • EL โ€” K: erase in line, same three modes, same "cursor stays put", same blank-with-default result.
  • SGR โ€” m: mutate the brush. Walk the parameter list (an empty list acts like a single 0):
    • 0 reset brush to defaults, 1 bold on, 4 underline on, 7 reverse on.
    • 30โ€“37 fg = pโˆ’30; 39 fg = default. 40โ€“47 bg = pโˆ’40; 49 bg = default.
    • 38 / 48 are the extended-color introducers. We won't store 256/truecolor in our one-byte cells, but you must skip their arguments correctly โ€” 38;5;n consumes two extra parameters, 38;2;r;g;b consumes four โ€” or you'll misread whatever follows them in the same sequence. (ESC[38;5;208;1m ends with a perfectly good bold you'd otherwise eat.)
    • Anything else: ignore, move on. Unknown SGR codes arrive constantly from the wild; a terminal that trips on them is a toy.
  • Private modes (priv flag set): ?25h/?25l show/hide cursor โ€” track it in a flag; the renderer will care. Everything else private: ignore politely.
  • Unknown finals: ignore. Same reasoning as unknown SGR.

Simple ESC commands

ESC 7 saves the cursor position (and, in real terminals, the brush); ESC 8 restores it. One saved slot, overwritten by each ESC 7. Full-screen programs bracket temporary excursions with these. ESC c is "reset to initial state" โ€” clear everything, home the cursor, default brush. Others: ignore.

The shape of the join

void term_apply(struct term *t, const struct vt_event *ev);

One function, one switch on ev->type, with a nested switch on ev->final for CSI. All the grid mechanics you already built; this layer is pure policy. Keep it boring: every branch a few lines, every default explicit. Boring code is what correctness looks like at this altitude.

(A design footnote worth absorbing: notice we do not wire the parser to the screen with callbacks or function pointers. Events are plain values; the caller feeds bytes to one object and hands the results to another. In C, dumb data flowing between dumb components beats architecture every time.)

โ€บ Applying Events to the Grid

35 pts

The starter provides the term struct (a cell grid with cursor, brush, saved-cursor slot, and a cursor-visibility flag) plus its init/free/accessor โ€” deliberately minimal so this challenge stands alone; substituting your own screen code afterwards is encouraged. The event struct is exactly your VT parser's.

You implement term_apply with the lesson's semantics:

  • VT_PRINT: stamp the glyph with the brush at the cursor; advance with wrap-at-right-edge; clamp at the bottom row (row stays, the wrap still returns the cursor to column 0).
  • VT_CTRL: \r, \n, \b, \t (tab stops every 8), ignore the rest.
  • VT_CSI: H f A B C D J K m, private 25 h/l, per the lesson.
  • VT_ESC: 7 save cursor, 8 restore (restoring with nothing saved: no-op), c full reset; ignore others.

The tests drive term_apply with hand-built event sequences and check the resulting grid โ€” including the classic traps: LF keeping its column, ED leaving the cursor alone, ESC[999;999H clamping, 38;5;208 not eating a following bold, and ESC[m acting as reset.

Log in to submit a solution and earn points.