tquery

Interactive command query tool
git clone https://git.sinitax.com/sinitax/tquery
Log | Files | Refs | LICENSE | sfeed.txt

tquery.c (11862B)


      1 #include <curses.h>
      2 
      3 #include <sys/wait.h>
      4 #include <sys/poll.h>
      5 #include <sys/fcntl.h>
      6 #include <signal.h>
      7 #include <dirent.h>
      8 #include <unistd.h>
      9 #include <errno.h>
     10 #include <string.h>
     11 #include <stdarg.h>
     12 #include <stdio.h>
     13 #include <stdlib.h>
     14 
     15 #define LOAD_BUFSIZ 16384
     16 #define INPUT_BUFSIZ 2048
     17 
     18 #define CTRL(x) ((x) & 0x1f)
     19 
     20 #define MIN(a, b) ((a) > (b) ? (b) : (a))
     21 #define MAX(a, b) ((a) > (b) ? (a) : (b))
     22 
     23 struct nav {
     24 	ssize_t wmin, wmax, wlen;
     25 	ssize_t sel, min, max;
     26 };
     27 
     28 static void sigint_handler(int sig);
     29 
     30 static const struct sigaction sigint_action = {
     31 	.sa_flags = SA_RESTART,
     32 	.sa_handler = sigint_handler
     33 };
     34 
     35 static int child_fd = -1;
     36 static pid_t child_pid = 0;
     37 static const char **child_argv = NULL;
     38 
     39 static pid_t hook_pid = -1;
     40 static char hook_key_arg[6] = { 0 };
     41 static char *hook_argv[4] = { NULL, hook_key_arg, NULL, NULL };
     42 static bool hook_io = false;
     43 
     44 static char *run_argv[4] = { NULL, NULL, NULL };
     45 static bool run_cmd = false;
     46 static bool run_io = false;
     47 
     48 static char *loadbuf = NULL;
     49 
     50 static char *output = NULL;
     51 static size_t output_len = 0;
     52 static size_t output_cap = 0;
     53 
     54 static size_t *delims = NULL;
     55 static size_t delims_len = 0;
     56 static size_t delims_cap = 0;
     57 
     58 static struct nav output_nav;
     59 
     60 static char *inputbuf = NULL;
     61 static size_t inputlen = 0;
     62 
     63 static bool split_args = false;
     64 static bool oneshot = false;
     65 static bool errlog = false;
     66 static char delim = '\n';
     67 
     68 static void
     69 die(const char *fmt, ...)
     70 {
     71 	va_list ap;
     72 
     73 	fputs("tquery: ", stderr);
     74 
     75 	va_start(ap, fmt);
     76 	vfprintf(stderr, fmt, ap);
     77 	va_end(ap);
     78 
     79 	if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
     80 		fputc(' ', stderr);
     81 		perror(NULL);
     82 	} else {
     83 		fputc('\n', stderr);
     84 	}
     85 
     86 	exit(1);
     87 }
     88 
     89 static void
     90 nav_update_win(struct nav *nav)
     91 {
     92 	nav->wmin = MAX(nav->wmax - nav->wlen, nav->min);
     93 	nav->wmax = MIN(nav->wmin + nav->wlen, nav->max);
     94 	nav->sel = MAX(MIN(nav->sel, nav->wmax - 1), nav->wmin);
     95 }
     96 
     97 static void
     98 nav_init(struct nav *nav)
     99 {
    100 	nav->sel = 0;
    101 	nav->min = 0;
    102 	nav->max = 0;
    103 	nav->wlen = 0;
    104 	nav->wmin = 0;
    105 	nav->wmax = 0;
    106 }
    107 
    108 static void
    109 nav_update_bounds(struct nav *nav, int min, int max)
    110 {
    111 	nav->min = min;
    112 	nav->max = max;
    113 	nav_update_win(nav);
    114 }
    115 
    116 static void
    117 nav_update_wlen(struct nav *nav, ssize_t wlen)
    118 {
    119 	nav->wlen = wlen;
    120 	nav_update_win(nav);
    121 }
    122 
    123 static void
    124 nav_update_sel(struct nav *nav, ssize_t sel)
    125 {
    126 	nav->sel = MAX(MIN(sel, nav->max - 1), nav->min);
    127 	if (nav->sel >= nav->wmax) {
    128 		nav->wmax = nav->sel + 1;
    129 		nav->wmin = MAX(nav->min, nav->wmax - nav->wlen);
    130 	} else if (nav->sel < nav->wmin) {
    131 		nav->wmin = nav->sel;
    132 		nav->wmax = MIN(nav->wmin + nav->wlen, nav->max);
    133 	}
    134 }
    135 
    136 static void
    137 invoke(const char **argv, pid_t *pid, int *fd, bool in, bool out, bool err)
    138 {
    139 	int out_pipe[2];
    140 	int zfd;
    141 
    142 	if (fd && *fd >= 0) {
    143 		close(*fd);
    144 		*fd = -1;
    145 	}
    146 
    147 	if (pipe(out_pipe)) die("pipe:");
    148 
    149 	*pid = fork();
    150 	if (*pid < 0) die("fork:");
    151 	if (!*pid) {
    152 		zfd = open("/dev/null", O_RDWR);
    153 		if (zfd < 0) die("open /dev/null");
    154 		if (!in) dup2(zfd, 0);
    155 		if (out && fd) dup2(out_pipe[1], 1);
    156 		else if (!out) dup2(zfd, 1);
    157 		if (!err) dup2(zfd, 2);
    158 		if (fd) close(out_pipe[0]);
    159 		close(zfd);
    160 		execvp(argv[0], (char *const *) argv);
    161 		die("execv '%s':");
    162 	} else {
    163 		if (fd) {
    164 			close(out_pipe[1]);
    165 			*fd = out_pipe[0];
    166 		}
    167 	}
    168 }
    169 
    170 static void *
    171 addcap(void *alloc, size_t dsize, size_t min, size_t *cap)
    172 {
    173 	if (min > *cap) {
    174 		*cap = *cap * 2;
    175 		if (*cap < min) *cap = min;
    176 		alloc = realloc(alloc, dsize * *cap);
    177 		if (!alloc) die("realloc:");
    178 	}
    179 
    180 	return alloc;
    181 }
    182 
    183 static void
    184 load(void)
    185 {
    186 	ssize_t nread;
    187 	size_t i;
    188 	char *start;
    189 
    190 	nread = read(child_fd, loadbuf, LOAD_BUFSIZ);
    191 	if (nread <= 0) {
    192 		child_pid = 0;
    193 		close(child_fd);
    194 		child_fd = -1;
    195 		return;
    196 	}
    197 
    198 	output = addcap(output, 1, output_len + (size_t) nread, &output_cap);
    199 	memcpy(output + output_len, loadbuf, (size_t) nread);
    200 
    201 	start = output + output_len;
    202 
    203 	for (i = output_len; i < output_len + (size_t) nread; i++, start++) {
    204 		if (*start == delim) {
    205 			*start = '\0';
    206 			delims = addcap(delims, sizeof(size_t),
    207 				delims_len + 1, &delims_cap);
    208 			delims[delims_len++] = i;
    209 		}
    210 	}
    211 
    212 	output_len += (size_t) nread;
    213 }
    214 
    215 static void
    216 unload(void)
    217 {
    218 	int status;
    219 
    220 	if (!child_pid) return;
    221 
    222 	kill(child_pid, SIGKILL);
    223 	do {
    224 		waitpid(child_pid, &status, 0);
    225 	} while (!WIFEXITED(status) && !WIFSIGNALED(status));
    226 
    227 	close(child_fd);
    228 	child_fd = -1;
    229 	child_pid = 0;
    230 }
    231 
    232 static char *
    233 entry(size_t i)
    234 {
    235 	return output + (i ? delims[i-1] + 1 : 0);
    236 }
    237 
    238 static void
    239 update(void)
    240 {
    241 	const ssize_t miny = 3;
    242 	int width, height;
    243 	ssize_t i, y;
    244 
    245 	erase();
    246 
    247 	width = getmaxx(stdscr);
    248 	height = getmaxy(stdscr);
    249 
    250 	mvprintw(1, 1, "%lu %li %lu %i", output_len,
    251 		output_nav.sel, delims_len, child_pid);
    252 	mvprintw(2, 1, "> %.*s", width - 1, inputbuf);
    253 
    254 	nav_update_bounds(&output_nav, 0, (int) delims_len);
    255 	nav_update_wlen(&output_nav, (int) (height - miny));
    256 
    257 	if (output_len) {
    258 		i = output_nav.wmin;
    259 		for (y = miny; y < height && i < output_nav.wmax; y++, i++) {
    260 			if (i == output_nav.sel)
    261 				attr_on(A_REVERSE, NULL);
    262 
    263 			mvprintw((int) y, 1, "%.*s", width - 1, entry((size_t) i));
    264 
    265 			if (i == output_nav.sel)
    266 				attr_off(A_REVERSE, NULL);
    267 		}
    268 	}
    269 
    270 	move(2, (int) (1 + 2 + inputlen));
    271 
    272 	refresh();
    273 }
    274 
    275 static void
    276 spawn(const char *query)
    277 {
    278 	static const char *argv[64];
    279 	const char **arg;
    280 	const char *tok, *ntok, *end;
    281 	size_t argc;
    282 
    283 	delims_len = 0;
    284 	output_len = 0;
    285 
    286 	argc = 0;
    287 	for (arg = child_argv; *arg && argc < 64; arg++) {
    288 		if (!strcmp(*arg, "{}")) {
    289 			arg += 1;
    290 			break;
    291 		}
    292 		argv[argc++] = *arg;
    293 	}
    294 
    295 	if (split_args) {
    296 		ntok = tok = query;
    297 		while (ntok && *ntok && argc < 64) {
    298 			ntok = strchr(tok, ' ');
    299 			end = ntok ? ntok : tok + strlen(tok);
    300 			argv[argc++] = strndup(tok, (size_t) (end - tok));
    301 			tok = ntok + 1;
    302 		}
    303 	} else if (argc < 64 && query) {
    304 		argv[argc++] = query;
    305 	}
    306 
    307 	for (; *arg && argc < 64; arg++) {
    308 		argv[argc++] = *arg;
    309 	}
    310 
    311 	if (argc < 64) {
    312 		argv[argc++] = NULL;
    313 	} else {
    314 		die("Too many arguments");
    315 	}
    316 
    317 	if (child_pid) unload();
    318 	invoke(argv, &child_pid, &child_fd, false, true, errlog);
    319 }
    320 
    321 static void
    322 run(char *input)
    323 {
    324 	if (run_io) {
    325 		def_prog_mode();
    326 		endwin();
    327 	}
    328 
    329 	int status, pid;
    330 	run_argv[1] = input;
    331 	invoke((const char **)run_argv, &pid, NULL, run_io, run_io, errlog);
    332 	do {
    333 		int rc = waitpid(pid, &status, 0);
    334 		if (rc != pid) die("waitpid:");
    335 	} while (!WIFEXITED(status) && !WIFSIGNALED(status));
    336 
    337 	if (oneshot) exit(0);
    338 
    339 	if (run_io) {
    340 		reset_prog_mode();
    341 	}
    342 
    343 	if (WIFEXITED(status) && WEXITSTATUS(status) == 3)
    344 		spawn(inputbuf);
    345 	update();
    346 }
    347 
    348 static void
    349 hook(uint8_t key, const char *line)
    350 {
    351 	int status, rc;
    352 
    353 	if (!hook_argv[0]) return;
    354 
    355 	snprintf(hook_argv[1], 6, "%u", key);
    356 	free(hook_argv[2]);
    357 	hook_argv[2] = strdup(line);
    358 
    359 	if (hook_io) {
    360 		def_prog_mode();
    361 		endwin();
    362 	}
    363 
    364 	invoke((const char **)hook_argv, &hook_pid, NULL, hook_io, hook_io, errlog);
    365 	do {
    366 		rc = waitpid(hook_pid, &status, 0);
    367 		if (rc != hook_pid) die("waitpid:");
    368 	} while (!WIFEXITED(status) && !WIFSIGNALED(status));
    369 
    370 	if (hook_io) {
    371 		reset_prog_mode();
    372 	}
    373 
    374 	if (WIFEXITED(status) && WEXITSTATUS(status) == 3)
    375 		spawn(inputbuf);
    376 	update();
    377 }
    378 
    379 static void
    380 input(void)
    381 {
    382 	bool dirty;
    383 	bool reload;
    384 	int c;
    385 
    386 	dirty = false;
    387 	reload = false;
    388 	while ((c = getch()) != ERR) {
    389 		switch (c) {
    390 		case KEY_BACKSPACE:
    391 			if (inputlen) inputlen--;
    392 			dirty = true;
    393 			reload = true;
    394 			break;
    395 		case KEY_PPAGE:
    396 			nav_update_sel(&output_nav,
    397 				output_nav.sel - output_nav.wlen / 2);
    398 			dirty = true;
    399 			break;
    400 		case KEY_NPAGE:
    401 			nav_update_sel(&output_nav,
    402 				output_nav.sel + output_nav.wlen / 2);
    403 			dirty = true;
    404 			break;
    405 		case KEY_DOWN:
    406 			nav_update_sel(&output_nav, output_nav.sel + 1);
    407 			dirty = true;
    408 			break;
    409 		case KEY_UP:
    410 			nav_update_sel(&output_nav, output_nav.sel - 1);
    411 			dirty = true;
    412 			break;
    413 		case '\x1b':
    414 			/* TODO: word-wise seek */
    415 			switch (getch()) {
    416 			case 'c':
    417 				break;
    418 			case 'd':
    419 				break;
    420 			}
    421 			break;
    422 		case CTRL('w'):
    423 			inputlen = 0;
    424 			dirty = true;
    425 			reload = true;
    426 			break;
    427 		case CTRL('l'):
    428 			clear();
    429 			dirty = true;
    430 			break;
    431 		case CTRL('b'):
    432 			nav_update_sel(&output_nav, output_nav.min);
    433 			dirty = true;
    434 			break;
    435 		case CTRL('g'):
    436 			nav_update_sel(&output_nav, output_nav.max-1);
    437 			dirty = true;
    438 			break;
    439 		case CTRL('x'):
    440 			if (output_nav.max > 0 && output_nav.sel >= 0) {
    441 				timeout(-1);
    442 				hook((uint8_t)getch(), entry((size_t)output_nav.sel));
    443 				timeout(0);
    444 			}
    445 			break;
    446 		case '\n':
    447 			if (output_nav.max > 0 && output_nav.sel >= 0) {
    448 				if (run_cmd) {
    449 					run(entry((size_t) output_nav.sel));
    450 				} else {
    451 					puts(entry((size_t) output_nav.sel));
    452 					if (oneshot) exit(0);
    453 				}
    454 			}
    455 			break;
    456 		default:
    457 			if (c < 32 || c >= 127)
    458 				continue;
    459 			if (inputlen < INPUT_BUFSIZ - 1)
    460 				inputbuf[inputlen++] = (char) c;
    461 			reload = true;
    462 			dirty = true;
    463 			break;
    464 		}
    465 		inputbuf[inputlen] = '\0';
    466 	}
    467 
    468 	if (dirty) {
    469 		if (reload) spawn(inputbuf);
    470 		update();
    471 	}
    472 }
    473 
    474 static void
    475 sigint_handler(int sig)
    476 {
    477 	if (child_pid) {
    478 		unload();
    479 		update();
    480 	} else {
    481 		exit(0);
    482 	}
    483 }
    484 
    485 static void
    486 usage(int rc, bool full)
    487 {
    488 	fprintf(stderr, "Usage: tquery [OPT..] -- CMD [ARG..]\n");
    489 	if (full) {
    490 		fprintf(stderr, "\n");
    491 		fprintf(stderr, "  -h, --help    Show this message\n");
    492 		fprintf(stderr, "  -d, --delim   Set input entry delim\n");
    493 		fprintf(stderr, "  -o, --oneshot  Exit after oneshot selection\n");
    494 		fprintf(stderr, "  -e, --stderr  Dont close child stderr\n");
    495 		fprintf(stderr, "  -s, --split   Split the query into args\n");
    496 		fprintf(stderr, "  -r, --run     Program to run on Enter\n");
    497 		fprintf(stderr, "  -r, --run-io  Program to run interactively on Enter\n");
    498 		fprintf(stderr, "  -x, --hook    Program to run on Ctrl-X\n");
    499 		fprintf(stderr, "  -X, --hook-io Program to run interactively on Ctrl-X\n");
    500 		fprintf(stderr, "\n");
    501 	}
    502 	exit(rc);
    503 }
    504 
    505 static void
    506 parse(int argc, char **argv)
    507 {
    508 	char **arg;
    509 
    510 	for (arg = &argv[1]; *arg; arg++) {
    511 		if (!strcmp(*arg, "-h") || !strcmp(*arg, "--help")) {
    512 			usage(0, true);
    513 		} else if (!strcmp(*arg, "-o") || !strcmp(*arg, "--oneshot")) {
    514 			oneshot = true;
    515 		} else if (!strcmp(*arg, "-e") || !strcmp(*arg, "--stderr")) {
    516 			errlog = true;
    517 		} else if (!strcmp(*arg, "-s") || !strcmp(*arg, "--split")) {
    518 			split_args = true;
    519 		} else if (!strcmp(*arg, "-d") || !strcmp(*arg, "--delim")) {
    520 			delim = **++arg;
    521 		} else if (!strcmp(*arg, "-r") || !strcmp(*arg, "--run")) {
    522 			run_argv[0] = *++arg;
    523 			run_cmd = true;
    524 		} else if (!strcmp(*arg, "-R") || !strcmp(*arg, "--run-io")) {
    525 			run_argv[0] = *++arg;
    526 			run_cmd = true;
    527 			run_io = true;
    528 		} else if (!strcmp(*arg, "-x") || !strcmp(*arg, "--hook")) {
    529 			hook_argv[0] = *++arg;
    530 		} else if (!strcmp(*arg, "-X") || !strcmp(*arg, "--hook-io")) {
    531 			hook_argv[0] = *++arg;
    532 			hook_io = true;
    533 		} else if (!strcmp(*arg, "--")) {
    534 			child_argv = (const char **)arg + 1;
    535 			break;
    536 		} else {
    537 			die("unknown option '%s'", arg[0]);
    538 		}
    539 	}
    540 
    541 	if (!child_argv || !*child_argv)
    542 		usage(0, false);
    543 }
    544 
    545 int
    546 main(int argc, char **argv)
    547 {
    548 	struct pollfd fds[2];
    549 	int rc;
    550 
    551 	parse(argc, argv);
    552 
    553 	nav_init(&output_nav);
    554 
    555 	sigaction(SIGINT, &sigint_action, NULL);
    556 
    557 	atexit((void (*)()) endwin);
    558 	if (!newterm(NULL, stderr, stdin))
    559 		die("newterm stdin -> stderr");
    560 	cbreak();
    561 	noecho();
    562 	keypad(stdscr, true);
    563 
    564 	timeout(0);
    565 
    566 	spawn("");
    567 
    568 	inputbuf = malloc(INPUT_BUFSIZ);
    569 	if (!inputbuf) die("malloc inputbuf:");
    570 
    571 	loadbuf = malloc(LOAD_BUFSIZ);
    572 	if (!loadbuf) die("malloc loadbuf:");
    573 
    574 	inputlen = 0;
    575 	inputbuf[inputlen] = '\0';
    576 
    577 	update();
    578 
    579 	while (1) {
    580 		fds[0].fd = 0;
    581 		fds[0].events = POLLIN;
    582 		fds[1].fd = child_fd;
    583 		fds[1].events = POLLIN;
    584 
    585 		rc = poll(fds, child_pid ? 2 : 1, -1);
    586 		if (rc < 0 && errno == EINTR) {
    587 			update();
    588 			continue;
    589 		} else if (rc < 0) {
    590 			die("select:");
    591 		}
    592 
    593 		if (fds[0].revents & POLLIN) {
    594 			input();
    595 		} else if (fds[1].revents & POLLIN) {
    596 			load();
    597 			update();
    598 		} else if (fds[1].revents & POLLHUP) {
    599 			unload();
    600 			update();
    601 		}
    602 	}
    603 }