Improve command input and adding resize handling.

This commit is contained in:
2026-08-23 13:16:08 +01:00
parent f4e3a190b8
commit 6f1c87400a
5 changed files with 162 additions and 60 deletions
+13 -4
View File
@@ -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<std::string, bool> get_command(BEd &);
std::pair<std::string, bool> 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<char> 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);
+33 -2
View File
@@ -3,12 +3,16 @@
namespace bed::internal::io {
std::pair<std::string, bool> IO::get_command(BEd &ctx) {
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);
uint16_t start = row;
uint16_t height = rows - row + 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<std::string, bool> 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<std::string, bool> 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);
+73 -48
View File
@@ -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()) {
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) {
+12 -1
View File
@@ -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);
+26
View File
@@ -0,0 +1,26 @@
#include "bed.h"
#include "internal/io/io.h"
namespace bed::internal::io {
std::pair<std::string, bool> 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