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