diff --git a/include/internal/io/io.h b/include/internal/io/io.h index e2426a2..2ceffc4 100644 --- a/include/internal/io/io.h +++ b/include/internal/io/io.h @@ -5,12 +5,18 @@ namespace bed::internal::io { struct KeyEvent { + enum struct ReadResult { + SUCCESS, + EOF_, + RESIZE + }; enum struct KeyType { NONE, CHAR, SPECIAL, MOUSE, - PASTE + PASTE, + RESIZE }; enum struct SpecialKey { UP, @@ -58,6 +64,7 @@ struct IO { void move_cursor(uint16_t row, uint16_t col); std::pair get_command(BEd &); + std::pair get_text(BEd &); KeyEvent read_key(); @@ -65,15 +72,17 @@ private: static termios orig_termios; static bool cleaned; static void cleanup(); + static volatile std::atomic_bool resized; + static void handle_sigwinch(int); std::deque input_queue; - bool get_next_byte(char &out); + KeyEvent::ReadResult 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::ReadResult read_next_unit(std::string &out); + KeyEvent::ReadResult read_bracketed_paste(std::string &out); KeyEvent parse_mouse(const std::string &buf); KeyEvent parse_escape(const std::string &buf); diff --git a/src/internal/io/command.cc b/src/internal/io/command.cc index f874dce..23ffeb6 100644 --- a/src/internal/io/command.cc +++ b/src/internal/io/command.cc @@ -3,12 +3,16 @@ 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 start; + uint16_t height; + { + auto [row, col] = cursor_position(); + auto [rows, cols] = terminal_size(); + if (row > rows) + throw fatal_error("Invalid cursor position.", 1); + start = row; + height = rows - row + 1; + } // uint16_t width = cols; // TODO: support multiline wrapped commands. // also handle ctrl+l to try and clear screen. @@ -33,6 +37,18 @@ std::pair IO::get_command(BEd &ctx) { while (true) { KeyEvent ev = read_key(); + if (ev.type == KeyEvent::KeyType::RESIZE) { + { + auto [row, col] = cursor_position(); + auto [rows, cols] = terminal_size(); + if (row > rows) + throw fatal_error("Invalid cursor position.", 1); + start = row; + height = rows - row + 1; + } + redraw(); + continue; + } if ( (ev.type == KeyEvent::KeyType::CHAR && ev.modifier == KeyEvent::Modifier::CTRL @@ -124,6 +140,21 @@ std::pair IO::get_command(BEd &ctx) { go_ahead = false; break; } + if (ev.type == KeyEvent::KeyType::RESIZE) { + { + auto [row, col] = cursor_position(); + auto [rows, cols] = terminal_size(); + if (row - 1 > rows) + throw fatal_error("Invalid cursor position.", 1); + start = row - 1; + height = rows - row; + } + redraw(); + move_cursor(start + 1, 1); + write_all(STDOUT_FILENO, "\x1b[2K", 4); + write_all(STDOUT_FILENO, msg.c_str(), msg.size()); + continue; + } } move_cursor(start + 1, 1); write_all(STDOUT_FILENO, "\x1b[2K", 4); diff --git a/src/internal/io/input.cc b/src/internal/io/input.cc index b9f7299..2ba3e9a 100644 --- a/src/internal/io/input.cc +++ b/src/internal/io/input.cc @@ -1,13 +1,22 @@ #include "internal/io/io.h" namespace bed::internal::io { -bool IO::get_next_byte(char &out) { +KeyEvent::ReadResult IO::get_next_byte(char &out) { if (!input_queue.empty()) { out = input_queue.front(); input_queue.pop_front(); - return true; + return KeyEvent::ReadResult::SUCCESS; } - return read(STDIN_FILENO, &out, 1) == 1; + if (resized.load()) + return KeyEvent::ReadResult::RESIZE; + ssize_t n = read(STDIN_FILENO, &out, 1); + if (n == 1) + return KeyEvent::ReadResult::SUCCESS; + if (n == -1 && errno == EINTR && resized.load()) + return KeyEvent::ReadResult::RESIZE; + if (n == -1 && errno == EINTR) + return get_next_byte(out); + return KeyEvent::ReadResult::EOF_; } void IO::enqueue_bytes(const std::string &bytes) { @@ -26,64 +35,66 @@ int IO::utf8_seq_len(uint8_t byte) { return 1; } -std::string IO::read_next_unit() { - std::string buf; +KeyEvent::ReadResult IO::read_next_unit(std::string &out) { + out.clear(); char header; - if (!get_next_byte(header)) - return buf; + KeyEvent::ReadResult res = get_next_byte(header); + if (res != KeyEvent::ReadResult::SUCCESS) + return res; if (header == '\x1b') { - buf.push_back(header); + out.push_back(header); char c; - if (!get_next_byte(c)) - return buf; - buf.push_back(c); + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) + return res; + out.push_back(c); if (c != '[') - return buf; - if (!get_next_byte(c)) - return buf; - buf.push_back(c); + return KeyEvent::ReadResult::SUCCESS; + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) + return res; + out.push_back(c); if (c == 'M') { for (int i = 0; i < 3; ++i) { - if (!get_next_byte(c)) - break; - buf.push_back(c); + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) + return res; + out.push_back(c); } - return buf; + return KeyEvent::ReadResult::SUCCESS; } while ((uint8_t)c < 0x40 || (uint8_t)c > 0x7E) { - if (buf.size() >= 32) + if (out.size() >= 32) break; - if (!get_next_byte(c)) - break; - buf.push_back(c); + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) + return res; + out.push_back(c); } - return buf; + return KeyEvent::ReadResult::SUCCESS; } int seq_len = utf8_seq_len((uint8_t)header); - buf.push_back(header); + out.push_back(header); if (seq_len == 1) - return buf; + return KeyEvent::ReadResult::SUCCESS; for (int i = 1; i < seq_len; i++) { char c; - if (!get_next_byte(c)) { - enqueue_bytes(buf); - return {}; + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) { + enqueue_bytes(out); + out.clear(); + return res; } - buf.push_back(c); + out.push_back(c); } uint32_t prev_cp, cur_cp; - grapheme_decode_utf8(buf.data(), buf.size(), &prev_cp); + grapheme_decode_utf8(out.data(), out.size(), &prev_cp); uint16_t state = 0; while (true) { char next_header; - if (!get_next_byte(next_header)) + if ((res = get_next_byte(next_header)) != KeyEvent::ReadResult::SUCCESS) 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)) { + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) { complete = false; break; } @@ -98,26 +109,27 @@ std::string IO::read_next_unit() { enqueue_bytes(next_seq); break; } - buf += next_seq; + out += next_seq; prev_cp = cur_cp; } - return buf; + return KeyEvent::ReadResult::SUCCESS; } -bool IO::read_bracketed_paste(std::string &out) { +KeyEvent::ReadResult IO::read_bracketed_paste(std::string &out) { + KeyEvent::ReadResult res = KeyEvent::ReadResult::SUCCESS; out.clear(); std::string window; while (true) { char c; - if (!get_next_byte(c)) - return false; + if ((res = get_next_byte(c)) != KeyEvent::ReadResult::SUCCESS) + return res; window.push_back(c); if (window.size() == 5 && window == "\x1b[201") { char tilde; - if (!get_next_byte(tilde)) - return false; + if ((res = get_next_byte(tilde)) != KeyEvent::ReadResult::SUCCESS) + return res; if (tilde == '~') - return true; + return res; out += window; out.push_back(tilde); window.clear(); @@ -199,32 +211,45 @@ KeyEvent IO::parse_escape(const std::string &buf) { KeyEvent IO::read_key() { while (true) { - std::string buf = read_next_unit(); - if (buf.empty()) { - KeyEvent ev; + std::string buf; + auto res = read_next_unit(buf); + KeyEvent ev; + switch (res) { + case KeyEvent::ReadResult::EOF_: ev.type = KeyEvent::KeyType::NONE; return ev; + case KeyEvent::ReadResult::RESIZE: + resized.store(false); + ev.type = KeyEvent::KeyType::RESIZE; + return ev; + case KeyEvent::ReadResult::SUCCESS: + break; } 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)) { + switch (read_bracketed_paste(pasted)) { + case KeyEvent::ReadResult::SUCCESS: ev.type = KeyEvent::KeyType::PASTE; ev.text = std::move(pasted); - } else { + break; + case KeyEvent::ReadResult::EOF_: ev.type = KeyEvent::KeyType::NONE; + break; + case KeyEvent::ReadResult::RESIZE: + resized.store(false); + ev.type = KeyEvent::KeyType::RESIZE; + break; } return ev; } if (buf.size() >= 3 && buf[0] == '\x1b' && buf[1] == '[' && buf[2] == 'M') { - KeyEvent ev = parse_mouse(buf); + 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) { diff --git a/src/internal/io/io.cc b/src/internal/io/io.cc index 9ca7955..c10faa6 100644 --- a/src/internal/io/io.cc +++ b/src/internal/io/io.cc @@ -3,11 +3,17 @@ namespace bed::internal::io { termios IO::orig_termios{}; bool IO::cleaned = false; +volatile std::atomic_bool IO::resized(false); IO::IO() { if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) throw fatal_error("Can't get terminal state.", 1); - atexit(cleanup); + struct sigaction sa{}; + sa.sa_handler = handle_sigwinch; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + if (sigaction(SIGWINCH, &sa, nullptr) == -1) + throw fatal_error("Can't install SIGWINCH handler.", 1); struct termios raw = orig_termios; raw.c_iflag &= ~(BRKINT | ISTRIP | IXON); raw.c_cflag |= (CS8); @@ -18,6 +24,7 @@ IO::IO() { throw fatal_error("Can't set terminal state.", 1); std::string os = "\x1b[?2004h"; write_all(STDOUT_FILENO, os.c_str(), os.size()); + atexit(cleanup); } IO::~IO() { @@ -73,6 +80,10 @@ void IO::cleanup() { cleaned = true; } +void IO::handle_sigwinch(int) { + resized.store(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); diff --git a/src/internal/io/text_mode.cc b/src/internal/io/text_mode.cc new file mode 100644 index 0000000..be89d42 --- /dev/null +++ b/src/internal/io/text_mode.cc @@ -0,0 +1,26 @@ +#include "bed.h" +#include "internal/io/io.h" + +namespace bed::internal::io { +std::pair IO::get_text(BEd &) { + 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; + enable_mouse(); + // uint16_t width = cols; + // TODO: support multiline wrapped commands. + // also handle ctrl+l to try and clear screen. + + disable_mouse(); + 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 {"", true}; +} +} // namespace bed::internal::io