Honeymoon Architecture


Version: 0.1 (Alpha)
Standard: C++20

Honeymoon Architecture

Honeymoon is a terminal editor written in C++20. It bypasses libraries like ncurses to interact directly with the terminal driver via termios and ANSI escape codes.


1. Event Loop

The core is a synchronous blocking loop. It renders the state to a memory buffer, flushes it to the terminal, and waits for a single keypress.

// src/editor.hpp
void run() {
    while (!should_quit) { 
        refresh_screen(); 
        process_keypress(); 
    }
    terminal.write_raw("\x1b[2J\x1b[H"); // Clear on exit
}

2. Raw Mode & Termios

We disable Canonical Mode (line buffering) and Echo to take full control of the terminal. This allows us to process every keystroke immediately.

// src/terminal.hpp
bool enable_raw_mode() {
    tcgetattr(STDIN_FILENO, &orig_termios);
    struct termios raw = orig_termios;
    
    // Disable signals (Ctrl-C/Z), canonical mode, and echo
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
    
    // Disable software flow control
    raw.c_iflag &= ~(IXON);
    
    // Turn off output processing (e.g. \n -> \r\n translation)
    raw.c_oflag &= ~(OPOST);
    
    return tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != -1;
}
Note: If the process crashes without resetting these flags, the terminal remains broken. Use `reset` to restore it.

3. Gap Buffer

Text is stored in a Gap Buffer. This is a std::vector that maintains a "hole" at the cursor position.

[ 'H', 'E',  _, _, _,  'L', 'L', 'O' ]
          ^ Cursor (Gap Start)   

Insertion is O(1) because we write into the gap. Moving the cursor is O(N) because we must shift the gap using memmove (via std::copy).

// src/buffer.hpp
void move_gap(size_type position) {
    if (position < gap_start) {
        size_type move = gap_start - position;
        std::copy(buffer.begin() + position, 
                 buffer.begin() + gap_start, 
                 buffer.begin() + gap_end - move);
        gap_start -= move; gap_end -= move;
    }
    // ...
}

4. Double-Buffered Rendering

To prevent tearing and flicker, we construct the entire frame in a std::string before making a single write syscall.

// src/editor.hpp
void refresh_screen() {
    output_buffer.clear();
    output_buffer.append("\x1b[?25l\x1b[H"); // Hide cursor, go home
    
    draw_rows();       // Render text content
    draw_status_bar(); // Render UI
    place_cursor();    // Move real terminal cursor
    
    output_buffer.append("\x1b[?25h"); // Show cursor
    terminal.write_raw(output_buffer); // Atomic update
}

5. Input Parsing

Escape sequences (like Arrow Keys) are parsed manually. We detect the Escape byte (27) and look ahead.

// src/terminal.hpp
Key read_key() {
    char c;
    if (read(STDIN_FILENO, &c, 1) != 1) return Key::None;
    
    if (c == 27) { // Escape sequence start
        char seq[3];
        if (read(STDIN_FILENO, &seq[0], 1) != 1) return Key::Esc;
        if (read(STDIN_FILENO, &seq[1], 1) != 1) return Key::Esc;
        
        if (seq[0] == '[') {
            switch (seq[1]) {
                case 'A': return Key::ArrowUp;
                case 'B': return Key::ArrowDown;
                // ...
            }
        }
        return Key::Esc;
    }
    return static_cast<Key>(c);
}