Skip to content

TTY Architecture ​

PTY, kernel TTY discipline, shell, terminal emulator

A "terminal" is not one thing β€” it's a stack of components connected by a kernel abstraction called a pseudo-terminal (PTY). Understanding this stack explains why Ctrl+C works even when the application is frozen, why SSH feels like a local terminal, and why tmux can detach sessions.

The Terminal Stack ​

Every terminal session involves these components, connected through the kernel:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Terminal Emulator              β”‚  Ghostty, Kitty, iTerm2, Terminal.app
β”‚  (renders text, captures keys)  β”‚  Converts keystrokes β†’ bytes
β”‚  Display ← GPU rendering       β”‚  Parses escape sequences β†’ pixels
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ PTY master fd
               β”‚ (read/write bytes)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Kernel PTY + Line Discipline   β”‚  Echo, line editing, signals
β”‚  (transforms input ↔ output)    β”‚  Ctrl+C β†’ SIGINT, Ctrl+Z β†’ SIGTSTP
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ PTY slave fd
               β”‚ (/dev/pts/N)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Shell or Application           β”‚  bash, zsh, vim, htop
β”‚  (reads stdin, writes stdout)   β”‚  Sends escape sequences for TUI rendering
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What a PTY Is ​

A pseudo-terminal (PTY) is a pair of virtual devices created by the kernel:

  • PTY master β€” the terminal emulator's end. It reads what the application writes and writes what the user types.
  • PTY slave β€” the application's end. It looks like a real serial terminal to the application (providing /dev/pts/N or /dev/ttyp*).

The PTY is what makes terminal emulators possible. Without it, applications would need to talk directly to hardware. With it, the application thinks it's connected to a physical terminal, and the terminal emulator can be any program β€” a GUI app, a web browser tab (via xterm.js), or even another terminal (tmux).

When a terminal emulator starts, it:

  1. Calls posix_openpt() (or openpty()) to create a PTY pair
  2. Forks a child process (the shell)
  3. Sets the PTY slave as the child's stdin, stdout, and stderr
  4. Reads from the PTY master to get the shell's output
  5. Writes to the PTY master to send the user's keystrokes

Why it's called "pseudo"

Real terminals were physical devices connected via serial cables β€” the DEC VT100 plugged into a RS-232 port. A PTY creates a virtual equivalent: the PTY slave behaves exactly like a serial terminal device, but the other end is a user-space program instead of a physical device. The kernel doesn't know the difference.

The Kernel TTY Line Discipline ​

Between the PTY master and slave sits the line discipline β€” a kernel-level transformer that processes both input and output. It's the reason terminals feel "smart" even before any shell is running.

The line discipline handles:

