Skip to content

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.

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.

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 SIGINT signal 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)

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.

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.

The terminal grid has a fixed size in columns × rows. You can query it in Node.js:

const cols = process.stdout.columns; // e.g. 220
const rows = process.stdout.rows; // e.g. 50

When 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.

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.

  • 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.