terminal-util: add terminal_get_cursor_position() helper
This commit is contained in:
+220
-111
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user