]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
basic/terminal-util: query terminal name by DCS
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Fri, 16 May 2025 13:30:02 +0000 (15:30 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Thu, 29 May 2025 17:20:31 +0000 (19:20 +0200)
As requested in https://github.com/systemd/systemd/issues/36994,
use DCS + q name ST. This works, but has limited terminal support:
xterm, foot, kitty.

src/basic/terminal-util.c
src/basic/terminal-util.h
src/test/test-terminal-util.c

index 51b3a83d29d9d5a774610d08dc76d5ee8f43ed92..4b1b0ecf6c8d8fd085759a7c449ea86c2301cb5e 100644 (file)
@@ -48,6 +48,9 @@
         "\033?12l"     /* reset cursor blinking */ \
         "\033 1q"      /* reset cursor style */
 
+/* How much to wait for a reply to a terminal sequence */
+#define CONSOLE_REPLY_WAIT_USEC  (333 * USEC_PER_MSEC)
+
 static volatile unsigned cached_columns = 0;
 static volatile unsigned cached_lines = 0;
 
@@ -2128,7 +2131,7 @@ int get_default_background_color(double *ret_red, double *ret_green, double *ret
         if (r < 0)
                 goto finish;
 
-        usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC);
+        usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC);
         char buf[STRLEN(ANSI_OSC "11;rgb:0/0/0" ANSI_ST)]; /* shortest possible reply */
         size_t buf_full = 0;
         BackgroundColorContext context = {};
@@ -2331,7 +2334,7 @@ int terminal_get_size_by_dsr(
         if (r < 0)
                 goto finish;
 
-        usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC);
+        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 = {};
@@ -2460,6 +2463,127 @@ int terminal_fix_size(int input_fd, int output_fd) {
         return 1;
 }
 
