tquery

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

commit da3524d8c0c51bd61d0db6e0e03ea8ba7e2a7b73
Author: Louis Burda <quent.burda@gmail.com>
Date:   Mon, 15 May 2023 15:11:03 +0200

Add initial version

Diffstat:
A.gitignore | 4++++
A.gitmodules | 6++++++
AMakefile | 30++++++++++++++++++++++++++++++
Alib/liballoc | 1+
Alib/libdvec | 1+
Atquery.c | 510+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 552 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +tquery +.cache +compile_commands.json +.gdb_history diff --git a/.gitmodules b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/liballoc"] + path = lib/liballoc + url = git@sinitax.com:sinitax/liballoc +[submodule "lib/libdvec"] + path = lib/libdvec + url = git@sinitax.com:sinitax/libdvec diff --git a/Makefile b/Makefile @@ -0,0 +1,30 @@ +PREFIX ?= /usr/local +BINDIR ?= /bin + +LDLIBS = -lcurses -I lib/libdvec/include -I lib/liballoc/include +CFLAGS = -std=c99 -Wunused-function -Wunused-variable -Wconversion + +all: tquery + +clean: + rm -f tquery + +cleanall: clean + make -C lib/liballoc cleanall + make -C lib/libdvec cleanall + +lib/libdvec/build/libdvec.a: + make -C lib/libdvec build/libdvec.a + +lib/liballoc/build/liballoc.a: + make -C lib/liballoc build/liballoc.a + +tquery: tquery.c lib/libdvec/build/libdvec.a lib/liballoc/build/liballoc.a + +install: + install -m755 tquery -t "$(DESTDIR)$(PREFIX)$(BINDIR)" + +uninstall: + rm -f "$(DESTDIR)$(PREFIX)$(BINDIR)/tquery" + +.PHONY: all clean install uninstall diff --git a/lib/liballoc b/lib/liballoc @@ -0,0 +1 @@ +Subproject commit 52903c46d996b927ff304bae66c522d317a218f3 diff --git a/lib/libdvec b/lib/libdvec @@ -0,0 +1 @@ +Subproject commit 93bafd206267f9a0f26b03d60f5b52b0571bef56 diff --git a/tquery.c b/tquery.c @@ -0,0 +1,510 @@ +#define _XOPEN_SOURCE 700 + +#include "allocator.h" +#include "dvec.h" + +#include <curses.h> + +#include <linux/limits.h> +#include <sys/wait.h> +#include <sys/select.h> +#include <limits.h> +#include <fcntl.h> +#include <dirent.h> +#include <unistd.h> +#include <termios.h> +#include <signal.h> +#include <errno.h> +#include <assert.h> +#include <string.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#define CTRL(x) ((x) & 0x1f) +#define MIN(a, b) ((a) > (b) ? (b) : (a)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +struct listnav { + int wmin, wmax, wlen; + int sel; + int min, max; +}; + +static const struct allocator *ga = &stdlib_strict_heap_allocator; + +static int locate_out_fd = -1; +static pid_t locate_pid = 0; + +static struct dvec locate_output; +static struct dvec locate_delims; + +static struct listnav locate_nav; + +static char inputbuf[2048] = { 0 }; +static size_t inputlen = 0; + +static char errbuf[1024] = { 0 }; +static int errtimer = 0; + +static bool single_out = false; +static bool debug = false; +static char delim = '\n'; + +static const char **cmd_argv = NULL; + +static void +sigint(int sig) +{ + if (locate_pid > 0) { + kill(locate_pid, SIGKILL); + waitpid(locate_pid, NULL, 0); + locate_pid = 0; + } else { + exit(0); + } +} + +static void __attribute__((noreturn)) +err(const char *fmt, ...) +{ + va_list ap; + + endwin(); + + va_start(ap, fmt); + printf("tquery: "); + vprintf(fmt, ap); + printf(": %s\n", strerror(errno)); + va_end(ap); + + exit(1); +} + +void +swapfd(int fd1, int fd2) +{ + int fd, rc; + + fd = dup(fd1); + if (fd < 0) err("dup"); + rc = dup2(fd2, fd1); + if (rc == -1) err("dup2"); + rc = dup2(fd, fd2); + if (rc == -1) err("dup2"); + close(fd); +} + +static void +listnav_update_win(struct listnav *nav) +{ + nav->wmin = MAX(nav->wmax - nav->wlen, nav->min); + nav->wmax = MIN(nav->wmin + nav->wlen, nav->max); + nav->sel = MAX(MIN(nav->sel, nav->wmax - 1), nav->wmin); +} + +static void +listnav_init(struct listnav *nav) +{ + nav->sel = 0; + nav->min = 0; + nav->max = 0; + nav->wlen = 0; + nav->wmin = 0; + nav->wmax = 0; +} + +static void +listnav_update_bounds(struct listnav *nav, int min, int max) +{ + nav->min = min; + nav->max = max; + listnav_update_win(nav); +} + +static void +listnav_update_wlen(struct listnav *nav, int wlen) +{ + nav->wlen = wlen; + listnav_update_win(nav); +} + +static void +listnav_update_sel(struct listnav *nav, int sel) +{ + nav->sel = MAX(MIN(sel, nav->max - 1), nav->min); + if (nav->sel >= nav->wmax) { + nav->wmax = nav->sel + 1; + nav->wmin = MAX(nav->min, nav->wmax - nav->wlen); + } else if (nav->sel < nav->wmin) { + nav->wmin = nav->sel; + nav->wmax = MIN(nav->wmin + nav->wlen, nav->max); + } +} + +void +reset(void) +{ + dvec_clear(&locate_delims); + dvec_clear(&locate_output); + + if (locate_out_fd >= 0) { + close(locate_out_fd); + locate_out_fd = -1; + } + + if (locate_pid > 0) { + kill(locate_pid, SIGKILL); + waitpid(locate_pid, NULL, 0); + locate_pid = 0; + } +} + +char * +findbin(const char *name) +{ + char buf[PATH_MAX]; + char *path, *tok, *ntok, *end; + struct dirent *ent; + DIR *dir; + + path = getenv("PATH"); + if (!path) return NULL; + + ntok = tok = path; + while (ntok) { + ntok = strchr(tok, ':'); + end = ntok ? ntok : tok + strlen(tok); + strncpy(buf, tok, (size_t) (end - tok)); + buf[end - tok] = '\0'; + tok = ntok + 1; + + dir = opendir(buf); + if (!dir) continue; + + while ((ent = readdir(dir))) { + if (!strcmp(ent->d_name, name)) { + closedir(dir); + if (strlen(buf) + 1 + strlen(name) >= PATH_MAX) + return NULL; + strcat(buf, "/"); + strcat(buf, name); + return strdup(buf); + } + } + + closedir(dir); + } + + return NULL; +} + +static void +spawn(const char **prefix, const char *search_args) +{ + struct dvec argv; + const char **arg; + const char *tok, *ntok, *end; + int out_pipe[2]; + + if (pipe(out_pipe)) err("pipe"); + + locate_pid = fork(); + if (locate_pid < 0) err("fork"); + if (!locate_pid) { + close(0); + if (debug) { + swapfd(1, 2); + close(1); + } else { + close(1); + close(2); + } + dup2(out_pipe[1], 1); + close(out_pipe[0]); + + dvec_init(&argv, sizeof(char *), 0, ga); + for (arg = prefix; *arg; arg++) { + dvec_add_back(&argv, 1); + *(const char **)dvec_back(&argv) = *arg; + } + + ntok = tok = search_args; + while (ntok && *ntok) { + ntok = strchr(tok, ' '); + end = ntok ? ntok : tok + strlen(tok); + dvec_add_back(&argv, 1); + *(const char **)dvec_back(&argv) + = strndup(tok, (size_t) (end - tok)); + tok = ntok + 1; + } + + dvec_add_back(&argv, 1); + *(const char **)dvec_back(&argv) = NULL; + + execv(*(const char **)dvec_front(&argv), argv.data); + fprintf(stderr, "tquery: execv '%s': %s", + *(const char **)dvec_front(&argv), strerror(errno)); + exit(1); + } else { + close(out_pipe[1]); + + locate_out_fd = out_pipe[0]; + } +} + +static void +load() +{ + char buf[BUFSIZ]; + ssize_t nread; + size_t i, len; + char *start; + + nread = read(locate_out_fd, buf, BUFSIZ); + if (nread <= 0) { + locate_pid = 0; + close(locate_out_fd); + locate_out_fd = -1; + return; + } + + len = dvec_len(&locate_output); + dvec_add_back(&locate_output, (size_t) nread); + start = dvec_at(&locate_output, len); + memcpy(start, buf, (size_t) nread); + + for (i = len; i < len + (size_t) nread; i++, start++) { + if (*start == delim) { + *start = '\0'; + dvec_add_back(&locate_delims, 1); + *(size_t *)dvec_back(&locate_delims) = i; + } + } +} + +static char * +entry(int i) +{ + size_t off; + + assert(i >= 0); + + if (i == 0) { + return dvec_at(&locate_output, 0); + } else { + off = *(size_t *)dvec_at(&locate_delims, (size_t) i - 1); + return dvec_at(&locate_output, off + 1); + } +} + +static void +update() +{ + const ssize_t miny = 3; + int width, height; + ssize_t i, y; + + erase(); + + width = getmaxx(stdscr); + height = getmaxy(stdscr); + + if (errbuf[0] && errtimer >= 0) { + attr_on(A_BOLD, NULL); + mvprintw(0, 1, "%s", errbuf); + attr_off(A_BOLD, NULL); + errtimer--; + } + + mvprintw(1, 1, "%lu %lu", dvec_len(&locate_output), + dvec_len(&locate_delims)); + mvprintw(2, 1, "> %.*s", width - 1, inputbuf); + + if (!dvec_empty(&locate_output)) { + listnav_update_bounds(&locate_nav, 0, (int) dvec_len(&locate_delims)); + listnav_update_wlen(&locate_nav, (int) (height - miny)); + + i = locate_nav.wmin; + for (y = miny; y < height && i < locate_nav.wmax; y++, i++) { + if (i == locate_nav.sel) + attr_on(A_REVERSE, NULL); + + mvprintw((int) y, 1, "%.*s", + width - 1, entry((int) i)); + + if (i == locate_nav.sel) + attr_off(A_REVERSE, NULL); + } + } + + move(2, (int) (1 + 2 + inputlen)); + + refresh(); +} + +static void +input() +{ + bool dirty; + bool reload; + int c; + + dirty = false; + reload = false; + while ((c = getch()) != ERR) { + switch (c) { + case KEY_BACKSPACE: + if (inputlen) inputlen--; + dirty = true; + reload = true; + break; + case KEY_PPAGE: + listnav_update_sel(&locate_nav, + locate_nav.sel - locate_nav.wlen / 2); + dirty = true; + break; + case KEY_NPAGE: + listnav_update_sel(&locate_nav, + locate_nav.sel + locate_nav.wlen / 2); + dirty = true; + break; + case KEY_DOWN: + listnav_update_sel(&locate_nav, locate_nav.sel + 1); + dirty = true; + break; + case KEY_UP: + listnav_update_sel(&locate_nav, locate_nav.sel - 1); + dirty = true; + break; + case '\x1b': + /* TODO: word-wise seek */ + switch (getch()) { + case 'c': + break; + case 'd': + break; + } + case CTRL('w'): + inputlen = 0; + dirty = true; + reload = true; + break; + case '\n': + if (!isatty(2)) { + fprintf(stderr, "%s\n", entry(locate_nav.sel)); + } + if (single_out) + exit(0); + break; + default: + if (c < 32 || c >= 127) + continue; + if (inputlen < sizeof(inputbuf) - 1) + inputbuf[inputlen++] = (char) c; + reload = true; + dirty = true; + break; + } + inputbuf[inputlen] = '\0'; + } + + if (dirty) { + update(); + if (reload) { + reset(); + spawn(cmd_argv, inputbuf); + } + } +} + +void +parse(int argc, const char **argv) +{ + const char **arg; + const char *cmd_path; + + for (arg = &argv[1]; *arg; arg++) { + if (!strcmp(*arg, "-s")) { + single_out = true; + } else if (!strcmp(*arg, "-d")) { + debug = true; + } else if (!strcmp(*arg, "-c")) { + delim = **++arg; + } else if (!strcmp(*arg, "--")) { + cmd_argv = arg + 1; + break; + } else { + fprintf(stderr, "tquery: unknown option '%s'\n", arg[0]); + exit(1); + } + } + + if (!cmd_argv || !*cmd_argv) { + fprintf(stderr, "tquery: empty command args\n"); + exit(1); + } + + if (*cmd_argv[0] != '/' && strncmp(cmd_argv[0], "./", 2)) { + cmd_path = findbin(cmd_argv[0]); + if (!cmd_path) { + fprintf(stderr, "tquery: no '%s' in PATH\n", cmd_argv[0]); + exit(1); + } + cmd_argv[0] = cmd_path; + } +} + +int +main(int argc, const char **argv) +{ + int maxfd, rc; + fd_set set; + + parse(argc, argv); + + swapfd(1, 2); + + dvec_init(&locate_output, 1, 1024 * 1024 * 25, ga); + dvec_init(&locate_delims, sizeof(size_t), 1024, ga); + listnav_init(&locate_nav); + + signal(SIGINT, sigint); + + atexit((void (*)()) endwin); + initscr(); + cbreak(); + noecho(); + keypad(stdscr, true); + + timeout(0); + + update(); + + reset(); + spawn(cmd_argv, ""); + + inputlen = 0; + while (1) { + maxfd = 0; + FD_ZERO(&set); + FD_SET(0, &set); + if (locate_pid) { + FD_SET(locate_out_fd, &set); + if (locate_out_fd > maxfd) + maxfd = locate_out_fd; + } + rc = select(maxfd+1, &set, NULL, NULL, NULL); + if (rc <= 0 && errno == EINTR) { + update(); + continue; + } else if (rc <= 0) err("select"); + + if (FD_ISSET(0, &set)) { + input(); + } else if (FD_ISSET(locate_out_fd, &set)) { + load(); + update(); + } + } +}