Version: 0.1 (Alpha)
Standard: C++20
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.
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
}
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;
}
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;
}
// ...
}
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
}
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);
}