From 8595ba890df8a1d20b6162d6dd70a176affc21b8 Mon Sep 17 00:00:00 2001 From: June McEnroe Date: Thu, 2 Jun 2022 20:13:13 -0400 Subject: Add initial working version of qf --- bin/.gitignore | 1 + bin/Makefile | 2 + bin/README.7 | 4 +- bin/man1/qf.1 | 56 +++++++++++++ bin/qf.c | 254 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 bin/man1/qf.1 create mode 100644 bin/qf.c diff --git a/bin/.gitignore b/bin/.gitignore index 59b60826..42269bac 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -24,6 +24,7 @@ pbd pngo psf2png ptee +qf quick relay scheme diff --git a/bin/Makefile b/bin/Makefile index 5d23ab3c..bb1535d6 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -26,6 +26,7 @@ BINS += pbd BINS += pngo BINS += psf2png BINS += ptee +BINS += qf BINS += quick BINS += scheme BINS += shotty @@ -55,6 +56,7 @@ LDLIBS.glitch = -lz LDLIBS.modem = -lutil LDLIBS.pngo = -lz LDLIBS.ptee = -lutil +LDLIBS.qf = -lcurses LDLIBS.relay = -ltls LDLIBS.scheme = -lm LDLIBS.title = -lcurl diff --git a/bin/README.7 b/bin/README.7 index c562e84c..100e183e 100644 --- a/bin/README.7 +++ b/bin/README.7 @@ -1,4 +1,4 @@ -.Dd January 30, 2022 +.Dd June 2, 2022 .Dt BIN 7 .Os "Causal Agency" . @@ -58,6 +58,8 @@ PNG optimizer PSF2 to PNG renderer .It Xr ptee 1 tee for PTYs +.It Xr qf 1 +grep pager .It Xr quick 1 terrible HTTP/CGI server .It Xr relay 1 diff --git a/bin/man1/qf.1 b/bin/man1/qf.1 new file mode 100644 index 00000000..06676087 --- /dev/null +++ b/bin/man1/qf.1 @@ -0,0 +1,56 @@ +.Dd June 2, 2022 +.Dt QF 1 +.Os +. +.Sh NAME +.Nm qf +.Nd grep pager +. +.Sh SYNOPSIS +.Nm +. +.Sh DESCRIPTION +.Nm +is a pager for +.Xr grep 1 , +.Xr ag 1 , +.Xr rg 1 , +etc.\& +which allows +jumping to matches in +.Ev $EDITOR . +It parses any input +prefixed by path +and line number +separated by a colon +.Ql ":" +followed by either a colon +or a hyphen +.Ql "-" . +It otherwise operates similar to +.Xr less 1 . +. +.Pp +The keys are as follows: +.Bl -tag -width Ds +.It Ic Enter +Open the currently selected line in +.Ev $EDITOR . +When the editor exits, +.Nm +resumes. +.It Ic {} +Jump between files. +.It Ic gG +Jump to first or last line. +.It Ic jk +Move to next or previous line. +.It Ic nN +Jump to next or previous match line. +.It Ic q +Exit. +.El +. +.Sh EXAMPLES +.Dl $ ag -C open | qf +.Dl $ git grep -n open | qf diff --git a/bin/qf.c b/bin/qf.c new file mode 100644 index 00000000..6cf415a3 --- /dev/null +++ b/bin/qf.c @@ -0,0 +1,254 @@ +/* Copyright (C) 2022 June McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum Type { + File, + Match, + Context, + Text, +}; + +struct Line { + enum Type type; + char *path; + unsigned nr; + char *text; +}; + +static struct { + struct Line *ptr; + size_t len, cap; +} lines; + +static void push(struct Line line) { + if (lines.len == lines.cap) { + lines.cap = (lines.cap ? lines.cap * 2 : 256); + lines.ptr = realloc(lines.ptr, sizeof(*lines.ptr) * lines.cap); + if (!lines.ptr) err(EX_OSERR, "realloc"); + } + lines.ptr[lines.len++] = line; +} + +static void parse(struct Line line) { + char *text = line.text; + size_t sep = strcspn(line.text, ":"); + if (!line.text[sep]) { + line.type = Text; + push(line); + return; + } + line.path = strndup(line.text, sep); + if (!line.path) err(EX_OSERR, "strndup"); + line.text += sep + 1; + if ( + !lines.len || + !lines.ptr[lines.len-1].path || + strcmp(line.path, lines.ptr[lines.len-1].path) + ) { + if (lines.len) { + push((struct Line) { .type = Text, .text = " " }); + } + line.type = File; + push(line); + } + char *rest; + line.nr = strtoul(line.text, &rest, 10); + if (rest != line.text && rest[0] == ':') { + line.type = Match; + line.text = &rest[1]; + } else if (rest != line.text && rest[0] == '-') { + line.type = Context; + line.text = &rest[1]; + } else { + line.type = Text; + line.text = text; + } + push(line); +} + +enum { + Path = 1, + Number = 2, + Highlight = 3, +}; + +static int tty = -1; + +static void curse(void) { + tty = open(_PATH_TTY, O_RDWR); + if (tty < 0) err(EX_IOERR, "%s", _PATH_TTY); + FILE *ttyin = fdopen(tty, "r"); + if (!ttyin) err(EX_OSERR, "fdopen"); + set_term(newterm(NULL, stdout, ttyin)); + cbreak(); + noecho(); + nodelay(stdscr, true); + curs_set(0); + start_color(); + use_default_colors(); + init_pair(Path, COLOR_GREEN, -1); + init_pair(Number, COLOR_YELLOW, -1); + init_pair(Highlight, COLOR_BLACK, COLOR_YELLOW); +} + +static size_t top; +static size_t cur; + +static void draw(void) { + int y, x; + for (int i = 0; i < LINES; ++i) { + move(i, 0); + clrtoeol(); + if (top + i >= lines.len) continue; + struct Line line = lines.ptr[top + i]; + if (top + i == cur) { + getyx(stdscr, y, x); + attron(A_REVERSE); + } else { + attroff(A_REVERSE); + } + switch (line.type) { + break; case File: { + color_set(Path, NULL); + addstr(line.path); + color_set(0, NULL); + } + break; case Match: case Context: { + color_set(Number, NULL); + printw("%u", line.nr); + color_set(0, NULL); + addch(line.type == Match ? ':' : '-'); + addstr(line.text); + } + break; case Text: addstr(line.text); + } + } + move(y, x); + refresh(); +} + +static void edit(struct Line line) { + char cmd[32]; + snprintf(cmd, sizeof(cmd), "+%u", (line.nr ? line.nr : 1)); + const char *editor = getenv("EDITOR"); + if (!editor) editor = "vi"; + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (!pid) { + dup2(tty, STDIN_FILENO); + close(tty); + execlp(editor, editor, cmd, line.path, NULL); + err(EX_CONFIG, "%s", editor); + } + int status; + pid = waitpid(pid, &status, 0); + if (pid < 0) err(EX_OSERR, "waitpid"); +} + +static void toPrev(enum Type type) { + if (!cur) return; + size_t prev = cur - 1; + while (prev && lines.ptr[prev].type != type) { + prev--; + } + if (lines.ptr[prev].type == type) { + cur = prev; + } +} + +static void toNext(enum Type type) { + size_t next = cur + 1; + while (next < lines.len && lines.ptr[next].type != type) { + next++; + } + if (next < lines.len && lines.ptr[next].type == type) { + cur = next; + } +} + +static void input(void) { + char ch; + while (ERR != (ch = getch())) { + switch (ch) { + break; case '\n': { + if (lines.ptr[cur].type == Text) break; + endwin(); + edit(lines.ptr[cur]); + refresh(); + } + break; case '{': toPrev(File); + break; case '}': toNext(File); + break; case 'G': cur = lines.len - 1; + break; case 'N': toPrev(Match); + break; case 'g': cur = 0; + break; case 'j': if (cur + 1 < lines.len) cur++; + break; case 'k': if (cur) cur--; + break; case 'n': toNext(Match); + break; case 'q': { + endwin(); + exit(EX_OK); + } + } + } + if (cur < top) top = cur; + if (cur >= top + LINES) top = cur - LINES + 1; +} + +int main(void) { + curse(); + struct pollfd fds[2] = { + { .fd = STDIN_FILENO, .events = POLLIN }, + { .fd = tty, .events = POLLIN }, + }; + char buf[4096]; + size_t len = 0; + while (poll(fds, 2, -1)) { + if (fds[0].revents) { + ssize_t n = read(fds[0].fd, &buf[len], sizeof(buf) - len); + if (n < 0) err(EX_IOERR, "read"); + if (!n) fds[0].events = 0; + len += n; + char *ptr = buf; + for ( + char *nl; + (nl = memchr(ptr, '\n', &buf[len] - ptr)); + ptr = &nl[1] + ) { + struct Line line = { .text = strndup(ptr, nl - ptr) }; + if (!line.text) err(EX_OSERR, "strndup"); + parse(line); + } + len -= ptr - buf; + memmove(buf, ptr, len); + } + if (fds[1].revents) { + input(); + } + draw(); + } + err(EX_IOERR, "poll"); +} -- cgit 1.4.1