valeri/src/lineedit.cpp

432 lines
9.7 KiB
C++
Raw Normal View History

2024-08-26 12:16:05 +00:00
#include "lineedit.hpp"
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
2024-08-26 12:16:05 +00:00
#include "common.hpp"
#include "fio.hpp"
enum class TtyEscape {
Null = 0,
CtrlA = 1,
CtrlB = 2,
CtrlC = 3,
CtrlD = 4,
CtrlE = 5,
CtrlF = 6,
CtrlH = 8,
Tab = 9,
LF = 10,
2024-08-26 12:16:05 +00:00
CtrlK = 11,
CtrlL = 12,
Enter = 13,
CtrlN = 14,
CtrlP = 16,
CtrlT = 20,
CtrlU = 21,
CtrlW = 23,
Esc = 27,
Backspace = 127,
};
static struct termios orig_termios;
static int rawmode = 0;
template <uint64_t buflen>
class CharBuf {
public:
CharBuf() : _len(0) {}
Result<void> append(char c) {
if (_len + 1 >= buflen) return ERROR(IndexOutOfRange);
_buf[_len] = c;
_len++;
return Result<void>();
}
Result<void> append(const char* s) {
uint64_t slen = strlen(s);
if (_len + slen >= buflen) return ERROR(IndexOutOfRange);
memcpy(_buf + _len, s, slen);
_len += slen;
return Result<void>();
}
uint64_t len() { return _len; }
const char* buf() { return _buf; }
private:
char _buf[buflen];
uint64_t _len;
};
Result<void> enableRawMode(int fd) {
struct termios raw;
if (!isatty(STDIN_FILENO)) return ERROR(IOError);
if (tcgetattr(fd, &orig_termios) == -1) return ERROR(IOError);
raw = orig_termios; /* modify the original mode */
/* input modes: no break, no CR to NL, no parity check, no strip char,
* no start/stop output control. */
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* output modes - disable post processing */
raw.c_oflag &= ~(OPOST);
/* control modes - set 8 bit chars */
raw.c_cflag |= (CS8);
/* local modes - choing off, canonical off, no extended functions,
* no signal chars (^Z,^C) */
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* control chars - set return condition: min number of bytes and timer.
* We want read to return every single byte, without timeout. */
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0; /* 1 byte, no timer */
/* put terminal in raw mode after flushing */
if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) return ERROR(IOError);
rawmode = 1;
return Result<void>();
}
void disableRawMode(int fd) {
/* Don't even check the return value as it's too late. */
if (rawmode && tcsetattr(fd, TCSAFLUSH, &orig_termios) != -1) rawmode = 0;
}
class RawModeGuard {
public:
RawModeGuard() : fd(-1) {}
RawModeGuard(int fd) : fd(fd) {}
RawModeGuard(RawModeGuard&& rhs) {
fd = rhs.fd;
rhs.fd = -1;
}
RawModeGuard(const RawModeGuard&) = delete;
~RawModeGuard() {
if (fd >= 0) disableRawMode(fd);
}
static Result<RawModeGuard> create(int fd) {
auto res = RawModeGuard(fd);
TRY(enableRawMode(fd));
return res;
}
private:
int fd;
};
RawModeGuard raw_mode_guard(STDIN_FILENO);
struct RefreshState {
uint64_t num_rows = 0;
uint64_t cursor_pos = 0;
};
class LineEdit {
public:
LineEdit()
: prompt(""),
cursor_pos(0),
line_len(0),
term_width(0),
input_fd(STDIN_FILENO),
output_fd(STDOUT_FILENO) {
buf[0] = 0;
}
LineEdit(const char* prompt, int input_fd = STDIN_FILENO,
int output_fd = STDOUT_FILENO)
: prompt(prompt),
cursor_pos(0),
line_len(0),
term_width(0),
input_fd(input_fd),
output_fd(output_fd) {
buf[0] = 0;
}
LineEdit(const LineEdit& rhs) {
prompt = rhs.prompt;
memcpy(buf, rhs.buf, buflen);
cursor_pos = rhs.cursor_pos;
line_len = rhs.line_len;
term_width = rhs.term_width;
input_fd = rhs.input_fd;
output_fd = rhs.output_fd;
last_refresh = rhs.last_refresh;
}
static Result<LineEdit> create(const char* prompt) {
LineEdit res(prompt);
TRY(res.init());
return res;
}
Result<uint64_t> get_term_width() {
winsize ws;
if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
/* ioctl() failed. Try to query the terminal itself. */
int start, cols;
/* Get the initial position so we can restore it later. */
start = TRY(get_cursor_column());
/* Go to right margin and get position. */
if (write(output_fd, "\x1b[999C", 6) != 6) return ERROR(IOError);
cols = TRY(get_cursor_column());
/* Restore position. */
if (cols > start) {
char seq[32];
snprintf(seq, 32, "\x1b[%dD", cols - start);
if (write(output_fd, seq, strlen(seq)) == -1) {
return ERROR(IOError);
}
}
return cols;
} else {
return ws.ws_col;
}
}
Result<uint64_t> get_cursor_column() {
char buf[32];
int cols, rows;
unsigned int i = 0;
/* Report cursor location */
if (write(output_fd, "\x1b[6n", 4) != 4) return -1;
/* Read the response: ESC [ rows ; cols R */
while (i < sizeof(buf) - 1) {
if (read(input_fd, buf + i, 1) != 1) break;
if (buf[i] == 'R') break;
i++;
}
buf[i] = '\0';
/* Parse it. */
if (buf[0] != (char)TtyEscape::Esc || buf[1] != '[') return -1;
if (sscanf(buf + 2, "%d;%d", &rows, &cols) != 2) return -1;
return cols;
}
Result<String> read_one() {
{
auto guard = TRY(RawModeGuard::create(input_fd));
if (write(output_fd, prompt, strlen(prompt)) == -1) return ERROR(IOError);
while (TRY(step()));
}
printf("\n");
auto res = TRY(String::create(buf));
return res;
}
Result<bool> step() {
char c;
int nread;
char seq[3];
nread = read(input_fd, &c, 1);
if (nread <= 0) return false;
switch (TtyEscape(c)) {
case TtyEscape::LF:
2024-08-26 12:16:05 +00:00
case TtyEscape::Enter:
return false;
break;
case TtyEscape::Backspace:
TRY(handle_backspace());
break;
case TtyEscape::CtrlC:
return ERROR(Interrupt);
default:
TRY(handle_regular_char(c));
break;
}
return true;
}
Result<void> handle_regular_char(char c) {
if (line_len >= buflen) return Result<void>();
if (line_len == cursor_pos) {
buf[cursor_pos] = c;
cursor_pos++;
line_len++;
buf[cursor_pos] = 0;
// if (strlen(prompt) + line_len < term_width) {
// if (write(output_fd, &c, 1) == -1) return ERROR(IOError);
// } else {
TRY(refresh_line());
//}
} else {
memmove(buf + cursor_pos + 1, buf + cursor_pos, line_len - cursor_pos);
buf[cursor_pos] = c;
cursor_pos++;
line_len++;
buf[line_len] = 0;
TRY(refresh_line());
}
return Result<void>();
}
Result<void> handle_backspace() {
if (cursor_pos == 0) return Result<void>();
memmove(buf + cursor_pos - 1, buf + cursor_pos, line_len - cursor_pos);
cursor_pos--;
line_len--;
buf[line_len] = 0;
TRY(refresh_line());
return Result<void>();
}
Result<void> refresh_line() {
char seq[64];
CharBuf<1024> cbuf;
RefreshState new_refresh = last_refresh;
uint64_t prompt_len = strlen(prompt);
uint64_t num_rows = (prompt_len + line_len + term_width - 1) / term_width;
uint64_t cursor_row =
(prompt_len + last_refresh.cursor_pos + term_width) / term_width;
uint64_t new_relative_row = 0;
new_refresh.num_rows = num_rows;
new_refresh.cursor_pos = cursor_pos;
// Go to last row
if (last_refresh.num_rows - cursor_row > 0) {
snprintf(seq, 64, "\x1b[%luB", last_refresh.num_rows - cursor_row);
TRY(cbuf.append(seq));
}
// Clear all rows one by one (except the first one)
if (last_refresh.num_rows > 1) {
for (uint64_t i = 0; i < last_refresh.num_rows - 1; i++) {
snprintf(seq, 64, "\r\x1b[0K\x1b[1A");
TRY(cbuf.append(seq));
}
}
// Clear the first line
snprintf(seq, 64, "\r\x1b[0K");
TRY(cbuf.append(seq));
// Write the prompt
cbuf.append(prompt);
// Write the buffer content
cbuf.append(buf);
if (cursor_pos > 0 && cursor_pos == line_len &&
(cursor_pos + prompt_len) % term_width == 0) {
cbuf.append("\n");
cbuf.append("\r");
num_rows++;
if (num_rows > last_refresh.num_rows) new_refresh.num_rows = num_rows;
}
cursor_row = (prompt_len + cursor_pos + term_width) / term_width;
// Go up to the cursor row
if (num_rows - cursor_row > 0) {
snprintf(seq, 64, "\x1b[%luA", num_rows - cursor_row);
TRY(cbuf.append(seq));
}
// Go to the cursor column
uint64_t col = (prompt_len + cursor_pos) % term_width;
if (col > 0) {
snprintf(seq, 64, "\r\x1b[%luC", col);
TRY(cbuf.append(seq));
} else {
TRY(cbuf.append("\r"));
}
if (write(output_fd, cbuf.buf(), cbuf.len()) == -1) return ERROR(IOError);
last_refresh = new_refresh;
return Result<void>();
}
private:
Result<void> init() {
term_width = TRY(get_term_width());
if (!stdin_isatty()) return ERROR(IOError);
return Result<void>();
}
const char* prompt;
static const uint64_t buflen = 1024;
char buf[buflen + 1];
uint64_t cursor_pos;
uint64_t line_len;
uint64_t term_width;
int input_fd;
int output_fd;
RefreshState last_refresh;
};
Result<String> read_dumb(const char* prompt) {
std::cout << prompt << std::flush;
String res = TRY(String::create(""));
int input_fd = STDIN_FILENO;
const size_t bufsize = 256;
char buf[bufsize];
bool endstr = false;
while (!endstr) {
size_t i = 0;
while (i < sizeof(buf) - 1) {
if (read(input_fd, buf + i, 1) != 1) break;
if (buf[i] == '\n') {
endstr = true;
break;
}
i++;
}
buf[i] = '\0';
res = TRY(res.concat(buf));
}
return res;
}
2024-08-26 12:16:05 +00:00
Result<String> read_line(const char* prompt) {
const char* term = std::getenv("TERM");
2024-08-26 12:16:05 +00:00
if (strncmp(term, "dumb", 4) == 0) {
return read_dumb(prompt);
} else {
auto rl = TRY(LineEdit::create(prompt));
return rl.read_one();
}
2024-08-26 12:16:05 +00:00
}