Frontend Labs
⋯

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.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36