From: Zbigniew Jędrzejewski-Szmek Date: Thu, 20 Nov 2025 14:49:29 +0000 (+0100) Subject: basic/terminal-util: add code to read window size using CSI 18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bfb8e9c5755cc95906c464fb7af8f95ff5177425;p=thirdparty%2Fsystemd.git basic/terminal-util: add code to read window size using CSI 18 In my tests, the sequence works on Ptyxis 49.0, gnome-terminal-3.56.3, xterm-401, rxvt-unicode-9.31, under tmux-3.5a, over ssh and serial console connected to ptyxis. It did not work on a text console in a VM (TERM=linux) or under kmscon-9.1.0 (TERM=vt102). --- diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index b7de72daedc..230c85936e1 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -2520,6 +2520,132 @@ finish: return r; } +/* + * See https://terminalguide.namepad.de/seq/csi_st-18/, + * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_. + */ +#define CSI18_Q "\x1B[18t" /* Report the size of the text area in characters */ +#define CSI18_Rp "\x1B[8;" /* Reply prefix */ +#define CSI18_R0 CSI18_Rp "1;1t" /* Shortest reply */ +#define CSI18_R1 CSI18_Rp "32766;32766t" /* Longest reply */ + +static int scan_text_area_size_response( + const char *buf, + size_t size, + unsigned *ret_rows, + unsigned *ret_columns) { + + assert(buf); + assert(ret_rows); + assert(ret_columns); + + /* Check if we have enough space for the shortest possible answer. */ + if (size < STRLEN(CSI18_R0)) + return -EAGAIN; + + /* Check if the terminating sequence is present */ + if (buf[size - 1] != 't') + return -EAGAIN; + + unsigned short rows, columns; + if (sscanf(buf, CSI18_Rp "%hu;%hut", &rows, &columns) != 2) + return -EINVAL; + + *ret_rows = rows; + *ret_columns = columns; + return 0; +} + +int terminal_get_size_by_csi18( + int input_fd, + int output_fd, + unsigned *ret_rows, + unsigned *ret_columns) { + int r; + + assert(input_fd >= 0); + assert(output_fd >= 0); + + /* Tries to determine the terminal dimension by means of an ANSI sequence CSI 18. */ + + 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"); + + /* Open a 2nd input fd, in non-blocking mode, so that we won't ever hang in read() + * should someone else process the POLLIN. Do all subsequent operations on the new fd. */ + _cleanup_close_ int nonblock_input_fd = r = fd_reopen(input_fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (r < 0) + return r; + + struct termios old_termios; + if (tcgetattr(nonblock_input_fd, &old_termios) < 0) + return log_debug_errno(errno, "Failed to get terminal settings: %m"); + + struct termios new_termios = old_termios; + termios_disable_echo(&new_termios); + + if (tcsetattr(nonblock_input_fd, TCSANOW, &new_termios) < 0) + return log_debug_errno(errno, "Failed to set new terminal settings: %m"); + + r = loop_write(output_fd, CSI18_Q, SIZE_MAX); + if (r < 0) + goto finish; + + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + char buf[STRLEN(CSI18_R1)]; + size_t bytes = 0; + + for (;;) { + usec_t n = now(CLOCK_MONOTONIC); + if (n >= end) { + r = -EOPNOTSUPP; + break; + } + + r = fd_wait_for_event(nonblock_input_fd, POLLIN, usec_sub_unsigned(end, n)); + if (r < 0) + break; + if (r == 0) { + r = -EOPNOTSUPP; + break; + } + + /* On the first read, read multiple characters, i.e. the shortest valid reply. Afterwards + * read byte by byte, since we don't want to read too much and drop characters from the input + * queue. */ + ssize_t l = read(nonblock_input_fd, buf + bytes, bytes == 0 ? STRLEN(CSI18_R0) : 1); + if (l < 0) { + if (errno == EAGAIN) + continue; + r = -errno; + break; + } + + assert((size_t) l <= sizeof(buf) - bytes); + bytes += l; + + r = scan_text_area_size_response(buf, bytes, ret_rows, ret_columns); + if (r != -EAGAIN) + break; + + if (bytes == sizeof(buf)) { + r = -EOPNOTSUPP; /* The response has the right prefix, but we didn't find a valid + * answer with a terminator in the allotted space. Something is + * wrong, possibly some unrelated bytes got injected into the + * answer. */ + break; + } + } + +finish: + (void) tcsetattr(nonblock_input_fd, TCSANOW, &old_termios); + return r; +} + int terminal_fix_size(int input_fd, int output_fd) { unsigned rows, columns; int r; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 48cce67833f..c9f4ae4b0db 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -150,6 +150,7 @@ 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); +int terminal_get_size_by_csi18(int input_fd, int output_fd, unsigned *ret_rows, unsigned *ret_columns); int terminal_fix_size(int input_fd, int output_fd); int terminal_get_terminfo_by_dcs(int fd, char **ret_name); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index 358d578ccb0..df55f172bbe 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -171,6 +171,27 @@ TEST(get_default_background_color) { log_notice("R=%g G=%g B=%g", red, green, blue); } +TEST(terminal_get_size_by_csi18) { + unsigned rows, columns; + int r; + + usec_t n = now(CLOCK_MONOTONIC); + r = terminal_get_size_by_csi18(STDIN_FILENO, STDOUT_FILENO, &rows, &columns); + log_info("%s took %s", __func__+5, + FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), n), USEC_PER_MSEC)); + if (r < 0) + return (void) log_notice_errno(r, "Can't get screen dimensions via CSI 18: %m"); + + log_notice("terminal size via CSI 18: 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); +} + TEST(terminal_get_size_by_dsr) { unsigned rows, columns; int r;