The full stack of terminals explained

Hacker News Top Tools

Summary

A comprehensive guide explaining the differences and relationships between terminals, shells, TTYs, consoles, and related concepts like POSIX and ANSI escape codes, including hands-on TUI creation.

No content available
Original Article
View Cached Full Text

Cached at: 07/05/26, 10:34 PM

# The Full Stack of Terminals Explained: Terminal, Shell, TTY, Console, POSIX, ANSI Escapes, PTYs, Raw/Canonical Modes Source: [https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/) This piece covers the full stack: from the vocabulary that trips people up, all the way down to building a TUI \(Text User Interface\) app from scratch\. If you’ve ever wondered what vim is actually doing when it takes over your screen, or why Raw Mode exists, or what ANSI escape sequences are, this is the piece\. ## They used to be the same thing[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#they-used-to-be-the-same-thing) This is the key insight\. All four words originally described the same physical object\. In the 1960s, you interacted with a computer by sitting at a machine with a keyboard and a printer\. That machine had three names depending on who was talking about it: ``` ┌───────────────────────────────────── │ ┌─────────────────────────────── │ │ $ hello world_ │ <-- "Console" (physical device) │ │ │ <-- "Terminal" (end of a wire) │ │ │ <-- "TTY" (it's a teletypewriter) │ └─────────────────────────────── │ ┌─────────────────────────────── │ │ ┌───┐┌───┐┌───┐┌───┐┌───┐ │ │ │ Q ││ W ││ E ││ R ││ T │... │ │ └───┘└───┘└───┘└───┘└───┘ │ └─────────────────────────────── └───────────────────────────────────── │ │ wire │ ┌───────┴──────── │ MAINFRAME │ COMPUTER └──────────────── ``` Three names, one thing\. That’s why they blur together\. As hardware evolved into software, each word drifted toward its own meaning\. But they drifted slowly\. --- ## Part 1: The Vocabulary[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#part-1-the-vocabulary) ### Console[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#console) The console is the physical input/output device of a computer\. Originally it meant a keyboard and display directly connected to the machine itself\. From the OS’s perspective, it’s treated as “the system’s primary standard I/O terminal\.” On a server, it’s what you see when you walk up and plug in a monitor\. On your laptop, it’s the text screen you’d land on if the graphical desktop crashed\. It’s the terminal of last resort, the one the kernel writes panic messages to, because it’s guaranteed to exist when everything else is broken\. ``` Remote access (not console): You --> laptop --> SSH --> internet --> server │ ┌────┴──── │ server └────┬──── │ Console access (the real deal): │ You --> keyboard ──────────────────────> monitor <────────────────────── ^ This is the console. Direct. Physical. Works even when the network is down. ``` The word has gotten looser over time\. Browser dev tools are “the console\.” macOS has a log viewer called Console\. But the original meaning is always the same: the direct, local, hardware interface\. ### Terminal[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#terminal) A terminal is the thing you*see*\. It handles input and output\. Keystrokes go in, text comes out\. That’s its entire job\. When you open iTerm2, Alacritty, Windows Terminal, or GNOME Terminal, you’re running a**terminal emulator**: a program that does in software what the old hardware terminals did in circuits\. The “emulator” part usually gets dropped, but it’s worth remembering, because it clarifies the abstraction\. Your terminal app is pretending to be a device\. Here’s the part that surprises people: the terminal doesn’t understand your commands\. It has no idea what`ls`or`git push`means\. It doesn’t parse anything\. It’s a display surface with a keyboard attached\. ``` ┌─ TERMINAL (what you see) ─────────────── │ │ Owns: │ - Rendering text on screen │ - Colors and fonts │ - Scrollback history │ - Copy-paste │ - Interpreting ANSI escape sequences │ │ Does NOT own: │ - Understanding commands │ - Running programs │ - Tab completion │ - Your prompt │ └───────────────────────────────────────── ``` Major terminal emulators by platform: - macOS: Terminal\.app, iTerm2, Ghostty, Alacritty, kitty - Linux: GNOME Terminal, Konsole - Windows: Windows Terminal, ConEmu - Web/Node: xterm This is a clean separation, and clean separations are worth noticing\. They tend to be load\-bearing\. ### Shell[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#shell) The shell is the program running*inside*your terminal\. It’s the command\-line interpreter, the thing that takes the text you type, figures out what program to run, runs it, and manages the lifecycle of that process\. You type`git status`\. Your terminal sends that text to the shell\. The shell parses it, finds the`git`program, spawns a process, and wires up the I/O\. The output flows back through the terminal to your eyes\. The shell handled the logic\. The terminal handled the pixels\. ``` You type: git status │ ▼ ┌─ TERMINAL ─────────────────────── │ │ Converts keystrokes to bytes │ Sends text to the shell │ └──────────────┬─────────────────── │ "git status\n" ▼ ┌─ SHELL (bash, zsh, fish) ──────── │ │ Parses the command │ Finds /usr/bin/git │ Spawns the process │ Manages its lifecycle │ └──────────────┬─────────────────── │ output bytes ▼ ┌─ TERMINAL ─────────────────────── │ │ Receives output │ Renders it on your screen │ │ On branch main │ nothing to commit │ └────────────────────────────────── ``` The shell’s main jobs are: command parsing \(token splitting, redirect processing\), environment variable management, process launching and control \(job control\), and providing scripting functionality\. There are many shells, and which one you use is a meaningful choice\. - **Bash**is the default, exists on virtually any Unix\-like system\. - **Zsh**is the default on macOS and popular for its extensibility\. - **PowerShell**takes a different approach entirely on Windows\. The crucial thing: your terminal and your shell are independent\. You can swap either one without touching the other\. ``` Any terminal works with Any shell ┌──────────────┐ ┌────────────── │ iTerm2 │─────────┐ ┌─────│ Bash │ Alacritty │─────────┼────┼─────│ Zsh │ Kitty │─────────┼────┼─────│ Fish │ Win Terminal│─────────┘ └─────│ PowerShell └──────────────┘ └────────────── They don't care about each other. ``` ### CLI \(Command Line Interface\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#cli-command-line-interface) CLI is the broader category\. It’s any interface where commands go in as text and output comes back as text, as opposed to a GUI\. Shells, REPLs, and TUI apps are all types of CLI\. When someone says “command line,” they usually mean the actual input line where you type a single command, like`ls \-l`or`git commit \-m "msg"`\. The shell parses that string and executes it\. ### TTY \(TeleTYpewriter\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#tty-teletypewriter) TTY is the most technical of the four terms, and the one you can safely ignore the longest\. In modern Unix, a TTY is a device file in the kernel that connects your terminal to your shell\. It’s a general term for terminal devices in Unix\-like OSes\. Originally it was a device for connecting physical teletypewriters, but now it refers to terminal devices in general, including virtual terminals\. Type`tty`right now and you’ll see something like`/dev/ttys003`or`/dev/pts/1`\. That’s your current session’s device file\. ``` ┌──────────────┐ ┌──────────────┐ ┌────────────── │ │ │ │ │ Terminal │◄───►│ TTY device │◄───►│ Shell │ (iTerm2) │ │ /dev/pts/1 │ │ (bash) │ │ │ │ └──────────────┘ └──────────────┘ └────────────── user-space kernel user-space The TTY sits in between. It's the kernel's middleman that handles the data flow. ``` For everyday purposes, “tty” and “terminal” mean the same thing\. The difference only shows up if you’re writing systems\-level code or debugging something weird with signals\. ### PTY \(Pseudo Terminal\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#pty-pseudo-terminal) A PTY is a virtual terminal device used by terminal emulators\. In practice, two device files operate as a pair: ``` ┌─────────────────┐ ┌────────────────── │ PTY Manager │ │ PTY Subsidiary │ (master side) │◄───────►│ (slave side) │ │ │ Operated by │ │ Where shells & │ the terminal │ │ TUI apps │ emulator │ │ connect └─────────────────┘ └────────────────── ``` In POSIX\.1\-2024, the master side is called “manager” and the slave side is called “subsidiary\.” Since shells and TUI apps treat the subsidiary side as a “real terminal,” they can use the same API \(termios, etc\.\) as physical terminals\. Applications don’t need to know whether they’re talking to a physical or virtual terminal\. Device file examples: - Linux:`/dev/pts/0`,`/dev/pts/1`… - macOS:`/dev/ttys000`,`/dev/ttys001`… ### TUI \(Text User Interface\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#tui-text-user-interface) A TUI is a character\-based interface that provides an interactive UI using the entire screen\. Unlike a regular CLI \(shell\) that executes commands line by line, a TUI controls the entire screen and achieves a rich user experience using cursor movement, colors, borders, menus, and forms\. Think of`vim`,`less`,`htop`, or`nmtui`\. They take over your whole terminal window and redraw it constantly\. ``` ┌───────────────────────────────────────────────────── │ │ CLI (shell) TUI (vim, htop) │ │ $ ls -la ┌────────────────────── │ total 48 │ htop - 3.2.1 │ drwxr-xr-x 5 user ... │ CPU[|||||||| 38.2%] │ -rw-r--r-- 1 user ... │ Mem[||||| 512/8G] │ $ _ │ │ │ PID USER CPU% MEM │ Text flows line by line │ 1234 root 12.3 1.2 │ downward. That's it. │ 5678 user 8.1 0.9 │ └────────────────────── │ Full screen. Interactive. │ Redraws constantly. │ └───────────────────────────────────────────────────── ``` Here’s how CLI and TUI compare: AspectCLI \(Shell\)TUI \(vim, htop\)InputLine\-based \(confirmed w/ Enter\)Key\-based \(immediate on press\)ScreenText flows downwardFull screen redrawnCursorMoves to next line automaticallyMoves anywhere via ANSI escapesEchoOn \(chars shown as you type\)Off \(app draws its own UI\)Terminal modeCanonical ModeRaw ModeExamplesbash, zsh, fish, Python REPLvim, less, htop, nmtui --- ## Part 2: The Plumbing Under the Hood[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#part-2-the-plumbing-under-the-hood) This is where most articles stop\. But if you ever want to build a TUI app, or even just understand what vim is doing to your terminal, you need to go one layer deeper\. ### POSIX \(Portable Operating System Interface\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#posix-portable-operating-system-interface) POSIX is a standard specification for maintaining compatibility across Unix\-like systems\. Established by IEEE, it defines APIs for system calls, terminal control, file I/O, threads, and more\. Most commands and system calls in macOS and Linux are POSIX\-compliant\. POSIX\-compliant OSes include Linux \(Ubuntu, Debian, Red Hat, etc\.\), macOS \(Darwin\), BSD systems \(FreeBSD, OpenBSD\), Solaris, and AIX\. The latest specification is POSIX\.1\-2024 \(IEEE Std 1003\.1\-2024\)\. ### POSIX Terminal Interface[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#posix-terminal-interface) The standard API defined by POSIX for terminal input/output control\. The`termios`structure and its related functions \(`tcgetattr\(\)`,`tcsetattr\(\)`\) are used to configure terminal modes, define special characters, and set baud rate\. This standardization means the same code works across POSIX\-compliant OSes\. ### Unix Terminal Interface[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#unix-terminal-interface) The traditional mechanism for terminal control in Unix\-like OSes\. The TTY driver manages terminal devices within the kernel, and the line discipline handles input/output processing\. POSIX is a standardization of this Unix terminal interface\. ### TTY Line Discipline[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#tty-line-discipline) Line discipline is a software layer in the kernel that sits between the TTY driver and user processes, handling terminal input/output processing\. ``` ┌──────────────────── │ Terminal │ Emulator └────────┬─────────── │ ┌────────▼─────────── │ PTY (master) └────────┬─────────── │ ┌────────▼────────────────────────────────────────── │ TTY Line Discipline │ │ 1. Line editing │ Backspace = delete char │ Ctrl+U = delete entire line │ Ctrl+W = delete word │ │ 2. Echo back │ Input chars automatically sent back to screen │ So the user can see what they're typing │ │ 3. Special character processing │ Ctrl+C --> SIGINT (kill process) │ Ctrl+Z --> SIGTSTP (suspend process) │ Ctrl+D --> EOF (end of input) │ │ 4. Character conversion │ Newline code conversion (CR <--> LF) │ │ 5. Input buffering │ Canonical mode: buffer until Enter │ Non-canonical: pass through immediately │ └────────┬────────────────────────────────────────── │ ┌────────▼─────────── │ PTY (subsidiary) └────────┬─────────── │ ┌────────▼─────────── │ Shell / TUI App └──────────────────── ``` This is important to understand because TUI apps work by*disabling*most of these line discipline features\. When you run vim, it tells the kernel: “stop doing line editing, stop echoing, stop interpreting Ctrl\+C as a signal\. Just give me the raw bytes\.” ### termios[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#termios) `termios`is the POSIX standard terminal control structure\. It’s how you talk to the line discipline\. It manages: ``` termios structure ┌──────────────────────────────────────────────────── │ │ c_iflag (Input flags) │ Newline conversion, flow control, etc. │ │ c_oflag (Output flags) │ Output processing settings │ │ c_cflag (Control flags) │ Baud rate, character size, etc. │ │ c_lflag (Local flags) │ Echo, canonical mode, signal generation, etc. │ │ c_cc (Special characters) │ Definitions for Ctrl+C, Ctrl+Z, EOF, etc. │ │ VMIN / VTIME (Timeout) │ Read control in non-canonical mode │ └──────────────────────────────────────────────────── ``` In TUI development, termios is used to implement Raw Mode\. ### ioctl \(Input/Output Control\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#ioctl-input-output-control) `ioctl`is a general\-purpose device control system call\. Through a file descriptor, it instructs device drivers to perform special operations that can’t be done with normal read/write\. Main operations include getting and setting termios, and getting window size\. ### tcgetattr / tcsetattr[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#tcgetattr-tcsetattr) High\-level API functions for terminal control defined in the POSIX standard\. They let you write more portable code than using`ioctl`directly\. In Node\.js, you don’t need to call these directly\. The high\-level equivalent is built in: ``` // High-level: Node.js handles termios internally process.stdin.setRawMode(true); // Equivalent to MakeRaw / tcsetattr process.stdin.setRawMode(false); // Equivalent to Restore / tcsetattr ``` For low\-level access to termios from JavaScript, you’d use a native addon or FFI\. More on this in the implementation section\. ## The Full I/O Flow[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#the-full-i-o-flow) Here’s everything connected, from your fingers to the application and back: ``` ┌──────┐ ┌─────────────────────── │ User │────>│ Terminal Emulator │ │ │ (iTerm2, xterm, etc.) └──────┘ └──────────┬──────────── │ ┌──────────▼──────────── │ PTY │ (manager <-> subsid.) └──────────┬──────────── │ ┌──────────▼──────────── │ TTY + Line Discipline │ (ICANON/ECHO/ISIG flags) └──────────┬──────────── │ ┌──────────▼──────────── │ TUI App │ (vim, htop) └──────────┬──────────── │ ┌───────────────┼─────────────── │ ▼ ▼ ▼ termios config Output (ANSI Rendering changes escape seqs) on screen (Raw Mode, etc.) back to via Terminal Terminal Emulator ``` --- ## Part 3: Terminal Behavior for TUI Development[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#part-3-terminal-behavior-for-tui-development) TUI apps don’t interpret commands like shells do\. Instead, they directly handle terminal I/O control\. They use the termios API to change terminal mode settings and ANSI escape sequences to render and update the screen\. In a normal shell environment, the terminal is set to canonical mode \(`ICANON`\), where input is buffered line by line and passed to the program after you press Enter\. ``` $ stty -a | grep icanon # icanon isig iexten echo echoe echok echoke -echonl echoctl # ^ Canonical mode is ON ``` Characters you type are automatically echoed to the screen and sent to the shell when Enter is pressed: ``` $ ls example.txt ``` But TUI apps like vim or less switch the terminal to non\-canonical mode\. Key input is passed to the application immediately, character by character, and the application handles its own rendering\. ``` $ vim # In another terminal, check vim's terminal mode: $ ps aux | grep vim # find vim's terminal $ stty -a < /dev/ttys049 | grep icanon # -icanon -isig -iexten -echo -echoe echok echoke -echonl echoctl # ^ Canonical mode is OFF. Echo is OFF. Signals are OFF. ``` ## The Five Elements of TUI Development[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#the-five-elements-of-tui-development) To build a TUI app, you need to understand and control five things: ``` ┌───────────────────────────────────────────── │ │ 1. Terminal Mode Settings │ Canonical --> Raw Mode │ │ 2. Input Processing │ Parsing keys, escape sequences, Ctrl+ │ │ 3. Screen Control │ ANSI escape sequences for rendering │ │ 4. Terminal Size Management │ Detecting and responding to resizes │ │ 5. Buffering │ Batch writes to prevent flicker │ └───────────────────────────────────────────── ``` Let’s go through each one\. --- ## 1\. Terminal Mode Settings[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#1-terminal-mode-settings) The line discipline has three operating modes\. You switch between them by setting flags in the termios structure\. ### Canonical Mode \(Cooked Mode\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#canonical-mode-cooked-mode) This is the default\. It’s what your shell uses\. Input is buffered line by line, line editing works \(Backspace, Ctrl\+U, Ctrl\+W\), echo is on, and special characters like Ctrl\+C generate signals\. ### Non\-Canonical Mode[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#non-canonical-mode) Canonical mode disabled \(`ICANON`off\)\. Input arrives character by character instead of line by line\. Line editing is off\. But echo and signal processing can optionally stay enabled\. ### Raw Mode[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#raw-mode) Almost everything disabled\. No echo, no signals, no newline conversion\. Input is passed to the application as a raw byte stream\. This is what TUI apps use\. AspectCanonical \(Cooked\)Non\-CanonicalRawInput bufferingLine\-basedChar\-basedChar\-basedLine editingEnabledDisabledDisabledEcho backEnabledConfigurableDisabledSignals \(Ctrl\+C\)EnabledConfigurableDisabledNewline conv\.EnabledConfigurableDisabledMain usesShell, interactiveCustom CLITUI, games`stty`‘s`\-cooked`is synonymous with`raw`\. Raw Mode is the opposite of Cooked Mode\. --- ## 2\. Input Processing[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#2-input-processing) Input processing means figuring out “what key was pressed\.” Normal characters are simple, but arrow keys, function keys, and mouse events are sent as multi\-byte escape sequences\. Your TUI app needs to parse these\. ### Special Key Escape Sequences[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#special-key-escape-sequences) ``` ┌──────────────┬──────────────┬────────────────── │ Key │ Sequence │ Byte Sequence ├──────────────┼──────────────┼────────────────── │ Up (↑) │ ESC[A │ \x1b[A │ Down (↓) │ ESC[B │ \x1b[B │ Right (→) │ ESC[C │ \x1b[C │ Left (←) │ ESC[D │ \x1b[D │ Home │ ESC[H │ \x1b[H │ End │ ESC[F │ \x1b[F │ Page Up │ ESC[5~ │ \x1b[5~ │ Page Down │ ESC[6~ │ \x1b[6~ │ F1-F4 │ ESC[OP-OS │ \x1b[OP, etc. └──────────────┴──────────────┴────────────────── ``` ### Control Characters[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#control-characters) ``` ┌──────────────┬──────────┬───────────────── │ Character │ ASCII │ Description ├──────────────┼──────────┼───────────────── │ Ctrl+C │ 3 │ SIGINT │ Ctrl+D │ 4 │ EOF │ Ctrl+Z │ 26 │ SIGTSTP │ Enter │ 13 / 10 │ CR / LF │ Tab │ 9 │ Tab │ Backspace │ 127 / 8 │ DEL / BS │ ESC │ 27 │ Escape └──────────────┴──────────┴───────────────── ``` --- ## 3\. Screen Control \(ANSI Escape Sequences\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#3-screen-control-ansi-escape-sequences) Screen control means telling the terminal “what to display where\.” ANSI escape sequences are special string commands that begin with the ESC character \(`\\x1b`or`\\033`\)\. They were standardized as ANSI X3\.64 and implemented in VT100 terminals, which is why they’re everywhere\. ### Cursor Control[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#cursor-control) ``` ┌──────────────────────┬─────────────────────────────── │ Sequence │ Description ├──────────────────────┼─────────────────────────────── │ ESC[H │ Move to home position (1,1) │ ESC[{row};{col}H │ Move to position (1-indexed) │ ESC[{n}A │ Move n rows up │ ESC[{n}B │ Move n rows down │ ESC[{n}C │ Move n columns right │ ESC[{n}D │ Move n columns left │ ESC[s │ Save cursor position │ ESC[u │ Restore cursor position │ ESC[?25l │ Hide cursor │ ESC[?25h │ Show cursor └──────────────────────┴─────────────────────────────── ``` ### Screen Clearing[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#screen-clearing) ``` ┌──────────────────────┬─────────────────────────────────── │ Sequence │ Description ├──────────────────────┼─────────────────────────────────── │ ESC[2J │ Clear entire screen │ ESC[H │ Move cursor to home │ ESC[K │ Clear from cursor to end of line │ ESC[1K │ Clear from start of line to cur. │ ESC[2K │ Clear entire line └──────────────────────┴─────────────────────────────────── ``` ### Basic Styles[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#basic-styles) ``` ┌──────────────────────┬────────────── │ Sequence │ Description ├──────────────────────┼────────────── │ ESC[0m │ Reset all │ ESC[1m │ Bold │ ESC[4m │ Underline │ ESC[7m │ Reverse └──────────────────────┴────────────── ``` ### Foreground Colors \(Text\)[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#foreground-colors-text) ``` ┌──────────────────────┬────────────── │ Sequence │ Color ├──────────────────────┼────────────── │ ESC[30m │ Black │ ESC[31m │ Red │ ESC[32m │ Green │ ESC[33m │ Yellow │ ESC[34m │ Blue │ ESC[35m │ Magenta │ ESC[36m │ Cyan │ ESC[37m │ White └──────────────────────┴────────────── ``` ### Background Colors[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#background-colors) ``` ┌──────────────────────┬────────────── │ Sequence │ Color ├──────────────────────┼────────────── │ ESC[40m │ Black │ ESC[41m │ Red │ ESC[42m │ Green │ ESC[43m │ Yellow │ ESC[44m │ Blue │ ESC[45m │ Magenta │ ESC[46m │ Cyan │ ESC[47m │ White └──────────────────────┴────────────── ``` ### Extended Color Modes[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#extended-color-modes) ``` ┌──────────────────────────────┬──────────────────────────────── │ Sequence │ Description ├──────────────────────────────┼──────────────────────────────── │ ESC[38;5;{n}m │ Foreground (n: 0-255) │ ESC[48;5;{n}m │ Background (n: 0-255) │ ESC[38;2;{r};{g};{b}m │ Foreground (RGB True Color) │ ESC[48;2;{r};{g};{b}m │ Background (RGB True Color) └──────────────────────────────┴──────────────────────────────── ``` ### Alternate Screen Buffer[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#alternate-screen-buffer) ``` ┌──────────────────────┬───────────────────────────────────────── │ Sequence │ Description ├──────────────────────┼───────────────────────────────────────── │ ESC[?1049h │ Switch to alternate screen buffer │ │ (used by vim, less, etc.) │ ESC[?1049l │ Return to normal screen buffer └──────────────────────┴───────────────────────────────────────── ``` This is why vim can take over your entire screen and then your terminal looks normal after you quit\. It switches to an alternate buffer on startup and switches back on exit\. --- ## 4\. Terminal Size Management[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#4-terminal-size-management) TUI apps need to render according to terminal size, and they need to redraw when users resize the window\. Here’s how that works: ``` User resizes window │ ▼ Terminal Emulator │ │ ioctl(TIOCSWINSZ) -- notify new rows/cols ▼ PTY (master --> subsidiary) │ │ Update winsize in kernel ▼ Kernel TTY Layer │ │ Send SIGWINCH signal ▼ Process (bash, vim, less) │ │ Receive signal │ Call ioctl(TIOCGWINSZ) to get new size │ Redraw ▼ Updated screen ``` Terminal size is managed by the`winsize`structure held in the kernel’s TTY structure\. When the size changes, the terminal emulator notifies via`ioctl\(TIOCSWINSZ\)`, and the kernel sends`SIGWINCH`to connected processes\. --- ## 5\. Buffering[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#5-buffering) When you’re sending many control sequences, sending them one by one is slow and causes visible flicker\. By buffering all your writes and flushing them in one batch, you get smooth, flicker\-free rendering\. Every serious TUI app does this\. --- ## Part 4: Building a TUI in JavaScript/TypeScript[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#part-4-building-a-tui-in-javascript-typescript) Now let’s actually build one\. We’ll implement all five elements using Node\.js\. ## High\-Level Implementation[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#high-level-implementation) This uses Node\.js’s built\-in`process\.stdin\.setRawMode\(\)`\. It abstracts away termios details\. ``` // Structure to manage terminal state interface Terminal { width: number; height: number; buffer: string; } // ───────────────────────────────────────────── // 1. Terminal Mode Settings // ───────────────────────────────────────────── function initTerminal(): Terminal { // Set to Raw Mode (equivalent to term.MakeRaw in Go) process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding("utf8"); const { columns, rows } = process.stdout; return { width: columns || 80, height: rows || 24, buffer: "", }; } function restoreTerminal(): void { write("\x1b[?25h"); // Show cursor write("\x1b[0m"); // Reset color flush(); process.stdin.setRawMode(false); } // ───────────────────────────────────────────── // 4. Terminal Size Management // ───────────────────────────────────────────── function getTerminalSize(): { width: number; height: number } { return { width: process.stdout.columns || 80, height: process.stdout.rows || 24, }; } // ───────────────────────────────────────────── // 2. Input Processing // ───────────────────────────────────────────── interface KeyEvent { char: string | null; key: string | null; } function parseKey(data: string): KeyEvent { // Ctrl+C if (data === "\x03") { return { char: null, key: "CTRL_C" }; } // Escape sequences (arrow keys, etc.) if (data === "\x1b[A") return { char: null, key: "UP" }; if (data === "\x1b[B") return { char: null, key: "DOWN" }; if (data === "\x1b[C") return { char: null, key: "RIGHT" }; if (data === "\x1b[D") return { char: null, key: "LEFT" }; if (data === "\x1b[H") return { char: null, key: "HOME" }; if (data === "\x1b[F") return { char: null, key: "END" }; if (data === "\x1b[5~") return { char: null, key: "PAGE_UP" }; if (data === "\x1b[6~") return { char: null, key: "PAGE_DOWN" }; // Bare ESC if (data === "\x1b") return { char: null, key: "ESC" }; // Normal character return { char: data, key: null }; } // ───────────────────────────────────────────── // 3. Screen Control (ANSI Escape Sequences) // ───────────────────────────────────────────── let outputBuffer = ""; function bufferWrite(s: string): void { outputBuffer += s; } function clear(): void { bufferWrite("\x1b[2J\x1b[H"); } function moveTo(row: number, col: number): void { bufferWrite(`\x1b[${row};${col}H`); } function setColor(fg: number): void { bufferWrite(`\x1b[${fg}m`); } function writeText(s: string): void { bufferWrite(s); } // ───────────────────────────────────────────── // 5. Buffering // ───────────────────────────────────────────── function flush(): void { process.stdout.write(outputBuffer); outputBuffer = ""; } function write(s: string): void { process.stdout.write(s); } // ───────────────────────────────────────────── // Main // ───────────────────────────────────────────── function main(): void { // Check if running in a terminal if (!process.stdin.isTTY) { process.stderr.write("Error: Must be run in an interactive terminal\n"); process.exit(1); } // Initialize const term = initTerminal(); // Always restore on exit process.on("exit", restoreTerminal); process.on("SIGINT", () => { restoreTerminal(); process.exit(0); }); // 4. Detect window size changes (SIGWINCH) process.on("SIGWINCH", () => { const size = getTerminalSize(); term.width = size.width; term.height = size.height; }); // Cursor position let x = Math.floor(term.width / 2); let y = Math.floor(term.height / 2); function render(): void { // Clear screen clear(); // Title moveTo(1, Math.floor(term.width / 2) - 10); setColor(36); // Cyan writeText("TUI Demo (press 'q' to quit)"); // Display information moveTo(3, 2); setColor(33); // Yellow writeText(`Terminal Size: ${term.width}x${term.height}`); moveTo(4, 2); writeText(`Cursor Position: (${x}, ${y})`); // Draw frame for (let row = 5; row < term.height - 1; row++) { moveTo(row, 1); setColor(34); // Blue writeText("|"); moveTo(row, term.width); writeText("|"); } // Display cursor marker moveTo(y, x); setColor(32); // Green writeText("●"); // Instructions moveTo(term.height, 2); setColor(37); // White writeText("Arrow keys: move | q: quit"); // Flush buffer (reflect to screen all at once) flush(); } // Initial render render(); // 2. Listen for key input process.stdin.on("data", (data: string) => { const { char, key } = parseKey(data); // Process key switch (key) { case "CTRL_C": restoreTerminal(); process.exit(0); return; case "UP": if (y > 5) y--; break; case "DOWN": if (y < term.height - 1) y++; break; case "LEFT": if (x > 2) x--; break; case "RIGHT": if (x < term.width - 1) x++; break; } if (char === "q" || char === "Q") { restoreTerminal(); process.exit(0); return; } render(); }); } main(); ``` Save this as program\.ts and run it: ``` npx tsx program.ts ``` Operation: arrow keys move the cursor marker \(●\),`q`or`Ctrl\+C`quits\. --- ## Low\-Level Implementation: Direct termios Manipulation[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#low-level-implementation-direct-termios-manipulation) The high\-level version uses`process\.stdin\.setRawMode\(true\)`, which handles termios internally\. To see what’s happening underneath, here’s a low\-level version using Node\.js FFI to directly manipulate termios flags\. ``` import { execSync } from "child_process"; import process from "process"; // ───────────────────────────────────────────── // Low-level termios manipulation via stty // ───────────────────────────────────────────── // termios flag reference (what each flag controls): // // Input flags (c_iflag): // IGNBRK - Ignore break condition // BRKINT - Signal interrupt on break // PARMRK - Mark parity errors // ISTRIP - Strip 8th bit // INLCR - Translate NL to CR on input // IGNCR - Ignore CR on input // ICRNL - Translate CR to NL on input // IXON - Enable XON/XOFF flow control // // Output flags (c_oflag): // OPOST - Post-process output // // Local flags (c_lflag): // ECHO - Echo input characters // ECHONL - Echo NL even if ECHO is off // ICANON - Canonical mode (line buffering) // ISIG - Enable signals (Ctrl+C, Ctrl+Z) // IEXTEN - Extended input processing // // Control flags (c_cflag): // CSIZE - Character size mask // PARENB - Parity enable // CS8 - 8 bits per character function saveTerminalState(): string { // Save all current termios settings return execSync("stty -g", { stdio: ["inherit", "pipe", "pipe"], encoding: "utf8" }).trim(); } function restoreTerminalState(savedState: string): void { // Restore saved termios settings execSync(`stty ${savedState}`, { stdio: "inherit" }); } function enableRawMode(): void { execSync("stty raw -echo -isig -icanon -iexten -opost min 1 time 0", { stdio: "inherit", }); } function getTerminalSize(): { width: number; height: number } { // Get window size try { const output = execSync("stty size", { stdio: ["inherit", "pipe", "pipe"], encoding: "utf8" }).trim(); const [rows, cols] = output.split(" ").map(Number); return { width: cols || 80, height: rows || 24 }; } catch { return { width: 80, height: 24 }; // Default values } } // ───────────────────────────────────────────── // Screen control and buffering (same as high-level) // ───────────────────────────────────────────── let outputBuffer = ""; function bufferWrite(s: string): void { outputBuffer += s; } function flush(): void { process.stdout.write(outputBuffer); outputBuffer = ""; } function clear(): void { bufferWrite("\x1b[2J\x1b[H"); } function moveTo(row: number, col: number): void { bufferWrite(`\x1b[${row};${col}H`); } function setColor(fg: number): void { bufferWrite(`\x1b[${fg}m`); } function writeText(s: string): void { bufferWrite(s); } // ───────────────────────────────────────────── // Input processing (same parsing logic) // ───────────────────────────────────────────── interface KeyEvent { char: string | null; key: string | null; } function parseKey(buf: Buffer): KeyEvent { // Ctrl+C (in raw mode with -isig, we get byte 3 directly) if (buf[0] === 3) { return { char: null, key: "CTRL_C" }; } // Escape sequences if (buf[0] === 27 && buf[1] === 91) { // ESC[ = 0x1b 0x5b switch (buf[2]) { case 65: return { char: null, key: "UP" }; // A case 66: return { char: null, key: "DOWN" }; // B case 67: return { char: null, key: "RIGHT" }; // C case 68: return { char: null, key: "LEFT" }; // D } } if (buf[0] === 27) return { char: null, key: "ESC" }; // Normal character return { char: String.fromCharCode(buf[0]), key: null }; } // ───────────────────────────────────────────── // Main // ───────────────────────────────────────────── function main(): void { // Save original termios state const savedState = saveTerminalState(); // Enable Raw Mode (direct termios flag manipulation) enableRawMode(); // Read stdin as raw bytes (not utf8 strings) process.stdin.resume(); // Always restore on exit const cleanup = (): void => { bufferWrite("\x1b[?25h"); // Show cursor bufferWrite("\x1b[0m"); // Reset color flush(); restoreTerminalState(savedState); }; process.on("exit", cleanup); // Detect window size changes (SIGWINCH) let { width, height } = getTerminalSize(); process.on("SIGWINCH", () => { const size = getTerminalSize(); width = size.width; height = size.height; }); // Cursor position let x = Math.floor(width / 2); let y = Math.floor(height / 2); function render(): void { clear(); // Title moveTo(1, Math.floor(width / 2) - 15); setColor(36); // Cyan writeText("TUI Demo (Low-level termios API)"); // termios info moveTo(3, 2); setColor(33); // Yellow writeText("Using stty (tcgetattr/tcsetattr under the hood)"); moveTo(4, 2); writeText(`Terminal Size: ${width}x${height}`); moveTo(5, 2); writeText(`Cursor Position: (${x}, ${y})`); // Explanation of configured flags moveTo(7, 2); setColor(37); // White writeText("Raw Mode flags:"); moveTo(8, 4); writeText("- ICANON off: Line buffering disabled"); moveTo(9, 4); writeText("- ECHO off: Echo back disabled"); moveTo(10, 4); writeText("- ISIG off: Signal generation disabled"); moveTo(11, 4); writeText("- VMIN=1, VTIME=0: Read 1 byte immediately"); // Draw frame for (let row = 13; row < height - 1; row++) { moveTo(row, 1); setColor(34); // Blue writeText("|"); moveTo(row, width); writeText("|"); } // Cursor marker moveTo(y, x); setColor(32); // Green writeText("●"); // Instructions moveTo(height, 2); setColor(37); // White writeText("Arrow keys: move | q: quit"); flush(); } render(); // Read raw bytes process.stdin.on("data", (data: Buffer) => { const { char, key } = parseKey(data); switch (key) { case "CTRL_C": cleanup(); process.exit(0); return; case "UP": if (y > 13) y--; break; case "DOWN": if (y < height - 1) y++; break; case "LEFT": if (x > 2) x--; break; case "RIGHT": if (x < width - 1) x++; break; } if (char === "q" || char === "Q") { cleanup(); process.exit(0); return; } render(); }); } main(); ``` With this low\-level implementation, you can see what each termios flag specifically controls, how POSIX’s`tcgetattr`/`tcsetattr`map to`stty`commands, and what`process\.stdin\.setRawMode\(true\)`is doing for you behind the scenes\. For true native access in Node\.js without shelling out, you’d use`node\-ffi\-napi`to call libc’s`tcgetattr`/`tcsetattr`directly, or a native addon\. --- ## The Full Picture[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#the-full-picture) ``` ┌───────────────────────────────────────────────────── │ YOUR SCREEN │ │ ┌─ Terminal (iTerm2, Alacritty, etc.) ────────── │ │ │ │ $ grep -r "TODO" ./src │ │ src/app.js: // TODO fix this │ │ src/util.js: // TODO refactor │ │ $ _ │ │ │ └──────────────────┬──────────────────────────── │ └─────────────────────┼─────────────────────────────── │ ┌─────────┴───────── │ TTY device Kernel layer │ /dev/pts/1 (the plumbing) │ │ Line Discipline │ termios flags └─────────┬───────── │ ┌─────────┴───────── │ Shell Interprets commands │ (zsh, bash) Runs programs └─────────┬───────── │ ┌─────────┴───────── │ grep process The actual program │ your shell launched └─────────────────── And if all of this is on a monitor + keyboard plugged directly into the machine? Then the terminal is also the CONSOLE. ``` --- ## Quick Reference: When Things Break[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#quick-reference-when-things-break) ``` Problem Which layer? ────────────────────────────── ───────────── Colors look wrong Terminal Tab completion broke Shell SSH session acting strange TTY GUI is dead, need to log in Console Script works in bash not zsh Shell Font rendering is ugly Terminal Ctrl+C not killing process TTY / Line Discipline TUI app not getting keypresses termios (Raw Mode) Screen flickers on redraw Buffering Layout breaks on window resize SIGWINCH handling ``` --- ## Summary: What We Covered[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#summary-what-we-covered) This was a summarized view of the full stack of terminal knowledge, from vocabulary to implementation: **Terminology and concepts**: the relationships between Terminal, Shell, TTY, Console, Line Discipline, termios, PTY, POSIX, and how they all fit together\. **Five elements of TUI development**: terminal mode settings \(Canonical/Non\-Canonical/Raw\), input processing \(parsing escape sequences and control characters\), screen control \(ANSI escape sequences for cursor, color, clearing, alternate screen buffer\), terminal size management \(SIGWINCH\), and buffering \(preventing flicker\)\. **Implementation**: a high\-level Node\.js TUI using`process\.stdin\.setRawMode\(\)`, and a low\-level version directly manipulating termios flags via`stty`to show what the high\-level API does underneath\. Both demonstrate the same five elements\. Understanding these layers means you know where to look when something breaks\. And knowing where to look is half of debugging\. --- ## References[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#references) ### Specifications and Standards[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#specifications-and-standards) - [POSIX\.1\-2024 – General Terminal Interface](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap11.html) - termios\(3\) – Linux manual page - [TTY Line Discipline – Linux Kernel Documentation](https://docs.kernel.org/driver-api/tty/tty_ldisc.html) ### Wikipedia[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#wikipedia) - [Computer terminal](https://en.wikipedia.org/wiki/Computer_terminal) - [Terminal emulator](https://en.wikipedia.org/wiki/Terminal_emulator) - [Text\-based user interface](https://en.wikipedia.org/wiki/Text-based_user_interface) - [POSIX terminal interface](https://en.wikipedia.org/wiki/POSIX_terminal_interface) - [Pseudoterminal](https://en.wikipedia.org/wiki/Pseudoterminal) - [ANSI escape code](https://en.wikipedia.org/wiki/ANSI_escape_code) ### Tutorials and Documentation[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#tutorials-and-documentation) - [Serial Programming/termios – Wikibooks](https://en.wikibooks.org/wiki/Serial_Programming/termios) - [Node\.js TTY documentation](https://nodejs.org/api/tty.html) - [ioctl\(2\) – Linux manual page](https://man7.org/linux/man-pages/man2/ioctl.2.html) ### Courses[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#courses) I also happen to have a course on building Node\.js based CLIs with 20\+ projects\. You can check it out at[NodeCLI\.com](https://nodecli.com/), the first 12 videos are also available for free in the[NodejsBeginner\.com](https://nodejsbeginner.com/)course\. The entire course exercises are open source in this[Node\.js CLI Tips and Tricks](https://github.com/ahmadawais/Node-CLI-Tips-Tricks)repo\. I recorded it in 2020, but most of the info should hold true\. ## What’s next?[\#](https://ahmadawais.com/the-full-stack-of-terminals-explained-terminal-shell-tty-console-posix-ansi-escapes-ptys/#whats-next) Check out hundreds of open source CLIs I have published on my[GitHub account](https://github.com/AhmadAwais)and we’re about to launch[Command Code](https://commandcode.ai/), the first coding agent that can learn your coding taste as you use it\. And yes, it’s a CLI as well\. Use your code for good\! Peace\! ✌️

Similar Articles

The terminal, the TTY, and the shell

Lobsters Hottest

A technical educational article explaining the three distinct layers that make up what users perceive as 'the terminal': the terminal emulator, the TTY/pseudo-terminal, and the shell. The article aims to give developers a precise understanding of these components on Unix-like systems.

The TTY Demystified (2008)

Hacker News Top

A detailed explanation of the TTY subsystem in Linux and UNIX, covering its history from teletypes to modern emulated terminals, and the role of line disciplines.

all the ways in which terminals' text rendering is bad (2024)

Lobsters Hottest

The article explores various fundamental problems with text rendering in terminal emulators, including character definition ambiguity, Unicode handling issues, flawed 2D grid assumptions, and cursor desyncs, highlighting the difficulty of supporting complex scripts and fonts.