HomeProjectskat800Under the hood

Under the hood

The pseudo-terminal, VTE, and the half century of escape sequences a terminal has to cope with.

A page of kat800.

A terminal has to deal with half a century of conventions for how terminals talk to programs. This is what kat800 handles under the surface.

One PTY master/slave pair per pane. For local panes kat800 holds the master ends; child processes see a normal /dev/pts/N tty.

The pseudo-terminal

A PTY is a kernel mechanism that creates a pair of file descriptors, master and slave, which together behave like a real hardware serial terminal. The slave end looks like an ordinary tty device, /dev/pts/N, to any process that opens it. The master end is what kat800 reads from and writes to.

Between the two sits the kernel’s line discipline: the layer handling input echoing, line buffering and control-character translation. Press Ctrl+C and kat800 writes one byte, 0x03, to the master; the line discipline turns it into SIGINT for the foreground process group. The child only ever sees an ordinary tty.

When kat800 is attached to a tmux session the PTY pairs live on the tmux server instead. tmux holds the master ends, and kat800 renders what tmux forwards over the control-mode connection. Detach, and those PTYs and everything running on them stay alive on the server.

VTE, the rendering engine

kat800 uses libvte, GNOME’s terminal emulation library, to render pane output. VTE implements the VT220 and xterm state machine: it parses the byte stream from each master descriptor, interprets escape sequences, maintains cursor position and colour attributes, manages the alternate screen buffer that vim and htop use, and draws to a GTK surface.

Using libvte means inheriting two decades of compatibility work: wide characters, combining characters, 24-bit colour, bracketed paste and mouse tracking are all handled there rather than reimplemented. kat800 gets VTE 0.84 from amber-vte, because Linux Mint’s own 0.76 lacks the termprops the shell integration relies on.

Half a century of escape sequences

Every byte a program sends to a terminal is either printable text or a control sequence: how it moves the cursor, sets colour, clears the screen. There has never been a single agreed standard for what those byte sequences should be.

Year What happened
1963 ASCII standardises the escape character, 0x1B, as a control extender. Its use for terminal control is left undefined: it means only “escape from normal character interpretation”.
1975 DEC’s VT52 uses two-character sequences: ESC A for cursor up, ESC J to erase to end of screen. There was no room for parameters.
1978 The VT100 introduces the Control Sequence Introducer, ESC [: a two-byte prefix, then parameters, then a final command byte. ESC [2J clears the screen; ESC [32m sets green. Every terminal emulator alive today still uses this structure.
1978 Termcap, and later terminfo, describe each terminal’s capabilities in a database and let curses translate requests into the right bytes. $TERM selects the entry. It acknowledged the chaos rather than resolving it, and every terminal still depends on it.
1979 ANSI X3.64 is published, the standard the VT100 had followed in draft (ECMA-48, its European twin, dates from 1976). In practice each vendor added its own sequences and omitted the parts it found inconvenient, so the standard became a floor rather than a ceiling.
1984-1999 xterm grows mouse tracking, window titles and, in 1999, 256 colours, with dozens of non-standard extensions; rxvt, Konsole and GNOME Terminal each add incompatible supersets. F1 sends ESC OP in xterm and ESC [11~ in rxvt, and programs have to accept both.
2021 The Kitty keyboard protocol proposes encoding every key combination, modifiers included, as an unambiguous CSI sequence. foot and WezTerm adopt it, among others; VTE does not.

The same key, four byte sequences

This is why Shift+F1 works in one application and does nothing in another, and why a terminal has to be careful about what it intercepts and what it passes through.

Key VT100 / xterm rxvt VTE Kitty protocol
F1 ESC OP ESC [11~ ESC OP ESC [P
Shift+F1 ESC [1;2P ESC [23~ ESC [1;2P ESC [1;2P
Ctrl+Left ESC [1;5D ESC Od ESC [1;5D ESC [1;5D
Backspace 0x7F DEL 0x08 BS 0x7F DEL 0x7F DEL
Alt+Enter ESC 0x0D ESC 0x0D ESC 0x0D ESC [13;3u
Ctrl+Shift+U undefined undefined input method (IBus) ESC [117;6u

How kat800 handles it

One VTE widget per pane. Each pane is its own VTE 0.84 widget with an independent VT220 and xterm state machine (cursor position, colour attributes, alternate screen mode) kept by VTE rather than reimplemented. kat800’s job is wiring panes to the right byte streams and staying out of VTE’s way.

Control mode instead of a raw stream. Attached with tmux -CC, kat800 receives structured notifications rather than a raw escape stream, and feeds each pane’s output into its own widget. Applications see whatever terminal type the remote tmux is set to, usually tmux-256color or screen-256color, which curses-based programs already know how to target.

Leader-key isolation. The leader key, Ctrl+A by default, is consumed before input reaches any pane, so it never appears in a program’s input stream. Press it twice and the second press cancels the chord and sends the keystroke on.