noice.c (15447B)
1 /* See LICENSE file for copyright and license details. */ 2 #include <sys/stat.h> 3 #include <sys/types.h> 4 5 #include <curses.h> 6 #include <dirent.h> 7 #include <errno.h> 8 #include <fcntl.h> 9 #include <libgen.h> 10 #include <limits.h> 11 #include <locale.h> 12 #include <regex.h> 13 #include <signal.h> 14 #include <stdarg.h> 15 #include <stdio.h> 16 #include <stdlib.h> 17 #include <string.h> 18 #include <unistd.h> 19 20 #include "arg.h" 21 #include "util.h" 22 23 #define ISODD(x) ((x) & 1) 24 #define CONTROL(c) ((c) ^ 0x40) 25 #define META(c) ((c) ^ 0x80) 26 27 struct cpair { 28 int fg; 29 int bg; 30 }; 31 32 /* Supported actions */ 33 enum action { 34 SEL_QUIT = 1, 35 SEL_BACK, 36 SEL_GOIN, 37 SEL_FLTR, 38 SEL_NEXT, 39 SEL_PREV, 40 SEL_PGDN, 41 SEL_PGUP, 42 SEL_HOME, 43 SEL_END, 44 SEL_CD, 45 SEL_CDHOME, 46 SEL_TOGGLEDOT, 47 SEL_DSORT, 48 SEL_MTIME, 49 SEL_ICASE, 50 SEL_VERS, 51 SEL_REDRAW, 52 SEL_RUN, 53 SEL_RUNARG, 54 }; 55 56 struct key { 57 int sym; /* Key pressed */ 58 enum action act; /* Action */ 59 char *run; /* Program to run */ 60 char *env; /* Environment variable override */ 61 }; 62 63 #include "noiceconf.h" 64 65 struct entry { 66 char name[PATH_MAX]; 67 mode_t mode; 68 time_t t; 69 }; 70 71 /* Global context */ 72 struct entry *dents; 73 char *argv0; 74 int ndents, cur; 75 int idle; 76 77 /* 78 * Layout: 79 * .--------- 80 * | /mnt/path 81 * | 82 * | file0 83 * | file1 84 * | > file2 85 * | file3 86 * | file4 87 * ... 88 * | filen 89 * | 90 * | Permission denied 91 * '------ 92 */ 93 94 void info(char *, ...); 95 void warn(char *, ...); 96 void fatal(char *, ...); 97 98 void * 99 xrealloc(void *p, size_t size) 100 { 101 p = realloc(p, size); 102 if (p == NULL) 103 fatal("realloc"); 104 return p; 105 } 106 107 /* Some implementations of dirname(3) may modify `path' and some 108 * return a pointer inside `path'. */ 109 char * 110 xdirname(const char *path) 111 { 112 static char out[PATH_MAX]; 113 char tmp[PATH_MAX], *p; 114 115 strlcpy(tmp, path, sizeof(tmp)); 116 p = dirname(tmp); 117 if (p == NULL) 118 fatal("dirname"); 119 strlcpy(out, p, sizeof(out)); 120 return out; 121 } 122 123 char * 124 xgetenv(char *name, char *fallback) 125 { 126 char *value; 127 128 if (name == NULL) 129 return fallback; 130 value = getenv(name); 131 return value && value[0] ? value : fallback; 132 } 133 134 int 135 setfilter(regex_t *regex, char *filter) 136 { 137 char errbuf[LINE_MAX]; 138 size_t len; 139 int r; 140 141 r = regcomp(regex, filter, REG_NOSUB | REG_EXTENDED | REG_ICASE); 142 if (r != 0) { 143 len = COLS; 144 if (len > sizeof(errbuf)) 145 len = sizeof(errbuf); 146 regerror(r, regex, errbuf, len); 147 info("%s", errbuf); 148 } 149 return r; 150 } 151 152 void 153 freefilter(regex_t *regex) 154 { 155 regfree(regex); 156 } 157 158 void 159 initfilter(int dot, char **ifilter) 160 { 161 *ifilter = dot ? "." : "^[^.]"; 162 } 163 164 int 165 visible(regex_t *regex, char *file) 166 { 167 return regexec(regex, file, 0, NULL, 0) == 0; 168 } 169 170 int 171 dircmp(mode_t a, mode_t b) 172 { 173 if (S_ISDIR(a) && S_ISDIR(b)) 174 return 0; 175 if (!S_ISDIR(a) && !S_ISDIR(b)) 176 return 0; 177 if (S_ISDIR(a)) 178 return -1; 179 else 180 return 1; 181 } 182 183 int 184 entrycmp(const void *va, const void *vb) 185 { 186 const struct entry *a = va, *b = vb; 187 188 if (dirorder) { 189 if (dircmp(a->mode, b->mode) != 0) 190 return dircmp(a->mode, b->mode); 191 } 192 193 if (mtimeorder) 194 return b->t - a->t; 195 if (icaseorder) 196 return strcasecmp(a->name, b->name); 197 if (versorder) 198 return strverscmp(a->name, b->name); 199 return strcmp(a->name, b->name); 200 } 201 202 void 203 initcolor(void) 204 { 205 int i; 206 207 start_color(); 208 use_default_colors(); 209 for (i = 1; i < LEN(pairs); i++) 210 init_pair(i, pairs[i].fg, pairs[i].bg); 211 } 212 213 void 214 initcurses(void) 215 { 216 char *term; 217 218 if (initscr() == NULL) { 219 term = getenv("TERM"); 220 if (term != NULL) 221 fprintf(stderr, "error opening terminal: %s\n", term); 222 else 223 fprintf(stderr, "failed to initialize curses\n"); 224 exit(1); 225 } 226 if (usecolor && has_colors()) 227 initcolor(); 228 cbreak(); 229 noecho(); 230 nonl(); 231 intrflush(stdscr, FALSE); 232 keypad(stdscr, TRUE); 233 curs_set(FALSE); /* Hide cursor */ 234 timeout(1000); /* One second */ 235 } 236 237 void 238 exitcurses(void) 239 { 240 endwin(); /* Restore terminal */ 241 } 242 243 /* Messages show up at the bottom */ 244 void 245 info(char *fmt, ...) 246 { 247 char buf[LINE_MAX]; 248 va_list ap; 249 250 va_start(ap, fmt); 251 vsnprintf(buf, sizeof(buf), fmt, ap); 252 va_end(ap); 253 move(LINES - 1, 0); 254 printw("%s\n", buf); 255 } 256 257 /* Display warning as a message */ 258 void 259 warn(char *fmt, ...) 260 { 261 char buf[LINE_MAX]; 262 va_list ap; 263 264 va_start(ap, fmt); 265 vsnprintf(buf, sizeof(buf), fmt, ap); 266 va_end(ap); 267 move(LINES - 1, 0); 268 printw("%s: %s\n", buf, strerror(errno)); 269 } 270 271 /* Kill curses and display error before exiting */ 272 void 273 fatal(char *fmt, ...) 274 { 275 va_list ap; 276 277 exitcurses(); 278 va_start(ap, fmt); 279 vfprintf(stderr, fmt, ap); 280 fprintf(stderr, ": %s\n", strerror(errno)); 281 va_end(ap); 282 exit(1); 283 } 284 285 /* Clear the last line */ 286 void 287 clearprompt(void) 288 { 289 info(""); 290 } 291 292 /* Print prompt on the last line */ 293 void 294 printprompt(char *str) 295 { 296 clearprompt(); 297 info("%s", str); 298 } 299 300 int 301 xgetch(void) 302 { 303 int c; 304 305 c = getch(); 306 if (c == -1) 307 idle++; 308 else 309 idle = 0; 310 return c; 311 } 312 313 /* Returns SEL_* if key is bound and 0 otherwise. 314 * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}) */ 315 int 316 nextsel(char **run, char **env) 317 { 318 int c, i; 319 320 c = xgetch(); 321 if (c == 033) 322 c = META(xgetch()); 323 324 for (i = 0; i < LEN(bindings); i++) 325 if (c == bindings[i].sym) { 326 *run = bindings[i].run; 327 *env = bindings[i].env; 328 return bindings[i].act; 329 } 330 return 0; 331 } 332 333 char * 334 readln(void) 335 { 336 static char ln[LINE_MAX]; 337 338 timeout(-1); 339 echo(); 340 curs_set(TRUE); 341 memset(ln, 0, sizeof(ln)); 342 wgetnstr(stdscr, ln, sizeof(ln) - 1); 343 noecho(); 344 curs_set(FALSE); 345 timeout(1000); 346 return ln[0] ? ln : NULL; 347 } 348 349 int 350 canopendir(char *path) 351 { 352 DIR *dirp; 353 354 dirp = opendir(path); 355 if (dirp == NULL) 356 return 0; 357 closedir(dirp); 358 return 1; 359 } 360 361 char * 362 mkpath(char *dir, char *name, char *out, size_t n) 363 { 364 /* Handle absolute path */ 365 if (name[0] == '/') { 366 strlcpy(out, name, n); 367 } else { 368 /* Handle root case */ 369 if (strcmp(dir, "/") == 0) { 370 strlcpy(out, "/", n); 371 strlcat(out, name, n); 372 } else { 373 strlcpy(out, dir, n); 374 strlcat(out, "/", n); 375 strlcat(out, name, n); 376 } 377 } 378 return out; 379 } 380 381 void 382 printent(struct entry *ent, int active) 383 { 384 char name[PATH_MAX]; 385 unsigned int len = COLS - strlen(CURSR) - 1; 386 char cm = 0; 387 int attr = 0; 388 389 /* Copy name locally */ 390 strlcpy(name, ent->name, sizeof(name)); 391 392 /* No text wrapping in entries */ 393 if (strlen(name) < len) 394 len = strlen(name) + 1; 395 396 if (S_ISDIR(ent->mode)) { 397 cm = '/'; 398 attr |= DIR_ATTR; 399 } else if (S_ISLNK(ent->mode)) { 400 cm = '@'; 401 attr |= LINK_ATTR; 402 } else if (S_ISSOCK(ent->mode)) { 403 cm = '='; 404 attr |= SOCK_ATTR; 405 } else if (S_ISFIFO(ent->mode)) { 406 cm = '|'; 407 attr |= FIFO_ATTR; 408 } else if (ent->mode & S_IXUSR) { 409 cm = '*'; 410 attr |= EXEC_ATTR; 411 } 412 413 if (active) 414 attr |= CURSR_ATTR; 415 416 if (cm) { 417 name[len - 1] = cm; 418 name[len] = '\0'; 419 } 420 421 attron(attr); 422 printw("%s%s\n", active ? CURSR : EMPTY, name); 423 attroff(attr); 424 } 425 426 int 427 dentfill(char *path, struct entry **dents, 428 int (*filter)(regex_t *, char *), regex_t *re) 429 { 430 char newpath[PATH_MAX]; 431 DIR *dirp; 432 struct dirent *dp; 433 struct stat sb; 434 int r, n = 0; 435 436 dirp = opendir(path); 437 if (dirp == NULL) 438 return 0; 439 440 while ((dp = readdir(dirp)) != NULL) { 441 /* Skip self and parent */ 442 if (strcmp(dp->d_name, ".") == 0 || 443 strcmp(dp->d_name, "..") == 0) 444 continue; 445 if (filter(re, dp->d_name) == 0) 446 continue; 447 *dents = xrealloc(*dents, (n + 1) * sizeof(**dents)); 448 strlcpy((*dents)[n].name, dp->d_name, sizeof((*dents)[n].name)); 449 /* Get mode flags */ 450 mkpath(path, dp->d_name, newpath, sizeof(newpath)); 451 r = lstat(newpath, &sb); 452 if (r == -1) 453 fatal("lstat"); 454 (*dents)[n].mode = sb.st_mode; 455 (*dents)[n].t = sb.st_mtime; 456 n++; 457 } 458 459 /* Should never be null */ 460 r = closedir(dirp); 461 if (r == -1) 462 fatal("closedir"); 463 return n; 464 } 465 466 void 467 dentfree(struct entry *dents) 468 { 469 free(dents); 470 } 471 472 /* Return the position of the matching entry or 0 otherwise */ 473 int 474 dentfind(struct entry *dents, int n, char *cwd, char *path) 475 { 476 char tmp[PATH_MAX]; 477 int i; 478 479 if (path == NULL) 480 return 0; 481 for (i = 0; i < n; i++) { 482 mkpath(cwd, dents[i].name, tmp, sizeof(tmp)); 483 DPRINTF_S(path); 484 DPRINTF_S(tmp); 485 if (strcmp(tmp, path) == 0) 486 return i; 487 } 488 return 0; 489 } 490 491 int 492 populate(char *path, char *oldpath, char *fltr) 493 { 494 regex_t re; 495 int r; 496 497 /* Can fail when permissions change while browsing */ 498 if (canopendir(path) == 0) 499 return -1; 500 501 /* Search filter */ 502 r = setfilter(&re, fltr); 503 if (r != 0) 504 return -1; 505 506 dentfree(dents); 507 508 ndents = 0; 509 dents = NULL; 510 511 ndents = dentfill(path, &dents, visible, &re); 512 freefilter(&re); 513 if (ndents == 0) 514 return 0; /* Empty result */ 515 516 qsort(dents, ndents, sizeof(*dents), entrycmp); 517 518 /* Find cur from history */ 519 cur = dentfind(dents, ndents, path, oldpath); 520 return 0; 521 } 522 523 void 524 redraw(char *path) 525 { 526 char cwd[PATH_MAX], cwdresolved[PATH_MAX]; 527 size_t ncols; 528 int nlines, odd; 529 int i; 530 531 nlines = MIN(LINES - 4, ndents); 532 533 /* Clean screen */ 534 erase(); 535 536 /* Strip trailing slashes */ 537 for (i = strlen(path) - 1; i > 0; i--) 538 if (path[i] == '/') 539 path[i] = '\0'; 540 else 541 break; 542 543 DPRINTF_D(cur); 544 DPRINTF_S(path); 545 546 /* No text wrapping in cwd line */ 547 ncols = COLS; 548 if (ncols > PATH_MAX) 549 ncols = PATH_MAX; 550 strlcpy(cwd, path, ncols); 551 cwd[ncols - strlen(CWD) - 1] = '\0'; 552 realpath(cwd, cwdresolved); 553 554 printw(CWD "%s\n\n", cwdresolved); 555 556 /* Print listing */ 557 odd = ISODD(nlines); 558 if (cur < nlines / 2) { 559 for (i = 0; i < nlines; i++) 560 printent(&dents[i], i == cur); 561 } else if (cur >= ndents - nlines / 2) { 562 for (i = ndents - nlines; i < ndents; i++) 563 printent(&dents[i], i == cur); 564 } else { 565 for (i = cur - nlines / 2; 566 i < cur + nlines / 2 + odd; i++) 567 printent(&dents[i], i == cur); 568 } 569 } 570 571 void 572 browse(char *ipath, char *ifilter) 573 { 574 char path[PATH_MAX], oldpath[PATH_MAX], newpath[PATH_MAX]; 575 char fltr[LINE_MAX]; 576 char *dir, *tmp, *run, *env; 577 struct stat sb; 578 regex_t re; 579 int r, fd; 580 581 strlcpy(path, ipath, sizeof(path)); 582 strlcpy(fltr, ifilter, sizeof(fltr)); 583 oldpath[0] = '\0'; 584 begin: 585 r = populate(path, oldpath, fltr); 586 if (r == -1) { 587 warn("populate"); 588 goto nochange; 589 } 590 591 for (;;) { 592 redraw(path); 593 nochange: 594 switch (nextsel(&run, &env)) { 595 case SEL_QUIT: 596 dentfree(dents); 597 return; 598 case SEL_BACK: 599 /* There is no going back */ 600 if (strcmp(path, "/") == 0 || 601 strcmp(path, ".") == 0 || 602 strchr(path, '/') == NULL) 603 goto nochange; 604 dir = xdirname(path); 605 if (canopendir(dir) == 0) { 606 warn("canopendir"); 607 goto nochange; 608 } 609 /* Save history */ 610 strlcpy(oldpath, path, sizeof(oldpath)); 611 strlcpy(path, dir, sizeof(path)); 612 /* Reset filter */ 613 strlcpy(fltr, ifilter, sizeof(fltr)); 614 goto begin; 615 case SEL_GOIN: 616 /* Cannot descend in empty directories */ 617 if (ndents == 0) 618 goto nochange; 619 620 mkpath(path, dents[cur].name, newpath, sizeof(newpath)); 621 DPRINTF_S(newpath); 622 623 /* Get path info */ 624 fd = open(newpath, O_RDONLY | O_NONBLOCK); 625 if (fd == -1) { 626 warn("open"); 627 goto nochange; 628 } 629 r = fstat(fd, &sb); 630 if (r == -1) { 631 warn("fstat"); 632 close(fd); 633 goto nochange; 634 } 635 close(fd); 636 DPRINTF_U(sb.st_mode); 637 638 switch (sb.st_mode & S_IFMT) { 639 case S_IFDIR: 640 if (canopendir(newpath) == 0) { 641 warn("canopendir"); 642 goto nochange; 643 } 644 strlcpy(path, newpath, sizeof(path)); 645 /* Reset filter */ 646 strlcpy(fltr, ifilter, sizeof(fltr)); 647 goto begin; 648 case S_IFREG: 649 exitcurses(); 650 run = xgetenv("NOPEN", NOPEN); 651 r = spawnlp(path, run, run, newpath, (void *)0); 652 initcurses(); 653 if (r == -1) { 654 info("Failed to execute plumber"); 655 goto nochange; 656 } 657 continue; 658 default: 659 info("Unsupported file"); 660 goto nochange; 661 } 662 case SEL_FLTR: 663 /* Read filter */ 664 printprompt("/"); 665 tmp = readln(); 666 if (tmp == NULL) 667 tmp = ifilter; 668 /* Check and report regex errors */ 669 r = setfilter(&re, tmp); 670 if (r != 0) 671 goto nochange; 672 freefilter(&re); 673 strlcpy(fltr, tmp, sizeof(fltr)); 674 DPRINTF_S(fltr); 675 /* Save current */ 676 if (ndents > 0) 677 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 678 goto begin; 679 case SEL_NEXT: 680 if (cur < ndents - 1) 681 cur++; 682 break; 683 case SEL_PREV: 684 if (cur > 0) 685 cur--; 686 break; 687 case SEL_PGDN: 688 if (cur < ndents - 1) 689 cur += MIN((LINES - 4) / 2, ndents - 1 - cur); 690 break; 691 case SEL_PGUP: 692 if (cur > 0) 693 cur -= MIN((LINES - 4) / 2, cur); 694 break; 695 case SEL_HOME: 696 cur = 0; 697 break; 698 case SEL_END: 699 cur = ndents - 1; 700 break; 701 case SEL_CD: 702 /* Read target dir */ 703 printprompt("chdir: "); 704 tmp = readln(); 705 if (tmp == NULL) { 706 clearprompt(); 707 goto nochange; 708 } 709 mkpath(path, tmp, newpath, sizeof(newpath)); 710 if (canopendir(newpath) == 0) { 711 warn("canopendir"); 712 goto nochange; 713 } 714 strlcpy(path, newpath, sizeof(path)); 715 /* Reset filter */ 716 strlcpy(fltr, ifilter, sizeof(fltr)); 717 DPRINTF_S(path); 718 goto begin; 719 case SEL_CDHOME: 720 tmp = getenv("HOME"); 721 if (tmp == NULL) { 722 clearprompt(); 723 goto nochange; 724 } 725 if (canopendir(tmp) == 0) { 726 warn("canopendir"); 727 goto nochange; 728 } 729 strlcpy(path, tmp, sizeof(path)); 730 /* Reset filter */ 731 strlcpy(fltr, ifilter, sizeof(fltr)); 732 DPRINTF_S(path); 733 goto begin; 734 case SEL_TOGGLEDOT: 735 showhidden ^= 1; 736 initfilter(showhidden, &ifilter); 737 strlcpy(fltr, ifilter, sizeof(fltr)); 738 goto begin; 739 case SEL_MTIME: 740 mtimeorder = !mtimeorder; 741 /* Save current */ 742 if (ndents > 0) 743 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 744 goto begin; 745 case SEL_DSORT: 746 dirorder = !dirorder; 747 /* Save current */ 748 if (ndents > 0) 749 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 750 goto begin; 751 case SEL_ICASE: 752 icaseorder = !icaseorder; 753 /* Save current */ 754 if (ndents > 0) 755 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 756 goto begin; 757 case SEL_VERS: 758 versorder = !versorder; 759 /* Save current */ 760 if (ndents > 0) 761 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 762 goto begin; 763 case SEL_REDRAW: 764 /* Save current */ 765 if (ndents > 0) 766 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 767 goto begin; 768 case SEL_RUN: 769 /* Save current */ 770 if (ndents > 0) 771 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 772 run = xgetenv(env, run); 773 exitcurses(); 774 spawnlp(path, run, run, (void *)0); 775 initcurses(); 776 goto begin; 777 case SEL_RUNARG: 778 /* Save current */ 779 if (ndents > 0) 780 mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); 781 run = xgetenv(env, run); 782 exitcurses(); 783 spawnlp(path, run, run, dents[cur].name, (void *)0); 784 initcurses(); 785 goto begin; 786 } 787 /* Screensaver */ 788 if (idletimeout != 0 && idle == idletimeout) { 789 idle = 0; 790 exitcurses(); 791 spawnlp(NULL, idlecmd, idlecmd, (void *)0); 792 initcurses(); 793 } 794 } 795 } 796 797 void 798 usage(void) 799 { 800 fprintf(stderr, "usage: %s [-c] [dir]\n", argv0); 801 exit(1); 802 } 803 804 int 805 main(int argc, char *argv[]) 806 { 807 char cwd[PATH_MAX], *ipath; 808 char *ifilter; 809 810 ARGBEGIN { 811 case 'c': 812 usecolor = 1; 813 break; 814 default: 815 usage(); 816 } ARGEND 817 818 if (argc > 1) 819 usage(); 820 821 /* Confirm we are in a terminal */ 822 if (!isatty(0) || !isatty(1)) { 823 fprintf(stderr, "stdin or stdout is not a tty\n"); 824 exit(1); 825 } 826 827 if (getuid() == 0) 828 showhidden = 1; 829 initfilter(showhidden, &ifilter); 830 831 if (argv[0] != NULL) { 832 ipath = argv[0]; 833 } else { 834 ipath = getcwd(cwd, sizeof(cwd)); 835 if (ipath == NULL) 836 ipath = "/"; 837 } 838 839 signal(SIGINT, SIG_IGN); 840 841 /* Test initial path */ 842 if (canopendir(ipath) == 0) { 843 fprintf(stderr, "%s: %s\n", ipath, strerror(errno)); 844 exit(1); 845 } 846 847 /* Set locale before curses setup */ 848 setlocale(LC_ALL, ""); 849 initcurses(); 850 browse(ipath, ifilter); 851 exitcurses(); 852 exit(0); 853 }