Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Text Editor Project

Advanced•1–2 weeks•raw terminal mode, gap buffer, file I/O

A real terminal editor: open files, type, move, search, save — like kilo or nano.

Share on X

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

Feature checklist

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

Compile and run

g++ -std=c++17 -o editor editor.cpp

The 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.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS