#include "lineedit.hpp" #include #include #include #include #include #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 class CharBuf { public: CharBuf() : _len(0) {} Result append(char c) { if (_len + 1 >= buflen) return ERROR(IndexOutOfRange); _buf[_len] = c; _len++; return Result(); } Result 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(); } uint64_t len() { return _len; } const char* buf() { return _buf; } private: char _buf[buflen]; uint64_t _len; }; Result 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 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 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 create(const char* prompt) { LineEdit res(prompt); TRY(res.init()); return res; } Result 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 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 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 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 handle_regular_char(char c) { if (line_len >= buflen) return Result(); 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(); } Result handle_backspace() { if (cursor_pos == 0) return Result(); 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(); } Result 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(); } private: Result init() { term_width = TRY(get_term_width()); if (!stdin_isatty()) return ERROR(IOError); return Result(); } 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 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 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(); } }