From 3563324a1c44cb321d6ddb988e980b1f078808aa Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Sat, 22 Aug 2026 22:57:18 +0100 Subject: [PATCH] Major update - Add an IO system to hijack a bit of the terminal. - Other minor fixes. --- include/bed.h | 17 +- include/definitions.h | 13 ++ include/internal/generic.h | 16 ++ include/internal/io/io.h | 81 ++++++++++ include/internal/theme/theme.h | 13 -- src/bed/run.cc | 9 +- src/internal/buffer/buffer.cc | 10 +- src/internal/commands/commands.cc | 1 - src/internal/io/command.cc | 151 ++++++++++++++++++ src/internal/io/input.cc | 253 ++++++++++++++++++++++++++++++ src/internal/io/io.cc | 81 ++++++++++ src/internal/vase/shard.cc | 16 -- src/main.cc | 3 +- 13 files changed, 621 insertions(+), 43 deletions(-) create mode 100644 include/internal/io/io.h create mode 100644 src/internal/io/command.cc create mode 100644 src/internal/io/input.cc create mode 100644 src/internal/io/io.cc diff --git a/include/bed.h b/include/bed.h index e0a379e..ade7696 100644 --- a/include/bed.h +++ b/include/bed.h @@ -4,11 +4,25 @@ #include "internal/buffer/buffer.h" #include "internal/commands/commands.h" #include "internal/commands/suffixes.h" +#include "internal/io/io.h" #include "internal/marks/marks.h" #include "internal/theme/theme.h" #include "pch.h" 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 { internal::trie::Trie commands; internal::commands::Command no_op; @@ -16,6 +30,7 @@ struct BEd { std::array, 26> suffixes; internal::theme::Theme theme; std::unordered_map languages; + internal::io::IO &io; bool help_mode = false; std::string last_help = ""; @@ -27,7 +42,7 @@ struct BEd { std::unordered_map buffers; internal::buffer::Buffer *active; - BEd(std::vector args); + BEd(std::vector args, internal::io::IO &io); ~BEd(); void handle(std::string_view cmd, bool eof); void run(); diff --git a/include/definitions.h b/include/definitions.h index a633e7f..97cb050 100644 --- a/include/definitions.h +++ b/include/definitions.h @@ -14,5 +14,18 @@ struct ed_error : std::runtime_error { 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; } // namespace bed diff --git a/include/internal/generic.h b/include/internal/generic.h index 0d61e08..6ebb453 100644 --- a/include/internal/generic.h +++ b/include/internal/generic.h @@ -7,4 +7,20 @@ enum struct Direction : uint8_t { Forward, 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 diff --git a/include/internal/io/io.h b/include/internal/io/io.h new file mode 100644 index 0000000..e2426a2 --- /dev/null +++ b/include/internal/io/io.h @@ -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 terminal_size(); + std::pair cursor_position(); + void move_cursor(uint16_t row, uint16_t col); + + std::pair get_command(BEd &); + + KeyEvent read_key(); + +private: + static termios orig_termios; + static bool cleaned; + static void cleanup(); + + std::deque 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 diff --git a/include/internal/theme/theme.h b/include/internal/theme/theme.h index 60cdd70..9011943 100644 --- a/include/internal/theme/theme.h +++ b/include/internal/theme/theme.h @@ -5,19 +5,6 @@ #include "pch.h" 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 { std::array hl; diff --git a/src/bed/run.cc b/src/bed/run.cc index 0d9dd09..baa96f4 100644 --- a/src/bed/run.cc +++ b/src/bed/run.cc @@ -1,8 +1,8 @@ #include "bed.h" namespace bed { -BEd::BEd(std::vector args) - : theme(internal::theme::Theme::default_theme()) { +BEd::BEd(std::vector 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) { diff --git a/src/internal/buffer/buffer.cc b/src/internal/buffer/buffer.cc index 8e7274e..f7861a7 100644 --- a/src/internal/buffer/buffer.cc +++ b/src/internal/buffer/buffer.cc @@ -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"; } diff --git a/src/internal/commands/commands.cc b/src/internal/commands/commands.cc index b837d35..d33a336 100644 --- a/src/internal/commands/commands.cc +++ b/src/internal/commands/commands.cc @@ -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 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) diff --git a/src/internal/io/command.cc b/src/internal/io/command.cc new file mode 100644 index 0000000..f874dce --- /dev/null +++ b/src/internal/io/command.cc @@ -0,0 +1,151 @@ +#include "bed.h" +#include "internal/io/io.h" + +namespace bed::internal::io { +std::pair 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 diff --git a/src/internal/io/input.cc b/src/internal/io/input.cc new file mode 100644 index 0000000..b9f7299 --- /dev/null +++ b/src/internal/io/input.cc @@ -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 diff --git a/src/internal/io/io.cc b/src/internal/io/io.cc new file mode 100644 index 0000000..87cbc45 --- /dev/null +++ b/src/internal/io/io.cc @@ -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 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 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 diff --git a/src/internal/vase/shard.cc b/src/internal/vase/shard.cc index 66045b2..4b311a0 100644 --- a/src/internal/vase/shard.cc +++ b/src/internal/vase/shard.cc @@ -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) diff --git a/src/main.cc b/src/main.cc index ef69b23..7178d7b 100644 --- a/src/main.cc +++ b/src/main.cc @@ -4,7 +4,8 @@ int main(int argc, char *argv[]) { std::vector 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)