Project 6: Mini Text Editor (Capstone)

Project 6: Mini Text Editor (Capstone)

Sprint: 1 - Memory & Control Difficulty: Advanced Time Estimate: 2-4 weeks Prerequisites: All previous Sprint 1 projects


Overview

What youโ€™ll build: A terminal-based text editor (think nano/micro) from scratch in C, with undo/redo, search, and file I/O.

Why it teaches everything: A text editor is the ultimate memory management crucible. You need:

  • Dynamic buffer management (growing arrays of lines)
  • String manipulation everywhere (every keystroke modifies strings)
  • Careful lifetime management (undo history holds old versions)
  • No leaks (users run editors for hours)
  • Performance (responsive to every keypress)

The Core Question Youโ€™re Answering:

โ€œCan I build a real, usable application that manages complex memory lifetimes without leaking or crashing?โ€

A text editor is the ultimate test of memory management skills. Users run editors for hours. Every keystroke modifies data structures. Undo/redo holds history. If you leak, the program eventually dies. If you double-free, it crashes immediately. Thereโ€™s nowhere to hide.


Learning Objectives

By the end of this project, you will be able to:

  1. Implement terminal raw mode for character-by-character input
  2. Use ANSI escape sequences for cursor positioning and screen control
  3. Design text buffer data structures (gap buffer or array of lines)
  4. Implement undo/redo with proper memory management
  5. Handle file I/O safely with error handling
  6. Build a responsive, interactive application in C
  7. Debug memory issues in a complex, stateful program
  8. Ship working software that you can actually use

Theoretical Foundation

Terminal Raw Mode

By default, terminals operate in โ€œcookedโ€ mode:

  • Input is line-buffered (program doesnโ€™t see input until Enter)
  • Ctrl-C sends SIGINT
  • Backspace is handled by terminal
  • Echo shows what you type

Text editors need โ€œrawโ€ mode:

  • Character-by-character input (no buffering)
  • Program handles all special keys
  • No automatic echo
  • Full control over terminal behavior
Cooked Mode (default):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ User types: H e l l o [Enter]                                  โ”‚
โ”‚                                                                โ”‚
โ”‚ Program receives: "Hello\n" (all at once, after Enter)         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Raw Mode (for editors):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ User types: H e l l o [Enter]                                  โ”‚
โ”‚                                                                โ”‚
โ”‚ Program receives: 'H' then 'e' then 'l' then 'l' then 'o'     โ”‚
โ”‚                   then '\r' (immediately, one at a time)       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ANSI Escape Sequences

Text editors control the terminal using escape codes:

