commit e4bd94f79edd257b927bf2169268687602760ff2 Author: Přemysl Eric Janouch Date: Fri Aug 28 18:38:45 2020 +0200 Initial commit 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; +}