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 factModern fossil
The carriage physically moved right as it printedCR (0x0D) means "return the carriage to column 0"
The platen rolled the paper up one lineLF (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 carriageEnter sends CR, not LF. Still. Today.
A mistake could not be un-printedYou overstruck: X BS X for bold, _ BS X for underline — man still does this
Punched tape: a hole meant 1DEL 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 movePadding characters and baud-rate delays, still in terminfo as pad
A bell rang to get the operator's attentionBEL (0x07)

Note: That DEL = 0x7F row 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 receives 0x7F when 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:

CodeNameThenNow
0x03ETX (end of text)"I am done transmitting"Ctrl+C → SIGINT
0x04EOT (end of transmission)"hang up the line"Ctrl+D → EOF
0x07BELring the bellterminal bell / visual flash
0x08BSmove the carriage left onecursor left; does not erase
0x09HTadvance to a mechanical tab stopnext tab stop
0x11/0x13DC1/DC3start/stop the paper tape reader^Q/^S flow control — why ^S freezes your terminal
0x1ASUB"substitute for a garbled character"Ctrl+Z → SIGTSTP (Unix repurposed it)
0x1BESC"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

FeatureSequenceStill with us
Cursor addressingCSI r ; c HYes, unchanged
EraseCSI J, CSI KYes
Scroll regionsCSI t ; b rYes — how vim keeps a status line
Character attributesCSI Ps m (SGR)Yes — now carrying 24-bit color
Private modesCSI ? Ps hYes — the ? marks a DEC extension
Application cursor keysCSI ? 1 hYes — why arrows differ in vim
Alternate character setESC ( 0Yes — box drawing before Unicode
Device queriesCSI 6 n → CSI r ; c RYes — the terminal talks back

Note: The ? private-parameter marker is worth understanding as a design idea. ANSI reserved 0x3C–0x3F for 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:

  1. Windowing systems. X11 (1984) wanted many terminal windows on one screen. There was no serial port behind them.
  2. Networks. telnet and later ssh wanted 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:

ExtensionYear (approx.)What it enabled
Mouse reporting (?1000)1980sClickable TUIs
Window title (OSC 0)1980sTitles in the window manager
256 colors (38;5;n)1999The palette everything now assumes
SGR mouse encoding (?1006)2000sTerminals wider than 223 columns
Bracketed paste (?2004)2000sEditors distinguishing paste from typing
24-bit color (38;2;r;g;b)2000sTruecolor
modifyOtherKeys2000sCtrl+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:

ProposalSolves
Synchronized output (?2026)Tearing during full-screen redraws
The kitty keyboard protocolKey release events; the Escape ambiguity; unrepresentable combinations
Grapheme clustering (?2027)Terminal and program disagreeing about width
OSC 8 hyperlinksClickable links without regex-guessing
OSC 133 semantic promptsThe terminal knowing where a command's output begins
Sixel revival / kitty graphicsImages

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.

QuestionAnswer
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:

DocumentRead it?
XTerm Control Sequences (ctlseqs.txt)Yes — this is the real spec. It documents what everything actually implements.
ECMA-48Skim. 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 ptyYes. Short, authoritative, and on your machine.
POSIX (IEEE 1003.1) "General Terminal Interface"Reference when a termios detail is disputed.
man 5 terminfoReference. Skim the capability names once.
kitty's protocol extension docsYes, 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.txt is the closest thing to it in writing.


Validation / Self-check

  1. Why is Enter 0x0D and Backspace 0x7F? Give the physical mechanism behind each.
  2. Derive Ctrl+A = 0x01 from the ASCII table. Which three common keys are identical to control bytes?
  3. What were DC1 and DC3 for, and what do they do to your terminal today?
  4. 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.
  5. What does SIGHUP literally mean, and what modern event triggers it?
  6. What problem does the PTY solve, and what does the master end replace?
  7. Why does the ? in CSI ? 1049 h exist, and what does its survival tell you about extension design?
  8. Explain pending wrap in terms of an 80-column form on a VT100.
  9. Why does terminfo exist, and what promise does TERM=xterm-256color make?
  10. In one sentence: why does tmux keep your shells alive?
  11. Name three protocol extensions proposed since 2015 and the problem each solves.
  12. 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.