Common Escape Sequences:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Sequence        โ”‚ Effect                                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ \x1b[2J         โ”‚ Clear entire screen                          โ”‚
โ”‚ \x1b[H          โ”‚ Move cursor to home position (1,1)           โ”‚
โ”‚ \x1b[{r};{c}H   โ”‚ Move cursor to row r, column c               โ”‚
โ”‚ \x1b[K          โ”‚ Clear from cursor to end of line             โ”‚
โ”‚ \x1b[?25l       โ”‚ Hide cursor                                  โ”‚
โ”‚ \x1b[?25h       โ”‚ Show cursor                                  โ”‚
โ”‚ \x1b[7m         โ”‚ Invert colors                                โ”‚
โ”‚ \x1b[m          โ”‚ Reset formatting                             โ”‚
โ”‚ \x1b[{n}A       โ”‚ Move cursor up n lines                       โ”‚
โ”‚ \x1b[{n}B       โ”‚ Move cursor down n lines                     โ”‚
โ”‚ \x1b[{n}C       โ”‚ Move cursor right n columns                  โ”‚
โ”‚ \x1b[{n}D       โ”‚ Move cursor left n columns                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The \x1b (27 in decimal) is the ESC character.
[ begins the "Control Sequence Introducer" (CSI).

Text Buffer Data Structures

Option 1: Array of Strings (Simplest)

file contents:
"Hello World"
"Goodbye World"
"End"

In memory:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ char** lines;                                                 โ”‚
โ”‚ int num_lines;                                               โ”‚
โ”‚                                                              โ”‚
โ”‚ lines[0] โ”€โ”€โ†’ "Hello World\0"   (malloc'd string)            โ”‚
โ”‚ lines[1] โ”€โ”€โ†’ "Goodbye World\0" (malloc'd string)            โ”‚
โ”‚ lines[2] โ”€โ”€โ†’ "End\0"           (malloc'd string)            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Pros: Simple, line-based operations are natural
Cons: Inserting in middle of long line is O(n)

Option 2: Gap Buffer (Recommended)

Text: "Hello World" with cursor after "Hello "

In memory (single buffer):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ H e l l o   [GAP GAP GAP GAP GAP] W o r l d                 โ”‚
โ”‚             ^gap_start           ^gap_end                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

To insert 'X':
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ H e l l o   X [GAP GAP GAP GAP] W o r l d                   โ”‚
โ”‚               ^gap_start       ^gap_end                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Pros: O(1) insert at cursor, efficient for editing at one location
Cons: Moving cursor far requires shifting text

Option 3: Piece Table (VS Code uses this)

Original file + changes tracked separately
Very efficient for large files
More complex to implement

Option 4: Rope (for very large files)

Tree of string segments
O(log n) operations
Overkill for learning project

Undo/Redo Implementation

Command Pattern: Store operations, not states

Operation Types:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ INSERT: text "Hello" at position 0                            โ”‚
โ”‚   Undo: delete 5 chars at position 0                          โ”‚
โ”‚   Redo: insert "Hello" at position 0                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ DELETE: removed "World" from position 10                      โ”‚
โ”‚   Undo: insert "World" at position 10                         โ”‚
โ”‚   Redo: delete 5 chars at position 10                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Undo Stack: [op1, op2, op3, op4] โ† most recent
Redo Stack: [] empty

User presses Ctrl-Z:
1. Pop op4 from undo stack
2. Perform reverse of op4
3. Push op4 to redo stack

Undo Stack: [op1, op2, op3]
Redo Stack: [op4]

User types new character:
1. Clear redo stack (can't redo after new edit)
2. Push new operation to undo stack

Memory Ownership in Undo:

The deleted text "World" must be saved somewhere for undo!

struct UndoAction {
    ActionType type;     // INSERT or DELETE
    int position;        // Where in document
    char* text;          // The text (OWNED by this struct)
    int length;          // Length of text
};

When you delete "World":
1. Copy "World" to newly allocated string
2. Store pointer in UndoAction
3. Actually delete from document

When you undo:
1. Reinsert the text at position
2. DON'T free the text - move to redo stack

When you redo:
1. Delete the text again
2. Move back to undo stack

When clearing redo stack (new edit after undo):
1. Free all text strings in redo stack
2. Free the UndoAction structs

Project Specification

Minimum Viable Features

  1. Open and display a file (or start empty)
  2. Navigate with arrow keys (up, down, left, right)
  3. Insert characters at cursor position
  4. Delete characters with Backspace and Delete
  5. Create new lines with Enter
  6. Save file with Ctrl-S
  7. Quit with Ctrl-Q (warn if unsaved)
  8. Undo/Redo with Ctrl-Z and Ctrl-Y

Extended Features

  1. Search with Ctrl-F
  2. Line numbers in left gutter
  3. Status bar showing filename, position, modified state
  4. Syntax highlighting (basic keyword coloring)
  5. Multiple buffers/tabs
  6. Copy/paste with selection

Expected Interface

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ hello.c - Modified                                                  Line 3/42  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  1  #include <stdio.h>                                                          โ”‚
โ”‚  2  #include <stdlib.h>                                                         โ”‚
โ”‚  3  int main() {โ–ˆ                                                               โ”‚
โ”‚  4      printf("Hello World\n");                                                โ”‚
โ”‚  5      return 0;                                                               โ”‚
โ”‚  6  }                                                                           โ”‚
โ”‚ ~                                                                               โ”‚
โ”‚ ~                                                                               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Ctrl-S: Save | Ctrl-Q: Quit | Ctrl-Z: Undo | Ctrl-F: Find                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Solution Architecture

Component Organization

mini_editor/
โ”œโ”€โ”€ main.c           # Entry point, main loop
โ”œโ”€โ”€ editor.c         # Core editor state and operations
โ”œโ”€โ”€ editor.h         # Editor struct and API
โ”œโ”€โ”€ buffer.c         # Text buffer (gap buffer implementation)
โ”œโ”€โ”€ buffer.h         # Buffer API
โ”œโ”€โ”€ terminal.c       # Raw mode, escape sequences
โ”œโ”€โ”€ terminal.h       # Terminal API
โ”œโ”€โ”€ undo.c           # Undo/redo stack management
โ”œโ”€โ”€ undo.h           # Undo API
โ”œโ”€โ”€ input.c          # Key reading and mapping
โ”œโ”€โ”€ input.h          # Input API
โ””โ”€โ”€ Makefile         # Build configuration

Data Structures

// buffer.h - Text storage
typedef struct {
    char* data;           // The actual text + gap
    int gap_start;        // Index where gap begins
    int gap_end;          // Index where gap ends
    int capacity;         // Total allocated size
} GapBuffer;

// Or simpler array-of-lines:
typedef struct {
    char** lines;         // Array of line strings
    int num_lines;        // Number of lines
    int capacity;         // Allocated slots for lines
} TextBuffer;

// editor.h - Editor state
typedef struct {
    TextBuffer buffer;    // The document
    char* filename;       // Current file (NULL if new)
    int cursor_x;         // Cursor column (0-indexed)
    int cursor_y;         // Cursor row (0-indexed)
    int scroll_offset;    // First visible line
    int modified;         // Has unsaved changes
    UndoStack undo;       // Undo history
    UndoStack redo;       // Redo history
} Editor;

// undo.h - Undo/redo
typedef enum { ACTION_INSERT, ACTION_DELETE, ACTION_NEWLINE } ActionType;

typedef struct {
    ActionType type;
    int x, y;             // Position of action
    char* text;           // Text involved (owned)
    int length;
} UndoAction;

typedef struct {
    UndoAction* actions;  // Dynamic array
    int count;
    int capacity;
} UndoStack;

Main Loop Structure

int main(int argc, char* argv[]) {
    // Initialize
    terminal_enable_raw_mode();
    atexit(terminal_disable_raw_mode);

    Editor editor;
    editor_init(&editor);

    if (argc > 1) {
        editor_open(&editor, argv[1]);
    }

    // Main loop
    while (1) {
        editor_refresh_screen(&editor);

        int key = terminal_read_key();

        if (key == CTRL_KEY('q')) {
            if (editor.modified) {
                // Warn about unsaved changes
            }
            break;
        }

        editor_process_key(&editor, key);
    }

    // Cleanup
    editor_free(&editor);
    return 0;
}

Implementation Guide

Phase 1: Terminal I/O (Weekend 1)

Goal: Read individual keypresses and position cursor.

Step 1.1: Enable raw mode

#include <termios.h>
#include <unistd.h>

struct termios orig_termios;

void terminal_disable_raw_mode() {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}

void terminal_enable_raw_mode() {
    tcgetattr(STDIN_FILENO, &orig_termios);
    atexit(terminal_disable_raw_mode);

    struct termios raw = orig_termios;
    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
    raw.c_oflag &= ~(OPOST);
    raw.c_cflag |= (CS8);
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 1;

    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

Step 1.2: Read keys

int terminal_read_key() {
    char c;
    while (read(STDIN_FILENO, &c, 1) != 1) {
        // Timeout handling
    }

    if (c == '\x1b') {
        // Handle escape sequences (arrow keys, etc.)
        char seq[3];
        if (read(STDIN_FILENO, &seq[0], 1) != 1) return '\x1b';
        if (read(STDIN_FILENO, &seq[1], 1) != 1) return '\x1b';

        if (seq[0] == '[') {
            switch (seq[1]) {
                case 'A': return ARROW_UP;
                case 'B': return ARROW_DOWN;
                case 'C': return ARROW_RIGHT;
                case 'D': return ARROW_LEFT;
            }
        }
        return '\x1b';
    }
    return c;
}

Step 1.3: Clear screen and position cursor

void terminal_clear_screen() {
    write(STDOUT_FILENO, "\x1b[2J", 4);  // Clear screen
    write(STDOUT_FILENO, "\x1b[H", 3);   // Cursor to home
}

void terminal_move_cursor(int row, int col) {
    char buf[32];
    snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, col + 1);
    write(STDOUT_FILENO, buf, strlen(buf));
}

Test: Display โ€œHelloโ€ and move cursor with arrow keys.

Phase 2: Text Display (Weekend 2)

Goal: Load file and display with scrolling.

Step 2.1: Load file into buffer

void editor_open(Editor* e, const char* filename) {
    FILE* fp = fopen(filename, "r");
    if (!fp) {
        // Handle new file
        return;
    }

    char* line = NULL;
    size_t cap = 0;
    ssize_t len;

    while ((len = getline(&line, &cap, fp)) != -1) {
        // Remove trailing newline
        while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r'))
            len--;
        line[len] = '\0';

        // Add to buffer
        buffer_append_line(&e->buffer, line, len);
    }

    free(line);
    fclose(fp);
    e->filename = strdup(filename);
}

Step 2.2: Render buffer to screen

void editor_refresh_screen(Editor* e) {
    // Hide cursor during redraw
    write(STDOUT_FILENO, "\x1b[?25l", 6);
    terminal_move_cursor(0, 0);

    // Draw each visible line
    int screen_rows = terminal_get_rows() - 2;  // Leave room for status
    for (int y = 0; y < screen_rows; y++) {
        int file_row = y + e->scroll_offset;
        if (file_row < e->buffer.num_lines) {
            write(STDOUT_FILENO, e->buffer.lines[file_row],
                  strlen(e->buffer.lines[file_row]));
        } else {
            write(STDOUT_FILENO, "~", 1);  // Empty line marker
        }
        write(STDOUT_FILENO, "\x1b[K", 3);  // Clear rest of line
        write(STDOUT_FILENO, "\r\n", 2);
    }

    // Draw status bar
    editor_draw_status_bar(e);

    // Position cursor
    terminal_move_cursor(e->cursor_y - e->scroll_offset, e->cursor_x);

    // Show cursor
    write(STDOUT_FILENO, "\x1b[?25h", 6);
}

Step 2.3: Handle scrolling

void editor_scroll(Editor* e) {
    int screen_rows = terminal_get_rows() - 2;

    // Scroll up if cursor above visible area
    if (e->cursor_y < e->scroll_offset) {
        e->scroll_offset = e->cursor_y;
    }

    // Scroll down if cursor below visible area
    if (e->cursor_y >= e->scroll_offset + screen_rows) {
        e->scroll_offset = e->cursor_y - screen_rows + 1;
    }
}

Phase 3: Editing (Week 2)

Goal: Insert, delete, and save.

Step 3.1: Insert character

void editor_insert_char(Editor* e, char c) {
    // Get current line
    char* line = e->buffer.lines[e->cursor_y];
    int len = strlen(line);

    // Allocate new line with extra space
    char* new_line = malloc(len + 2);

    // Copy before cursor
    memcpy(new_line, line, e->cursor_x);
    // Insert character
    new_line[e->cursor_x] = c;
    // Copy after cursor
    memcpy(new_line + e->cursor_x + 1, line + e->cursor_x, len - e->cursor_x + 1);

    // Replace line
    free(line);
    e->buffer.lines[e->cursor_y] = new_line;

    // Move cursor
    e->cursor_x++;
    e->modified = 1;

    // Record for undo
    undo_push_insert(&e->undo, e->cursor_y, e->cursor_x - 1, &c, 1);
    undo_clear(&e->redo);  // Clear redo on new edit
}

Step 3.2: Delete character

void editor_delete_char(Editor* e) {
    if (e->cursor_x == 0 && e->cursor_y == 0) return;

    if (e->cursor_x > 0) {
        // Delete within line
        char* line = e->buffer.lines[e->cursor_y];
        int len = strlen(line);

        // Save deleted char for undo
        char deleted = line[e->cursor_x - 1];
        undo_push_delete(&e->undo, e->cursor_y, e->cursor_x - 1, &deleted, 1);

        // Shift remaining chars left
        memmove(line + e->cursor_x - 1, line + e->cursor_x, len - e->cursor_x + 1);

        e->cursor_x--;
        e->modified = 1;
    } else {
        // Merge with previous line
        // (join current line to end of previous)
        // ... more complex logic ...
    }
    undo_clear(&e->redo);
}

Step 3.3: Save file

void editor_save(Editor* e) {
    if (!e->filename) {
        // Prompt for filename
        return;
    }

    FILE* fp = fopen(e->filename, "w");
    if (!fp) {
        // Handle error
        return;
    }

    for (int i = 0; i < e->buffer.num_lines; i++) {
        fprintf(fp, "%s\n", e->buffer.lines[i]);
    }

    fclose(fp);
    e->modified = 0;
}

Phase 4: Undo/Redo (Week 3)

Goal: Full undo/redo with no memory leaks.

Step 4.1: Undo stack operations

void undo_push(UndoStack* stack, UndoAction action) {
    if (stack->count >= stack->capacity) {
        stack->capacity = stack->capacity ? stack->capacity * 2 : 8;
        stack->actions = realloc(stack->actions,
                                  stack->capacity * sizeof(UndoAction));
    }
    stack->actions[stack->count++] = action;
}

UndoAction undo_pop(UndoStack* stack) {
    return stack->actions[--stack->count];
}

void undo_clear(UndoStack* stack) {
    for (int i = 0; i < stack->count; i++) {
        free(stack->actions[i].text);  // Free owned strings!
    }
    stack->count = 0;
}

Step 4.2: Perform undo

void editor_undo(Editor* e) {
    if (e->undo.count == 0) return;

    UndoAction action = undo_pop(&e->undo);

    switch (action.type) {
        case ACTION_INSERT:
            // To undo insert, delete the text
            editor_delete_at(e, action.y, action.x, action.length);
            break;
        case ACTION_DELETE:
            // To undo delete, insert the text
            editor_insert_at(e, action.y, action.x, action.text, action.length);
            break;
    }

    // Move to redo stack (don't free the text!)
    undo_push(&e->redo, action);
}

Step 4.3: Perform redo

void editor_redo(Editor* e) {
    if (e->redo.count == 0) return;

    UndoAction action = undo_pop(&e->redo);

    switch (action.type) {
        case ACTION_INSERT:
            // Redo insert
            editor_insert_at(e, action.y, action.x, action.text, action.length);
            break;
        case ACTION_DELETE:
            // Redo delete
            editor_delete_at(e, action.y, action.x, action.length);
            break;
    }

    // Move back to undo stack
    undo_push(&e->undo, action);
}

Phase 5: Polish (Week 4+)

Search (Ctrl-F):

void editor_find(Editor* e) {
    char* query = editor_prompt(e, "Search: ");
    if (!query) return;

    for (int y = 0; y < e->buffer.num_lines; y++) {
        char* match = strstr(e->buffer.lines[y], query);
        if (match) {
            e->cursor_y = y;
            e->cursor_x = match - e->buffer.lines[y];
            editor_scroll(e);
            break;
        }
    }
    free(query);
}

Status bar:

void editor_draw_status_bar(Editor* e) {
    write(STDOUT_FILENO, "\x1b[7m", 4);  // Invert colors

    char status[80];
    int len = snprintf(status, sizeof(status), "%.20s%s - %d lines",
                       e->filename ? e->filename : "[No Name]",
                       e->modified ? " (modified)" : "",
                       e->buffer.num_lines);

    char rstatus[40];
    int rlen = snprintf(rstatus, sizeof(rstatus), "%d/%d",
                        e->cursor_y + 1, e->buffer.num_lines);

    // Pad to fill screen width
    while (len < terminal_get_cols() - rlen) {
        status[len++] = ' ';
    }
    strcpy(status + len, rstatus);

    write(STDOUT_FILENO, status, strlen(status));
    write(STDOUT_FILENO, "\x1b[m", 3);  // Reset colors
    write(STDOUT_FILENO, "\r\n", 2);
}

Testing Strategy

Memory Verification with AddressSanitizer

# Compile with sanitizer
clang -fsanitize=address -g -O1 *.c -o mini_editor

# Run through test operations
./mini_editor test.txt
# 1. Type 100 characters
# 2. Delete 50 characters
# 3. Undo 50 times
# 4. Redo 50 times
# 5. Save and quit

# Check output for errors
# Should show: no leaks, no use-after-free, no buffer overflows

Stress Test

// stress_test.c
void stress_test() {
    Editor e;
    editor_init(&e);

    // Insert many characters
    for (int i = 0; i < 10000; i++) {
        editor_insert_char(&e, 'A' + (i % 26));
    }

    // Undo all
    while (e.undo.count > 0) {
        editor_undo(&e);
    }

    // Redo all
    while (e.redo.count > 0) {
        editor_redo(&e);
    }

    editor_free(&e);
    printf("Stress test passed!\n");
}

Edge Cases

  • Empty file
  • Single character file
  • Very long lines (1000+ characters)
  • Many short lines (10000+ lines)
  • Cursor at start of file, try backspace
  • Cursor at end of file, try delete
  • Undo with empty undo stack
  • Redo after new edit (should clear redo)
  • Save to read-only location
  • Open non-existent file
  • Quit with unsaved changes

Common Pitfalls

Pitfall 1: Terminal State Corruption

// WRONG: If program crashes, terminal stays in raw mode
int main() {
    terminal_enable_raw_mode();
    // ... crash here ...
    terminal_disable_raw_mode();  // Never called!
}

// CORRECT: Use atexit to ensure cleanup
int main() {
    terminal_enable_raw_mode();
    atexit(terminal_disable_raw_mode);  // Always runs on exit
    // ... safe even if crash ...
}

Pitfall 2: Undo Memory Leaks

// WRONG: Not freeing text when clearing redo stack
void editor_insert_char(Editor* e, char c) {
    // ...
    e->redo.count = 0;  // Leaks all text in redo actions!
}

// CORRECT: Free owned memory
void editor_insert_char(Editor* e, char c) {
    // ...
    undo_clear(&e->redo);  // Properly frees all text
}

Pitfall 3: Off-by-One in Line Operations

// WRONG: Inserting newline doesn't account for current line content
void editor_insert_newline(Editor* e) {
    // Need to split current line at cursor position
    // Move text after cursor to new line
}

// Be very careful with:
// - cursor_x vs line length
// - cursor_y vs num_lines
// - scroll_offset boundaries

Pitfall 4: Forgetting Screen Refresh

// WRONG: Data changed but screen not updated
void editor_process_key(Editor* e, int key) {
    switch (key) {
        case ARROW_DOWN:
            e->cursor_y++;
            // Forgot to scroll check!
            break;
    }
}

// CORRECT: Always check scroll after cursor movement
void editor_process_key(Editor* e, int key) {
    switch (key) {
        case ARROW_DOWN:
            if (e->cursor_y < e->buffer.num_lines - 1) {
                e->cursor_y++;
            }
            editor_scroll(e);  // Adjust scroll if needed
            break;
    }
}

Pitfall 5: Not Handling Long Lines

// WRONG: Assuming line fits on screen
void editor_render_line(Editor* e, int y) {
    char* line = e->buffer.lines[y];
    write(STDOUT_FILENO, line, strlen(line));  // May overflow screen!
}

// CORRECT: Truncate or wrap
void editor_render_line(Editor* e, int y) {
    char* line = e->buffer.lines[y];
    int len = strlen(line);
    int screen_cols = terminal_get_cols();
    if (len > screen_cols) {
        len = screen_cols;  // Truncate
    }
    write(STDOUT_FILENO, line, len);
}

Extensions and Challenges

Challenge 1: Syntax Highlighting (Medium)

Implement basic keyword coloring:

// Define color escape codes
#define COLOR_KEYWORD  "\x1b[33m"  // Yellow
#define COLOR_STRING   "\x1b[32m"  // Green
#define COLOR_COMMENT  "\x1b[36m"  // Cyan
#define COLOR_RESET    "\x1b[m"

// Detect and color keywords
char* keywords[] = {"if", "else", "while", "for", "return", NULL};

Challenge 2: Multiple Buffers (Hard)

  • Tab switching between files
  • Split view (horizontal/vertical)
  • Buffer list command

Challenge 3: Selection and Copy/Paste (Medium)

  • Shift+arrow for selection
  • Visual mode highlighting
  • System clipboard integration

Challenge 4: Configuration File (Medium)

  • Read .minirc for settings
  • Tab width, color scheme, keybindings
  • Parse simple key=value format

Challenge 5: Plugin System (Hard)

  • Lua scripting integration
  • Hook points for key events
  • Custom commands

Interview Preparation

Common Questions

  1. โ€œWhat data structure would you use to represent text in an editor? Why?โ€
    • Array of strings: simple, good for line-based operations
    • Gap buffer: O(1) insert at cursor, good for local edits
    • Piece table: efficient for large files with few changes
    • Rope: O(log n) everything, good for very large files
    • Trade-offs depend on expected usage patterns
  2. โ€œHow would you implement undo/redo?โ€
    • Command pattern: store operations, not states
    • Each edit creates an UndoAction with type, position, text
    • Undo reverses the operation, pushes to redo stack
    • Redo re-applies, pushes back to undo stack
    • New edit clears redo stack
  3. โ€œHow do you handle terminal input in raw mode?โ€
    • Use termios to disable canonical mode and echo
    • Read character by character
    • Handle escape sequences for arrow keys
    • Restore terminal state on exit with atexit()
  4. โ€œWhat happens if the user opens a 1GB file?โ€
    • Array of strings: may work if RAM available
    • Consider memory-mapped I/O
    • Only load visible portion + buffer
    • Virtual scrolling
  5. โ€œHow do you ensure your editor doesnโ€™t leak memory over time?โ€
    • Careful ownership semantics
    • Free text when undo actions are discarded
    • Use AddressSanitizer during development
    • Test with long editing sessions
  6. โ€œHow would you search for text efficiently in a large file?โ€
    • Simple: strstr for each line
    • Better: Boyer-Moore algorithm
    • Indexed: build suffix array for repeated searches
    • Trade-off between build time and search time

Self-Assessment Checklist

Functionality (Does It Work?)

  • Can open existing file
  • Can create new file
  • Arrow keys move cursor correctly
  • Type characters, they appear
  • Backspace deletes correctly
  • Enter creates new line
  • Save works, file is correct
  • Quit works, warns if unsaved
  • Undo reverses last change
  • Redo reverses undo
  • Multiple undo/redo in sequence

Memory Safety (Is It Safe?)

  • AddressSanitizer shows no errors
  • Valgrind shows no leaks
  • Can edit for extended session without crash
  • Opening large file doesnโ€™t crash
  • Stress test with many edits passes

Code Quality (Is It Good?)

  • Clean separation of concerns
  • Consistent naming conventions
  • Minimal global state
  • Error handling for file I/O
  • Comments for complex logic

Polish (Is It Usable?)

  • Status bar shows useful info
  • Scroll works smoothly
  • Cursor doesnโ€™t go out of bounds
  • Screen doesnโ€™t flicker
  • Terminal restored on exit

Resources

Essential Reading

Topic Book Chapter
Terminal programming โ€œAdvanced Programming in the UNIX Environmentโ€ Ch. 18
C systems programming โ€œThe C Programming Languageโ€ All
Memory management โ€œUnderstanding and Using C Pointersโ€ Ch. 1-4
UNIX I/O โ€œThe Linux Programming Interfaceโ€ Ch. 4-5, 62

Tutorial

Reference Implementations

  • Kilo - Antirezโ€™s minimal editor (~1000 lines)
  • Micro - Modern terminal editor (Go, good reference)
  • Nano - GNU nano source code

Summary

The Mini Text Editor capstone proves you can build real software with C:

  1. Systems Programming: Terminal I/O, escape codes, raw mode
  2. Data Structures: Gap buffers or line arrays, undo stacks
  3. Memory Management: Complex ownership, no leaks under stress
  4. User Experience: Responsive, handles edge cases, doesnโ€™t crash
  5. Software Engineering: Clean architecture, testable components

After completion:

  • You can demo a real application you built
  • You understand memory management at a deep level
  • Youโ€™ve debugged complex, stateful code
  • Youโ€™ve shipped something useful
  • Youโ€™ve crossed from โ€œlearning Cโ€ to โ€œbuilding with Cโ€

This is the ultimate proof that you understand Sprint 1โ€™s core question: what is memory, and how do you control it?


Congratulations on completing Sprint 1: Memory & Control!

Youโ€™ve gone from โ€œwhat is a pointer?โ€ to building a real text editor. You understand memory at the level that separates professional C programmers from beginners. Youโ€™re ready for Sprint 2: Data & Invariants.