What Actually Happens When You Press a Key in Your Terminal?
Terminals can feel like ancient magic. You press a key, text appears on a screen, and programs respond — but the mechanism between your keystroke and the output is often a black box. To demystify it, let's trace exactly which bytes travel between a terminal client and a server when you press various keys.
This might seem like a modern problem, especially when you're running a terminal in a browser with xterm.js, but it's actually a very old technology. In the 1970s, terminals like the VT100 were essentially dumb displays. They didn't run programs; they just showed whatever bytes the main computer sent them. The fundamental protocol hasn't changed much since then.
Experimenting with goterm
To see the raw byte traffic, we can use a small Go server called goterm that pairs xterm.js in a browser with a real terminal session. By adding logging to the websocket, we can see exactly what the client sends and receives.
Let's start with a basic command: ls. When you press l, the client doesn't assume you want an l on screen. It sends the byte for l to the server, and the server's process (the shell) echoes it back so the client knows to display it. The client is intentionally dumb; it waits for the server to tell it what to render.
A few other details stand out from this simple interaction:
- Carriage return: Pressing Enter sends
\r, not the newline character\nyou might expect. - Escape sequences: The output contains bytes like
\x1b[?2004h. The\x1bis the ASCII escape character, and it starts a control sequence the terminal interprets for formatting or cursor control.
Control Characters: Ctrl+C and Ctrl+D
Interrupting a process with Ctrl+C is revealing. When you press it, the client sends a single byte: \x03. In the ASCII table, this is "End of Text." The server-side kernel receives this character, recognizes it as the interrupt signal, and sends a SIGINT to the process group attached to the pseudoterminal. The entire mechanism is handled in kernel space, not by the user-space program.
Pressing Ctrl+D works the same way but sends \x04, which is ASCII "End of Transmission." So instead of interrupting, it signals end-of-input to the waiting program, like closing cat.
The Pattern Behind Ctrl Combinations
If you map Ctrl plus a letter, a clear pattern emerges: the byte sent is simply the letter's position in the alphabet.
Ctrl+a=> 1Ctrl+b=> 2Ctrl+c=> 3- ...
Ctrl+z=> 26
Notably, Ctrl+Shift+b produces the exact same byte as Ctrl+b — the shift modifier is ignored for these combos. Other special keys have their own mappings:
- Tab:
0x9(same asCtrl+I) - Escape:
\x1b - Backspace:
\x7f - Home:
\x1b[H - End:
\x1b[F - Delete:
\x1b\x5b\x33\x7e - Insert:
\x1b\x5b\x32\x7e
What about Alt? It behaves like pressing Escape first. Pressing Alt+d sends \x1bd — the escape byte followed by the letter. But unlike the Escape key itself, pressing Alt alone sends nothing.
Escape Sequences and Why Timing Matters
When you run a full-screen editor like nano, the traffic becomes dense with escape sequences like \x1b[27m. These are ANSI escape codes, primarily derived from the VT100 standard, and they control cursor position, text style, and color. If you cat binary data to your screen, you'll eventually hit a stray 0x1b byte that triggers one of these codes and garbles your terminal.
Because many keys map to escape sequences, there's an ambiguity. Pressing Home sends \x1b[H, which is the same as manually typing Escape, [, then H. However, many programs implement a timeout to distinguish between a user pressing Escape and starting to type an escape sequence. In the fish shell, you can adjust this delay with fish_escape_delay_ms. Setting it to 1000 milliseconds lets you hand-type escape codes that would otherwise be misinterpreted.
Beyond Keystrokes: Resizing the Window
Not all terminal interactions go through the byte stream. When you resize the terminal window, the client must use a different mechanism. In the goterm source, Go code makes the ioctl system call with the syscall.TIOCSWINSZ constant to inform the Linux kernel of the new window dimensions.
All of this applies equally to a local terminal emulator like xterm. Whether the bytes cross a network or stay within a single machine, the protocol between your keyboard and the running process is identical. The result is an encoding scheme designed in the 70s and frozen for backward compatibility — which is why Ctrl+a ignores shift, Alt is a pseudo-Escape, and your terminal sometimes relies on millisecond timing to tell the difference between a keystroke and a formatting command.



