Initial commit

This commit is contained in:
Přemysl Eric Janouch 2020-08-28 18:38:45 +02:00
commit e4bd94f79e
Signed by: p
GPG Key ID: A0420B94F92B9493
3 changed files with 339 additions and 0 deletions

12
LICENSE Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
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.

30
README.adoc Normal file
View File

@ -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.

297
termtest.c Normal file
View File

@ -0,0 +1,297 @@
//
// termtest: terminal evaluation tool
//
// Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
//
// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <poll.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <curses.h>
#include <term.h>
#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;
}