Files
email-tracker/external/duckdb/tools/shell/linenoise/terminal.cpp
2025-10-24 19:21:19 -05:00

497 lines
12 KiB
C++

#include "terminal.hpp"
#include "history.hpp"
#include "linenoise.hpp"
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>
namespace duckdb {
static int mlmode = 1; /* Multi line mode. Default is multi line. */
static struct termios orig_termios; /* In order to restore at exit.*/
static int atexit_registered = 0; /* Register atexit just 1 time. */
static int rawmode = 0; /* For atexit() function to check if restore is needed*/
static const char *unsupported_term[] = {"dumb", "cons25", "emacs", NULL};
/* At exit we'll try to fix the terminal to the initial conditions. */
static void linenoiseAtExit(void) {
Terminal::DisableRawMode();
History::Free();
}
/* Return true if the terminal name is in the list of terminals we know are
* not able to understand basic escape sequences. */
int Terminal::IsUnsupportedTerm() {
char *term = getenv("TERM");
int j;
if (!term) {
return 0;
}
for (j = 0; unsupported_term[j]; j++) {
if (!strcasecmp(term, unsupported_term[j])) {
return 1;
}
}
return 0;
}
/* Raw mode: 1960 magic shit. */
int Terminal::EnableRawMode() {
int fd = STDIN_FILENO;
if (!isatty(STDIN_FILENO)) {
errno = ENOTTY;
return -1;
}
if (!atexit_registered) {
atexit(linenoiseAtExit);
atexit_registered = 1;
}
if (tcgetattr(fd, &orig_termios) == -1) {
errno = ENOTTY;
return -1;
}
auto 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);
#ifdef IUTF8
/* control modes - set 8 bit chars */
raw.c_iflag |= IUTF8;
#endif
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, TCSADRAIN, &raw) < 0) {
errno = ENOTTY;
return -1;
}
rawmode = 1;
return 0;
}
void Terminal::DisableRawMode() {
int fd = STDIN_FILENO;
/* Don't even check the return value as it's too late. */
if (rawmode && tcsetattr(fd, TCSADRAIN, &orig_termios) != -1) {
rawmode = 0;
}
}
bool Terminal::IsMultiline() {
return mlmode;
}
bool Terminal::IsAtty() {
return isatty(STDIN_FILENO);
}
/* This function is called when linenoise() is called with the standard
* input file descriptor not attached to a TTY. So for example when the
* program using linenoise is called in pipe or with a file redirected
* to its standard input. In this case, we want to be able to return the
* line regardless of its length (by default we are limited to 4k). */
char *Terminal::EditNoTTY() {
char *line = NULL;
size_t len = 0, maxlen = 0;
while (1) {
if (len == maxlen) {
if (maxlen == 0)
maxlen = 16;
maxlen *= 2;
char *oldval = line;
line = (char *)realloc(line, maxlen);
if (line == NULL) {
if (oldval)
free(oldval);
return NULL;
}
}
int c = fgetc(stdin);
if (c == EOF || c == '\n') {
if (c == EOF && len == 0) {
free(line);
return NULL;
} else {
line[len] = '\0';
return line;
}
} else {
line[len] = c;
len++;
}
}
}
/* This function calls the line editing function linenoiseEdit() using
* the STDIN file descriptor set in raw mode. */
int Terminal::EditRaw(char *buf, size_t buflen, const char *prompt) {
int count;
if (buflen == 0) {
errno = EINVAL;
return -1;
}
if (Terminal::EnableRawMode() == -1) {
return -1;
}
Linenoise l(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt);
count = l.Edit();
Terminal::DisableRawMode();
printf("\n");
return count;
}
// returns true if there is more data available to read in a particular stream
int Terminal::HasMoreData(int fd) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
// no timeout: return immediately
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 0;
return select(1, &rfds, NULL, NULL, &tv);
}
/* ======================= Low level terminal handling ====================== */
/* Set if to use or not the multi line mode. */
void Terminal::SetMultiLine(int ml) {
mlmode = ml;
}
static int parseInt(const char *s, int *offset = nullptr) {
int result = 0;
int idx;
for (idx = 0; s[idx]; idx++) {
char c = s[idx];
if (c < '0' || c > '9') {
break;
}
result = result * 10 + c - '0';
if (result > 1000000) {
result = 1000000;
}
}
if (offset) {
*offset = idx;
}
return result;
}
static int tryParseEnv(const char *env_var) {
char *s;
s = getenv(env_var);
if (!s) {
return 0;
}
return parseInt(s);
}
/* Use the ESC [6n escape sequence to query the cursor position
* and return it. On error -1 is returned, on success the position of the
* cursor. */
TerminalSize Terminal::GetCursorPosition() {
int ifd = STDIN_FILENO;
int ofd = STDOUT_FILENO;
TerminalSize ws;
char buf[32];
unsigned int i = 0;
/* Report cursor location */
if (write(ofd, "\x1b[6n", 4) != 4) {
return ws;
}
/* Read the response: ESC [ rows ; cols R */
while (i < sizeof(buf) - 1) {
if (read(ifd, buf + i, 1) != 1) {
break;
}
if (buf[i] == 'R') {
break;
}
i++;
}
buf[i] = '\0';
/* Parse it. */
if (buf[0] != ESC || buf[1] != '[') {
return ws;
}
int offset = 2;
int new_offset;
ws.ws_row = parseInt(buf + offset, &new_offset);
offset += new_offset;
if (buf[offset] != ';') {
return ws;
}
offset++;
ws.ws_col = parseInt(buf + offset);
return ws;
}
TerminalSize Terminal::TryMeasureTerminalSize() {
int ofd = STDOUT_FILENO;
/* ioctl() failed. Try to query the terminal itself. */
TerminalSize start, result;
/* Get the initial position so we can restore it later. */
start = GetCursorPosition();
if (!start.ws_col) {
return result;
}
/* Go to bottom-right margin */
if (write(ofd, "\x1b[999;999f", 10) != 10) {
return result;
}
result = GetCursorPosition();
if (!result.ws_col) {
return result;
}
/* Restore position. */
char seq[32];
snprintf(seq, 32, "\x1b[%d;%df", start.ws_row, start.ws_col);
if (write(ofd, seq, strlen(seq)) == -1) {
/* Can't recover... */
}
return result;
}
/* Try to get the number of columns in the current terminal, or assume 80
* if it fails. */
TerminalSize Terminal::GetTerminalSize() {
TerminalSize result;
// try ioctl first
{
struct winsize ws;
ioctl(1, TIOCGWINSZ, &ws);
result.ws_col = ws.ws_col;
result.ws_row = ws.ws_row;
}
// try ROWS and COLUMNS env variables
if (!result.ws_col) {
result.ws_col = tryParseEnv("COLUMNS");
}
if (!result.ws_row) {
result.ws_row = tryParseEnv("ROWS");
}
// if those fail measure the size by moving the cursor to the corner and fetching the position
if (!result.ws_col || !result.ws_row) {
TerminalSize measured_size = TryMeasureTerminalSize();
Linenoise::Log("measured size col %d,row %d -- ", measured_size.ws_row, measured_size.ws_col);
if (measured_size.ws_row) {
result.ws_row = measured_size.ws_row;
}
if (measured_size.ws_col) {
result.ws_col = measured_size.ws_col;
}
}
// if all else fails use defaults (80,24)
if (!result.ws_col) {
result.ws_col = 80;
}
if (!result.ws_row) {
result.ws_row = 24;
}
return result;
}
/* Clear the screen. Used to handle ctrl+l */
void Terminal::ClearScreen() {
if (write(STDOUT_FILENO, "\x1b[H\x1b[2J", 7) <= 0) {
/* nothing to do, just to avoid warning. */
}
}
/* Beep, used for completion when there is nothing to complete or when all
* the choices were already shown. */
void Terminal::Beep() {
fprintf(stderr, "\x7");
fflush(stderr);
}
EscapeSequence Terminal::ReadEscapeSequence(int ifd) {
char seq[5];
idx_t length = ReadEscapeSequence(ifd, seq);
if (length == 0) {
return EscapeSequence::INVALID;
}
Linenoise::Log("escape of length %d\n", length);
switch (length) {
case 1:
if (seq[0] >= 'a' && seq[0] <= 'z') {
return EscapeSequence(idx_t(EscapeSequence::ALT_A) + (seq[0] - 'a'));
}
if (seq[0] >= 'A' && seq[0] <= 'Z') {
return EscapeSequence(idx_t(EscapeSequence::ALT_A) + (seq[0] - 'A'));
}
switch (seq[0]) {
case BACKSPACE:
return EscapeSequence::ALT_BACKSPACE;
case ESC:
return EscapeSequence::ESCAPE;
case '<':
return EscapeSequence::ALT_LEFT_ARROW;
case '>':
return EscapeSequence::ALT_RIGHT_ARROW;
case '\\':
return EscapeSequence::ALT_BACKSLASH;
default:
Linenoise::Log("unrecognized escape sequence of length 1 - %d\n", seq[0]);
break;
}
break;
case 2:
if (seq[0] == 'O') {
switch (seq[1]) {
case 'A': /* Up */
return EscapeSequence::UP;
case 'B': /* Down */
return EscapeSequence::DOWN;
case 'C': /* Right */
return EscapeSequence::RIGHT;
case 'D': /* Left */
return EscapeSequence::LEFT;
case 'H': /* Home */
return EscapeSequence::HOME;
case 'F': /* End*/
return EscapeSequence::END;
case 'c':
return EscapeSequence::ALT_F;
case 'd':
return EscapeSequence::ALT_B;
default:
Linenoise::Log("unrecognized escape sequence (O) %d\n", seq[1]);
break;
}
} else if (seq[0] == '[') {
switch (seq[1]) {
case 'A': /* Up */
return EscapeSequence::UP;
case 'B': /* Down */
return EscapeSequence::DOWN;
case 'C': /* Right */
return EscapeSequence::RIGHT;
case 'D': /* Left */
return EscapeSequence::LEFT;
case 'H': /* Home */
return EscapeSequence::HOME;
case 'F': /* End*/
return EscapeSequence::END;
case 'Z': /* Shift Tab */
return EscapeSequence::SHIFT_TAB;
default:
Linenoise::Log("unrecognized escape sequence (seq[1]) %d\n", seq[1]);
break;
}
} else {
Linenoise::Log("unrecognized escape sequence of length %d (%d %d)\n", length, seq[0], seq[1]);
}
break;
case 3:
if (seq[2] == '~') {
switch (seq[1]) {
case '1':
return EscapeSequence::HOME;
case '3': /* Delete key. */
return EscapeSequence::DELETE;
case '4':
case '8':
return EscapeSequence::END;
default:
Linenoise::Log("unrecognized escape sequence (~) %d\n", seq[1]);
break;
}
} else if (seq[1] == '5' && seq[2] == 'C') {
return EscapeSequence::ALT_F;
} else if (seq[1] == '5' && seq[2] == 'D') {
return EscapeSequence::ALT_B;
} else {
Linenoise::Log("unrecognized escape sequence of length %d\n", length);
}
break;
case 5:
if (memcmp(seq, "[1;5C", 5) == 0 || memcmp(seq, "[1;3C", 5) == 0) {
// [1;5C: move word right
return EscapeSequence::CTRL_MOVE_FORWARDS;
} else if (memcmp(seq, "[1;5D", 5) == 0 || memcmp(seq, "[1;3D", 5) == 0) {
// [1;5D: move word left
return EscapeSequence::CTRL_MOVE_BACKWARDS;
} else {
Linenoise::Log("unrecognized escape sequence (;) %d\n", seq[1]);
}
break;
default:
Linenoise::Log("unrecognized escape sequence of length %d\n", length);
break;
}
return EscapeSequence::UNKNOWN;
}
idx_t Terminal::ReadEscapeSequence(int ifd, char seq[]) {
if (read(ifd, seq, 1) == -1) {
return 0;
}
switch (seq[0]) {
case 'O':
case '[':
// these characters have multiple bytes following them
break;
default:
return 1;
}
if (read(ifd, seq + 1, 1) == -1) {
return 0;
}
if (seq[0] != '[') {
return 2;
}
if (seq[1] < '0' || seq[1] > '9') {
return 2;
}
/* Extended escape, read additional byte. */
if (read(ifd, seq + 2, 1) == -1) {
return 0;
}
if (seq[2] == ';') {
// read 2 extra bytes
if (read(ifd, seq + 3, 2) == -1) {
return 0;
}
return 5;
} else {
return 3;
}
}
} // namespace duckdb