25. Capstone: a kilo-style terminal text editor
kilo (by Salvatore Sanfilippo, the same author as Redis) is a ~1000-line
text editor that fits in a single C file and is a rite of passage for
learning raw terminal I/O. Here is the core mechanism — putting the
terminal into raw mode — which is the part every tutorial-follower
finds most magical:
#include <termios.h>
#include <unistd.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
static struct termios orig_termios;
void disable_raw_mode(void) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); // restore on exit
}
void enable_raw_mode(void) {
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(disable_raw_mode); // guarantee terminal is restored even on early exit
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN); // no echo, read byte-by-byte, no Ctrl-C/Z, no Ctrl-V
raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);
raw.c_oflag &= ~(OPOST); // don't translate \n to \r\n on output
raw.c_cc[VMIN] = 0; // read() returns as soon as any input is available
raw.c_cc[VTIME] = 1; // ...or after 100ms with nothing, whichever first
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
int main(void) {
enable_raw_mode();
printf("Raw mode on. Press 'q' to quit.\r\n");
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
if (c == '\r') { printf("\r\n"); continue; }
if (iscntrl((unsigned char)c)) printf("%d\r\n", c);
else printf("%d ('%c')\r\n", c, c);
}
return 0; // disable_raw_mode runs automatically via atexit
}
By default, your terminal is in canonical mode: it buffers a whole
line and does its own editing (backspace, etc.) before your program ever
sees a byte, and Ctrl-C sends SIGINT before you can intercept it. Raw
mode turns all of that off so your program becomes the terminal driver
— this is exactly what vim, nano, less, and every TUI application
does on startup, and exactly why they all restore the terminal on exit
(via atexit, as here, or a signal handler) — a crash that skips this
step is the classic "my terminal is broken, why does nothing echo" bug
you fix by blindly typing reset and pressing enter.
From here, a full kilo adds: reading the whole file into a line array
(the dynamic-array pattern from Section 16), rendering a screen buffer
with ANSI escape codes, and mapping arrow-key escape sequences
(\x1b[A, \x1b[B, …) to cursor movement — all built from primitives
you already have.