diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 8c3af0bcb3..1cfde3e3ad 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1842,3 +1842,233 @@ finish: RET_GATHER(r, RET_NERRNO(tcsetattr(STDIN_FILENO, TCSADRAIN, &old_termios))); return r; } + +typedef enum CursorPositionState { + CURSOR_TEXT, + CURSOR_ESCAPE, + CURSOR_ROW, + CURSOR_COLUMN, +} CursorPositionState; + +typedef struct CursorPositionContext { + CursorPositionState state; + unsigned row, column; +} CursorPositionContext; + +static int scan_cursor_position_response( + CursorPositionContext *context, + const char *buf, + size_t size, + size_t *ret_processed) { + + assert(context); + assert(buf || size == 0); + + for (size_t i = 0; i < size; i++) { + char c = buf[i]; + + switch (context->state) { + + case CURSOR_TEXT: + context->state = c == '\x1B' ? CURSOR_ESCAPE : CURSOR_TEXT; + break; + + case CURSOR_ESCAPE: + context->state = c == '[' ? CURSOR_ROW : CURSOR_TEXT; + break; + + case CURSOR_ROW: + if (c == ';') + context->state = context->row > 0 ? CURSOR_COLUMN : CURSOR_TEXT; + else { + int d = undecchar(c); + + /* We read a decimal character, let's suffix it to the number we so far read, + * but let's do an overflow check first. */ + if (d < 0 || context->row > (UINT_MAX-d)/10) + context->state = CURSOR_TEXT; + else + context->row = context->row * 10 + d; + } + break; + + case CURSOR_COLUMN: + if (c == 'R') { + if (context->column > 0) { + if (ret_processed) + *ret_processed = i + 1; + + return 1; /* success! */ + } + + context->state = CURSOR_TEXT; + } else { + int d = undecchar(c); + + /* As above, add the decimal charatcer to our column number */ + if (d < 0 || context->column > (UINT_MAX-d)/10) + context->state = CURSOR_TEXT; + else + context->column = context->column * 10 + d; + } + + break; + } + + /* Reset any positions we might have picked up */ + if (IN_SET(context->state, CURSOR_TEXT, CURSOR_ESCAPE)) + context->row = context->column = 0; + } + + if (ret_processed) + *ret_processed = size; + + return 0; /* all good, but not enough data yet */ +} + +int terminal_get_size_by_dsr( + int input_fd, + int output_fd, + unsigned *ret_rows, + unsigned *ret_columns) { + + assert(input_fd >= 0); + assert(output_fd >= 0); + + int r; + + /* Tries to determine the terminal dimension by means of ANSI sequences rather than TIOCGWINSZ + * ioctl(). Why bother with this? The ioctl() information is often incorrect on serial terminals + * (since there's no handshake or protocol to determine the right dimensions in RS232), but since the + * ANSI sequences are interpreted by the final terminal instead of an intermediary tty driver they + * should be more accurate. + * + * Unfortunately there's no direct ANSI sequence to query terminal dimensions. But we can hack around + * it: we position the cursor briefly at an absolute location very far down and very far to the + * right, and then read back where we actually ended up. Because cursor locations are capped at the + * terminal width/height we should then see the right values. In order to not risk integer overflows + * in terminal applications we'll use INT16_MAX-1 as location to jump to — hopefully a value that is + * large enough for any real-life terminals, but small enough to not overflow anything or be + * recognized as a "niche" value. (Note that the dimension fields in "struct winsize" are 16bit only, + * too). */ + + if (terminal_is_dumb()) + return -EOPNOTSUPP; + + r = terminal_verify_same(input_fd, output_fd); + if (r < 0) + return log_debug_errno(r, "Called with distinct input/output fds: %m"); + + struct termios old_termios; + if (tcgetattr(input_fd, &old_termios) < 0) + return log_debug_errno(errno, "Failed to to get terminal settings: %m"); + + struct termios new_termios = old_termios; + termios_disable_echo(&new_termios); + + if (tcsetattr(input_fd, TCSADRAIN, &new_termios) < 0) + return log_debug_errno(errno, "Failed to to set new terminal settings: %m"); + + unsigned saved_row = 0, saved_column = 0; + + r = loop_write(output_fd, + "\x1B[6n" /* Request cursor position (DSR/CPR) */ + "\x1B[32766;32766H" /* Position cursor really far to the right and to the bottom, but let's stay within the 16bit signed range */ + "\x1B[6n", /* Request cursor position again */ + SIZE_MAX); + if (r < 0) + goto finish; + + usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC); + char buf[STRLEN("\x1B[1;1R")]; /* The shortest valid reply possible */ + size_t buf_full = 0; + CursorPositionContext context = {}; + + for (bool first = true;; first = false) { + if (buf_full == 0) { + usec_t n = now(CLOCK_MONOTONIC); + + if (n >= end) { + r = -EOPNOTSUPP; + goto finish; + } + + r = fd_wait_for_event(input_fd, POLLIN, usec_sub_unsigned(end, n)); + if (r < 0) + goto finish; + if (r == 0) { + r = -EOPNOTSUPP; + goto finish; + } + + /* On the first try, read multiple characters, i.e. the shortest valid + * reply. Afterwards read byte-wise, since we don't want to read too much, and + * unnecessarily drop too many characters from the input queue. */ + ssize_t l = read(input_fd, buf, first ? sizeof(buf) : 1); + if (l < 0) { + r = -errno; + goto finish; + } + + assert((size_t) l <= sizeof(buf)); + buf_full = l; + } + + size_t processed; + r = scan_cursor_position_response(&context, buf, buf_full, &processed); + if (r < 0) + goto finish; + + assert(processed <= buf_full); + buf_full -= processed; + memmove(buf, buf + processed, buf_full); + + if (r > 0) { + if (saved_row == 0) { + assert(saved_column == 0); + + /* First sequence, this is the cursor position before we set it somewhere + * into the void at the bottom right. Let's save where we are so that we can + * return later. */ + + /* Superficial validity checks */ + if (context.row <= 0 || context.column <= 0 || context.row >= 32766 || context.column >= 32766) { + r = -ENODATA; + goto finish; + } + + saved_row = context.row; + saved_column = context.column; + + /* Reset state */ + context = (CursorPositionContext) {}; + } else { + /* Second sequence, this is the cursor position after we set it somewhere + * into the void at the bottom right. */ + + /* Superficial validity checks (no particular reason to check for < 4, it's + * just a way to look for unreasonably small values) */ + if (context.row < 4 || context.column < 4 || context.row >= 32766 || context.column >= 32766) { + r = -ENODATA; + goto finish; + } + + if (ret_rows) + *ret_rows = context.row; + if (ret_columns) + *ret_columns = context.column; + + r = 0; + goto finish; + } + } + } + +finish: + /* Restore cursor position */ + if (saved_row > 0 && saved_column > 0) + RET_GATHER(r, terminal_set_cursor_position(output_fd, saved_row, saved_column)); + + RET_GATHER(r, RET_NERRNO(tcsetattr(input_fd, TCSADRAIN, &old_termios))); + return r; +} diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 51569a73f5..e2c21f7fb7 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -295,3 +295,4 @@ static inline const char* ansi_highlight_green_red(bool b) { void termios_disable_echo(struct termios *termios); int get_default_background_color(double *ret_red, double *ret_green, double *ret_blue); +int terminal_get_size_by_dsr(int input_fd, int output_fd, unsigned *ret_rows, unsigned *ret_columns); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index 9f8ca15a65..3d15d45558 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include "alloc-util.h" @@ -176,6 +178,25 @@ TEST(get_default_background_color) { log_notice("R=%g G=%g B=%g", red, green, blue); } +TEST(terminal_get_size_by_dsr) { + unsigned rows, columns; + int r; + + r = terminal_get_size_by_dsr(STDIN_FILENO, STDOUT_FILENO, &rows, &columns); + if (r < 0) + log_notice_errno(r, "Can't get screen dimensions via DSR: %m"); + else { + log_notice("terminal size via DSR: rows=%u columns=%u", rows, columns); + + struct winsize ws = {}; + + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) + log_warning_errno(errno, "Can't get terminal size via ioctl, ignoring: %m"); + else + log_notice("terminal size via ioctl: rows=%u columns=%u", ws.ws_row, ws.ws_col); + } +} + static void test_get_color_mode_with_env(const char *key, const char *val, ColorMode expected) { ASSERT_OK(setenv(key, val, true)); reset_terminal_feature_caches();