Major update

- Add an IO system to hijack a bit of the terminal.
- Other minor fixes.
This commit is contained in:
2026-08-22 22:57:18 +01:00
parent 1f3b53753a
commit 3563324a1c
13 changed files with 621 additions and 43 deletions
+16 -1
View File
@@ -4,11 +4,25 @@
#include "internal/buffer/buffer.h" #include "internal/buffer/buffer.h"
#include "internal/commands/commands.h" #include "internal/commands/commands.h"
#include "internal/commands/suffixes.h" #include "internal/commands/suffixes.h"
#include "internal/io/io.h"
#include "internal/marks/marks.h" #include "internal/marks/marks.h"
#include "internal/theme/theme.h" #include "internal/theme/theme.h"
#include "pch.h" #include "pch.h"
namespace bed { namespace bed {
struct Suggestion {
enum : uint8_t {
Command,
Address,
Suffix,
History,
Regex
} type;
std::string_view name;
std::string_view desc;
std::string completion;
};
struct BEd { struct BEd {
internal::trie::Trie<internal::commands::Command> commands; internal::trie::Trie<internal::commands::Command> commands;
internal::commands::Command no_op; internal::commands::Command no_op;
@@ -16,6 +30,7 @@ struct BEd {
std::array<std::optional<internal::commands::Suffix>, 26> suffixes; std::array<std::optional<internal::commands::Suffix>, 26> suffixes;
internal::theme::Theme theme; internal::theme::Theme theme;
std::unordered_map<std::string, internal::syntax::Language> languages; std::unordered_map<std::string, internal::syntax::Language> languages;
internal::io::IO &io;
bool help_mode = false; bool help_mode = false;
std::string last_help = ""; std::string last_help = "";
@@ -27,7 +42,7 @@ struct BEd {
std::unordered_map<std::string, internal::buffer::Buffer *> buffers; std::unordered_map<std::string, internal::buffer::Buffer *> buffers;
internal::buffer::Buffer *active; internal::buffer::Buffer *active;
BEd(std::vector<std::string> args); BEd(std::vector<std::string> args, internal::io::IO &io);
~BEd(); ~BEd();
void handle(std::string_view cmd, bool eof); void handle(std::string_view cmd, bool eof);
void run(); void run();
+13
View File
@@ -14,5 +14,18 @@ struct ed_error : std::runtime_error {
ed_error(std::string msg) : std::runtime_error(msg) {} ed_error(std::string msg) : std::runtime_error(msg) {}
}; };
struct Highlight {
enum : uint8_t {
None = 0,
Bold = 1 << 0,
Italic = 1 << 1,
Strikethrough = 1 << 2,
Underline = 1 << 3,
};
uint32_t fg;
uint32_t bg;
uint8_t flags;
};
struct BEd; struct BEd;
} // namespace bed } // namespace bed
+16
View File
@@ -7,4 +7,20 @@ enum struct Direction : uint8_t {
Forward, Forward,
Backward Backward
}; };
inline static bool write_all(int fd, const void *data, size_t len) {
const char *p = (const char *)data;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n > 0) {
p += n;
len -= (size_t)n;
continue;
} }
if (n == -1 && errno == EINTR)
continue;
return false;
}
return true;
}
} // namespace bed::internal
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include "definitions.h"
#include "pch.h"
namespace bed::internal::io {
struct KeyEvent {
enum struct KeyType {
NONE,
CHAR,
SPECIAL,
MOUSE,
PASTE
};
enum struct SpecialKey {
UP,
DOWN,
LEFT,
RIGHT,
DELETE,
UNKNOWN
};
enum struct Modifier {
NONE,
SHIFT,
ALT,
CTRL,
CTRL_ALT
};
enum struct MouseState {
PRESS,
RELEASE
};
KeyType type = KeyType::NONE;
std::string text;
SpecialKey special_key = SpecialKey::UNKNOWN;
Modifier modifier = Modifier::NONE;
MouseState mouse_state = MouseState::PRESS;
uint16_t mouse_x = 0;
uint16_t mouse_y = 0;
};
struct IO {
IO();
~IO();
IO(const IO &) = delete;
IO &operator=(const IO &) = delete;
void enable_mouse();
void disable_mouse();
std::pair<uint16_t, uint16_t> terminal_size();
std::pair<uint16_t, uint16_t> cursor_position();
void move_cursor(uint16_t row, uint16_t col);
std::pair<std::string, bool> get_command(BEd &);
KeyEvent read_key();
private:
static termios orig_termios;
static bool cleaned;
static void cleanup();
std::deque<char> input_queue;
bool get_next_byte(char &out);
void enqueue_bytes(const std::string &bytes);
static int utf8_seq_len(uint8_t byte);
std::string read_next_unit();
bool read_bracketed_paste(std::string &out);
KeyEvent parse_mouse(const std::string &buf);
KeyEvent parse_escape(const std::string &buf);
};
} // namespace bed::internal::io
-13
View File
@@ -5,19 +5,6 @@
#include "pch.h" #include "pch.h"
namespace bed::internal::theme { namespace bed::internal::theme {
struct Highlight {
enum : uint8_t {
None = 0,
Bold = 1 << 0,
Italic = 1 << 1,
Strikethrough = 1 << 2,
Underline = 1 << 3,
};
uint32_t fg;
uint32_t bg;
uint8_t flags;
};
struct Theme { struct Theme {
std::array<Highlight, internal::syntax::Token::Count> hl; std::array<Highlight, internal::syntax::Token::Count> hl;
+3 -6
View File
@@ -1,8 +1,8 @@
#include "bed.h" #include "bed.h"
namespace bed { namespace bed {
BEd::BEd(std::vector<std::string> args) BEd::BEd(std::vector<std::string> args, internal::io::IO &io)
: theme(internal::theme::Theme::default_theme()) { : theme(internal::theme::Theme::default_theme()), io(io) {
internal::commands::Command::register_posix(*this); internal::commands::Command::register_posix(*this);
internal::commands::Suffix::register_suffixes(*this); internal::commands::Suffix::register_suffixes(*this);
std::string prompt_ = ""; std::string prompt_ = "";
@@ -40,10 +40,7 @@ BEd::~BEd() {
void BEd::run() { void BEd::run() {
while (true) { while (true) {
std::string cmd; auto [cmd, eof] = io.get_command(*this);
if (prompt_mode)
std::cout << prompt(*this);
bool eof = !std::getline(std::cin, cmd);
try { try {
handle(cmd, eof); handle(cmd, eof);
} catch (ed_error &e) { } catch (ed_error &e) {
+5 -5
View File
@@ -106,7 +106,7 @@ void Buffer::join(uint64_t start_line, uint64_t end_line) {
modified = true; modified = true;
} }
inline void apply(std::ostream &out, const theme::Highlight &hl) { inline void apply(std::ostream &out, const Highlight &hl) {
out << "\x1b[0m"; out << "\x1b[0m";
const uint8_t r = (hl.fg >> 16) & 0xff; const uint8_t r = (hl.fg >> 16) & 0xff;
const uint8_t g = (hl.fg >> 8) & 0xff; const uint8_t g = (hl.fg >> 8) & 0xff;
@@ -124,13 +124,13 @@ inline void apply(std::ostream &out, const theme::Highlight &hl) {
<< (unsigned)bg << ';' << (unsigned)bg << ';'
<< (unsigned)bb << 'm'; << (unsigned)bb << 'm';
} }
if (hl.flags & theme::Highlight::Bold) if (hl.flags & Highlight::Bold)
out << "\x1b[1m"; out << "\x1b[1m";
if (hl.flags & theme::Highlight::Italic) if (hl.flags & Highlight::Italic)
out << "\x1b[3m"; out << "\x1b[3m";
if (hl.flags & theme::Highlight::Underline) if (hl.flags & Highlight::Underline)
out << "\x1b[4m"; out << "\x1b[4m";
if (hl.flags & theme::Highlight::Strikethrough) if (hl.flags & Highlight::Strikethrough)
out << "\x1b[9m"; out << "\x1b[9m";
} }
-1
View File
@@ -72,7 +72,6 @@ void Command::register_posix(BEd &ctx) {
.desc = "Join a set of lines (default: .,.+1)", .desc = "Join a set of lines (default: .,.+1)",
.accept_zero = true, .accept_zero = true,
.handle = [](BEd &ctx, std::span<const uint64_t> addresses, std::string_view) { .handle = [](BEd &ctx, std::span<const uint64_t> addresses, std::string_view) {
std::cout << addresses.size() << "\n";
uint64_t start_line = ctx.active->line; uint64_t start_line = ctx.active->line;
uint64_t end_line = ctx.active->line + 1; uint64_t end_line = ctx.active->line + 1;
if (addresses.size() == 1) if (addresses.size() == 1)
+151
View File
@@ -0,0 +1,151 @@
#include "bed.h"
#include "internal/io/io.h"
namespace bed::internal::io {
std::pair<std::string, bool> IO::get_command(BEd &ctx) {
auto [row, col] = cursor_position();
auto [rows, cols] = terminal_size();
if (row > rows)
throw fatal_error("Invalid cursor position.", 1);
uint16_t start = row;
uint16_t height = rows - row + 1;
// uint16_t width = cols;
// TODO: support multiline wrapped commands.
// also handle ctrl+l to try and clear screen.
std::string cmd;
size_t cursor = 0;
std::string prompt;
if (ctx.prompt_mode)
prompt = ctx.prompt(ctx);
auto redraw = [&] {
move_cursor(start, 1);
write_all(STDOUT_FILENO, "\x1b[2K", 4);
write_all(STDOUT_FILENO, prompt.c_str(), prompt.size());
write_all(STDOUT_FILENO, cmd.c_str(), cmd.size());
move_cursor(start, cursor + prompt.size() + 1);
};
redraw();
bool eof = false;
while (true) {
KeyEvent ev = read_key();
if (
(ev.type == KeyEvent::KeyType::CHAR
&& ev.modifier == KeyEvent::Modifier::CTRL
&& ev.text[0] == 'd')
|| ev.type == KeyEvent::KeyType::NONE
) {
eof = true;
break;
}
if (ev.type == KeyEvent::KeyType::CHAR
&& ev.modifier == KeyEvent::Modifier::CTRL
&& ev.text[0] == 'w') {
// TODO: delete previous word here.
if (cursor == 0)
continue;
cmd.erase(cursor -= 1, 1);
redraw();
continue;
}
if (ev.type == KeyEvent::KeyType::CHAR
&& ev.modifier == KeyEvent::Modifier::NONE) {
if (ev.text == "\r" || ev.text == "\n")
break;
if (ev.text.size() == 1
&& (ev.text[0] == '\x7f' || ev.text[0] == '\x08')) {
if (cursor == 0)
continue;
cmd.erase(cursor -= 1, 1);
redraw();
continue;
}
cmd.insert(cursor, ev.text);
cursor += ev.text.size();
redraw();
continue;
}
if (ev.type == KeyEvent::KeyType::SPECIAL) {
if (ev.special_key == KeyEvent::SpecialKey::LEFT) {
if (cursor == 0)
continue;
cursor -= 1;
} else if (ev.special_key == KeyEvent::SpecialKey::RIGHT) {
if (cursor >= cmd.size())
continue;
cursor += 1;
} else if (ev.special_key == KeyEvent::SpecialKey::DELETE) {
if (cursor >= cmd.size())
continue;
cmd.erase(cursor, 1);
}
redraw();
continue;
}
if (ev.type == KeyEvent::KeyType::PASTE) {
std::string text = ev.text;
if (!text.empty() && text.front() == '\n')
text.erase(text.begin());
if (!text.empty() && text.back() == '\n')
text.pop_back();
bool multiline = text.find('\n') != std::string::npos;
if (multiline) {
size_t line_count = 1 + std::count(text.begin(), text.end(), '\n');
if (height == 1) {
write_all(STDOUT_FILENO, "\n", 1);
--start;
++height;
}
bool go_ahead;
std::string msg =
"* paste "
+ std::to_string(line_count)
+ " lines into a single-line command? (y/n) ";
move_cursor(start + 1, 1);
write_all(STDOUT_FILENO, "\x1b[2K", 4);
write_all(STDOUT_FILENO, msg.c_str(), msg.size());
while (true) {
KeyEvent ev = read_key();
if (ev.type == KeyEvent::KeyType::CHAR && ev.text.size() == 1) {
if (ev.text[0] == 'y' || ev.text[0] == 'Y') {
go_ahead = true;
break;
}
if (ev.text[0] == 'n' || ev.text[0] == 'N') {
go_ahead = false;
break;
}
}
if (ev.type == KeyEvent::KeyType::NONE) {
go_ahead = false;
break;
}
}
move_cursor(start + 1, 1);
write_all(STDOUT_FILENO, "\x1b[2K", 4);
if (!go_ahead) {
redraw();
continue;
}
std::replace(text.begin(), text.end(), '\n', ' ');
}
cmd.insert(cursor, text);
cursor += text.size();
redraw();
continue;
}
}
for (uint16_t i = 1; i < height; ++i) {
move_cursor(start + i, 1);
write_all(STDOUT_FILENO, "\x1b[2K", 4);
}
move_cursor(start, 1);
write_all(STDOUT_FILENO, "\n", 1);
return {cmd, eof};
}
} // namespace bed::internal::io
+253
View File
@@ -0,0 +1,253 @@
#include "internal/io/io.h"
namespace bed::internal::io {
bool IO::get_next_byte(char &out) {
if (!input_queue.empty()) {
out = input_queue.front();
input_queue.pop_front();
return true;
}
return read(STDIN_FILENO, &out, 1) == 1;
}
void IO::enqueue_bytes(const std::string &bytes) {
input_queue.insert(input_queue.begin(), bytes.begin(), bytes.end());
}
int IO::utf8_seq_len(uint8_t byte) {
if ((byte & 0x80) == 0x00)
return 1;
if ((byte & 0xE0) == 0xC0)
return 2;
if ((byte & 0xF0) == 0xE0)
return 3;
if ((byte & 0xF8) == 0xF0)
return 4;
return 1;
}
std::string IO::read_next_unit() {
std::string buf;
char header;
if (!get_next_byte(header))
return buf;
if (header == '\x1b') {
buf.push_back(header);
char c;
if (!get_next_byte(c))
return buf;
buf.push_back(c);
if (c != '[')
return buf;
if (!get_next_byte(c))
return buf;
buf.push_back(c);
if (c == 'M') {
for (int i = 0; i < 3; ++i) {
if (!get_next_byte(c))
break;
buf.push_back(c);
}
return buf;
}
while ((uint8_t)c < 0x40 || (uint8_t)c > 0x7E) {
if (buf.size() >= 32)
break;
if (!get_next_byte(c))
break;
buf.push_back(c);
}
return buf;
}
int seq_len = utf8_seq_len((uint8_t)header);
buf.push_back(header);
if (seq_len == 1)
return buf;
for (int i = 1; i < seq_len; i++) {
char c;
if (!get_next_byte(c)) {
enqueue_bytes(buf);
return {};
}
buf.push_back(c);
}
uint32_t prev_cp, cur_cp;
grapheme_decode_utf8(buf.data(), buf.size(), &prev_cp);
uint16_t state = 0;
while (true) {
char next_header;
if (!get_next_byte(next_header))
break;
int next_len = utf8_seq_len((uint8_t)next_header);
std::string next_seq(1, next_header);
bool complete = true;
for (int i = 1; i < next_len; i++) {
char c;
if (!get_next_byte(c)) {
complete = false;
break;
}
next_seq.push_back(c);
}
if (!complete) {
enqueue_bytes(next_seq);
break;
}
grapheme_decode_utf8(next_seq.data(), next_seq.size(), &cur_cp);
if (grapheme_is_character_break(prev_cp, cur_cp, &state)) {
enqueue_bytes(next_seq);
break;
}
buf += next_seq;
prev_cp = cur_cp;
}
return buf;
}
bool IO::read_bracketed_paste(std::string &out) {
out.clear();
std::string window;
while (true) {
char c;
if (!get_next_byte(c))
return false;
window.push_back(c);
if (window.size() == 5 && window == "\x1b[201") {
char tilde;
if (!get_next_byte(tilde))
return false;
if (tilde == '~')
return true;
out += window;
out.push_back(tilde);
window.clear();
continue;
}
if (window.size() == 5) {
out.push_back(window.front());
window.erase(window.begin());
}
}
}
KeyEvent IO::parse_mouse(const std::string &buf) {
KeyEvent ev;
if (buf.size() < 6)
return ev;
uint8_t code = (uint8_t)buf[3] - 32;
uint8_t button = code & 0x03;
if (button != 0 && button != 3)
return ev;
ev.type = KeyEvent::KeyType::MOUSE;
ev.mouse_state = (button == 3) ? KeyEvent::MouseState::RELEASE
: KeyEvent::MouseState::PRESS;
ev.mouse_x = (uint8_t)buf[4] - 33;
ev.mouse_y = (uint8_t)buf[5] - 33;
return ev;
}
KeyEvent IO::parse_escape(const std::string &buf) {
KeyEvent ev;
ev.type = KeyEvent::KeyType::SPECIAL;
bool has_modifier = buf.size() > 3 && buf[3] == ';';
size_t pos;
if (!has_modifier) {
pos = 2;
} else {
pos = 5;
switch (buf.size() > 4 ? buf[4] : 0) {
case '2':
ev.modifier = KeyEvent::Modifier::SHIFT;
break;
case '3':
ev.modifier = KeyEvent::Modifier::ALT;
break;
case '5':
ev.modifier = KeyEvent::Modifier::CTRL;
break;
case '7':
ev.modifier = KeyEvent::Modifier::CTRL_ALT;
break;
default:
ev.modifier = KeyEvent::Modifier::NONE;
break;
}
}
char key = pos < buf.size() ? buf[pos] : 0;
switch (key) {
case 'A':
ev.special_key = KeyEvent::SpecialKey::UP;
break;
case 'B':
ev.special_key = KeyEvent::SpecialKey::DOWN;
break;
case 'C':
ev.special_key = KeyEvent::SpecialKey::RIGHT;
break;
case 'D':
ev.special_key = KeyEvent::SpecialKey::LEFT;
break;
case '3':
ev.special_key = KeyEvent::SpecialKey::DELETE;
break;
default:
ev.special_key = KeyEvent::SpecialKey::UNKNOWN;
break;
}
return ev;
}
KeyEvent IO::read_key() {
while (true) {
std::string buf = read_next_unit();
if (buf.empty()) {
KeyEvent ev;
ev.type = KeyEvent::KeyType::NONE;
return ev;
}
if (buf.size() >= 6 && buf[0] == '\x1b' && buf[1] == '[' && buf.compare(2, 4, "200~") == 0) {
KeyEvent ev;
std::string pasted;
if (read_bracketed_paste(pasted)) {
ev.type = KeyEvent::KeyType::PASTE;
ev.text = std::move(pasted);
} else {
ev.type = KeyEvent::KeyType::NONE;
}
return ev;
}
if (buf.size() >= 3 && buf[0] == '\x1b' && buf[1] == '[' && buf[2] == 'M') {
KeyEvent ev = parse_mouse(buf);
if (ev.type == KeyEvent::KeyType::NONE)
continue;
return ev;
}
if (buf.size() >= 2 && buf[0] == '\x1b' && buf[1] == '[')
return parse_escape(buf);
KeyEvent ev;
ev.type = KeyEvent::KeyType::CHAR;
ev.modifier = KeyEvent::Modifier::NONE;
if (buf.size() == 1) {
uint8_t c = (uint8_t)buf[0];
if (c >= 1 && c <= 26 && c != '\t' && c != '\n' && c != '\r' && c != '\x08') {
ev.modifier = KeyEvent::Modifier::CTRL;
ev.text = 'a' + c - 1;
return ev;
}
}
if (buf.size() == 2 && (uint8_t)buf[0] == 0x1B) {
uint8_t c = (uint8_t)buf[1];
if (c >= 1 && c <= 26 && c != '\t' && c != '\n' && c != '\r' && c != '\x08') {
ev.modifier = KeyEvent::Modifier::CTRL_ALT;
ev.text = 'a' + c - 1;
return ev;
}
ev.modifier = KeyEvent::Modifier::ALT;
ev.text = buf.substr(1);
return ev;
}
ev.text = std::move(buf);
return ev;
}
}
} // namespace bed::internal::io
+81
View File
@@ -0,0 +1,81 @@
#include "internal/io/io.h"
namespace bed::internal::io {
termios IO::orig_termios{};
bool IO::cleaned = false;
IO::IO() {
if (tcgetattr(STDIN_FILENO, &orig_termios) == -1)
throw fatal_error("Can't get terminal state.", 1);
atexit(cleanup);
struct termios raw = orig_termios;
raw.c_iflag &= ~(BRKINT | ISTRIP | IXON);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | ISIG);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
throw fatal_error("Can't set terminal state.", 1);
std::string os = "\x1b[?2004h";
write_all(STDOUT_FILENO, os.c_str(), os.size());
}
IO::~IO() {
cleanup();
}
void IO::enable_mouse() {
const char *seq = "\x1b[?1000h";
write_all(STDOUT_FILENO, seq, 8);
}
void IO::disable_mouse() {
const char *seq = "\x1b[?1000l";
write_all(STDOUT_FILENO, seq, 8);
}
std::pair<uint16_t, uint16_t> IO::terminal_size() {
struct winsize ws{};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1)
throw fatal_error("Can't get terminal size.", 1);
return {ws.ws_row, ws.ws_col};
}
std::pair<uint16_t, uint16_t> IO::cursor_position() {
write_all(STDOUT_FILENO, "\x1b[6n", 4);
std::string response;
char c;
if (read(STDIN_FILENO, &c, 1) != 1 || c != '\x1b')
throw fatal_error("Invalid cursor position response.", 1);
if (read(STDIN_FILENO, &c, 1) != 1 || c != '[')
throw fatal_error("Invalid cursor position response.", 1);
while (true) {
if (read(STDIN_FILENO, &c, 1) != 1)
throw fatal_error("Invalid cursor position response.", 1);
if (c == 'R')
break;
response += c;
}
unsigned row;
unsigned col;
if (sscanf(response.c_str(), "%u;%u", &row, &col) != 2)
throw fatal_error("Invalid cursor position response.", 1);
return {(uint16_t)row, (uint16_t)col};
}
void IO::cleanup() {
if (cleaned)
return;
std::string os = "\x1b[?2004l";
write_all(STDOUT_FILENO, os.c_str(), os.size());
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1)
perror("Can't clean up terminal.");
cleaned = true;
}
void IO::move_cursor(uint16_t row, uint16_t col) {
char buf[32];
int n = snprintf(buf, sizeof(buf), "\x1b[%u;%uH", row, col);
write_all(STDOUT_FILENO, buf, n);
}
} // namespace bed::internal::io
-16
View File
@@ -200,22 +200,6 @@ Shard *Shard::build(Shard **pieces, uint64_t lo, uint64_t hi) {
return node; return node;
} }
static bool write_all(int fd, const void *data, size_t len) {
const char *p = (const char *)data;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n > 0) {
p += n;
len -= (size_t)n;
continue;
}
if (n == -1 && errno == EINTR)
continue;
return false;
}
return true;
}
Shard *Shard::from_command(const char *cmd, OriginalBuffer *o, bool posix_ending) { Shard *Shard::from_command(const char *cmd, OriginalBuffer *o, bool posix_ending) {
int dest_fd = o->fd; int dest_fd = o->fd;
if (dest_fd == -1) if (dest_fd == -1)
+2 -1
View File
@@ -4,7 +4,8 @@
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
std::vector<std::string> args(argv, argv + argc); std::vector<std::string> args(argv, argv + argc);
try { try {
bed::BEd ed(args); bed::internal::io::IO io = bed::internal::io::IO();
bed::BEd ed(args, io);
ed.run(); ed.run();
} catch (bed::fatal_error &e) { } catch (bed::fatal_error &e) {
if (e.code) if (e.code)