iomenu

interactive terminal dmenu-style menu
Log | Files | Refs | README | LICENSE

iomenu.c (8225B)


      1 #include <ctype.h>
      2 #include <errno.h>
      3 #include <fcntl.h>
      4 #include <limits.h>
      5 #include <signal.h>
      6 #include <stddef.h>
      7 #include <stdio.h>
      8 #include <stdlib.h>
      9 #include <string.h>
     10 #include <sys/ioctl.h>
     11 #include <termios.h>
     12 #include <unistd.h>
     13 #include <assert.h>
     14 
     15 #include "compat.h"
     16 #include "term.h"
     17 #include "utf8.h"
     18 
     19 struct {
     20 	char input[LINE_MAX];
     21 	size_t cur;
     22 
     23 	char **lines_buf;
     24 	size_t lines_count;
     25 
     26 	char **match_buf;
     27 	size_t match_count;
     28 } ctx;
     29 
     30 int opt_comment;
     31 
     32 /*
     33  * Keep the line if it match every token (in no particular order,
     34  * and allowed to be overlapping).
     35  */
     36 static int
     37 match_line(char *line, char **tokv)
     38 {
     39 	if (opt_comment && line[0] == '#')
     40 		return 2;
     41 	for (; *tokv != NULL; tokv++)
     42 		if (strcasestr(line, *tokv) == NULL)
     43 			return 0;
     44 	return 1;
     45 }
     46 
     47 /*
     48  * Free the structures, reset the terminal state and exit with an
     49  * error message.
     50  */
     51 static void
     52 die(const char *msg)
     53 {
     54 	int e = errno;
     55 
     56 	term_raw_off(2);
     57 
     58 	fprintf(stderr, "iomenu: ");
     59 	errno = e;
     60 	perror(msg);
     61 
     62 	exit(1);
     63 }
     64 
     65 void *
     66 xrealloc(void *ptr, size_t sz)
     67 {
     68 	ptr = realloc(ptr, sz);
     69 	if (ptr == NULL)
     70 		die("realloc");
     71 	return ptr;
     72 }
     73 
     74 void *
     75 xmalloc(size_t sz)
     76 {
     77 	void *ptr;
     78 
     79 	ptr = malloc(sz);
     80 	if (ptr == NULL)
     81 		die("malloc");
     82 	return ptr;
     83 }
     84 
     85 static void
     86 do_move(int sign)
     87 {
     88 	/* integer overflow will do what we need */
     89 	for (size_t i = ctx.cur + sign; i < ctx.match_count; i += sign) {
     90 		if (opt_comment == 0 || ctx.match_buf[i][0] != '#') {
     91 			ctx.cur = i;
     92 			break;
     93 		}
     94 	}
     95 }
     96 
     97 /*
     98  * First split input into token, then match every token independently against
     99  * every line.  The matching lines fills matches.  Matches are searched inside
    100  * of `searchv' of size `searchc'
    101  */
    102 static void
    103 do_filter(char **search_buf, size_t search_count)
    104 {
    105 	char **t, *tokv[(sizeof ctx.input + 1) * sizeof(char *)];
    106 	char *b, buf[sizeof ctx.input];
    107 
    108 	strlcpy(buf, ctx.input, sizeof buf);
    109 
    110 	for (b = buf, t = tokv; (*t = strsep(&b, " \t")) != NULL; t++)
    111 		continue;
    112 	*t = NULL;
    113 
    114 	ctx.cur = ctx.match_count = 0;
    115 	for (size_t n = 0; n < search_count; n++)
    116 		if (match_line(search_buf[n], tokv))
    117 			ctx.match_buf[ctx.match_count++] = search_buf[n];
    118 	if (opt_comment && ctx.match_buf[ctx.cur][0] == '#')
    119 		do_move(+1);
    120 }
    121 
    122 static void
    123 do_move_page(signed int sign)
    124 {
    125 	int rows = term.winsize.ws_row - 1;
    126 	size_t i = ctx.cur - ctx.cur % rows + rows * sign;
    127 
    128 	if (i >= ctx.match_count)
    129 		return;
    130 	ctx.cur = i - 1;
    131 
    132 	do_move(+1);
    133 }
    134 
    135 static void
    136 do_move_header(signed int sign)
    137 {
    138 	do_move(sign);
    139 
    140 	if (opt_comment == 0)
    141 		return;
    142 	for (ctx.cur += sign;; ctx.cur += sign) {
    143 		char *cur = ctx.match_buf[ctx.cur];
    144 
    145 		if (ctx.cur >= ctx.match_count) {
    146 			ctx.cur--;
    147 			break;
    148 		}
    149 		if (cur[0] == '#')
    150 			break;
    151 	}
    152 
    153 	do_move(+1);
    154 }
    155 
    156 static void
    157 do_remove_word(void)
    158 {
    159 	int len, i;
    160 
    161 	len = strlen(ctx.input) - 1;
    162 	for (i = len; i >= 0 && isspace(ctx.input[i]); i--)
    163 		ctx.input[i] = '\0';
    164 	len = strlen(ctx.input) - 1;
    165 	for (i = len; i >= 0 && !isspace(ctx.input[i]); i--)
    166 		ctx.input[i] = '\0';
    167 	do_filter(ctx.lines_buf, ctx.lines_count);
    168 }
    169 
    170 static void
    171 do_add_char(char c)
    172 {
    173 	int len;
    174 
    175 	len = strlen(ctx.input);
    176 	if (len + 1 == sizeof ctx.input)
    177 		return;
    178 	if (isprint(c)) {
    179 		ctx.input[len] = c;
    180 		ctx.input[len + 1] = '\0';
    181 	}
    182 	do_filter(ctx.match_buf, ctx.match_count);
    183 }
    184 
    185 static void
    186 do_print_selection(void)
    187 {
    188 	if (opt_comment) {
    189 		char **match = ctx.match_buf + ctx.cur;
    190 
    191 		while (--match >= ctx.match_buf) {
    192 			if ((*match)[0] == '#') {
    193 				fprintf(stdout, "%s", *match + 1);
    194 				break;
    195 			}
    196 		}
    197 		fprintf(stdout, "%c", '\t');
    198 	}
    199 	term_raw_off(2);
    200 	if (ctx.match_count == 0
    201 	  || (opt_comment && ctx.match_buf[ctx.cur][0] == '#'))
    202 		fprintf(stdout, "%s\n", ctx.input);
    203 	else
    204 		fprintf(stdout, "%s\n", ctx.match_buf[ctx.cur]);
    205 	term_raw_on(2);
    206 }
    207 
    208 /*
    209  * Big case table, that calls itself back for with TERM_KEY_ALT (aka Esc), TERM_KEY_CSI
    210  * (aka Esc + [).  These last two have values above the range of ASCII.
    211  */
    212 static int
    213 key_action(void)
    214 {
    215 	int key;
    216 
    217 	key = term_get_key(stderr);
    218 	switch (key) {
    219 	case TERM_KEY_CTRL('Z'):
    220 		term_raw_off(2);
    221 		kill(getpid(), SIGSTOP);
    222 		term_raw_on(2);
    223 		break;
    224 	case TERM_KEY_CTRL('C'):
    225 	case TERM_KEY_CTRL('D'):
    226 		return -1;
    227 	case TERM_KEY_CTRL('U'):
    228 		ctx.input[0] = '\0';
    229 		do_filter(ctx.lines_buf, ctx.lines_count);
    230 		break;
    231 	case TERM_KEY_CTRL('W'):
    232 		do_remove_word();
    233 		break;
    234 	case TERM_KEY_DELETE:
    235 	case TERM_KEY_BACKSPACE:
    236 		ctx.input[strlen(ctx.input) - 1] = '\0';
    237 		do_filter(ctx.lines_buf, ctx.lines_count);
    238 		break;
    239 	case TERM_KEY_ARROW_UP:
    240 	case TERM_KEY_CTRL('P'):
    241 		do_move(-1);
    242 		break;
    243 	case TERM_KEY_ALT('p'):
    244 		do_move_header(-1);
    245 		break;
    246 	case TERM_KEY_ARROW_DOWN:
    247 	case TERM_KEY_CTRL('N'):
    248 		do_move(+1);
    249 		break;
    250 	case TERM_KEY_ALT('n'):
    251 		do_move_header(+1);
    252 		break;
    253 	case TERM_KEY_PAGE_UP:
    254 	case TERM_KEY_ALT('v'):
    255 		do_move_page(-1);
    256 		break;
    257 	case TERM_KEY_PAGE_DOWN:
    258 	case TERM_KEY_CTRL('V'):
    259 		do_move_page(+1);
    260 		break;
    261 	case TERM_KEY_TAB:
    262 		if (ctx.match_count == 0)
    263 			break;
    264 		strlcpy(ctx.input, ctx.match_buf[ctx.cur], sizeof(ctx.input));
    265 		do_filter(ctx.match_buf, ctx.match_count);
    266 		break;
    267 	case TERM_KEY_ENTER:
    268 	case TERM_KEY_CTRL('M'):
    269 		do_print_selection();
    270 		return 0;
    271 	default:
    272 		do_add_char(key);
    273 	}
    274 
    275 	return 1;
    276 }
    277 
    278 static void
    279 print_line(char *line, int highlight)
    280 {
    281 	if (opt_comment && line[0] == '#') {
    282 		fprintf(stderr, "\n\x1b[1m\r%.*s\x1b[m",
    283 		  term_at_width(line + 1, term.winsize.ws_col, 0), line + 1);
    284 	} else if (highlight) {
    285 		fprintf(stderr, "\n\x1b[47;30m\x1b[K\r%.*s\x1b[m",
    286 		  term_at_width(line, term.winsize.ws_col, 0), line);
    287 	} else {
    288 		fprintf(stderr, "\n%.*s",
    289 		  term_at_width(line, term.winsize.ws_col, 0), line);
    290 	}
    291 }
    292 
    293 static void
    294 do_print_screen(void)
    295 {
    296 	char **m;
    297 	int p, c, cols, rows;
    298 	size_t i;
    299 
    300 	cols = term.winsize.ws_col;
    301 	rows = term.winsize.ws_row - 1; /* -1 to keep one line for user input */
    302 	p = c = 0;
    303 	i = ctx.cur - ctx.cur % rows;
    304 	m = ctx.match_buf + i;
    305 	fprintf(stderr, "\x1b[2J");
    306 	while (p < rows && i < ctx.match_count) {
    307 		print_line(*m, i == ctx.cur);
    308 		p++, i++, m++;
    309 	}
    310 	fprintf(stderr, "\x1b[H%.*s",
    311 	  term_at_width(ctx.input, cols, c), ctx.input);
    312 	fflush(stderr);
    313 }
    314 
    315 static void
    316 sig_winch(int sig)
    317 {
    318 	if (ioctl(STDERR_FILENO, TIOCGWINSZ, &term.winsize) == -1)
    319 		die("ioctl");
    320 	do_print_screen();
    321 	signal(sig, sig_winch);
    322 }
    323 
    324 static void
    325 usage(char const *arg0)
    326 {
    327 	fprintf(stderr, "usage: %s [-#] <lines\n", arg0);
    328 	exit(1);
    329 }
    330 
    331 static int
    332 read_stdin(char **buf)
    333 {
    334 	size_t len = 0;
    335 
    336 	assert(*buf == NULL);
    337 
    338 	for (int c; (c = fgetc(stdin)) != EOF;) {
    339 		if (c == '\0') {
    340 			fprintf(stderr, "iomenu: ignoring '\\0' byte in input\r\n");
    341 			continue;
    342 		}
    343 		*buf = xrealloc(*buf, sizeof *buf + len + 1);
    344 		(*buf)[len++] = c;
    345 	}
    346 	*buf = xrealloc(*buf, sizeof *buf + len + 1);
    347 	(*buf)[len] = '\0';
    348 
    349 	return 0;
    350 }
    351 
    352 /*
    353  * Split a buffer into an array of lines, without allocating memory for every
    354  * line, but using the input buffer and replacing '\n' by '\0'.
    355  */
    356 static void
    357 split_lines(char *s)
    358 {
    359 	size_t sz;
    360 
    361 	ctx.lines_count = 0;
    362 	for (;;) {
    363 		sz = (ctx.lines_count + 1) * sizeof s;
    364 		ctx.lines_buf = xrealloc(ctx.lines_buf, sz);
    365 		ctx.lines_buf[ctx.lines_count++] = s;
    366 
    367 		s = strchr(s, '\n');
    368 		if (s == NULL)
    369 			break;
    370 		*s++ = '\0';
    371 	}
    372 	sz = ctx.lines_count * sizeof s;
    373 	ctx.match_buf = xmalloc(sz);
    374 	memcpy(ctx.match_buf, ctx.lines_buf, sz);
    375 }
    376 
    377 /*
    378  * Read stdin in a buffer, filling a table of lines, then re-open stdin to
    379  * /dev/tty for an interactive (raw) session to let the user filter and select
    380  * one line by searching words within stdin.  This was inspired from dmenu.
    381  */
    382 int
    383 main(int argc, char *argv[])
    384 {
    385 	char *buf = NULL, *arg0;
    386 
    387 	arg0 = *argv;
    388 	for (int opt; (opt = getopt(argc, argv, "#v")) > 0;) {
    389 		switch (opt) {
    390 		case 'v':
    391 			fprintf(stdout, "%s\n", VERSION);
    392 			exit(0);
    393 		case '#':
    394 			opt_comment = 1;
    395 			break;
    396 		default:
    397 			usage(arg0);
    398 		}
    399 	}
    400 	argc -= optind;
    401 	argv += optind;
    402 
    403 	read_stdin(&buf);
    404 	split_lines(buf);
    405 
    406 	do_filter(ctx.lines_buf, ctx.lines_count);
    407 
    408 	if (!isatty(2))
    409 		die("file descriptor 2 (stderr)");
    410 
    411 	freopen("/dev/tty", "w+", stderr);
    412 	if (stderr == NULL)
    413 		die("re-opening standard error read/write");
    414 
    415 	term_raw_on(2);
    416 	sig_winch(SIGWINCH);
    417 
    418 #ifdef __OpenBSD__
    419 	pledge("stdio tty", NULL);
    420 #endif
    421 
    422 	while (key_action() > 0)
    423 		do_print_screen();
    424 
    425 	term_raw_off(2);
    426 
    427 	return 0;
    428 }