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).
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user