C++ Text Editor Project
A real terminal editor: open files, type, move, search, save — like kilo or nano.
What you will build
Build a text editor that runs in the terminal: put the terminal into raw mode, render a screenful of text, move a cursor with the arrow keys, edit, search, and save. It's a rite-of-passage systems project — you control every byte in and out.
What you will learn
- Raw terminal mode with termios
- Reading escape sequences for arrow keys
- Gap buffer or piece-table text storage
- Screen rendering with a scroll offset
Feature checklist
- Open and save files
- Cursor movement in all directions
- Insert and delete characters and lines
- Scrolling for files longer than the screen
- Search (Ctrl+F) with jump-to-match
- Status bar: filename, line count, position, dirty flag
Starter code
// Terminal editor core (POSIX). Editor state:
#include <string>
#include <vector>
struct Editor {
std::vector<std::string> rows; // the document
int cx = 0, cy = 0; // cursor
int rowOff = 0; // first visible row
bool dirty = false;
std::string filename;
void insertChar(char c) {
if (cy >= (int)rows.size()) rows.push_back("");
rows[cy].insert(cx, 1, c);
++cx; dirty = true;
}
void insertNewline() {
if (cy >= (int)rows.size()) { rows.push_back(""); ++cy; cx = 0; return; }
std::string tail = rows[cy].substr(cx);
rows[cy].erase(cx);
rows.insert(rows.begin() + cy + 1, tail);
++cy; cx = 0; dirty = true;
}
void deleteChar() { // backspace
if (cx > 0) { rows[cy].erase(cx - 1, 1); --cx; }
else if (cy > 0) { // join with previous line
cx = (int)rows[cy - 1].size();
rows[cy - 1] += rows[cy];
rows.erase(rows.begin() + cy);
--cy;
}
dirty = true;
}
};
// Rendering: on each keypress, hide cursor, redraw visible
// rows with escape codes, place cursor, show cursor.Full walkthrough
Step 1: Model the document as rows of text
struct Editor { std::vector<std::string> rows; int cx, cy; int rowOff; bool dirty; std::string filename; } — the document is a vector of lines, the cursor is (cx, cy) INTO those lines, rowOff is the first visible row for scrolling, and dirty tracks unsaved changes. Every editor feature is a manipulation of these six fields.
Step 2: Put the terminal into raw mode
Using termios, save the terminal's original flags, then clear ECHO and ICANON so keystrokes arrive one at a time, unbuffered, without waiting for Enter. Restore the saved flags on EVERY exit path — a crashed editor that leaves the terminal raw leaves the user's shell echo-less, the rudest bug in this project. An atexit hook or RAII guard is the professional way to guarantee restoration.
Step 3: insertChar() and insertNewline(): the editing core
insertChar(c): if the cursor is below the document, push an empty row; rows[cy].insert(cx, 1, c); ++cx; dirty = true. insertNewline() splits the current row at cx: the tail becomes a new row below, cursor to its start. Delete is the mirror: erase at cx, or at line start, join with the row above. Four operations — insert char, split line, delete char, join line — are 90% of a text editor.
void insertChar(char c) {
if (cy >= (int)rows.size()) rows.push_back("");
rows[cy].insert(cx, 1, c);
++cx; dirty = true;
}Step 4: Move the cursor with clamping rules
Arrow keys arrive as escape sequences (ESC [ A/B/C/D) — parse them in the input loop. Movement rules: cx can never exceed the current row's length; moving to a shorter row clamps cx. Decide on the 'goal column' behaviour (remembering the intended column across short rows) — editors differ here, and noticing the choice is the education.
Step 5: Render a screenful with scrolling
The screen shows rows[rowOff .. rowOff + screenRows): adjust rowOff so cy is always visible (scroll up when cy < rowOff, down when cy passes the bottom). Redraw with ANSI escapes (home the cursor, clear, print rows, print the status bar, place the cursor). The status bar shows filename, line count, cursor position, and the dirty flag — the checklist's exact list.
Step 6: Open and save files as row splits
Load: read the file with std::getline into rows (an empty file = one empty row). Save: join rows with '\n' back through std::ofstream and clear dirty. Guard the quit command: if dirty, demand a confirmation (or a second quit press) — silently discarding edits is the one unforgivable editor sin.
Step 7: Add search (Ctrl+F)
Prompt for a needle, then scan rows from the cursor position with std::string::find, wrapping around at the end; on a hit, move cy/cx to the match (adjusting rowOff), on a miss, say 'not found' in the status bar. Search reuses everything: it is just cursor movement with a destination computed from content.
Common mistakes
- Leaving the terminal in raw mode after exit or a crash — always restore saved termios flags, ideally via RAII.
- Letting cx sit beyond the end of a shorter row after vertical movement, so the next insert lands in the void or crashes.
- Joining/splitting rows on delete/insert without setting dirty, so the editor happily quits over unsaved work.
Compile and run
g++ -std=c++17 -o editor editor.cppThe starter core is POSIX: it needs termios for raw terminal mode, so it builds on Linux/macOS (on Windows, use WSL, or port the input layer to <conio.h>). No extra libraries.
Sample run
$ ./editor notes.txt
(the editor opens in the terminal — screen snapshot)
+------------------------------------------------+
| shopping list |
| - rice |
| - beans |
| |
+------------------------------------------------+
notes.txt — 3 lines · Ln 2, Col 3 · modified
(you type 'eggs' on a new line, then Ctrl+S to save)
+------------------------------------------------+
| shopping list |
| - rice |
| - beans |
| - eggs |
+------------------------------------------------+
notes.txt — 4 lines · Ln 4, Col 7 · saved
(Ctrl+F, you search for 'beans')
search: beans -> found on line 3
(Ctrl+Q — no unsaved changes, the editor exits)
$Try it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.