terminal-util: add terminal_get_cursor_position() helper

This commit is contained in:
Lennart Poettering
2025-09-24 15:46:30 +02:00
parent fa350969ab
commit 30aeab7883
2 changed files with 221 additions and 111 deletions
+220 -111
View File
@@ -1855,6 +1855,226 @@ int terminal_set_cursor_position(int fd, unsigned row, unsigned column) {
return loop_write(fd, cursor_position, SIZE_MAX);
}
static int terminal_verify_same(int input_fd, int output_fd) {
assert(input_fd >= 0);
assert(output_fd >= 0);
/* Validates that the specified fds reference the same TTY */
if (input_fd != output_fd) {
struct stat sti;
if (fstat(input_fd, &sti) < 0)
return -errno;
if (!S_ISCHR(sti.st_mode)) /* TTYs are character devices */
return -ENOTTY;
struct stat sto;
if (fstat(output_fd, &sto) < 0)
return -errno;
if (!S_ISCHR(sto.st_mode))
return -ENOTTY;
if (sti.st_rdev != sto.st_rdev)
return -ENOLINK;
}
if (!isatty_safe(input_fd)) /* The check above was just for char device, but now let's ensure it's actually a tty */
return -ENOTTY;
return 0;
}
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);
assert(ret_processed);
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) {
*ret_processed = i + 1;
return 1; /* success! */
}
context->state = CURSOR_TEXT;
} else {
int d = undecchar(c);
/* As above, add the decimal character 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;
}
*ret_processed = size;
return 0; /* all good, but not enough data yet */
}
int terminal_get_cursor_position(
int input_fd,
int output_fd,
unsigned *ret_row,
unsigned *ret_column) {
_cleanup_close_ int nonblock_input_fd = -EBADF;
int r;
assert(input_fd >= 0);
assert(output_fd >= 0);
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 get terminal settings: %m");
struct termios new_termios = old_termios;
termios_disable_echo(&new_termios);
if (tcsetattr(input_fd, TCSANOW, &new_termios) < 0)
return log_debug_errno(errno, "Failed to set new terminal settings: %m");
/* Request cursor position (DSR/CPR) */
r = loop_write(output_fd, "\x1B[6n", SIZE_MAX);
if (r < 0)
goto finish;
/* Open a 2nd input fd, in non-blocking mode, so that we won't ever hang in read() should someone
* else process the POLLIN. */
nonblock_input_fd = r = fd_reopen(input_fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
if (r < 0)
goto finish;
usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC);
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(nonblock_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(nonblock_input_fd, buf, first ? sizeof(buf) : 1);
if (l < 0) {
if (errno == EAGAIN)
continue;
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) {
/* Superficial validity check */
if (context.row >= 32766 || context.column >= 32766) {
r = -ENODATA;
goto finish;
}
if (ret_row)
*ret_row = context.row;
if (ret_column)
*ret_column = context.column;
r = 0;
goto finish;
}
}
finish:
RET_GATHER(r, RET_NERRNO(tcsetattr(input_fd, TCSANOW, &old_termios)));
return r;
}
int terminal_reset_defensive(int fd, TerminalResetFlags flags) {
int r = 0;
@@ -1894,37 +2114,6 @@ void termios_disable_echo(struct termios *termios) {
termios->c_cc[VTIME] = 0;
}
static int terminal_verify_same(int input_fd, int output_fd) {
assert(input_fd >= 0);
assert(output_fd >= 0);
/* Validates that the specified fds reference the same TTY */
if (input_fd != output_fd) {
struct stat sti;
if (fstat(input_fd, &sti) < 0)
return -errno;
if (!S_ISCHR(sti.st_mode)) /* TTYs are character devices */
return -ENOTTY;
struct stat sto;
if (fstat(output_fd, &sto) < 0)
return -errno;
if (!S_ISCHR(sto.st_mode))
return -ENOTTY;
if (sti.st_rdev != sto.st_rdev)
return -ENOLINK;
}
if (!isatty_safe(input_fd)) /* The check above was just for char device, but now let's ensure it's actually a tty */
return -ENOTTY;
return 0;
}
typedef enum BackgroundColorState {
BACKGROUND_TEXT,
BACKGROUND_ESCAPE,
@@ -2174,86 +2363,6 @@ finish:
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);
assert(ret_processed);
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) {
*ret_processed = i + 1;
return 1; /* success! */
}
context->state = CURSOR_TEXT;
} else {
int d = undecchar(c);
/* As above, add the decimal character 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;
}
*ret_processed = size;
return 0; /* all good, but not enough data yet */
}
int terminal_get_size_by_dsr(
int input_fd,
int output_fd,
+1
View File
@@ -46,6 +46,7 @@ int terminal_reset_defensive(int fd, TerminalResetFlags flags);
int terminal_reset_defensive_locked(int fd, TerminalResetFlags flags);
int terminal_set_cursor_position(int fd, unsigned row, unsigned column);
int terminal_get_cursor_position(int input_fd, int output_fd, unsigned *ret_rows, unsigned *ret_column);
int open_terminal(const char *name, int mode);