1.2 How Terminals Work
Before writing a single line of TUI code, you need a mental model of what a terminal is. This knowledge will make everything else — raw mode, escape codes, input handling — feel logical rather than magical.
The Terminal Is Not the Shell
Section titled “The Terminal Is Not the Shell”These are three separate things:
┌──────────────────────┐│ Terminal Emulator │ e.g. Konsole, Alacritty, Windows Terminal│ (the window) │ Renders pixels. Handles fonts, colors.└──────────┬───────────┘ │ PTY (pseudo-terminal)┌──────────▼───────────┐│ Shell │ e.g. bash, zsh, fish│ (the program) │ Interprets commands, manages jobs.└──────────┬───────────┘ │ stdin / stdout / stderr┌──────────▼───────────┐│ Your Program │ The TUI app you're writing└──────────────────────┘Terminal emulator — a graphical application that renders a grid of characters. It converts keyboard presses into bytes and renders bytes as characters. It knows nothing about your program.
PTY (Pseudo-Terminal) — a kernel-level pair of file descriptors that act like a physical serial terminal. One end connects to the terminal emulator, the other to the shell/program. It handles line discipline (see below).
Shell — a program that reads commands and runs them. When you type npx tsx app.ts, the shell forks a child process for your app. Your app inherits the shell’s stdin/stdout — which are connected to the PTY.
Line Discipline: Cooked Mode
Section titled “Line Discipline: Cooked Mode”By default, the PTY operates in cooked mode (also called canonical mode). In this mode:
- Keypresses are buffered — the kernel holds them until you press Enter
- Backspace is processed by the kernel — it erases the last character in the buffer
- Ctrl+C sends a
SIGINTsignal to kill the process - The kernel echoes your keystrokes back to the terminal so you see what you type
This is perfect for a shell. You type a command, edit it freely, press Enter, and the finished line is delivered to the program.
You type: h e l l <backspace> o <enter>Kernel sees: h e l o (after processing backspace)Program receives: "helo\n" (only after Enter)Raw Mode
Section titled “Raw Mode”A TUI cannot work in cooked mode. If you type j to move a cursor, the character must arrive at your program immediately — not after Enter. And it must not be echoed to screen, because your rendering code handles what appears on screen.
Raw mode disables all that kernel processing:
- Bytes arrive immediately as typed
- No echoing — your program controls what appears on screen
- No special handling of Ctrl+C (you have to handle it yourself — or your program will become unkillable without
kill -9) - No line buffering
In Node.js, you enable raw mode with:
process.stdin.setRawMode(true);We will use this in Module 2. For now, just know: cooked mode = comfortable shell, raw mode = full control for TUI.
stdout: A Stream of Bytes
Section titled “stdout: A Stream of Bytes”Your program writes to stdout. The terminal reads stdout and renders characters. That’s it — there is no magic drawing API.
The terminal emulator maintains an internal grid — a 2D array of cells, each holding a character and style attributes (color, bold, underline). When it reads a byte from stdout:
- If it’s a printable character (like
A), it places it at the current cursor position and advances the cursor - If it’s an escape sequence (starts with
ESC,\x1b, or\033), it executes a command: move cursor, set color, clear screen, etc.
So a TUI works by sending a carefully crafted sequence of escape sequences and characters that tell the terminal emulator exactly what to draw and where.
Terminal Size
Section titled “Terminal Size”The terminal grid has a fixed size in columns × rows. You can query it in Node.js:
const cols = process.stdout.columns; // e.g. 220const rows = process.stdout.rows; // e.g. 50When the user resizes the terminal window, the OS sends a SIGWINCH signal to the foreground process:
process.stdout.on('resize', () => { const cols = process.stdout.columns; const rows = process.stdout.rows; // re-render everything});Your TUI must listen for this and re-draw. A program that doesn’t handle resize looks broken when the window is resized.
The Rendering Mental Model
Section titled “The Rendering Mental Model”Think of the terminal as a projector showing a grid of characters. Your program is a director sending instructions:
"Move projector to column 5, row 3""Set text color to green""Write: Hello""Move to column 5, row 4""Set text color to default""Write: World"Each instruction is an ANSI escape sequence. The next lesson covers exactly what those look like.
Key Takeaways
Section titled “Key Takeaways”- A terminal emulator renders pixels; a PTY connects it to your program; your program writes bytes.
- Cooked mode buffers input until Enter; raw mode delivers every keystroke immediately.
- stdout is just bytes — escape sequences are commands disguised as characters.
- The terminal grid has columns × rows; your program must handle resize.