Input processing (terminal β†’ application):

  • Echo β€” when you type, the line discipline writes the character back to the terminal so you can see it (the application hasn't done anything yet)
  • Line editing β€” backspace, Ctrl+U (kill line), Ctrl+W (delete word) are handled by the line discipline in canonical mode
  • Signal generation β€” Ctrl+C β†’ SIGINT, Ctrl+Z β†’ SIGTSTP, Ctrl+\ β†’ SIGQUIT
  • Line buffering β€” in canonical mode, input isn't delivered to the application until you press Enter

Output processing (application β†’ terminal):

  • Newline translation β€” LF (0x0A) is translated to CR+LF so the cursor returns to column 1. Controlled by the onlcr stty flag.
  • Tab expansion β€” optionally converts tabs to spaces
  • Flow control β€” Ctrl+S pauses output, Ctrl+Q resumes (XON/XOFF)

Why Ctrl+C works on frozen programs

When you press Ctrl+C, the terminal emulator writes byte 0x03 to the PTY master. The kernel line discipline intercepts it before the application ever sees it, and sends SIGINT to the entire foreground process group. This is why Ctrl+C works even when the application is stuck in an infinite loop and not reading input β€” the signal comes from the kernel, not the application.

TUI Apps Bypass the Line Discipline ​

Interactive applications like vim, htop, and less need to:

  • Receive every keypress immediately (not wait for Enter)
  • Handle Ctrl+C themselves (not let the kernel kill them)
  • Control exactly what appears on screen (not have the kernel echo input)

They do this by switching to raw mode β€” disabling the line discipline's input processing. See stty & Line Discipline for the details.

In raw mode:

  • Every byte is delivered to the application immediately
  • No echo, no line editing, no signal generation
  • The application is fully responsible for its own display
  • Ctrl+C is just byte 0x03 β€” the application decides what to do with it

This is why exiting a TUI application that crashes can leave your terminal in a broken state β€” it was in raw mode, and the cleanup code (restoring canonical mode) never ran. The fix is stty sane or reset.

Why SSH Works ​

SSH creates a terminal session across a network by adding a PTY layer on the remote machine:

LOCAL                              REMOTE
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Terminal        β”‚                β”‚ Shell (bash)            β”‚
β”‚ Emulator        β”‚                β”‚ reads from PTY slave    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚ PTY                                β”‚ PTY (remote)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ssh client     │◄──TCP/SSH──────│ sshd                    β”‚
β”‚ (raw mode)     │────tunnel──────►│ (allocates remote PTY)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The ssh client puts the local terminal into raw mode (so the local line discipline doesn't interfere) and tunnels all bytes over the encrypted connection. The SSH server allocates a PTY on the remote machine and connects it to the remote shell. The remote line discipline handles Ctrl+C, echo, and line editing. To the remote shell, it looks like a normal terminal session.

This is why terminal escape sequences work over SSH β€” they're just bytes flowing through the tunnel. The remote terminal emulator (well, the remote PTY + line discipline) handles them exactly as a local session would.

Why tmux and screen Add a Layer ​

Terminal multiplexers insert an extra PTY between the terminal emulator and the shell:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Terminal Emulator   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ PTY 1
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ tmux server        β”‚  ← Maintains its own virtual terminal buffer
β”‚ (virtual terminal) β”‚  ← Re-renders content to the outer PTY
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ PTY 2
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Shell / Application β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The tmux server is both a PTY master (for the shell) and a terminal emulator (parsing escape sequences from the shell, maintaining a screen buffer). It then re-renders that buffer as escape sequences to the outer terminal.

This double-PTY architecture is why:

  • Sessions persist when you disconnect β€” the tmux server keeps the inner PTY alive
  • Some escape sequences don't pass through β€” tmux must understand and re-emit every escape sequence, and it doesn't support all of them (notably, some modern protocols like Kitty graphics)
  • There's a latency cost β€” every byte takes an extra hop through tmux's terminal emulator

Multiplexer pass-through

terminfo.dev tests multiplexer compatibility separately β€” see the multiplexer results to check which features pass through tmux and screen correctly, and which ones get lost or mangled.

Process Groups and Job Control ​

The kernel's TTY subsystem also manages process groups β€” the mechanism behind job control (fg, bg, Ctrl+Z):

  • Each terminal session has a session leader (usually the shell)
  • The session has one foreground process group β€” the one that receives keyboard signals and can read from the terminal
  • Background processes that try to read from the terminal get stopped with SIGTTIN
  • Ctrl+Z sends SIGTSTP to the foreground process group, suspending it
  • fg moves a process group to the foreground; bg lets it continue in the background

This is why Ctrl+Z works universally β€” it's handled by the kernel, not the application. And it's why background processes can write to the terminal but can't read from it (they'd compete with the foreground process for input).

Key Takeaways ​

ComponentResponsibilityExamples
Terminal emulatorRender text, capture input, parse escape sequencesGhostty, Kitty, iTerm2
PTY (kernel)Virtual serial device pair connecting emulator to application/dev/pts/N
Line discipline (kernel)Echo, line editing, signals, newline translationstty controls its behavior
ShellCommand interpretation, job control, prompt displaybash, zsh, fish
ApplicationWhatever it does β€” TUI rendering, file editing, process monitoringvim, htop, less

The terminal emulator and the application never communicate directly. Everything flows through the PTY and the line discipline. This indirection is what makes the entire system work β€” any terminal emulator can host any application, any application can run in any terminal, and the kernel provides the glue.


Powered by Termless
Playwright for Terminals