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);
}

6. Keybinding System

Honeymoon uses a trie (prefix tree) to handle multi-key chords like C-x C-s. All keybindings are loaded from keybinds.moon, making them fully customizable without recompilation.

Trie-Based Key Dispatch

Each node in the trie represents a key in the sequence. Leaf nodes store action names that map to lambdas.

// src/editor.hpp
struct KeyNode {
    std::map<Key, std::shared_ptr<KeyNode>> children;
    std::string action;
};

void handle_input(EditorState &, Key k) {
    auto it = current_node->children.find(k);
    if (it != current_node->children.end()) {
        current_node = it->second;
        pending_keys.push_back(k);

        if (!current_node->action.empty() && current_node->children.empty()) {
            // Leaf node - execute action
            actions[current_node->action]();
            current_node = root_node;
            pending_keys.clear();
        }
    }
    // ...
}

M- Prefix Expansion

The config parser expands M-x (Meta key) into [Esc, x] automatically. This provides clean Emacs-style syntax in the config file.

// src/keybinder.hpp
for (size_t i = 0; i < words.size() - 1; ++i) {
    std::string token = words[i];

    // Handle M- prefix (Meta key = Esc + key)
    if (token.size() >= 3 && token[0] == 'M' && token[1] == '-') {
        b.keys.push_back(Key::Esc);
        token = token.substr(2); // Remove "M-"
    }

    Key k = key_from_string(token);
    if (k != Key::None)
        b.keys.push_back(k);
}

Config File Format

keybinds.moon uses a simple whitespace-separated format:

# Comment lines start with #
M-w copy                  # Meta-w copies selection
C-x C-s save_file        # Chord: Ctrl-x then Ctrl-s
C-g cancel               # Single key binding

Supported key formats:

All default keybindings are now in keybinds.moon. Edit the file and restart the editor to apply changes.