// 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\ninto\r\n(that'sONLCR, 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 twinf): 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;999His 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 followESC[2JwithESC[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):0reset brush to defaults,1bold on,4underline on,7reverse on.30โ37fg = pโ30;39fg = default.40โ47bg = pโ40;49bg = default.38/48are the extended-color introducers. We won't store 256/truecolor in our one-byte cells, but you must skip their arguments correctly โ38;5;nconsumes two extra parameters,38;2;r;g;bconsumes four โ or you'll misread whatever follows them in the same sequence. (ESC[38;5;208;1mends 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 (
privflag set):?25h/?25lshow/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 ptsThe 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:7save cursor,8restore (restoring with nothing saved: no-op),cfull 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.