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 }