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:
- Implement terminal raw mode for character-by-character input
- Use ANSI escape sequences for cursor positioning and screen control
- Design text buffer data structures (gap buffer or array of lines)
- Implement undo/redo with proper memory management
- Handle file I/O safely with error handling
- Build a responsive, interactive application in C
- Debug memory issues in a complex, stateful program
- 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
- Open and display a file (or start empty)
- Navigate with arrow keys (up, down, left, right)
- Insert characters at cursor position
- Delete characters with Backspace and Delete
- Create new lines with Enter
- Save file with Ctrl-S
- Quit with Ctrl-Q (warn if unsaved)
- Undo/Redo with Ctrl-Z and Ctrl-Y
Extended Features
- Search with Ctrl-F
- Line numbers in left gutter
- Status bar showing filename, position, modified state
- Syntax highlighting (basic keyword coloring)
- Multiple buffers/tabs
- 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
.minircfor 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
- โ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
- โ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
- โ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()
- โ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
- โ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
- โ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
- Build Your Own Text Editor - The Kilo tutorial by Antirez, highly recommended starting point
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:
- Systems Programming: Terminal I/O, escape codes, raw mode
- Data Structures: Gap buffers or line arrays, undo stacks
- Memory Management: Complex ownership, no leaks under stress
- User Experience: Responsive, handles edge cases, doesnโt crash
- 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.