446 lines
10 KiB
C++
446 lines
10 KiB
C++
#include "lineedit.hpp"
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#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,
|
|
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:
|
|
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;
|
|
bool in_paste = false;
|
|
while (!endstr) {
|
|
size_t i = 0;
|
|
while (i < sizeof(buf) - 10) {
|
|
if (read(input_fd, buf + i, 1) != 1) break;
|
|
if (buf[i] == '\n' && !in_paste) {
|
|
endstr = true;
|
|
break;
|
|
} else if (buf[i] == '\x1b') {
|
|
// TODO: this implementation has a bug with processing weird escape
|
|
// sequences, but fixing it will require a proper state machine.
|
|
// Example of input where this will break: "\x1b\n"
|
|
for (size_t j = 1; j <= 5; ++j) {
|
|
if (read(input_fd, buf + i + j, 1) != 1) break;
|
|
}
|
|
if (strncmp(buf + i, "\x1b[200~", 5) == 0) {
|
|
in_paste = true;
|
|
}
|
|
if (strncmp(buf + i, "\x1b[201~", 5) == 0) {
|
|
in_paste = false;
|
|
}
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
buf[i] = '\0';
|
|
|
|
res = TRY(res.concat(buf));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
Result<String> read_line(const char* prompt) {
|
|
const char* term = std::getenv("TERM");
|
|
|
|
if (strncmp(term, "dumb", 4) == 0) {
|
|
return read_dumb(prompt);
|
|
} else {
|
|
auto rl = TRY(LineEdit::create(prompt));
|
|
|
|
return rl.read_one();
|
|
}
|
|
}
|