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
+3 -6
View File
@@ -1,8 +1,8 @@
#include "bed.h"
namespace bed {
BEd::BEd(std::vector<std::string> args)
: theme(internal::theme::Theme::default_theme()) {
BEd::BEd(std::vector<std::string> args, internal::io::IO &io)
: theme(internal::theme::Theme::default_theme()), io(io) {
internal::commands::Command::register_posix(*this);
internal::commands::Suffix::register_suffixes(*this);
std::string prompt_ = "";
@@ -40,10 +40,7 @@ BEd::~BEd() {
void BEd::run() {
while (true) {
std::string cmd;
if (prompt_mode)
std::cout << prompt(*this);
bool eof = !std::getline(std::cin, cmd);
auto [cmd, eof] = io.get_command(*this);
try {
handle(cmd, eof);
} 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;
}
inline void apply(std::ostream &out, const theme::Highlight &hl) {
inline void apply(std::ostream &out, const Highlight &hl) {
out << "\x1b[0m";
const uint8_t r = (hl.fg >> 16) & 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)bb << 'm';
}
if (hl.flags & theme::Highlight::Bold)
if (hl.flags & Highlight::Bold)
out << "\x1b[1m";
if (hl.flags & theme::Highlight::Italic)
if (hl.flags & Highlight::Italic)
out << "\x1b[3m";
if (hl.flags & theme::Highlight::Underline)
if (hl.flags & Highlight::Underline)
out << "\x1b[4m";
if (hl.flags & theme::Highlight::Strikethrough)
if (hl.flags & Highlight::Strikethrough)
out << "\x1b[9m";
}
-1
View File
@@ -72,7 +72,6 @@ void Command::register_posix(BEd &ctx) {
.desc = "Join a set of lines (default: .,.+1)",
.accept_zero = true,
.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 end_line = ctx.active->line + 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;
}
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) {
int dest_fd = o->fd;
if (dest_fd == -1)
+2 -1
View File
@@ -4,7 +4,8 @@
int main(int argc, char *argv[]) {
std::vector<std::string> args(argv, argv + argc);
try {
bed::BEd ed(args);
bed::internal::io::IO io = bed::internal::io::IO();
bed::BEd ed(args, io);
ed.run();
} catch (bed::fatal_error &e) {
if (e.code)