The Hitchhiker's Guide to Terminals, Teletypes & the Unix TTY
Nothing about terminals makes sense until you know why they are like this. Every strange behavior —
Enter sending 0x0D, Backspace sending 0x7F, ^S freezing your screen, the existence of SIGHUP —
is a fossil. This chapter is the fossil record.
Read it once for the story. Come back to it when a behavior seems arbitrary, because it never is.
1868–1960: The Teletypewriter
A teletypewriter ("teletype", "TTY") was an electromechanical typewriter connected to a wire. You typed; the machine punched a paper tape and sent the characters as electrical pulses. At the other end, an identical machine typed them onto paper. Two machines, one wire, no computer.
The consequences that survive to this day:
| Teletype fact | Modern fossil |
|---|---|
| The carriage physically moved right as it printed | CR (0x0D) means "return the carriage to column 0" |
| The platen rolled the paper up one line | LF (0x0A) means "feed one line" — it does not return the carriage |
| A newline needed both motions | \r\n, and ONLCR to insert the \r for programs that only send \n |
| The RETURN key returned the carriage | Enter sends CR, not LF. Still. Today. |
| A mistake could not be un-printed | You overstruck: X BS X for bold, _ BS X for underline — man still does this |
| Punched tape: a hole meant 1 | DEL is 0x7F — all seven bits punched, i.e. "ignore this, I punched over it". That is why Backspace sends DEL. |
| Mechanical parts needed time to move | Padding characters and baud-rate delays, still in terminfo as pad |
| A bell rang to get the operator's attention | BEL (0x07) |
Note: That
DEL = 0x7Frow is the most satisfying fact in this chapter. On punched tape you could not remove a hole, only add one. Punching all holes meant "this character is void." The key that erases has sent "all holes punched" for over a century, and your terminal emulator still receives0x7Fwhen you press Backspace.
1960s: ASCII and the C0 Controls
ASCII (1963) fixed the character set and reserved 0x00–0x1F for control characters — codes
that command the device rather than print. The layout was not arbitrary: control characters were
generated by holding a CTRL key, which cleared bit 6.
'A' = 0x41 = 100 0001
Ctrl+A = 000 0001 = 0x01 ← bit 6 cleared
'M' = 0x4D = 100 1101
Ctrl+M = 000 1101 = 0x0D = CR ← Ctrl+M IS Enter
'[' = 0x5B = 101 1011
Ctrl+[ = 001 1011 = 0x1B = ESC ← Ctrl+[ IS Escape
This is why Ctrl + char = char & 0x1F, and why Ctrl+M, Ctrl+I, and Ctrl+[ are the same bytes as
Enter, Tab, and Escape — not similar, identical. A program cannot distinguish them, which is why
vim cannot bind Ctrl+M separately from Enter. Sixty years later, this is still true.
The control characters that mattered, and still do:
| Code | Name | Then | Now |
|---|---|---|---|
0x03 | ETX (end of text) | "I am done transmitting" | Ctrl+C → SIGINT |
0x04 | EOT (end of transmission) | "hang up the line" | Ctrl+D → EOF |
0x07 | BEL | ring the bell | terminal bell / visual flash |
0x08 | BS | move the carriage left one | cursor left; does not erase |
0x09 | HT | advance to a mechanical tab stop | next tab stop |
0x11/0x13 | DC1/DC3 | start/stop the paper tape reader | ^Q/^S flow control — why ^S freezes your terminal |
0x1A | SUB | "substitute for a garbled character" | Ctrl+Z → SIGTSTP (Unix repurposed it) |
0x1B | ESC | "the next characters are commands, not text" | the escape sequence introducer |
That last row is the whole edifice. ESC meaning "what follows is a command" is the seed from which
every escape sequence grew.
1969–1975: Unix Inherits the Wire
Unix was written on a PDP-7 and then a PDP-11 with teletypes attached. The kernel needed code to manage the serial line: buffer input, echo it back so the human could see what they typed, handle the correction characters, and turn "the human is panicking" into something a program could notice.
That code is the line discipline, and it has barely changed in concept since.
┌──────────┐ RS-232 ┌──────────────────────────────────────┐
│ Teletype │◀───────────▶│ PDP-11 running Unix │
│ or VT52 │ bytes │ ┌────────────────────────────────┐ │
└──────────┘ │ │ tty driver (device-specific) │ │
│ ├────────────────────────────────┤ │
│ │ LINE DISCIPLINE │ │
│ │ • echo │ │
│ │ • line buffering (canonical) │ │
│ │ • ^C → signal │ │
│ │ • CR → NL translation │ │
│ └────────────────────────────────┘ │
│ ┌────────────────────────────────┐ │
│ │ getty → login → shell │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
Three decisions from this era that you inherit whole:
1. Echo is the kernel's job. On a printing teletype, the terminal could not echo — it had no memory and no way to know what the computer would accept. So the computer echoed. That decision survived into every terminal since, which is why — right now, in your terminal — the characters you see as you type are being written by the kernel, not by your shell and not by your terminal emulator.
2. Line buffering is the kernel's job. A human at a printing terminal makes typos and needs to
correct them. Every program would otherwise have to implement backspace. So the kernel buffers a
line, handles VERASE and VKILL, and hands the program a finished line. That is canonical
mode, and it is still the default.
3. ^C must work even when the program is not reading. A runaway program will not read your
"please stop." So the kernel intercepts the byte and converts it to a signal — an asynchronous
interrupt the program cannot ignore by not reading. This is why ISIG exists, why signals go to a
process group rather than a process, and ultimately why job control exists at all.
The hangup
Terminals were connected over modems. When the modem dropped the carrier — the user hung up —
the kernel had to tell everything attached that its human was gone. That signal is SIGHUP,
literally "hangup."
Today there is no modem, but the semantics are preserved exactly: when the last file descriptor to
the master end of a PTY closes, the kernel sends SIGHUP to the session leader. Closing a terminal
window is emulating a modem hanging up in 1975. Everything about nohup, disown, and the existence
of tmux follows from that one line.
1978: The DEC VT100 and the Birth of Escape Sequences
Printing terminals gave way to video terminals with a cathode ray tube. Now the terminal had a screen — a two-dimensional addressable surface — and a program needed a way to say "put the cursor at row 5, column 20."
The answer was to extend the ESC convention into a grammar. DEC's VT100 (1978) implemented
ANSI X3.64 (later ECMA-48), and because the VT100 sold enormously, its sequences became the de-facto
standard.
ESC [ 5 ; 20 H move the cursor to row 5, column 20
───┬─── ──┬── ┬
│ │ └── FINAL BYTE: which command
│ └────── PARAMETERS: semicolon-separated numbers
└───────────── CSI: Control Sequence Introducer (ESC [)
This is the structure you implement in the parser chapter. It has not changed.
What the VT100 established
| Feature | Sequence | Still with us |
|---|---|---|
| Cursor addressing | CSI r ; c H | Yes, unchanged |
| Erase | CSI J, CSI K | Yes |
| Scroll regions | CSI t ; b r | Yes — how vim keeps a status line |
| Character attributes | CSI Ps m (SGR) | Yes — now carrying 24-bit color |
| Private modes | CSI ? Ps h | Yes — the ? marks a DEC extension |
| Application cursor keys | CSI ? 1 h | Yes — why arrows differ in vim |
| Alternate character set | ESC ( 0 | Yes — box drawing before Unicode |
| Device queries | CSI 6 n → CSI r ; c R | Yes — the terminal talks back |
Note: The
?private-parameter marker is worth understanding as a design idea. ANSI reserved0x3C–0x3Ffor vendor extensions, so DEC could add modes without colliding with the standard. Forty-five years later,CSI ? 2026 h(synchronized output, proposed in the 2020s) uses the same escape hatch. The extension mechanism outlived the company.
The pending-wrap fossil
The VT100 had a specific behavior: writing into the last column did not move the cursor to the next line. It set an internal flag, and the wrap happened when the next character arrived.
Why? Because otherwise a program printing exactly 80 characters followed by a newline would produce two line breaks — one from the auto-wrap, one from the newline — and every 80-column form would be double-spaced.
Every terminal since has replicated this, because programs depend on it. It is pending wrap, and omitting it is the most visible bug you can ship.
1983–1990: The Pseudo-Terminal
The wire disappeared. Two things killed it:
- Windowing systems. X11 (1984) wanted many terminal windows on one screen. There was no serial port behind them.
- Networks.
telnetand latersshwanted to give a remote user a shell. There was no serial port there either.
Both needed the same thing: a program that behaves like the hardware end of a serial line.
The pseudo-terminal is the kernel's answer. A pair: a slave that is indistinguishable from a
real tty (line discipline, termios, window size, controlling-terminal semantics, all of it), and a
master that a user-space program holds where the wire used to be.
1978 1990 →
┌────────┐ wire ┌────────┐ ┌──────────┐ ┌────────┐
│ VT100 │◀──────▶│ kernel │ │ xterm │◀──────▶│ kernel │
│ (metal)│ │ tty │ │(software)│ master │ PTY │
└────────┘ └───┬────┘ └──────────┘ └───┬────┘
│ slave │
┌───▼───┐ ┌──▼────┐
│ shell │ │ shell │
└───────┘ └───────┘
THE MASTER END REPLACES THE HARDWARE. That is the entire idea.
The shell cannot tell the difference, and that is the point: every program written for a VT100 works unmodified inside a GPU-accelerated terminal emulator written in 2026.
1984–2000: xterm and the Extension Explosion
xterm (1984, still maintained) implemented the VT100/VT220 sequences and then kept adding:
| Extension | Year (approx.) | What it enabled |
|---|---|---|
Mouse reporting (?1000) | 1980s | Clickable TUIs |
Window title (OSC 0) | 1980s | Titles in the window manager |
256 colors (38;5;n) | 1999 | The palette everything now assumes |
SGR mouse encoding (?1006) | 2000s | Terminals wider than 223 columns |
Bracketed paste (?2004) | 2000s | Editors distinguishing paste from typing |
24-bit color (38;2;r;g;b) | 2000s | Truecolor |
modifyOtherKeys | 2000s | Ctrl+Shift+A being expressible at all |
Because xterm was ubiquitous, TERM=xterm and later TERM=xterm-256color became what everything
claims to be — including terminals that implement a fraction of it. That is the compatibility
bargain you inherit in
Milestone 14: you claim
xterm, and you owe an honest list of what you do not implement.
Why terminfo exists
By 1980 there were hundreds of incompatible terminals. A program that wanted to clear the screen
could not simply emit CSI 2 J — an ADM-3A wanted something else entirely.
termcap (1978) and then terminfo (1981) solved it with a database: look up $TERM, ask for
the "clear screen" capability, emit whatever string it returns. ncurses is the library that made
this bearable.
infocmp # the full capability list for your $TERM
tput clear | xxd # what "clear screen" is on YOUR terminal
tput cup 5 20 | xxd # cursor addressing
echo $TERM
terminfo is why programs still work across terminals, and it is also why claiming
TERM=xterm-256color is a promise: programs will look up xterm's capabilities and emit them at you.
1987–Present: The Multiplexer
screen (1987) and tmux (2007) solved a problem the PTY created. If your terminal is a program,
then when that program dies — or your SSH connection drops — the kernel drops the carrier and
SIGHUP kills your shell and everything it was running.
The fix follows directly from the mechanism: keep the master fd open in a process that does not die.
Your terminal emulator The multiplexer server
owns PTY-A master owns PTY-B, C, D masters
│ │
you close the window the CLIENT exits
│ │
master closes NO master closes
│ │
SIGHUP → shell dies nothing happens at all
That is the whole trick, and it is Section 4. The corollary — that the server must contain a headless terminal emulator per pane, because someone has to parse the output of a program nobody is watching — is the insight most people miss.
2010–Present: The Modern Era
Three things changed at once.
1. Unicode won. UTF-8 became universal, and terminals had to reconcile a fixed grid with a character set containing zero-width combining marks, double-width CJK, and seven-codepoint family emoji. This is genuinely unsolved: there is no protocol-level way for a program to tell a terminal "I consider this one grapheme," which is why mode 2027 was proposed and why CJK text still misaligns in some stacks.
2. GPUs. kitty (2017) and alacritty (2017) rendered the cell grid on the GPU, and terminal
throughput stopped being a bottleneck. The architecture — a glyph atlas texture plus one instanced
quad per cell — is now standard.
3. The protocol started moving again. After two decades of stasis:
| Proposal | Solves |
|---|---|
Synchronized output (?2026) | Tearing during full-screen redraws |
| The kitty keyboard protocol | Key release events; the Escape ambiguity; unrepresentable combinations |
Grapheme clustering (?2027) | Terminal and program disagreeing about width |
OSC 8 hyperlinks | Clickable links without regex-guessing |
OSC 133 semantic prompts | The terminal knowing where a command's output begins |
| Sixel revival / kitty graphics | Images |
This is the most interesting time to work on terminals in thirty years, and it is why the capstone portfolio is full of things that do not exist yet.
The Layer Cake, Annotated with Dates
┌────────────────────────────────────────────────────────────────┐
│ YOUR PROGRAM vim (1991), htop (2004), your shell │
├────────────────────────────────────────────────────────────────┤
│ TERMINFO the capability database (1981) │
├────────────────────────────────────────────────────────────────┤
│ ESCAPE SEQUENCES ANSI X3.64 / ECMA-48 (1976) │
│ + DEC private modes (1978) │
│ + xterm extensions (1984-2020) │
│ + modern proposals (2020s) │
├────────────────────────────────────────────────────────────────┤
│ THE LINE DISCIPLINE echo, canonical mode, ^C (1970) │
│ termios API (1988) │
├────────────────────────────────────────────────────────────────┤
│ THE PTY master/slave pair (1983) │
├────────────────────────────────────────────────────────────────┤
│ SESSIONS & JOB CONTROL setsid, process groups (1980) │
├────────────────────────────────────────────────────────────────┤
│ THE CHARACTER SET ASCII (1963) → UTF-8 (1993) │
├────────────────────────────────────────────────────────────────┤
│ THE PHYSICAL LAYER RS-232 → nothing at all (1960) │
└────────────────────────────────────────────────────────────────┘
Every layer is still present. Not as legacy code to be removed — as load-bearing structure. Your terminal emulator in 2026 implements a 1978 protocol over a 1983 kernel abstraction driven by a 1970 line discipline, and if it did not, nothing would work.
Twelve "Why Is It Like This?" Answers
Keep these; they answer most of the questions people ask.
| Question | Answer |
|---|---|
Why does Enter send CR and not LF? | The RETURN key returned the carriage. ICRNL converts it for you. |
Why is Backspace 0x7F (DEL) and not 0x08 (BS)? | On punched tape, all-holes-punched meant "void." You cannot un-punch a hole. |
Why does \n alone stair-step in raw mode? | LF only feeds a line. ONLCR normally adds the CR; raw mode clears OPOST. |
Why does ^S freeze my terminal? | DC3 stopped the paper-tape reader. IXON still honors it. stty -ixon. |
Why is ^C a signal instead of a byte the program reads? | A runaway program is not reading. The kernel must be able to interrupt it anyway. |
| Why does closing a window kill my jobs? | SIGHUP — the modem hung up. This is why tmux exists. |
Why do arrow keys send different bytes in vim? | DECCKM (?1). The program sets it; the terminal obeys. |
Why does vim not pollute my scrollback? | The alternate screen (?1049) — a second buffer with no history. |
Why does man show bold as NNAAMMEE in a bad terminal? | Overstrike: X BS X, from printing terminals. Predates SGR. |
| Why does a full-width line not get a blank line after it? | Pending wrap — the VT100's deferred auto-wrap. |
Why does my terminal claim to be xterm-256color when it is not xterm? | terminfo keys on $TERM; xterm's entry is the lingua franca. Claiming it is a promise. |
| Why are there so many mouse-reporting modes? | X10's encoding put coordinates in single bytes, capping at 223 columns. ?1006 fixed it; the old ones stayed for compatibility. |
The Standards, and Which to Actually Read
Full citations in Primary Sources. The short version:
| Document | Read it? |
|---|---|
XTerm Control Sequences (ctlseqs.txt) | Yes — this is the real spec. It documents what everything actually implements. |
| ECMA-48 | Skim. Formally correct, and describes a world nobody implements exactly. |
| vt100.net (Paul Williams' parser + DEC manuals) | Yes, the parser diagram. It is the state machine you will build. |
man 3 termios, man 4 tty, man 7 pty | Yes. Short, authoritative, and on your machine. |
| POSIX (IEEE 1003.1) "General Terminal Interface" | Reference when a termios detail is disputed. |
man 5 terminfo | Reference. Skim the capability names once. |
| kitty's protocol extension docs | Yes, for anything post-2015. |
Warning: Do not try to implement ECMA-48 faithfully. It specifies sequences nothing emits, omits everything xterm added, and disagrees with reality on details like C1 handling in UTF-8. The operative specification for a modern terminal is "what xterm does, plus what kitty and Ghostty proposed, minus what nothing uses."
ctlseqs.txtis the closest thing to it in writing.
Validation / Self-check
- Why is Enter
0x0Dand Backspace0x7F? Give the physical mechanism behind each. - Derive
Ctrl+A = 0x01from the ASCII table. Which three common keys are identical to control bytes? - What were DC1 and DC3 for, and what do they do to your terminal today?
- Name the three things the line discipline took over from the terminal, and why each was the computer's job rather than the terminal's.
- What does
SIGHUPliterally mean, and what modern event triggers it? - What problem does the PTY solve, and what does the master end replace?
- Why does the
?inCSI ? 1049 hexist, and what does its survival tell you about extension design? - Explain pending wrap in terms of an 80-column form on a VT100.
- Why does terminfo exist, and what promise does
TERM=xterm-256colormake? - In one sentence: why does
tmuxkeep your shells alive? - Name three protocol extensions proposed since 2015 and the problem each solves.
- Which single document is the operative specification for a modern terminal, and why is it not ECMA-48?
Next: The Warm-Up — an hour of poking at the terminal you already have.