Who does what in the terminal

Everything that happens in a terminal session is a collaboration between four parties: your operating system, your shell, your terminal emulator, and whatever program happens to be running at the moment. The first three are relatively well understood — if you’re running bash in GNOME Terminal on Linux, POSIX gives you a reasonable model of how they interact. But the program itself feels like it could do anything. How do you predict the behavior of a program you've never run before?

There are no real standards governing terminal program behavior. POSIX mostly covers the OS/shell/terminal emulator side of things, and while it touches on core utilities like cp, it has nothing to say about interactive programs like htop. The closest thing to a guideline is the command line interface guidelines, but even that isn't a formal standard.

Despite the lack of standards, terminal programs behave remarkably consistently in practice. What follows is a set of observed "rules" — descriptive, not prescriptive — that hold across most programs you'll encounter.

Who implements what

Some conventions are clearly the program’s responsibility: config files go in ~/.BLAHrc or ~/.config/BLAH/FILE, --help prints help, regular output goes to stdout and errors to stderr. But the interesting cases are the ones that feel like laws of nature yet actually require explicit implementation in every program.

Press Ctrl-D to quit a REPL, for example. It seems like the OS should handle that, but cat gets it for free from cooked mode while ipython has to implement it explicitly. Understanding which behaviors are the program’s job makes it far less surprising when implementations differ between programs.

Rule 1: Ctrl-C quits noninteractive programs

Noninteractive programs quit on Ctrl-C by default because they don’t install a SIGINT handler. The rule is essentially: act like the default.

The trap is that this doesn’t apply to interactive programs like python3, bc, or less. In those, Ctrl-C interrupts the current operation — a search in less, or running Python code — without stopping the program itself. This is implemented explicitly, for instance in prompt-toolkit’s key bindings, which abort a search on Ctrl-C.

Rule 2: q quits TUIs

Terminal user interfaces like less and htop quit on q. This naturally doesn’t apply where q would be meaningless — tmux and text editors are the obvious exceptions.

Rule 3: Ctrl-D on an empty line quits a REPL

REPLs from python3 to ed quit on Ctrl-D at an empty line. The rationale mirrors the Ctrl-C rule: in cooked mode, the OS returns EOF for that keypress, so programs mimic it.

Most REPLs — sqlite3, python3, fish, bash — don’t actually run in cooked mode, yet they all implement this shortcut anyway. The implementation lives in input libraries: prompt-toolkit handles it, and so does readline. This one felt like a fundamental law until someone pointed out that the Erlang REPL doesn’t follow it.

Rule 4: Stick to the base 16 colors

Terminal programs rarely use colors beyond the standard 16 ANSI values. A hex color like #EEEEEE looks fine on a dark background but nearly vanishes on white. The base 16 colors are safer because users can configure them in their terminal emulator to work with their preferred background, and they make fewer assumptions about what the terminal supports.

The notable exceptions are text editors — Helix uses a non-ANSI purple background by default. That’s acceptable because any user who dislikes it will change the theme anyway.

Rule 5: Vaguely support readline keybindings

Almost every program that would benefit from line editing implements readline-style keybindings, even when it doesn’t use readline itself:

The mimicry isn’t exact. Atuin, for instance, uses Ctrl-A as a command prefix, so it can’t also use it to jump to the beginning of the line. Programs like git, cat, and nc have no line editing at all beyond backspace, Ctrl-W, and Ctrl-U. Text editors, as usual, each go their own way.

Rule 5.1: Ctrl-W deletes the last word

No non-editor program has ever been observed where Ctrl-W doesn’t delete the last word. This follows the same pattern as Ctrl-C: in cooked mode, the OS performs word deletion on Ctrl-W and full-line deletion on Ctrl-U, so programs imitate that behavior.

Rule 6: Disable colors on pipes

Most programs turn off color when their output isn’t a terminal. rg blah highlights matches when writing to a TTY but suppresses highlighting when writing to a pipe or file. ls --color=auto behaves the same way. Both also change formatting — ls switches to column layout and ripgrep groups output with headings under a TTY.

To force color output when you need it — say, to pipe into less — you can use unbuffer to make the program think its output is a TTY:

unbuffer rg blah |  less -R

Alternatively, many programs offer an explicit flag: rg --color=always | less -R achieves the same result.

Rule 7: - means stdin or stdout

Passing - in place of a filename conventionally means "read from stdin" or "write to stdout." For instance, to format clipboard contents with black and capture the result:

pbpaste | black - | pbcopy

pbpaste is macOS-specific; on Linux you’d use something like xclip. Most programs implement - when it makes sense, though exceptions surely exist.

How these rules are learned

None of this is obvious at first encounter. Learning these patterns typically involves four stages over a long period:

  1. Discover the rule exists at all: "Ctrl-C exits programs"
  2. Notice exceptions: "Ctrl-C exits find but not less"
  3. Identify the underlying pattern subconsciously: "Ctrl-C quits noninteractive programs but interrupts operations in interactive ones"
  4. Finally articulate it as an explicit rule

Much of what experienced users understand about the terminal still lives in subconscious pattern recognition. The point of writing these rules down is to let others skip straight to the explicit stage.