+#define MAX_TERMINFO_LENGTH 64
+/* python -c 'print("".join(hex(ord(i))[2:] for i in "name").upper())' */
+#define DCS_TERMINFO_Q ANSI_DCS "+q" "6E616D65" ANSI_ST
+/* The answer is either 0+r… (invalid) or 1+r… (OK). */
+#define DCS_TERMINFO_R0 ANSI_DCS "0+r" ANSI_ST
+#define DCS_TERMINFO_R1 ANSI_DCS "1+r" "6E616D65" "=" /* This is followed by Pt ST. */
+assert_cc(STRLEN(DCS_TERMINFO_R0) <= STRLEN(DCS_TERMINFO_R1 ANSI_ST));
+
+static int scan_terminfo_response(
+                const char *buf,
+                size_t size,
+                char **ret_name) {
+        int r;
+
+        assert(buf);
+        assert(ret_name);
+
+        /* Check if we have enough space for the shortest possible answer. */
+        if (size < STRLEN(DCS_TERMINFO_R0))
+                return -EAGAIN;
+
+        /* Check if the terminating sequence is present */
+        if (memcmp(buf + size - STRLEN(ANSI_ST), ANSI_ST, STRLEN(ANSI_ST)) != 0)
+                return -EAGAIN;
+
+        if (size <= STRLEN(DCS_TERMINFO_R1 ANSI_ST))
+                return -EINVAL;  /* The answer is invalid or empty */
+
+        if (memcmp(buf, DCS_TERMINFO_R1, STRLEN(DCS_TERMINFO_R1)) != 0)
+                return -EINVAL;  /* The answer is not valid */
+
+        _cleanup_free_ void *dec = NULL;
+        size_t dec_size;
+        r = unhexmem_full(buf + STRLEN(DCS_TERMINFO_R1), size - STRLEN(DCS_TERMINFO_R1 ANSI_ST),
+                          /* secure= */ false,
+                          &dec, &dec_size);
+        if (r < 0)
+                return r;
+
+        assert(((const char *) dec)[dec_size] == '\0'); /* unhexmem appends NUL for our convenience */
+        if (memchr(dec, '\0', dec_size) || string_has_cc(dec, NULL) || !filename_is_valid(dec))
+                return -EUCLEAN;
+
+        *ret_name = TAKE_PTR(dec);
+        return 0;
+}
+
+int terminal_get_terminfo_by_dcs(int fd, char **ret_name) {
+        int r;
+
+        assert(fd >= 0);
+        assert(ret_name);
+
+        /* Note: fd must be in non-blocking read-write mode! */
+
+        struct termios old_termios;
+        if (tcgetattr(fd, &old_termios) < 0)
+                return -errno;
+
+        struct termios new_termios = old_termios;
+        termios_disable_echo(&new_termios);
+
+        if (tcsetattr(fd, TCSADRAIN, &new_termios) < 0)
+                return -errno;
+
+        r = loop_write(fd, DCS_TERMINFO_Q, SIZE_MAX);
+        if (r < 0)
+                goto finish;
+
+        usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC);
+        char buf[STRLEN(DCS_TERMINFO_R1) + MAX_TERMINFO_LENGTH + STRLEN(ANSI_ST)];
+        size_t bytes = 0;
+
+        for (;;) {
+                usec_t n = now(CLOCK_MONOTONIC);
+                if (n >= end) {
+                        r = -EOPNOTSUPP;
+                        break;
+                }
+
+                r = fd_wait_for_event(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(fd, buf + bytes, bytes == 0 ? STRLEN(DCS_TERMINFO_R0) : 1);
+                if (l < 0) {
+                        if (errno == EAGAIN)
+                                continue;
+                        r = -errno;
+                        break;
+                }
+
+                assert((size_t) l <= sizeof(buf) - bytes);
+                bytes += l;
+
+                r = scan_terminfo_response(buf, bytes, ret_name);
+                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 alloted space. Something is
+                                          * wrong, possibly some unrelated bytes got injected into the
+                                          * answer. */
+                        break;
+                }
+        }
+
+finish:
+        /* We ignore failure here. We already got a reply and if cleanup fails, we can't help that. */
+        (void) tcsetattr(fd, TCSADRAIN, &old_termios);
+        return r;
+}
+
 int terminal_is_pty_fd(int fd) {
         int r;
 
index 17dbdfdfd583c77e1650f725dedc09b97c26a5f2..79683f5a85741668e86142c01c86bdf4cccc4203 100644 (file)
 #define ANSI_WINDOW_TITLE_PUSH "\x1b[22;2t"
 #define ANSI_WINDOW_TITLE_POP "\x1b[23;2t"
 
-/* ANSI "string terminator" character ("ST"). Terminal emulators typically allow three different ones: 0x07,
- * 0x9c, and 0x1B 0x5C. We'll avoid 0x07 (BEL, aka ^G) since it might trigger unexpected TTY signal
- * handling. And we'll avoid 0x9c since that's also valid regular codepoint in UTF-8 and elsewhere, and
- * creates ambiguities. Because of that some terminal emulators explicitly choose not to support it. Hence we
- * use 0x1B 0x5c */
-#define ANSI_ST "\e\\"
+/* The "device control string" ("DCS") start sequence */
+#define ANSI_DCS "\eP"
 
 /* The "operating system command" ("OSC") start sequence */
 #define ANSI_OSC "\e]"
 
+/* ANSI "string terminator" character ("ST"). Terminal emulators typically allow three different ones: 0x07,
+ * 0x9c, and 0x1B 0x5C. We'll avoid 0x07 (BEL, aka ^G) since it might trigger unexpected TTY signal handling.
+ * And we'll avoid 0x9c since that's also valid regular codepoint in UTF-8 and elsewhere, and creates
+ * ambiguities. Because of that some terminal emulators explicitly choose not to support it. Hence we use
+ * 0x1B 0x5c. */
+#define ANSI_ST "\e\\"
+
 bool isatty_safe(int fd);
 
 typedef enum TerminalResetFlags {
@@ -142,9 +145,10 @@ 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_fix_size(int input_fd, int output_fd);
 
+int terminal_get_terminfo_by_dcs(int fd, char **ret_name);
+
 int terminal_is_pty_fd(int fd);
 
 int pty_open_peer(int fd, int mode);
index f128ddca1f1e7d133162b45b1f804f22aea61c3f..41bd4fbcecbee48c899ce5c18c3f0bfdcffffa6d 100644 (file)
@@ -213,6 +213,21 @@ TEST(terminal_fix_size) {
                 log_notice("Fixed terminal size.");
 }
 
+TEST(terminal_get_terminfo_by_dcs) {
+        _cleanup_free_ char *name = NULL;
+        int r;
+
+        /* We need a non-blocking read-write fd. */
+        _cleanup_close_ int fd = fd_reopen(STDIN_FILENO, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+        if (fd < 0)
+                return (void) log_info_errno(fd, "Cannot reopen stdin in read-write mode: %m");
+
+        r = terminal_get_terminfo_by_dcs(fd, &name);
+        if (r < 0)
+                return (void) log_info_errno(r, "Can't get terminal terminfo via DCS: %m");
+        log_info("terminal terminfo via DCS: %s, $TERM: %s", name, strnull(getenv("TERM")));
+}
+
 TEST(terminal_is_pty_fd) {
         _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
         int r;