Major update
- Add an IO system to hijack a bit of the terminal. - Other minor fixes.
This commit is contained in:
+3
-6
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user