From e4bd94f79edd257b927bf2169268687602760ff2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Fri, 28 Aug 2020 18:38:45 +0200
Subject: [PATCH] Initial commit
---
LICENSE | 12 +++
README.adoc | 30 ++++++
termtest.c | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 339 insertions(+)
create mode 100644 LICENSE
create mode 100644 README.adoc
create mode 100644 termtest.c
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3f9109f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,12 @@
+Copyright (c) 2020, Přemysl Eric Janouch
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..35c8d62
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,30 @@
+termtest
+========
+
+'termtest' is a tool to test the terminal's features in a semi-automated way.
+
+I considered writing this in Python but it has a shitty curses library where you
+need to call `initscr()` before anything else. Overall, it is a good idea to
+avoid such layers on top of the terminfo library.
+
+Using
+-----
+Build dependencies: a C99 compiler (tcc is guaranteed to work) +
+Runtime dependencies: ncurses
+
+ $ git clone https://git.janouch.name/p/termtest.git
+ $ c99 termtest.c -o termtest -lncurses
+ $ ./termtest
+
+Contributing and Support
+------------------------
+Use https://git.janouch.name/p/termtest to report any bugs, request features,
+or submit pull requests. `git send-email` is tolerated. If you want to discuss
+the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
+
+Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
+
+License
+-------
+This software is released under the terms of the 0BSD license, the text of which
+is included within the package along with the list of authors.
diff --git a/termtest.c b/termtest.c
new file mode 100644
index 0000000..8adc256
--- /dev/null
+++ b/termtest.c
@@ -0,0 +1,297 @@
+//
+// termtest: terminal evaluation tool
+//
+// Copyright (c) 2020, Přemysl Eric Janouch
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// NOTE: We don't need to and will not free any memory. This is intentional.
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#define CSI "\x1b["
+
+extern char **environ;
+static struct termios saved_termios;
+struct winsize ws;
+
+// tty_atexit restores the terminal into its original mode. Some of the tested
+// extensions can't be reset by terminfo strings, so don't bother with that.
+static void tty_atexit() { tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios); }
+
+// tty_cbreak puts the terminal in the cbreak mode.
+static bool tty_cbreak() {
+ if (tcgetattr(STDIN_FILENO, &saved_termios) < 0)
+ return false;
+
+ struct termios buf = saved_termios;
+ buf.c_lflag &= ~(ECHO | ICANON);
+ buf.c_cc[VMIN] = 1;
+ buf.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &buf) < 0)
+ return false;
+
+ if (tcgetattr(STDIN_FILENO, &buf) < 0 || (buf.c_lflag & (ECHO | ICANON)) ||
+ buf.c_cc[VMIN] != 1 || buf.c_cc[VTIME] != 0) {
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios);
+ return false;
+ }
+ return true;
+}
+
+// comm writes a string to the terminal and waits for a result. Returns NULL
+// if it didn't manage to get a response, or when an error has happened.
+static char *comm(const char *req, bool wait_first) {
+ ssize_t len = write(STDOUT_FILENO, req, strlen(req));
+ if (len < strlen(req)) return NULL;
+
+ char buf[1000] = ""; size_t buf_len = 0; int n = 0;
+ struct pollfd pfd = { .fd = STDIN_FILENO, .events = POLLIN };
+ if (wait_first) poll(&pfd, 1, -1);
+ while ((n = poll(&pfd, 1, 15 /* unreliable, timing-dependent */))) {
+ if (n < 0) return NULL;
+ len = read(STDIN_FILENO, buf + buf_len, sizeof buf - buf_len - 1);
+ if (len <= 0) return NULL;
+ buf_len += len;
+ }
+ return strdup(buf);
+}
+
+enum { DEC_UNKNOWN, DEC_SET, DEC_RESET, DEC_PERMSET, DEC_PERMRESET };
+
+// decrpmstr returns a textual description of a DECRPM response.
+static const char *decrpmstr(int status) {
+ if (status == DEC_UNKNOWN) return "unknown";
+ if (status == DEC_SET) return "set";
+ if (status == DEC_RESET) return "reset";
+ if (status == DEC_PERMSET) return "permanently set";
+ if (status == DEC_PERMRESET) return "permanently reset";
+ return "?";
+}
+
+// parse_decrpm returns whether the mode response is valid (result >= 0),
+// as well as the terminal's response if it is, see the DEC_* constants.
+static int parse_decrpm(const char *resp) {
+ // E.g., \x1b[?1000;2$y
+ if (resp[0] != '\x1b' || resp[1] != '[' || resp[2] != '?') return -1;
+ char *end = NULL; errno = 0; long mode = strtol(resp + 3, &end, 10);
+ if (errno || mode < 0 || *end != ';') return -1;
+ if (!isdigit(end[1]) || end[2] != '$' || end[3] != 'y' || end[4]) return -1;
+ return end[1] - '0';
+}
+
+// deccheck checks whether a particular DEC mode is supported, whether it is
+// enabled, and returns that information as a string.
+static const char *deccheck(int number) {
+ char buf[1000] = ""; snprintf(buf, sizeof buf, CSI "?%d$p", number);
+ return decrpmstr(parse_decrpm(comm(buf, false)));
+}
+
+// test_mouse tests whether a particular mouse mode is supported.
+static void test_mouse(int mode) {
+ ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
+ comm(CSI "?1002l" CSI "?1003l" CSI "?1005l"
+ CSI "?1006l" CSI "?1015l" CSI "?1016l" CSI "?1000h", false);
+
+ char buf[100] = "";
+ snprintf(buf, sizeof buf, CSI "?%dh" "%d: ", mode, mode);
+ char *resp = comm(buf, true);
+
+ unsigned int b = -1, x = -1, y = -1;
+ unsigned char bc = -1, xc = -1, yc = -1, m = -1;
+ if (sscanf(resp, CSI "M%c%c%c", &bc, &xc, &yc) == 3
+ && bc >= 32 && xc >= 32 && yc >= 32) {
+ // Beware that this isn't compatible with xterm run with the -lc switch.
+ if (strlen(resp) > 6) printf("1005\n");
+ else printf("1000/1005 (%d @ %d,%d)\n", bc - 32, xc - 32, yc - 32);
+ } else if (sscanf(resp, CSI "<%u;%u;%u%c", &b, &x, &y, &m) == 4
+ && (m == 'm' || m == 'M')) {
+ printf("%s (%u%c @ %u,%u)\n",
+ (x > ws.ws_col || y > ws.ws_row) ? "1016" : "1006/1016",
+ b, m, x, y);
+ } else if (sscanf(resp, CSI "%u;%u;%u%c", &b, &x, &y, &m) == 4
+ && m == 'M') {
+ printf("1015 (%u @ %u,%u)\n", b - 32, x, y);
+ } else {
+ printf("Failed to parse.\n");
+ }
+
+ comm("Waiting for button up events, press a key if hanging.\n", true);
+}
+
+int main(int argc, char *argv[]) {
+ if (!tty_cbreak())
+ abort();
+
+ // Identify the terminal emulator, which is passed by arguments.
+ for (int i = 1; i < argc; i++)
+ printf("%s ", argv[i]);
+ printf("\n");
+
+ // Initialise terminfo, this should definitely succeed.
+ int err; char *term = getenv("TERM");
+ if (setupterm((char *)term, 1, &err) != OK)
+ abort();
+
+ // VTE wouldn't have sent a response to DECRQM otherwise!
+ comm("-- Press any key to start\n", true);
+
+ printf("-- Identification\nTERM=%s\n", term);
+ char *upperterm = strdup(term);
+ for (char *p = upperterm; *p; p++)
+ *p = toupper(*p);
+ printf("Version env var candidates: ");
+ for (char **p = environ; *p; p++)
+ if (strstr(*p, "VERSION") || strstr(*p, upperterm))
+ printf("%s ", *p);
+ printf("\n");
+
+ printf("-- DECRQM: ");
+ char *rpm = comm(CSI "?1000$p", false);
+ bool decrqm_supported = rpm && parse_decrpm(rpm) >= 0;
+ printf("%d\n", decrqm_supported);
+
+ printf("-- Colours\n");
+ char *colorterm = getenv("COLORTERM");
+ if (colorterm) {
+ printf("COLORTERM=%s", colorterm);
+ if (!strcmp(colorterm, "truecolor") || !strcmp(colorterm, "24bit"))
+ printf(" - Claims to support 24-bit colours");
+ printf("\n");
+ }
+
+ // TODO:
+ // - terminfo
+ // - hardcoded visual check
+
+ printf("-- Colour change\n");
+ // TODO:
+ // - terminfo
+ // - hardcoded visual check
+ // - see acolors.sh from xterm's vttests, it changes terminal colours
+ // (and urxvt passed that, at least apparently)
+
+ printf("-- Blink attribute\n");
+ bool bbc_supported = enter_bold_mode && enter_blink_mode
+ && set_a_foreground && set_a_background && exit_attribute_mode;
+ printf("Terminfo: %d\n", bbc_supported);
+ if (bbc_supported) {
+ tputs(tiparm(set_a_foreground, COLOR_GREEN), 1, putchar);
+ tputs(tiparm(set_a_background, COLOR_BLUE), 1, putchar);
+ printf("Terminfo%s ", exit_attribute_mode);
+ tputs(enter_bold_mode, 1, putchar);
+ tputs(tiparm(set_a_foreground, COLOR_GREEN), 1, putchar);
+ tputs(tiparm(set_a_background, COLOR_BLUE), 1, putchar);
+ printf("Bold%s ", exit_attribute_mode);
+ tputs(enter_blink_mode, 1, putchar);
+ tputs(tiparm(set_a_foreground, COLOR_GREEN), 1, putchar);
+ tputs(tiparm(set_a_background, COLOR_BLUE), 1, putchar);
+ printf("Blink%s ", exit_attribute_mode);
+ printf("\n");
+ }
+
+ printf("\x1b[0;32;44m" "SGR" "\x1b[m ");
+ printf("\x1b[1;32;44m" "Bold" "\x1b[m ");
+ printf("\x1b[5;32;44m" "Blink" "\x1b[m ");
+ printf("\n");
+ printf("\x1b[0;5m" "Blink with default colours." "\x1b[m");
+ printf("\n");
+
+ printf("-- Italic attribute\n");
+ bool italic_supported = enter_italics_mode && exit_italics_mode;
+ printf("Terminfo: %d\n", italic_supported);
+ if (italic_supported)
+ printf("%sTerminfo test.%s\n", enter_italics_mode, exit_italics_mode);
+ printf(CSI "3m" "SGR test.\n" CSI "0m");
+
+ printf("-- Bar cursor\n");
+ comm(CSI "5 q" "Blinking (press a key): ", true);
+ printf("\n");
+ comm(CSI "6 q" "Steady (press a key): ", true);
+ printf("\n");
+
+ // There's no actual way of restoring this to what it was before.
+ comm(CSI "2 q", false);
+
+ printf("-- w3mimgdisplay\n");
+ const char *windowid = getenv("WINDOWID");
+ if (windowid) {
+ printf("WINDOWID=%s\n", windowid);
+
+ char buf[1000] = "";
+ snprintf(buf, sizeof buf, "/usr/lib/w3m:%s", getenv("PATH"));
+ setenv("PATH", buf, true /* replace */);
+
+ // TODO:
+ // - run w3mimgdisplay now, hardcoded visual check
+ // - I guess I'll need a picture to show
+ // - /usr/share/pixmaps/debian-logo.png
+ // - /usr/share/icons/HighContrast/48x48/stock/gtk-yes.png
+ // - we get run from a relative path, so not sure about local
+ // - or we can create our own picture, something easily compressible
+ // - can even pass that as an fd during fork and use /dev/fd/N
+ }
+
+ printf("-- Sixel graphics\n");
+ // TODO:
+ // - hardcoded visual check
+
+ printf("-- Mouse protocol\n");
+ printf("Maximise the terminal window and click the rightmost column.\n");
+ // TODO: Bug the user into resizing it wide enough, or at least making
+ // the font smaller. Or maybe just warn, and say how many columns there
+ // need to be (255 - 32 + 1 = 224).
+ int mouses[] = { 1005, 1006, 1015, 1016 };
+ for (size_t i = 0; i < sizeof mouses / sizeof *mouses; i++) {
+ if (decrqm_supported)
+ printf("DECRQM(%d): %s\n", mouses[i], deccheck(mouses[i]));
+ test_mouse(mouses[i]);
+ }
+ comm(CSI "?1000l", false);
+
+ // TODO: Should test the ability to copy arbitrary text to clipboard,
+ // see ctlseqs: Manipulate Selection Data
+
+ printf("-- Bracketed paste\n");
+ if (decrqm_supported)
+ printf("DECRQM: %s\n", deccheck(2004));
+
+ // We might consider xdotool... though it can't operate the clipboard,
+ // so we'd have to use Xlib, and that is too much effort.
+ char *pasted = comm(CSI "?2004h" "Paste something: ", true);
+ printf("%d\n", !strncmp(pasted, CSI "200~", 6));
+
+ // Let the user see the results when run outside an interactive shell.
+ comm("-- Finished\n", true);
+
+ // TODO: see about unusual (new) terminfo entries.
+ // - see man tmux, TERMINFO EXTENSIONS
+ // - none of the terminfos seem to include these,
+ // though I can look for them
+
+ // atexit is broken in tcc -run, see https://savannah.nongnu.org/bugs/?56495
+ tty_atexit();
+ return 0;
+}