tquery

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

tquery.c (12213B)


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