tmenu.c (15636B)
1#include <sys/ioctl.h> 2#include <signal.h> 3#include <unistd.h> 4#include <termios.h> 5#include <fcntl.h> 6#include <stdbool.h> 7#include <stdarg.h> 8#include <string.h> 9#include <stdio.h> 10#include <stdlib.h> 11 12#define ARRLEN(x) (sizeof(x)/sizeof((x)[0])) 13#define MAX(a, b) ((a) > (b) ? (a) : (b)) 14#define MIN(a, b) ((a) < (b) ? (a) : (b)) 15#define EPRINTF(...) fprintf(stderr, __VA_ARGS__) 16 17#define KEY_CTRL(c) (((int) (c)) & 0b11111) 18 19#define CSI_CLEAR_LINE "\x1b[K\r" 20#define CSI_CUR_HIDE "\x1b[?25l" 21#define CSI_CUR_SHOW "\x1b[?25h" 22#define CSI_CUR_UP "\x1b[A" 23#define CSI_CUR_DOWN "\x1b[B" 24#define CSI_CUR_RIGHT "\x1b[C" 25#define CSI_CUR_LEFT "\x1b[D" 26#define CSI_STYLE_BOLD "\x1b[1m" 27#define CSI_STYLE_RESET "\x1b[0m" 28#define CSI_CLEAR_SCREEN "\x1b[2J" 29#define CSI_STYLE_ULINE "\x1b[4m" 30#define CSI_STYLE_NULINE "\x1b[24m" 31#define CSI_CUR_GOTO "\x1b[%i%iH" 32 33enum { 34 BWD = -1, 35 FWD = 1, 36}; 37 38enum { 39 MODE_BROWSE, 40 MODE_SEARCH, 41}; 42 43enum { 44 SEARCH_SUBSTR, 45 SEARCH_FUZZY, 46}; 47 48enum { 49 CASE_SENSITIVE, 50 CASE_INSENSITIVE, 51}; 52 53enum { 54 KEY_NONE = 0, 55 KEY_DEL = 0x7f, 56 KEY_UP = 0x100, 57 KEY_DOWN, 58 KEY_LEFT, 59 KEY_RIGHT, 60 KEY_PGUP, 61 KEY_PGDN, 62}; 63 64struct mode { 65 void (*prompt)(void); 66 void (*cleanup)(void); 67 bool (*handlekey)(int c); 68}; 69 70struct searchmode { 71 char c; 72 ssize_t (*match)(ssize_t, int, size_t, ssize_t, bool, bool); 73}; 74 75static void browse_prompt(void); 76static bool browse_handlekey(int c); 77static void browse_cleanup(void); 78 79static void search_prompt(void); 80static bool search_handlekey(int c); 81static void search_cleanup(void); 82 83static ssize_t search_match(ssize_t start, int dir, 84 size_t cnt, ssize_t fallback, bool new, bool closest); 85static ssize_t search_match_substr(ssize_t start, int dir, 86 size_t cnt, ssize_t fallback, bool new, bool closest); 87static ssize_t search_match_fuzzy(ssize_t start, int dir, 88 size_t cnt, ssize_t fallback, bool new, bool closest); 89 90static const struct searchmode searchmodes[] = { 91 [SEARCH_SUBSTR] = { 92 .c = 'S', 93 .match = search_match_substr, 94 }, 95 [SEARCH_FUZZY] = { 96 .c = 'F', 97 .match = search_match_fuzzy, 98 } 99}; 100 101static const struct mode modes[] = { 102 [MODE_BROWSE] = { 103 .prompt = browse_prompt, 104 .handlekey = browse_handlekey, 105 .cleanup = browse_cleanup 106 }, 107 [MODE_SEARCH] = { 108 .prompt = search_prompt, 109 .handlekey = search_handlekey, 110 .cleanup = search_cleanup 111 } 112}; 113 114static bool init = false; 115 116static char *input = NULL; 117static size_t input_len = 0; 118static size_t input_cap = 0; 119 120static size_t *delims = NULL; 121static size_t delims_cnt = 0; 122static size_t delims_cap = 0; 123 124static ssize_t selected = -1; 125static const char *entry = NULL; 126static size_t entry_max = 0; 127static size_t entry_off = 0; 128 129static char searchbuf[1024]; 130static size_t searchlen = 0; 131 132static int mode = MODE_BROWSE; 133static int searchcase = CASE_SENSITIVE; 134static int searchmode = SEARCH_SUBSTR; 135 136static size_t fwdctx = 1; 137static size_t bwdctx = 1; 138static size_t termw = 80; 139 140static bool multiout = false; 141static bool verbose = false; 142static bool show_prompt = true; 143static bool top_prompt = false; 144 145static char delim = '\n'; 146 147static void 148die(const char *fmt, ...) 149{ 150 va_list ap; 151 152 va_start(ap, fmt); 153 fputs("tmenu: ", stderr); 154 vfprintf(stderr, fmt, ap); 155 if (*fmt && fmt[strlen(fmt) - 1] == ':') { 156 fputc(' ', stderr); 157 perror(NULL); 158 } else { 159 fputc('\n', stderr); 160 } 161 va_end(ap); 162 163 exit(1); 164} 165 166static void 167prompt(void) 168{ 169 struct winsize ws = { 0 }; 170 171 if (ioctl(2, TIOCGWINSZ, &ws) != -1) 172 termw = ws.ws_col; 173 174 modes[mode].prompt(); 175} 176 177static void 178sigwinch(int sig) 179{ 180 if (init) prompt(); 181 signal(SIGWINCH, sigwinch); 182} 183 184static void * 185addcap(void *alloc, size_t dsize, size_t min, size_t *cap) 186{ 187 if (min > *cap) { 188 *cap = *cap * 2; 189 if (*cap < min) *cap = min; 190 alloc = realloc(alloc, dsize * *cap); 191 if (!alloc) die("realloc:"); 192 } 193 194 return alloc; 195} 196 197static inline char 198lower(char c) 199{ 200 if (c >= 'A' && c <= 'Z') 201 c += 'a' - 'A'; 202 return c; 203} 204 205static size_t 206entry_len(size_t index) 207{ 208 return delims[index] - (index > 0 ? delims[index-1] + 1 : 0); 209} 210 211static inline char * 212get_entry(size_t index) 213{ 214 return input + (index > 0 ? delims[index-1] + 1 : 0); 215} 216 217static int 218readkey(FILE *f) 219{ 220 int c; 221 222 c = fgetc(f); 223 if (c != '\x1b') 224 return c; 225 226 if (fgetc(f) != '[') 227 return KEY_NONE; 228 229 switch (fgetc(f)) { 230 case 'A': 231 return KEY_UP; 232 case 'B': 233 return KEY_DOWN; 234 case 'C': 235 return KEY_RIGHT; 236 case 'D': 237 return KEY_LEFT; 238 case '5': 239 return fgetc(f) == '~' ? KEY_PGUP : KEY_NONE; 240 case '6': 241 return fgetc(f) == '~' ? KEY_PGDN : KEY_NONE; 242 } 243 244 return KEY_NONE; 245} 246 247static int 248search_eq(const char *a, const char *b, size_t size) 249{ 250 size_t i; 251 252 for (i = 0; i < size; i++) { 253 if (searchcase == CASE_SENSITIVE) { 254 if (a[i] != b[i]) return false; 255 } else { 256 if (lower(a[i]) != lower(b[i])) 257 return false; 258 } 259 } 260 261 return true; 262} 263 264static const char* 265search_find(const char *a, char c, size_t size) 266{ 267 size_t i; 268 269 for (i = 0; i < size; i++) { 270 if (searchcase == CASE_SENSITIVE) { 271 if (a[i] == c) return a + i; 272 } else if (searchcase == CASE_INSENSITIVE) { 273 if (lower(a[i]) == lower(c)) 274 return a + i; 275 } 276 } 277 278 return NULL; 279} 280 281static void 282browse_prompt(void) 283{ 284 size_t linew, entlen; 285 ssize_t i; 286 287 if (selected < 0) selected = 0; 288 289 if (show_prompt && top_prompt) 290 EPRINTF(CSI_STYLE_BOLD "[B] " CSI_STYLE_RESET "\n"); 291 292 i = (ssize_t) selected - (ssize_t) bwdctx; 293 for (; i <= (ssize_t) selected + (ssize_t) fwdctx; i++) { 294 EPRINTF(CSI_CLEAR_LINE); 295 296 if (i == selected) 297 EPRINTF(CSI_STYLE_BOLD); 298 299 if (show_prompt && !top_prompt) { 300 linew = termw - 4; 301 if (i == selected) { 302 EPRINTF("[B] "); 303 } else { 304 EPRINTF(" "); 305 } 306 } else { 307 linew = termw; 308 } 309 310 if (selected >= 0 && i >= 0 && i < delims_cnt) { 311 entry = get_entry((size_t) i); 312 entlen = entry_len((size_t) i); 313 if (entlen > linew) { 314 EPRINTF(" ..%.*s\n", (int) (linew - 3), 315 entry + entlen - (linew - 3)); 316 } else { 317 EPRINTF("%.*s\n", (int) linew, entry); 318 } 319 } else { 320 EPRINTF("\n"); 321 } 322 323 if (i == selected) 324 EPRINTF(CSI_STYLE_RESET); 325 } 326 327 for (i = 0; i < bwdctx + fwdctx + 1 + show_prompt * top_prompt; i++) 328 EPRINTF(CSI_CUR_UP); 329} 330 331static bool 332browse_handlekey(int c) 333{ 334 size_t cnt; 335 336 switch (c) { 337 case 'g': 338 selected = 0; 339 break; 340 case 'G': 341 selected = (ssize_t) delims_cnt - 1; 342 break; 343 case 'q': 344 return true; 345 case KEY_PGUP: 346 cnt = fwdctx + bwdctx + 1; 347 if (selected > cnt) 348 selected -= (ssize_t) cnt; 349 else 350 selected = 0; 351 break; 352 case KEY_PGDN: 353 cnt = fwdctx + bwdctx + 1; 354 if (selected < delims_cnt - cnt) 355 selected += (ssize_t) cnt; 356 else 357 selected = (ssize_t) delims_cnt - 1; 358 break; 359 case KEY_UP: 360 if (selected != 0) 361 selected--; 362 break; 363 case KEY_DOWN: 364 if (selected != delims_cnt - 1) 365 selected++; 366 break; 367 } 368 369 return false; 370} 371 372static void 373browse_cleanup(void) 374{ 375 size_t i; 376 377 for (i = 0; i < bwdctx + 1 + fwdctx; i++) 378 EPRINTF(CSI_CLEAR_LINE "\n"); 379 for (i = 0; i < bwdctx + 1 + fwdctx; i++) 380 EPRINTF(CSI_CUR_UP); 381} 382 383static void 384search_prompt(void) 385{ 386 size_t linew, off, entlen; 387 ssize_t i, index; 388 389 if (selected < 0) selected = 0; 390 391 index = search_match(selected, FWD, 1, -1, false, true); 392 if (index != -1) { 393 selected = index; 394 } else { 395 selected = search_match(selected, BWD, 1, -1, true, true); 396 } 397 398 if (show_prompt && top_prompt) { 399 EPRINTF(CSI_STYLE_BOLD "[%c] " CSI_STYLE_ULINE "%.*s" 400 CSI_STYLE_NULINE "%.*s\n" CSI_STYLE_RESET, 401 (searchcase == CASE_SENSITIVE ? 402 searchmodes[searchmode].c 403 : lower(searchmodes[searchmode].c)), 404 (int) searchlen, searchbuf, (int) termw, " "); 405 } 406 407 for (i = -(ssize_t) bwdctx; i <= (ssize_t) fwdctx; i++) { 408 if (selected >= 0) { 409 if (i < 0) { 410 index = search_match(selected, 411 BWD, (size_t) -i, -1, true, false); 412 } else if (i == 0) { 413 index = selected; 414 } else if (i > 0) { 415 index = search_match(selected, 416 FWD, (size_t) i, -1, true, false); 417 } 418 } else { 419 index = -1; 420 } 421 422 EPRINTF(CSI_CLEAR_LINE); 423 424 if (i == 0) EPRINTF(CSI_STYLE_BOLD); 425 426 if (show_prompt && !top_prompt) { 427 if (i == 0) { 428 EPRINTF("[%c] " CSI_STYLE_ULINE "%.*s" 429 CSI_STYLE_NULINE " ", 430 (searchcase == CASE_SENSITIVE ? 431 searchmodes[searchmode].c 432 : lower(searchmodes[searchmode].c)), 433 (int) searchlen, searchbuf); 434 } else { 435 EPRINTF("%*.s", (int) (4 + searchlen + 1), " "); 436 } 437 linew = (size_t) MAX(0, (ssize_t) termw 438 - (ssize_t) searchlen - 5); 439 } else { 440 linew = termw; 441 } 442 443 if (index < 0) { 444 EPRINTF("\n"); 445 } else { 446 entlen = entry_len((size_t) index); 447 entry = get_entry((size_t) index); 448 off = entlen >= linew ? entlen - linew : 0; 449 off = MIN(entry_off, off); 450 EPRINTF("%.*s\n", (int) linew, entry + off); 451 } 452 453 if (i == 0) EPRINTF(CSI_STYLE_RESET); 454 } 455 456 for (i = 0; i < bwdctx + fwdctx + 1 + show_prompt * top_prompt; i++) 457 EPRINTF(CSI_CUR_UP); 458} 459 460static bool 461search_handlekey(int c) 462{ 463 size_t cnt; 464 465 switch (c) { 466 case KEY_CTRL('I'): 467 searchcase ^= 1; 468 break; 469 case KEY_PGUP: 470 cnt = fwdctx + bwdctx + 1; 471 selected = search_match(selected, BWD, cnt, selected, true, true); 472 break; 473 case KEY_PGDN: 474 cnt = fwdctx + bwdctx + 1; 475 selected = search_match(selected, FWD, cnt, selected, true, true); 476 break; 477 case KEY_CTRL('K'): 478 case KEY_UP: 479 selected = search_match(selected, BWD, 1, selected, true, true); 480 break; 481 case KEY_CTRL('L'): 482 case KEY_DOWN: 483 selected = search_match(selected, FWD, 1, selected, true, true); 484 break; 485 case 0x20 ... 0x7e: 486 if (searchlen < sizeof(searchbuf) - 1) 487 searchbuf[searchlen++] = (char) (c & 0xff); 488 break; 489 case KEY_DEL: 490 if (searchlen) searchlen--; 491 break; 492 } 493 494 return false; 495} 496 497static void 498search_cleanup(void) 499{ 500 size_t i; 501 502 for (i = 0; i < bwdctx + 1 + fwdctx; i++) 503 EPRINTF(CSI_CLEAR_LINE "\n"); 504 for (i = 0; i < bwdctx + 1 + fwdctx; i++) 505 EPRINTF(CSI_CUR_UP); 506} 507 508static ssize_t 509search_match(ssize_t start, int dir, 510 size_t cnt, ssize_t fallback, bool new, bool closest) 511{ 512 return searchmodes[searchmode].match(start, dir, 513 cnt, fallback, new, closest); 514} 515 516static ssize_t 517search_match_linear(ssize_t start, int dir, 518 size_t cnt, ssize_t fallback, bool new, bool closest) 519{ 520 ssize_t index; 521 522 index = start + dir * (ssize_t) (new + cnt - 1); 523 if (index < 0) return closest ? 0 : fallback; 524 if (index >= delims_cnt) 525 return closest ? (ssize_t) delims_cnt - 1 : fallback; 526 527 return index; 528} 529 530static ssize_t 531search_match_substr(ssize_t start, int dir, 532 size_t cnt, ssize_t fallback, bool new, bool closest) 533{ 534 const char *end, *bp; 535 size_t i, found; 536 ssize_t index, prev; 537 538 if (!searchlen) 539 return search_match_linear(start, dir, 540 cnt, fallback, new, closest); 541 542 prev = -1; 543 found = 0; 544 for (i = new; i < delims_cnt; i++) { 545 index = start + dir * (ssize_t) i; 546 if (index < 0 || index >= delims_cnt) 547 break; 548 549 entry = get_entry((size_t) index); 550 end = entry + entry_len((size_t) index); 551 552 for (bp = entry; *bp; bp++) { 553 if (searchlen > end - bp) continue; 554 if (search_eq(bp, searchbuf, searchlen)) { 555 if (++found == cnt) 556 return index; 557 prev = index; 558 break; 559 } 560 } 561 } 562 563 return closest ? prev : fallback; 564} 565 566static ssize_t 567search_match_fuzzy(ssize_t start, int dir, 568 size_t cnt, ssize_t fallback, bool new, bool closest) 569{ 570 const char *end, *pos, *c; 571 size_t i, found; 572 ssize_t index, prev; 573 574 if (!searchlen) 575 return search_match_linear(start, dir, 576 cnt, fallback, new, closest); 577 578 prev = -1; 579 found = 0; 580 for (i = new; i < delims_cnt; i++) { 581 index = start + dir * (ssize_t) i; 582 if (index < 0 || index >= delims_cnt) 583 break; 584 585 entry = get_entry((size_t) index); 586 end = entry + entry_len((size_t) index); 587 588 pos = entry; 589 for (c = searchbuf; c - searchbuf < searchlen; c++) { 590 pos = search_find(pos, *c, (size_t) (end - pos)); 591 if (!pos) break; 592 pos++; 593 } 594 if (c == searchbuf + searchlen) { 595 if (++found == cnt) 596 return index; 597 prev = index; 598 } 599 } 600 601 return closest ? prev : fallback; 602} 603 604static void 605load(int fd) 606{ 607 ssize_t nread; 608 size_t i; 609 char *c; 610 611 while (1) { 612 input = addcap(input, 1, input_len + BUFSIZ, &input_cap); 613 nread = read(fd, input + input_len, BUFSIZ); 614 if (nread <= 0) break; 615 616 c = input + input_len; 617 for (i = 0; i < (size_t) nread; i++, c++) { 618 if (*c != delim) continue; 619 *c = '\0'; 620 delims = addcap(delims, sizeof(size_t), 621 delims_cnt + 1, &delims_cap); 622 delims[delims_cnt++] = input_len + i; 623 entry_max = MAX(entry_max, entry_len(delims_cnt-1)); 624 } 625 626 input_len += (size_t) nread; 627 } 628 629 if (!delims_cnt && input_len 630 || delims_cnt && delims[delims_cnt-1] != input_len-1) { 631 delims = addcap(delims, sizeof(size_t), 632 delims_cnt + 1, &delims_cap); 633 delims[delims_cnt++] = input_len; 634 entry_max = MAX(entry_max, entry_len(delims_cnt-1)); 635 } 636 637 if (verbose) 638 EPRINTF("Loaded %lu entries\n", delims_cnt); 639} 640 641static void 642run(void) 643{ 644 struct termios prevterm, newterm = { 0 }; 645 int c; 646 647 if (!delims_cnt) return; 648 649 if (tcgetattr(0, &prevterm)) 650 die("tcgetattr:"); 651 652 cfmakeraw(&newterm); 653 newterm.c_oflag |= ONLCR | OPOST; 654 if (tcsetattr(0, TCSANOW, &newterm)) 655 die("tcsetattr:"); 656 657 EPRINTF(CSI_CUR_HIDE); 658 659 init = true; 660 selected = 0; 661 searchlen = 0; 662 do { 663 prompt(); 664 665 switch ((c = readkey(stdin))) { 666 case KEY_CTRL('C'): 667 goto exit; 668 case KEY_CTRL('D'): 669 if (!multiout) goto exit; 670 break; 671 case KEY_CTRL('S'): 672 searchmode = SEARCH_SUBSTR; 673 mode = MODE_SEARCH; 674 break; 675 case KEY_CTRL('F'): 676 searchmode = SEARCH_FUZZY; 677 mode = MODE_SEARCH; 678 break; 679 case KEY_CTRL('Q'): 680 case KEY_CTRL('B'): 681 mode = MODE_BROWSE; 682 break; 683 case KEY_CTRL('L'): 684 EPRINTF(CSI_CLEAR_SCREEN CSI_CUR_GOTO, 0, 0); 685 break; 686 case KEY_CTRL('W'): 687 searchlen = 0; 688 break; 689 case KEY_CTRL('J'): 690 case '\r': 691 if (selected < 0) break; 692 entry = get_entry((size_t) selected); 693 modes[mode].cleanup(); 694 printf("%.*s\n", (int) entry_len((size_t) selected), entry); 695 if (!multiout) goto exit; 696 break; 697 default: 698 if (modes[mode].handlekey(c)) 699 goto exit; 700 break; 701 } 702 } while (c >= 0); 703 704exit: 705 modes[mode].cleanup(); 706 707 EPRINTF(CSI_CUR_SHOW); 708 709 tcsetattr(fileno(stdin), TCSANOW, &prevterm); 710} 711 712static int 713parseopt(const char *flag, const char **args) 714{ 715 char *end; 716 717 if (flag[0] && flag[1]) { 718 EPRINTF("Invalid flag: -%s\n", flag); 719 exit(1); 720 } 721 722 switch (flag[0]) { 723 case 'm': 724 multiout = true; 725 return 0; 726 case 'v': 727 verbose = true; 728 return 0; 729 case 's': 730 mode = MODE_SEARCH; 731 searchmode = SEARCH_SUBSTR; 732 return 0; 733 case 'n': 734 show_prompt = false; 735 return 0; 736 case 'a': 737 if (!*args) die("missing -a arg"); 738 fwdctx = strtoull(*args, &end, 10); 739 if (end && *end) die("bad -a arg '%s'", *args); 740 return 1; 741 case 'b': 742 if (!*args) die("missing -b arg"); 743 bwdctx = strtoull(*args, &end, 10); 744 if (end && *end) die("bad -b arg '%s'", *args); 745 return 1; 746 case 'c': 747 if (!*args) die("missing -c arg"); 748 fwdctx = bwdctx = strtoull(*args, &end, 10); 749 if (end && *end) die("bad -c arg '%s'", *args); 750 return 1; 751 case 'd': 752 if (!*args) die("missing -d arg"); 753 if (args[0][0] && args[0][1]) die("bad -d arg"); 754 delim = **args; 755 return 1; 756 case 't': 757 top_prompt = true; 758 return 0; 759 case 'h': 760 printf("Usage: tmenu [-h] [-m] [-a LINES] [-b LINES]"); 761 exit(0); 762 default: 763 die("unknown opt '%s'", *flag); 764 } 765 766 return 0; 767} 768 769int 770main(int argc, const char **argv) 771{ 772 const char **arg; 773 int fd; 774 775 signal(SIGWINCH, sigwinch); 776 setvbuf(stdout, NULL, _IONBF, 0); 777 setvbuf(stderr, NULL, _IONBF, 0); 778 779 for (arg = argv + 1; *arg; arg++) { 780 if (**arg == '-') { 781 arg += parseopt(*arg + 1, arg + 1); 782 } else { 783 fd = open(*arg, O_RDONLY); 784 if (fd < 0) die("open '%s':", *arg); 785 load(fd); 786 close(fd); 787 } 788 } 789 790 if (!input) { 791 load(0); 792 if (!freopen("/dev/tty", "r", stdin)) 793 die("freopen tty:"); 794 } 795 796 run(); 797}