Home
       iomenu.c - iomenu - interactive terminal-based selection menu
  HTML git clone git://bitreich.org/iomenu git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/iomenu
   DIR Log
   DIR Files
   DIR Refs
   DIR Tags
   DIR README
   DIR LICENSE
       ---
       iomenu.c (8228B)
       ---
            1 #include <ctype.h>
            2 #include <errno.h>
            3 #include <fcntl.h>
            4 #include <limits.h>
            5 #include <signal.h>
            6 #include <stddef.h>
            7 #include <stdio.h>
            8 #include <stdlib.h>
            9 #include <string.h>
           10 #include <sys/ioctl.h>
           11 #include <termios.h>
           12 #include <unistd.h>
           13 #include <assert.h>
           14 #include "compat.h"
           15 #include "term.h"
           16 #include "utf8.h"
           17 
           18 struct {
           19         char input[256];
           20         size_t cur;
           21 
           22         char **lines_buf;
           23         size_t lines_count;
           24 
           25         char **match_buf;
           26         size_t match_count;
           27 } ctx;
           28 
           29 int flag_comment;
           30 
           31 /*
           32  * Keep the line if it match every token (in no particular order,
           33  * and allowed to be overlapping).
           34  */
           35 static int
           36 match_line(char *line, char **tokv)
           37 {
           38         if (flag_comment && line[0] == '#')
           39                 return 2;
           40         for (; *tokv != NULL; tokv++)
           41                 if (strcasestr(line, *tokv) == NULL)
           42                         return 0;
           43         return 1;
           44 }
           45 
           46 /*
           47  * Free the structures, reset the terminal state and exit with an
           48  * error message.
           49  */
           50 static void
           51 die(const char *msg)
           52 {
           53         int e = errno;
           54 
           55         term_raw_off(2);
           56 
           57         fprintf(stderr, "iomenu: ");
           58         errno = e;
           59         perror(msg);
           60 
           61         exit(1);
           62 }
           63 
           64 void *
           65 xrealloc(void *ptr, size_t sz)
           66 {
           67         ptr = realloc(ptr, sz);
           68         if (ptr == NULL)
           69                 die("realloc");
           70         return ptr;
           71 }
           72 
           73 void *
           74 xmalloc(size_t sz)
           75 {
           76         void *ptr;
           77 
           78         ptr = malloc(sz);
           79         if (ptr == NULL)
           80                 die("malloc");
           81         return ptr;
           82 }
           83 
           84 static void
           85 do_move(int sign)
           86 {
           87         /* integer overflow will do what we need */
           88         for (size_t i = ctx.cur + sign; i < ctx.match_count; i += sign) {
           89                 if (flag_comment == 0 || ctx.match_buf[i][0] != '#') {
           90                         ctx.cur = i;
           91                         break;
           92                 }
           93         }
           94 }
           95 
           96 /*
           97  * First split input into token, then match every token independently against
           98  * every line.  The matching lines fills matches.  Matches are searched inside
           99  * of `searchv' of size `searchc'
          100  */
          101 static void
          102 do_filter(char **search_buf, size_t search_count)
          103 {
          104         char **t, *tokv[(sizeof ctx.input + 1) * sizeof(char *)];
          105         char *b, buf[sizeof ctx.input];
          106 
          107         strlcpy(buf, ctx.input, sizeof buf);
          108 
          109         for (b = buf, t = tokv; (*t = strsep(&b, " \t")) != NULL; t++)
          110                 continue;
          111         *t = NULL;
          112 
          113         ctx.cur = ctx.match_count = 0;
          114         for (size_t n = 0; n < search_count; n++)
          115                 if (match_line(search_buf[n], tokv))
          116                         ctx.match_buf[ctx.match_count++] = search_buf[n];
          117         if (flag_comment && ctx.match_buf[ctx.cur][0] == '#')
          118                 do_move(+1);
          119 }
          120 
          121 static void
          122 do_move_page(signed int sign)
          123 {
          124         int rows = term.winsize.ws_row - 1;
          125         size_t i = ctx.cur - ctx.cur % rows + rows * sign;
          126 
          127         if (i >= ctx.match_count)
          128                 return;
          129         ctx.cur = i - 1;
          130 
          131         do_move(+1);
          132 }
          133 
          134 static void
          135 do_move_header(signed int sign)
          136 {
          137         do_move(sign);
          138 
          139         if (flag_comment == 0)
          140                 return;
          141         for (ctx.cur += sign;; ctx.cur += sign) {
          142                 char *cur = ctx.match_buf[ctx.cur];
          143 
          144                 if (ctx.cur >= ctx.match_count) {
          145                         ctx.cur--;
          146                         break;
          147                 }
          148                 if (cur[0] == '#')
          149                         break;
          150         }
          151 
          152         do_move(+1);
          153 }
          154 
          155 static void
          156 do_remove_word(void)
          157 {
          158         int len, i;
          159 
          160         len = strlen(ctx.input) - 1;
          161         for (i = len; i >= 0 && isspace(ctx.input[i]); i--)
          162                 ctx.input[i] = '\0';
          163         len = strlen(ctx.input) - 1;
          164         for (i = len; i >= 0 && !isspace(ctx.input[i]); i--)
          165                 ctx.input[i] = '\0';
          166         do_filter(ctx.lines_buf, ctx.lines_count);
          167 }
          168 
          169 static void
          170 do_add_char(char c)
          171 {
          172         int len;
          173 
          174         len = strlen(ctx.input);
          175         if (len + 1 == sizeof ctx.input)
          176                 return;
          177         if (isprint(c)) {
          178                 ctx.input[len] = c;
          179                 ctx.input[len + 1] = '\0';
          180         }
          181         do_filter(ctx.match_buf, ctx.match_count);
          182 }
          183 
          184 static void
          185 do_print_selection(void)
          186 {
          187         if (flag_comment) {
          188                 char **match = ctx.match_buf + ctx.cur;
          189 
          190                 while (--match >= ctx.match_buf) {
          191                         if ((*match)[0] == '#') {
          192                                 fprintf(stdout, "%s", *match + 1);
          193                                 break;
          194                         }
          195                 }
          196                 fprintf(stdout, "%c", '\t');
          197         }
          198         term_raw_off(2);
          199         if (ctx.match_count == 0
          200           || (flag_comment && ctx.match_buf[ctx.cur][0] == '#'))
          201                 fprintf(stdout, "%s\n", ctx.input);
          202         else
          203                 fprintf(stdout, "%s\n", ctx.match_buf[ctx.cur]);
          204         term_raw_on(2);
          205 }
          206 
          207 /*
          208  * Big case table, that calls itself back for with TERM_KEY_ALT (aka Esc), TERM_KEY_CSI
          209  * (aka Esc + [).  These last two have values above the range of ASCII.
          210  */
          211 static int
          212 key_action(void)
          213 {
          214         int key;
          215 
          216         key = term_get_key(stderr);
          217         switch (key) {
          218         case TERM_KEY_CTRL('Z'):
          219                 term_raw_off(2);
          220                 kill(getpid(), SIGSTOP);
          221                 term_raw_on(2);
          222                 break;
          223         case TERM_KEY_CTRL('C'):
          224         case TERM_KEY_CTRL('D'):
          225                 return -1;
          226         case TERM_KEY_CTRL('U'):
          227                 ctx.input[0] = '\0';
          228                 do_filter(ctx.lines_buf, ctx.lines_count);
          229                 break;
          230         case TERM_KEY_CTRL('W'):
          231                 do_remove_word();
          232                 break;
          233         case TERM_KEY_DELETE:
          234         case TERM_KEY_BACKSPACE:
          235                 ctx.input[strlen(ctx.input) - 1] = '\0';
          236                 do_filter(ctx.lines_buf, ctx.lines_count);
          237                 break;
          238         case TERM_KEY_ARROW_UP:
          239         case TERM_KEY_CTRL('P'):
          240                 do_move(-1);
          241                 break;
          242         case TERM_KEY_ALT('p'):
          243                 do_move_header(-1);
          244                 break;
          245         case TERM_KEY_ARROW_DOWN:
          246         case TERM_KEY_CTRL('N'):
          247                 do_move(+1);
          248                 break;
          249         case TERM_KEY_ALT('n'):
          250                 do_move_header(+1);
          251                 break;
          252         case TERM_KEY_PAGE_UP:
          253         case TERM_KEY_ALT('v'):
          254                 do_move_page(-1);
          255                 break;
          256         case TERM_KEY_PAGE_DOWN:
          257         case TERM_KEY_CTRL('V'):
          258                 do_move_page(+1);
          259                 break;
          260         case TERM_KEY_TAB:
          261                 if (ctx.match_count == 0)
          262                         break;
          263                 strlcpy(ctx.input, ctx.match_buf[ctx.cur], sizeof(ctx.input));
          264                 do_filter(ctx.match_buf, ctx.match_count);
          265                 break;
          266         case TERM_KEY_ENTER:
          267         case TERM_KEY_CTRL('M'):
          268                 do_print_selection();
          269                 return 0;
          270         default:
          271                 do_add_char(key);
          272         }
          273 
          274         return 1;
          275 }
          276 
          277 static void
          278 print_line(char *line, int highlight)
          279 {
          280         if (flag_comment && line[0] == '#') {
          281                 fprintf(stderr, "\n\x1b[1m\r%.*s\x1b[m",
          282                   term_at_width(line + 1, term.winsize.ws_col, 0), line + 1);
          283         } else if (highlight) {
          284                 fprintf(stderr, "\n\x1b[47;30m\x1b[K\r%.*s\x1b[m",
          285                   term_at_width(line, term.winsize.ws_col, 0), line);
          286         } else {
          287                 fprintf(stderr, "\n%.*s",
          288                   term_at_width(line, term.winsize.ws_col, 0), line);
          289         }
          290 }
          291 
          292 static void
          293 do_print_screen(void)
          294 {
          295         char **m;
          296         int p, c, cols, rows;
          297         size_t i;
          298 
          299         cols = term.winsize.ws_col;
          300         rows = term.winsize.ws_row - 1; /* -1 to keep one line for user input */
          301         p = c = 0;
          302         i = ctx.cur - ctx.cur % rows;
          303         m = ctx.match_buf + i;
          304         fprintf(stderr, "\x1b[2J");
          305         while (p < rows && i < ctx.match_count) {
          306                 print_line(*m, i == ctx.cur);
          307                 p++, i++, m++;
          308         }
          309         fprintf(stderr, "\x1b[H%.*s",
          310           term_at_width(ctx.input, cols, c), ctx.input);
          311         fflush(stderr);
          312 }
          313 
          314 static void
          315 sig_winch(int sig)
          316 {
          317         if (ioctl(STDERR_FILENO, TIOCGWINSZ, &term.winsize) == -1)
          318                 die("ioctl");
          319         do_print_screen();
          320         signal(sig, sig_winch);
          321 }
          322 
          323 static void
          324 usage(char const *arg0)
          325 {
          326         fprintf(stderr, "usage: %s [-#] <lines\n", arg0);
          327         exit(1);
          328 }
          329 
          330 static int
          331 read_stdin(char **buf)
          332 {
          333         size_t len = 0;
          334 
          335         assert(*buf == NULL);
          336 
          337         for (int c; (c = fgetc(stdin)) != EOF;) {
          338                 if (c == '\0') {
          339                         fprintf(stderr, "iomenu: ignoring '\\0' byte in input\r\n");
          340                         continue;
          341                 }
          342                 *buf = xrealloc(*buf, sizeof *buf + len + 1);
          343                 (*buf)[len++] = c;
          344         }
          345         *buf = xrealloc(*buf, sizeof *buf + len + 1);
          346         (*buf)[len] = '\0';
          347 
          348         return 0;
          349 }
          350 
          351 /*
          352  * Split a buffer into an array of lines, without allocating memory for every
          353  * line, but using the input buffer and replacing '\n' by '\0'.
          354  */
          355 static void
          356 split_lines(char *s)
          357 {
          358         size_t sz;
          359 
          360         ctx.lines_count = 0;
          361         for (;;) {
          362                 sz = (ctx.lines_count + 1) * sizeof s;
          363                 ctx.lines_buf = xrealloc(ctx.lines_buf, sz);
          364                 ctx.lines_buf[ctx.lines_count++] = s;
          365 
          366                 s = strchr(s, '\n');
          367                 if (s == NULL)
          368                         break;
          369                 *s++ = '\0';
          370         }
          371         sz = ctx.lines_count * sizeof s;
          372         ctx.match_buf = xmalloc(sz);
          373         memcpy(ctx.match_buf, ctx.lines_buf, sz);
          374 }
          375 
          376 /*
          377  * Read stdin in a buffer, filling a table of lines, then re-open stdin to
          378  * /dev/tty for an interactive (raw) session to let the user filter and select
          379  * one line by searching words within stdin.  This was inspired from dmenu.
          380  */
          381 int
          382 main(int argc, char *argv[])
          383 {
          384         char *buf = NULL, *arg0;
          385 
          386         arg0 = *argv;
          387         for (int opt; (opt = getopt(argc, argv, "#v")) > 0;) {
          388                 switch (opt) {
          389                 case 'v':
          390                         fprintf(stdout, "%s\n", VERSION);
          391                         exit(0);
          392                 case '#':
          393                         flag_comment = 1;
          394                         break;
          395                 default:
          396                         usage(arg0);
          397                 }
          398         }
          399         argc -= optind;
          400         argv += optind;
          401 
          402         read_stdin(&buf);
          403         split_lines(buf);
          404 
          405         do_filter(ctx.lines_buf, ctx.lines_count);
          406 
          407         if (!isatty(2))
          408                 die("file descriptor 2 (stderr)");
          409 
          410         freopen("/dev/tty", "w+", stderr);
          411         if (stderr == NULL)
          412                 die("re-opening standard error read/write");
          413 
          414         term_raw_on(2);
          415         sig_winch(SIGWINCH);
          416 
          417 #ifdef __OpenBSD__
          418         pledge("stdio tty", NULL);
          419 #endif
          420 
          421         while (key_action() > 0)
          422                 do_print_screen();
          423 
          424         term_raw_off(2);
          425 
          426         return 0;
          427 }