The full stack of terminals explained
The full stack of terminals explained
This piece covers the full stack: from the vocabulary that trips people up, all the way down to building a TUI (Text User Interface) app from scratch. If youβve ever wondered what vim is actually doing when it takes over your screen, or why Raw Mode exists, or what ANSI escape sequences are, this is the piece.
The Vocabulary Problem
They used to be the same thing. This is the key insight. All four words originally described the same physical object. In the 1960s, you interacted with a computer by sitting at a machine with a keyboard and a printer. That machine had three names depending on who was talking about it:
ββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββ
β β $ hello world_
β
laptop --> SSH --> internet --> server
β
ββββββ΄ββββ
β server
ββββββ¬ββββ
β
Console access (the real deal):
You --> keyboard ββββββββββββββββββββββ> monitor
The Line Discipline
SIGINT (kill process)
Ctrl+Z --> SIGTSTP (suspend process)
Ctrl+D --> EOF (end of input)
4. Character conversion
Newline code conversion (CR LF)
5. Input buffering
Canonical mode: buffer until Enter
Non-canonical: pass through immediately
ββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββΌβββββββββββ
β PTY (subsidiary)
ββββββββββ¬βββββββββββ
β
ββββββββββΌβββββββββββ
β Shell / TUI App
βββββββββββββββββββββ
This is important to understand because TUI apps work by disabling most of these line discipline features. When you run vim, it tells the kernel: βstop doing line editing, stop echoing, stop interpreting Ctrl+C as a signal. Just give me the raw bytes.β
termios
termios is the POSIX standard terminal control structure. Itβs how you talk to the line discipline. It manages:
termios structure
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β c_iflag (Input flags)
β Newline conversion, flow control, etc.
β
β c_oflag (Output flags)
β Output processing settings
β
β c_cflag (Control flags)
β Baud rate, character size, etc.
β
β c_lflag (Local flags)
β Echo, canonical mode, signal generation, etc.
β
β c_cc (Special characters)
β Definitions for Ctrl+C, Ctrl+Z, EOF, etc.
β
β VMIN / VTIME (Timeout)
β Read control in non-canonical mode
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
In TUI development, termios is used to implement Raw Mode.
ioctl is a general-purpose device control system call. Through a file descriptor, it instructs device drivers to perform special operations that canβt be done with normal read/write. Main operations include getting and setting termios, and getting window size.
tcgetattr / tcsetattr
High-level API functions for terminal control defined in the POSIX standard. They let you write more portable code than using ioctl directly.
In Node.js, you donβt need to call these directly. The high-level equivalent is built in:
// High-level: Node.js handles termios internally
process.stdin.setRawMode(true); // Equivalent to MakeRaw / tcsetattr
process.stdin.setRawMode(false); // Equivalent to Restore / tcsetattr
For low-level access to termios from JavaScript, youβd use a native addon or FFI. More on this in the implementation section.
The Full I/O Flow
Hereβs everything connected, from your fingers to the application and back:
ββββββββ ββββββββββββββββββββββββ
β User βββββ>β Terminal Emulator β
β β β (iTerm2, xterm, etc.) β
ββββββββ ββββββββββββ¬ββββββββββββ
β
ββββββββββΌββββββββββββ
β PTY β
β (manager subsid.) β
ββββββββββ¬ββββββββββββ
β
ββββββββββΌββββββββββββ
β TTY + Line Disciplineβ
β (ICANON/ECHO/ISIG β
β flags) β
ββββββββββ¬ββββββββββββ
β
ββββββββββΌββββββββββββ
β TUI App β
β (vim, htop) β
ββββββββββ¬ββββββββββββ
β
ββββββββββΌβββββββββββββ
βΌ βΌ βΌ
termios Output (ANSI Rendering
changes escape seqs) on screen
(Raw back to via Terminal
Mode, Terminal Emulator
etc.)
Part 3: Terminal Behavior for TUI Development
TUI apps donβt interpret commands like shells do. Instead, they directly handle terminal I/O control. They use the termios API to change terminal mode settings and ANSI escape sequences to render and update the screen.
In a normal shell environment, the terminal is set to canonical mode (ICANON), where input is buffered line by line and passed to the program after you press Enter:
$ stty -a | grep icanon
# icanon isig iexten echo echoe echok echoke -echonl echoctl
# ^ Canonical mode is ON
Characters you type are automatically echoed to the screen and sent to the shell when Enter is pressed:
$ ls example.txt
But TUI apps like vim or less switch the terminal to non-canonical mode. Key input is passed to the application immediately, character by character, and the application handles its own rendering:
$ vim
# In another terminal, check vim's terminal mode:
$ ps aux | grep vim # find vim's terminal
$ stty -a
Raw Mode
β
β 2. Input Processing
β Parsing keys, escape sequences, Ctrl+
β
β 3. Screen Control
β ANSI escape sequences for rendering
β
β 4. Terminal Size Management
β Detecting and responding to resizes
β
β 5. Buffering
β Batch writes to prevent flicker
β
ββββββββββββββββββββββββββββββββββββββββββββββ
Letβs go through each one.
1. Terminal Mode Settings
The line discipline has three operating modes. You switch between them by setting flags in the termios structure.
Canonical Mode (Cooked Mode)
This is the default. Itβs what your shell uses. Input is buffered line by line, line editing works (Backspace, Ctrl+U, Ctrl+W), echo is on, and special characters like Ctrl+C generate signals.
Non-Canonical Mode
Canonical mode disabled (ICANON off). Input arrives character by character instead of line by line. Line editing is off. But echo and signal processing can optionally stay enabled.
Raw Mode
Almost everything disabled. No echo, no signals, no newline conversion. Input is passed to the application as a raw byte stream. This is what TUI apps use.
| Aspect | Canonical (Cooked) | Non-Canonical | Raw |
|---|---|---|---|
| Input buffering | Line-based | Char-based | Char-based |
| Line editing | Enabled | Disabled | Disabled |
| Echo back | Enabled | Configurable | Disabled |
| Signals (Ctrl+C) | Enabled | Configurable | Disabled |
| Newline conv. | Enabled | Configurable | Disabled |
| Main uses | Shell, interactive | Custom CLI | TUI, games |
sttyβs -cooked is synonymous with raw. Raw Mode is the opposite of Cooked Mode.
2. Input Processing
Input processing means figuring out βwhat key was pressed.β Normal characters are simple, but arrow keys, function keys, and mouse events are sent as multi-byte escape sequences. Your TUI app needs to parse these.
Special Key Escape Sequences
| Key | Sequence | Byte Sequence |
|---|---|---|
| Up (β) | ESC[A | \x1b[A |
| Down (β) | ESC[B | \x1b[B |
| Right (β) | ESC[C | \x1b[C |
| Left (β) | ESC[D | \x1b[D |
| Home | ESC[H | \x1b[H |
| End | ESC[F | \x1b[F |
| Page Up | ESC[5~ | \x1b[5~ |
| Page Down | ESC[6~ | \x1b[6~ |
| F1-F4 | ESC[OP-OS | \x1b[OP, etc. |
Control Characters
| Character | ASCII | Description |
|---|---|---|
| Ctrl+C | 3 | SIGINT |
| Ctrl+D | 4 | EOF |
| Ctrl+Z | 26 | SIGTSTP |
| Enter | 13 / 10 | CR / LF |
| Tab | 9 | Tab |
| Backspace | 127 / 8 | DEL / BS |
| ESC | 27 | Escape |
3. Screen Control (ANSI Escape Sequences)
Screen control means telling the terminal βwhat to display where.β ANSI escape sequences are special string commands that begin with the ESC character (\x1b or \033). They were standardized as ANSI X3.64 and implemented in VT100 terminals, which is why theyβre everywhere.
Cursor Control
| Sequence | Description |
|---|---|
ESC[H |
Move to home position (1,1) |
ESC[{row};{col}H |
Move to position (1-indexed) |
ESC[{n}A |
Move n rows up |
ESC[{n}B |
Move n rows down |
ESC[{n}C |
Move n columns right |
ESC[{n}D |
Move n columns left |
ESC[s |
Save cursor position |
ESC[u |
Restore cursor position |
ESC[?25l |
Hide cursor |
ESC[?25h |
Show cursor |
Screen Clearing
| Sequence | Description |
|---|---|
ESC[2J |
Clear entire screen |
ESC[H |
Move cursor to home |
ESC[K |
Clear from cursor to end of line |
ESC[1K |
Clear from start of line to cur. |
ESC[2K |
Clear entire line |
Basic Styles
| Sequence | Description |
|---|---|
ESC[0m |
Reset all |
ESC[1m |
Bold |
ESC[4m |
Underline |
ESC[7m |
Reverse |
Foreground Colors (Text)
| Sequence | Color |
|---|---|
ESC[30m |
Black |
ESC[31m |
Red |
ESC[32m |
Green |
ESC[33m |
Yellow |
ESC[34m |
Blue |
ESC[35m |
Magenta |
ESC[36m |
Cyan |
ESC[37m |
White |
Background Colors
| Sequence | Color |
|---|---|
ESC[40m |
Black |
ESC[41m |
Red |
ESC[42m |
Green |
ESC[43m |
Yellow |
ESC[44m |
Blue |
ESC[45m |
Magenta |
ESC[46m |
Cyan |
ESC[47m |
White |
Extended Color Modes
| Sequence | Description |
|---|---|
ESC[38;5;{n}m |
Foreground (n: 0-255) |
ESC[48;5;{n}m |
Background (n: 0-255) |
ESC[38;2;{r};{g};{b}m |
Foreground (RGB True Color) |
ESC[48;2;{r};{g};{b}m |
Background (RGB True Color) |
Alternate Screen Buffer
| Sequence | Description |
|---|---|
ESC[?1049h |
Switch to alternate screen buffer (used by vim, less, etc.) |
ESC[?1049l |
Return to normal screen buffer |
This is why vim can take over your entire screen and then your terminal looks normal after you quit. It switches to an alternate buffer on startup and switches back on exit.
4. Terminal Size Management
TUI apps need to render according to terminal size, and they need to redraw when users resize the window. Hereβs how that works:
User resizes window
β
βΌ
Terminal Emulator
β
β ioctl(TIOCSWINSZ) -- notify new rows/cols
βΌ
PTY (master --> subsidiary)
β
β Update winsize in kernel
βΌ
Kernel TTY Layer
β
β Send SIGWINCH signal
βΌ
Process (bash, vim, less)
β
β Receive signal
β Call ioctl(TIOCGWINSZ) to get new size
β Redraw
βΌ
Updated screen
Terminal size is managed by the winsize structure held in the kernelβs TTY structure. When the size changes, the terminal emulator notifies via ioctl(TIOCSWINSZ), and the kernel sends SIGWINCH to connected processes.
5. Buffering
When youβre sending many control sequences, sending them one by one is slow and causes visible flicker. By buffering all your writes and flushing them in one batch, you get smooth, flicker-free rendering. Every serious TUI app does this.
Part 4: Building a TUI in JavaScript/TypeScript
Now letβs actually build one. Weβll implement all five elements using Node.js.
High-Level Implementation
This uses Node.jsβs built-in process.stdin.setRawMode(). It abstracts away termios details.
// Structure to manage terminal state
interface Terminal {
width: number;
height: number;
buffer: string;
}
// βββββββββββββββββββββββββββββββββββββββββββββ
// 1. Terminal Mode Settings
// βββββββββββββββββββββββββββββββββββββββββββββ
function initTerminal(): Terminal {
// Set to Raw Mode (equivalent to term.MakeRaw in Go)
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding("utf8");
const { columns, rows } = process.stdout;
return {
width: columns || 80,
height: rows || 24,
buffer: "",
};
}
function restoreTerminal(): void {
write("\x1b[?25h"); // Show cursor
write("\x1b[0m"); // Reset color
flush();
process.stdin.setRawMode(false);
}
// βββββββββββββββββββββββββββββββββββββββββββββ
// 4. Terminal Size Management
// βββββββββββββββββββββββββββββββββββββββββββββ
function getTerminalSize(): { width: number; height: number } {
return {
width: process.stdout.columns || 80,
height: process.stdout.rows || 24,
};
}
// βββββββββββββββββββββββββββββββββββββββββββββ
// 2. Input Processing
// βββββββββββββββββββββββββββββββββββββββββββββ
interface KeyEvent {
char: string | null;
key: string | null;
}
function parseKey(data: string): KeyEvent {
// Ctrl+C
if (data === "\x03") {
return { char: null, key: "CTRL_C" };
}
// Escape sequences (arrow keys, etc.)
if (data === "\x1b[A") return { char: null, key: "UP" };
if (data === "\x1b[B") return { char: null, key: "DOWN" };
if (data === "\x1b[C") return { char: null, key: "RIGHT" };
if (data === "\x1b[D") return { char: null, key: "LEFT" };
if (data === "\x1b[H") return { char: null, key: "HOME" };
if (data === "\x1b[F") return { char: null, key: "END" };
if (data === "\x1b[5~") return { char: null, key: "PAGE_UP" };
if (data === "\x1b[6~") return { char: null, key: "PAGE_DOWN" };
// Bare ESC
if (data === "\x1b") return { char: null, key: "ESC" };
// Normal character
return { char: data, key: null };
}
// βββββββββββββββββββββββββββββββββββββββββββββ
// 3. Screen Control (ANSI Escape Sequences)
// βββββββββββββββββββββββββββββββββββββββββββββ
let outputBuffer = "";
function bufferWrite(s: string): void {
outputBuffer += s;
}
function clear(): void {
bufferWrite("\x1b[2J\x1b[H");
}
function moveTo(row: number, col: number): void {
bufferWrite(`\x1b[${row};${col}H`);
}
function setColor(fg: number): void {
bufferWrite(`\x1b[${fg}m`);
}
function writeText(s: string): void {
bufferWrite(s);
}
// βββββββββββββββββββββββββββββββββββββββββββββ
// 5. Buffering
// βββββββββββββββββββββββββββββββββββββββββββββ
Comments
No comments yet. Start the discussion.