tquery

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

tquery.c (12180B)


      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 (!in || !out || !err) {
    160			int zfd = open("/dev/null", O_RDWR);
    161			if (zfd < 0) die("open /dev/null");
    162			if (!in) dup2(zfd, 0);
    163			if (!out) dup2(zfd, 1);
    164			if (!err) dup2(zfd, 2);
    165			close(zfd);
    166		}
    167		if (fd) {
    168			if (out) dup2(out_pipe[1], 1);
    169			close(out_pipe[0]);
    170			close(out_pipe[1]);
    171		}
    172		execvp(argv[0], (char *const *) argv);
    173		die("execv '%s':");
    174	} else {
    175		if (fd) {
    176			*fd = out_pipe[0];
    177			close(out_pipe[1]);
    178		}
    179	}
    180}
    181
    182static void *
    183addcap(void *alloc, size_t dsize, size_t min, size_t *cap)
    184{
    185	if (min > *cap) {
    186		*cap = *cap * 2;
    187		if (*cap < min) *cap = min;
    188		alloc = realloc(alloc, dsize * *cap);
    189		if (!alloc) die("realloc:");
    190	}
    191
    192	return alloc;
    193}
    194
    195static void
    196load(void)
    197{
    198	ssize_t nread;
    199	size_t i;
    200	char *start;
    201
    202	nread = read(child_fd, loadbuf, LOAD_BUFSIZ);
    203	if (nread <= 0) {
    204		child_pid = 0;
    205		close(child_fd);
    206		child_fd = -1;
    207		return;
    208	}
    209
    210	output = addcap(output, 1, output_len + (size_t) nread, &output_cap);
    211	memcpy(output + output_len, loadbuf, (size_t) nread);
    212
    213	start = output + output_len;
    214
    215	for (i = output_len; i < output_len + (size_t) nread; i++, start++) {
    216		if (*start == delim) {
    217			*start = '\0';
    218			delims = addcap(delims, sizeof(size_t),
    219				delims_len + 1, &delims_cap);
    220			delims[delims_len++] = i;
    221		}
    222	}
    223
    224	output_len += (size_t) nread;
    225}
    226
    227static void
    228unload(void)
    229{
    230	int status;
    231
    232	if (!child_pid) return;
    233
    234	kill(child_pid, SIGKILL);
    235	do {
    236		waitpid(child_pid, &status, 0);
    237	} while (!WIFEXITED(status) && !WIFSIGNALED(status));
    238
    239	close(child_fd);
    240	child_fd = -1;
    241	child_pid = 0;
    242}
    243
    244static char *
    245entry(size_t i)
    246{
    247	return output + (i ? delims[i-1] + 1 : 0);
    248}
    249
    250static void
    251update(void)
    252{
    253	const ssize_t miny = 3;
    254	int width, height;
    255	ssize_t i, y;
    256
    257	erase();
    258
    259	width = getmaxx(stdscr);
    260	height = getmaxy(stdscr);
    261
    262	mvprintw(1, 1, "%lu %li %lu %i", output_len,
    263		output_nav.sel, delims_len, child_pid);
    264	mvprintw(2, 1, "> %.*s", width - 1, inputbuf);
    265
    266	nav_update_bounds(&output_nav, 0, (int) delims_len);
    267	nav_update_wlen(&output_nav, (int) (height - miny));
    268
    269	if (output_len) {
    270		i = output_nav.wmin;
    271		for (y = miny; y < height && i < output_nav.wmax; y++, i++) {
    272			if (i == output_nav.sel)
    273				attr_on(A_REVERSE, NULL);
    274
    275			mvprintw((int) y, 1, "%.*s", width - 1, entry((size_t) i));
    276
    277			if (i == output_nav.sel)
    278				attr_off(A_REVERSE, NULL);
    279		}
    280	}
    281
    282	move(2, (int) (1 + 2 + inputlen));
    283
    284	refresh();
    285}
    286
    287static void
    288spawn(const char *query)
    289{
    290	static const char *argv[64];
    291	const char **arg;
    292	const char *tok, *ntok, *end;
    293	size_t argc;
    294
    295	delims_len = 0;
    296	output_len = 0;
    297
    298	argc = 0;
    299	for (arg = child_argv; *arg && argc < 64; arg++) {
    300		if (!strcmp(*arg, "{}")) {
    301			arg += 1;
    302			break;
    303		}
    304		argv[argc++] = *arg;
    305	}
    306
    307	if (split_args) {
    308		ntok = tok = query;
    309		while (ntok && *ntok && argc < 64) {
    310			ntok = strchr(tok, ' ');
    311			end = ntok ? ntok : tok + strlen(tok);
    312			argv[argc++] = strndup(tok, (size_t) (end - tok));
    313			tok = ntok + 1;
    314		}
    315	} else if (argc < 64 && query) {
    316		argv[argc++] = query;
    317	}
    318
    319	for (; *arg && argc < 64; arg++) {
    320		argv[argc++] = *arg;
    321	}
    322
    323	if (argc < 64) {
    324		argv[argc++] = NULL;
    325	} else {
    326		die("Too many arguments");
    327	}
    328
    329	if (child_pid) unload();
    330	invoke(argv, &child_pid, &child_fd, false, true, errlog);
    331}
    332
    333static void
    334run(char *input)
    335{
    336	if (run_io) {
    337		def_prog_mode();
    338		endwin();
    339	}
    340
    341	int status, pid;
    342	run_argv[1] = input;
    343	invoke((const char **)run_argv, &pid, NULL, run_io, run_io, errlog);
    344	do {
    345		int rc = waitpid(pid, &status, 0);
    346		if (rc != pid) die("waitpid:");
    347	} while (!WIFEXITED(status) && !WIFSIGNALED(status));
    348
    349	if (oneshot) exit(0);
    350
    351	if (run_io) {
    352		reset_prog_mode();
    353	}
    354
    355	if (WIFEXITED(status) && WEXITSTATUS(status) == 3)
    356		spawn(inputbuf);
    357	update();
    358}
    359
    360static void
    361hook(uint8_t key, const char *line)
    362{
    363	int status, rc;
    364
    365	if (!hook_argv[0]) return;
    366
    367	snprintf(hook_argv[1], 6, "%u", key);
    368	free(hook_argv[2]);
    369	hook_argv[2] = strdup(line);
    370
    371	if (hook_io) {
    372		def_prog_mode();
    373		endwin();
    374	}
    375
    376	invoke((const char **)hook_argv, &hook_pid, NULL, hook_io, hook_io, errlog);
    377	do {
    378		rc = waitpid(hook_pid, &status, 0);
    379		if (rc != hook_pid) die("waitpid:");
    380	} while (!WIFEXITED(status) && !WIFSIGNALED(status));
    381
    382	if (hook_io) {
    383		reset_prog_mode();
    384	}
    385
    386	if (WIFEXITED(status) && WEXITSTATUS(status) == 3)
    387		spawn(inputbuf);
    388	update();
    389}
    390
    391static void
    392input(void)
    393{
    394	bool dirty;
    395	bool reload;
    396	int c;
    397
    398	dirty = false;
    399	reload = false;
    400	while ((c = getch()) != ERR) {
    401		switch (c) {
    402		case KEY_BACKSPACE:
    403			if (inputlen) inputlen--;
    404			dirty = true;
    405			reload = true;
    406			break;
    407		case KEY_PPAGE:
    408			nav_update_sel(&output_nav,
    409				output_nav.sel - output_nav.wlen / 2);
    410			dirty = true;
    411			break;
    412		case KEY_NPAGE:
    413			nav_update_sel(&output_nav,
    414				output_nav.sel + output_nav.wlen / 2);
    415			dirty = true;
    416			break;
    417		case KEY_DOWN:
    418			nav_update_sel(&output_nav, output_nav.sel + 1);
    419			dirty = true;
    420			break;
    421		case KEY_UP:
    422			nav_update_sel(&output_nav, output_nav.sel - 1);
    423			dirty = true;
    424			break;
    425		case '\x1b':
    426			/* TODO: word-wise seek */
    427			switch (getch()) {
    428			case 'c':
    429				break;
    430			case 'd':
    431				break;
    432			}
    433			break;
    434		case CTRL('w'):
    435			inputlen = 0;
    436			dirty = true;
    437			reload = true;
    438			break;
    439		case CTRL('l'):
    440			clear();
    441			dirty = true;
    442			break;
    443		case CTRL('b'):
    444			nav_update_sel(&output_nav, output_nav.min);
    445			dirty = true;
    446			break;
    447		case CTRL('g'):
    448			nav_update_sel(&output_nav, output_nav.max-1);
    449			dirty = true;
    450			break;
    451		case CTRL('x'):
    452			if (output_nav.max > 0 && output_nav.sel >= 0) {
    453				timeout(-1);
    454				hook((uint8_t)getch(), entry((size_t)output_nav.sel));
    455				timeout(0);
    456			}
    457			break;
    458		case '\n':
    459			if (output_nav.max > 0 && output_nav.sel >= 0) {
    460				if (run_cmd) {
    461					run(entry((size_t) output_nav.sel));
    462				} else {
    463					puts(entry((size_t) output_nav.sel));
    464					if (oneshot) exit(0);
    465				}
    466			}
    467			break;
    468		default:
    469			if (c < 32 || c >= 127)
    470				continue;
    471			if (inputlen < INPUT_BUFSIZ - 1)
    472				inputbuf[inputlen++] = (char) c;
    473			reload = true;
    474			dirty = true;
    475			break;
    476		}
    477		inputbuf[inputlen] = '\0';
    478	}
    479
    480	if (dirty) {
    481		if (reload) spawn(inputbuf);
    482		update();
    483	}
    484}
    485
    486static void
    487sigint_handler(int sig)
    488{
    489	if (child_pid) {
    490		unload();
    491		update();
    492	} else {
    493		exit(0);
    494	}
    495}
    496
    497static void
    498usage(int rc, bool full)
    499{
    500	fprintf(stderr, "Usage: tquery [OPT..] -- CMD [ARG..]\n");
    501	if (full) {
    502		fprintf(stderr, "\n");
    503		fprintf(stderr, "  -h, --help    Show this message\n");
    504		fprintf(stderr, "  -d, --delim   Set input entry delim\n");
    505		fprintf(stderr, "  -o, --oneshot  Exit after oneshot selection\n");
    506		fprintf(stderr, "  -e, --stderr  Dont close child stderr\n");
    507		fprintf(stderr, "  -s, --split   Split the query into args\n");
    508		fprintf(stderr, "  -r, --run     Program to run on Enter\n");
    509		fprintf(stderr, "  -R, --run-io  Program to run interactively on Enter\n");
    510		fprintf(stderr, "  -x, --hook    Program to run on Ctrl-X\n");
    511		fprintf(stderr, "  -X, --hook-io Program to run interactively on Ctrl-X\n");
    512		fprintf(stderr, "\n");
    513	}
    514	exit(rc);
    515}
    516
    517static void
    518parse(int argc, char **argv)
    519{
    520	char **arg;
    521
    522	for (arg = &argv[1]; *arg; arg++) {
    523		if (!strcmp(*arg, "-h") || !strcmp(*arg, "--help")) {
    524			usage(0, true);
    525		} else if (!strcmp(*arg, "-o") || !strcmp(*arg, "--oneshot")) {
    526			oneshot = true;
    527		} else if (!strcmp(*arg, "-e") || !strcmp(*arg, "--stderr")) {
    528			errlog = true;
    529		} else if (!strcmp(*arg, "-s") || !strcmp(*arg, "--split")) {
    530			split_args = true;
    531		} else if (!strcmp(*arg, "-d") || !strcmp(*arg, "--delim")) {
    532			delim = **++arg;
    533		} else if (!strcmp(*arg, "-r") || !strcmp(*arg, "--run")) {
    534			run_argv[0] = *++arg;
    535			run_cmd = true;
    536		} else if (!strcmp(*arg, "-R") || !strcmp(*arg, "--run-io")) {
    537			run_argv[0] = *++arg;
    538			run_cmd = true;
    539			run_io = true;
    540		} else if (!strcmp(*arg, "-x") || !strcmp(*arg, "--hook")) {
    541			hook_argv[0] = *++arg;
    542		} else if (!strcmp(*arg, "-X") || !strcmp(*arg, "--hook-io")) {
    543			hook_argv[0] = *++arg;
    544			hook_io = true;
    545		} else if (!strcmp(*arg, "--")) {
    546			child_argv = (const char **)arg + 1;
    547			break;
    548		} else {
    549			die("unknown option '%s'", arg[0]);
    550		}
    551	}
    552
    553	if (!child_argv || !*child_argv)
    554		usage(0, false);
    555}
    556
    557int
    558main(int argc, char **argv)
    559{
    560	struct pollfd fds[2];
    561	int rc;
    562
    563	parse(argc, argv);
    564
    565	nav_init(&output_nav);
    566
    567	atexit((void (*)()) endwin);
    568	if (!newterm(NULL, stderr, stdin))
    569		die("newterm stdin -> stderr");
    570	cbreak();
    571	noecho();
    572	keypad(stdscr, true);
    573
    574	sigaction(SIGINT, &sigint_action, NULL);
    575	sigaction(SIGWINCH, NULL, &sigwinch_action);
    576	sigwinch_action.sa_flags |= SA_RESTART;
    577	sigaction(SIGWINCH, &sigwinch_action, NULL);
    578
    579	timeout(0);
    580
    581	spawn("");
    582
    583	inputbuf = malloc(INPUT_BUFSIZ);
    584	if (!inputbuf) die("malloc inputbuf:");
    585
    586	loadbuf = malloc(LOAD_BUFSIZ);
    587	if (!loadbuf) die("malloc loadbuf:");
    588
    589	inputlen = 0;
    590	inputbuf[inputlen] = '\0';
    591
    592	update();
    593
    594	while (1) {
    595		fds[0].fd = 0;
    596		fds[0].events = POLLIN;
    597		fds[1].fd = child_fd;
    598		fds[1].events = POLLIN;
    599
    600		rc = poll(fds, child_pid ? 2 : 1, -1);
    601		if (rc < 0 && errno == EINTR) {
    602			update();
    603			continue;
    604		} else if (rc < 0) {
    605			die("select:");
    606		}
    607
    608		if (fds[0].revents & POLLIN) {
    609			input();
    610		} else if (fds[1].revents & POLLIN) {
    611			load();
    612			update();
    613		} else if (fds[1].revents & POLLHUP) {
    614			unload();
    615			update();
    616		}
    617	}
    618}