My build of nnn with minor changes
 
 
 
 
 
 

881 行
15 KiB

  1. #include <sys/stat.h>
  2. #include <sys/types.h>
  3. #include <sys/wait.h>
  4. #include <curses.h>
  5. #include <dirent.h>
  6. #include <errno.h>
  7. #include <fcntl.h>
  8. #include <libgen.h>
  9. #include <limits.h>
  10. #include <locale.h>
  11. #include <regex.h>
  12. #include <signal.h>
  13. #include <stdarg.h>
  14. #include <stdio.h>
  15. #include <stdlib.h>
  16. #include <string.h>
  17. #include <unistd.h>
  18. #include "util.h"
  19. #ifdef DEBUG
  20. #define DEBUG_FD 8
  21. #define DPRINTF_D(x) dprintf(DEBUG_FD, #x "=%d\n", x)
  22. #define DPRINTF_U(x) dprintf(DEBUG_FD, #x "=%u\n", x)
  23. #define DPRINTF_S(x) dprintf(DEBUG_FD, #x "=%s\n", x)
  24. #define DPRINTF_P(x) dprintf(DEBUG_FD, #x "=0x%p\n", x)
  25. #else
  26. #define DPRINTF_D(x)
  27. #define DPRINTF_U(x)
  28. #define DPRINTF_S(x)
  29. #define DPRINTF_P(x)
  30. #endif /* DEBUG */
  31. #define LEN(x) (sizeof(x) / sizeof(*(x)))
  32. #undef MIN
  33. #define MIN(x, y) ((x) < (y) ? (x) : (y))
  34. #define ISODD(x) ((x) & 1)
  35. #define CONTROL(c) ((c) ^ 0x40)
  36. struct assoc {
  37. char *regex; /* Regex to match on filename */
  38. char *bin; /* Program */
  39. };
  40. /* Supported actions */
  41. enum action {
  42. SEL_QUIT = 1,
  43. SEL_BACK,
  44. SEL_GOIN,
  45. SEL_FLTR,
  46. SEL_TYPE,
  47. SEL_NEXT,
  48. SEL_PREV,
  49. SEL_PGDN,
  50. SEL_PGUP,
  51. SEL_CD,
  52. SEL_MTIME,
  53. SEL_REDRAW,
  54. SEL_RUN,
  55. SEL_RUNARG,
  56. };
  57. struct key {
  58. int sym; /* Key pressed */
  59. enum action act; /* Action */
  60. char *run; /* Program to run */
  61. };
  62. #include "config.h"
  63. struct entry {
  64. char *name;
  65. mode_t mode;
  66. time_t t;
  67. };
  68. /*
  69. * Layout:
  70. * .---------
  71. * | cwd: /mnt/path
  72. * |
  73. * | file0
  74. * | file1
  75. * | > file2
  76. * | file3
  77. * | file4
  78. * ...
  79. * | filen
  80. * |
  81. * | Permission denied
  82. * '------
  83. */
  84. void printmsg(char *msg);
  85. void printwarn(void);
  86. void printerr(int ret, char *prefix);
  87. char *makepath(char *dir, char *name);
  88. #undef dprintf
  89. int
  90. dprintf(int fd, const char *fmt, ...)
  91. {
  92. char buf[BUFSIZ];
  93. int r;
  94. va_list ap;
  95. va_start(ap, fmt);
  96. r = vsnprintf(buf, sizeof(buf), fmt, ap);
  97. if (r > 0)
  98. write(fd, buf, r);
  99. va_end(ap);
  100. return r;
  101. }
  102. void *
  103. xmalloc(size_t size)
  104. {
  105. void *p;
  106. p = malloc(size);
  107. if (p == NULL)
  108. printerr(1, "malloc");
  109. return p;
  110. }
  111. void *
  112. xrealloc(void *p, size_t size)
  113. {
  114. p = realloc(p, size);
  115. if (p == NULL)
  116. printerr(1, "realloc");
  117. return p;
  118. }
  119. char *
  120. xstrdup(const char *s)
  121. {
  122. char *p;
  123. p = strdup(s);
  124. if (p == NULL)
  125. printerr(1, "strdup");
  126. return p;
  127. }
  128. char *
  129. xdirname(const char *path)
  130. {
  131. char *p, *tmp;
  132. /* Some implementations of dirname(3) may modify `path' and some
  133. * return a pointer inside `path' and we cannot free(3) the
  134. * original string if we lose track of it. */
  135. tmp = xstrdup(path);
  136. p = dirname(tmp);
  137. if (p == NULL) {
  138. free(tmp);
  139. printerr(1, "dirname");
  140. }
  141. /* Make sure this is a malloc(3)-ed string */
  142. p = xstrdup(p);
  143. free(tmp);
  144. return p;
  145. }
  146. void
  147. spawn(const char *file, const char *arg, const char *dir)
  148. {
  149. pid_t pid;
  150. int status;
  151. pid = fork();
  152. if (pid == 0) {
  153. if (dir != NULL)
  154. chdir(dir);
  155. execlp(file, file, arg, NULL);
  156. _exit(1);
  157. } else {
  158. /* Ignore interruptions */
  159. while (waitpid(pid, &status, 0) == -1)
  160. DPRINTF_D(status);
  161. DPRINTF_D(pid);
  162. }
  163. }
  164. char *
  165. openwith(char *file)
  166. {
  167. regex_t regex;
  168. char *bin = NULL;
  169. int i;
  170. for (i = 0; i < LEN(assocs); i++) {
  171. if (regcomp(&regex, assocs[i].regex,
  172. REG_NOSUB | REG_EXTENDED | REG_ICASE) != 0)
  173. continue;
  174. if (regexec(&regex, file, 0, NULL, 0) == 0) {
  175. bin = assocs[i].bin;
  176. break;
  177. }
  178. }
  179. DPRINTF_S(bin);
  180. return bin;
  181. }
  182. int
  183. setfilter(regex_t *regex, char *filter)
  184. {
  185. char *errbuf;
  186. int r;
  187. r = regcomp(regex, filter, REG_NOSUB | REG_EXTENDED | REG_ICASE);
  188. if (r != 0) {
  189. errbuf = xmalloc(COLS * sizeof(char));
  190. regerror(r, regex, errbuf, COLS * sizeof(char));
  191. printmsg(errbuf);
  192. free(errbuf);
  193. }
  194. return r;
  195. }
  196. int
  197. visible(regex_t *regex, char *file)
  198. {
  199. return regexec(regex, file, 0, NULL, 0) == 0;
  200. }
  201. int
  202. entrycmp(const void *va, const void *vb)
  203. {
  204. const struct entry *a, *b;
  205. a = (struct entry *)va;
  206. b = (struct entry *)vb;
  207. if (mtimeorder)
  208. return b->t - a->t;
  209. return strcmp(a->name, b->name);
  210. }
  211. void
  212. initcurses(void)
  213. {
  214. initscr();
  215. cbreak();
  216. noecho();
  217. nonl();
  218. intrflush(stdscr, FALSE);
  219. keypad(stdscr, TRUE);
  220. curs_set(FALSE); /* Hide cursor */
  221. }
  222. void
  223. exitcurses(void)
  224. {
  225. endwin(); /* Restore terminal */
  226. }
  227. /* Messages show up at the bottom */
  228. void
  229. printmsg(char *msg)
  230. {
  231. move(LINES - 1, 0);
  232. printw("%s\n", msg);
  233. }
  234. /* Display warning as a message */
  235. void
  236. printwarn(void)
  237. {
  238. printmsg(strerror(errno));
  239. }
  240. /* Kill curses and display error before exiting */
  241. void
  242. printerr(int ret, char *prefix)
  243. {
  244. exitcurses();
  245. fprintf(stderr, "%s: %s\n", prefix, strerror(errno));
  246. exit(ret);
  247. }
  248. /* Clear the last line */
  249. void
  250. clearprompt(void)
  251. {
  252. printmsg("");
  253. }
  254. /* Print prompt on the last line */
  255. void
  256. printprompt(char *str)
  257. {
  258. clearprompt();
  259. printw(str);
  260. }
  261. /* Returns SEL_* if key is bound and 0 otherwise
  262. Also modifies the run pointer (used on SEL_{RUN,RUNARG}) */
  263. int
  264. nextsel(char **run)
  265. {
  266. int c, i;
  267. c = getch();
  268. for (i = 0; i < LEN(bindings); i++)
  269. if (c == bindings[i].sym) {
  270. *run = bindings[i].run;
  271. return bindings[i].act;
  272. }
  273. return 0;
  274. }
  275. char *
  276. readln(void)
  277. {
  278. int c;
  279. int i = 0;
  280. char *ln = NULL;
  281. int y, x, x0;
  282. echo();
  283. curs_set(TRUE);
  284. /* Starting point */
  285. getyx(stdscr, y, x);
  286. x0 = x;
  287. while ((c = getch()) != ERR) {
  288. if (c == KEY_ENTER || c == '\r')
  289. break;
  290. if (c == KEY_BACKSPACE || c == CONTROL('H')) {
  291. getyx(stdscr, y, x);
  292. if (x >= x0) {
  293. i--;
  294. if (i > 0) {
  295. ln = xrealloc(ln, i * sizeof(*ln));
  296. } else {
  297. free(ln);
  298. ln = NULL;
  299. }
  300. move(y, x);
  301. printw("%c", ' ');
  302. move(y, x);
  303. } else {
  304. move(y, x0);
  305. }
  306. continue;
  307. }
  308. ln = xrealloc(ln, (i + 1) * sizeof(*ln));
  309. ln[i] = c;
  310. i++;
  311. }
  312. if (ln != NULL) {
  313. ln = xrealloc(ln, (i + 1) * sizeof(*ln));
  314. ln[i] = '\0';
  315. }
  316. curs_set(FALSE);
  317. noecho();
  318. return ln;
  319. }
  320. /*
  321. * Read one key and modify the provided string accordingly.
  322. * Returns 0 when more input is expected and 1 on completion.
  323. */
  324. int
  325. readmore(char **str)
  326. {
  327. int c, ret = 0;
  328. int i;
  329. char *ln = *str;
  330. if (ln != NULL)
  331. i = strlen(ln);
  332. else
  333. i = 0;
  334. DPRINTF_D(i);
  335. curs_set(TRUE);
  336. c = getch();
  337. switch (c) {
  338. case KEY_ENTER:
  339. case '\r':
  340. ret = 1;
  341. break;
  342. case KEY_BACKSPACE:
  343. case CONTROL('H'):
  344. i--;
  345. if (i > 0) {
  346. ln = xrealloc(ln, (i + 1) * sizeof(*ln));
  347. ln[i] = '\0';
  348. } else {
  349. free(ln);
  350. ln = NULL;
  351. }
  352. break;
  353. default:
  354. i++;
  355. ln = xrealloc(ln, (i + 1) * sizeof(*ln));
  356. ln[i - 1] = c;
  357. ln[i] = '\0';
  358. }
  359. curs_set(FALSE);
  360. *str = ln;
  361. return ret;
  362. }
  363. int
  364. canopendir(char *path)
  365. {
  366. DIR *dirp;
  367. dirp = opendir(path);
  368. if (dirp == NULL)
  369. return 0;
  370. closedir(dirp);
  371. return 1;
  372. }
  373. void
  374. printent(struct entry *ent, int active)
  375. {
  376. char *name;
  377. unsigned int maxlen = COLS - strlen(CURSR) - 1;
  378. char cm = 0;
  379. /* Copy name locally */
  380. name = xstrdup(ent->name);
  381. if (S_ISDIR(ent->mode)) {
  382. cm = '/';
  383. maxlen--;
  384. } else if (S_ISLNK(ent->mode)) {
  385. cm = '@';
  386. maxlen--;
  387. } else if (S_ISSOCK(ent->mode)) {
  388. cm = '=';
  389. maxlen--;
  390. } else if (S_ISFIFO(ent->mode)) {
  391. cm = '|';
  392. maxlen--;
  393. } else if (ent->mode & S_IXUSR) {
  394. cm = '*';
  395. maxlen--;
  396. }
  397. /* No text wrapping in entries */
  398. if (strlen(name) > maxlen)
  399. name[maxlen] = '\0';
  400. if (cm == 0)
  401. printw("%s%s\n", active ? CURSR : EMPTY, name);
  402. else
  403. printw("%s%s%c\n", active ? CURSR : EMPTY, name, cm);
  404. free(name);
  405. }
  406. int
  407. dentfill(char *path, struct entry **dents,
  408. int (*filter)(regex_t *, char *), regex_t *re)
  409. {
  410. DIR *dirp;
  411. struct dirent *dp;
  412. struct stat sb;
  413. char *newpath;
  414. int r, n = 0;
  415. dirp = opendir(path);
  416. if (dirp == NULL)
  417. return 0;
  418. while ((dp = readdir(dirp)) != NULL) {
  419. /* Skip self and parent */
  420. if (strcmp(dp->d_name, ".") == 0
  421. || strcmp(dp->d_name, "..") == 0)
  422. continue;
  423. if (filter(re, dp->d_name) == 0)
  424. continue;
  425. *dents = xrealloc(*dents, (n + 1) * sizeof(**dents));
  426. (*dents)[n].name = xstrdup(dp->d_name);
  427. /* Get mode flags */
  428. newpath = makepath(path, dp->d_name);
  429. r = lstat(newpath, &sb);
  430. if (r == -1)
  431. printerr(1, "lstat");
  432. (*dents)[n].mode = sb.st_mode;
  433. (*dents)[n].t = sb.st_mtime;
  434. n++;
  435. }
  436. /* Should never be null */
  437. r = closedir(dirp);
  438. if (r == -1)
  439. printerr(1, "closedir");
  440. return n;
  441. }
  442. void
  443. dentfree(struct entry *dents, int n)
  444. {
  445. int i;
  446. for (i = 0; i < n; i++)
  447. free(dents[i].name);
  448. free(dents);
  449. }
  450. char *
  451. makepath(char *dir, char *name)
  452. {
  453. char path[PATH_MAX];
  454. /* Handle absolute path */
  455. if (name[0] == '/') {
  456. strlcpy(path, name, sizeof(path));
  457. } else {
  458. /* Handle root case */
  459. if (strcmp(dir, "/") == 0) {
  460. strlcpy(path, "/", sizeof(path));
  461. strlcat(path, name, sizeof(path));
  462. } else {
  463. strlcpy(path, dir, sizeof(path));
  464. strlcat(path, "/", sizeof(path));
  465. strlcat(path, name, sizeof(path));
  466. }
  467. }
  468. return xstrdup(path);
  469. }
  470. /* Return the position of the matching entry or 0 otherwise */
  471. int
  472. dentfind(struct entry *dents, int n, char *cwd, char *path)
  473. {
  474. int i;
  475. char *tmp;
  476. if (path == NULL)
  477. return 0;
  478. for (i = 0; i < n; i++) {
  479. tmp = makepath(cwd, dents[i].name);
  480. DPRINTF_S(path);
  481. DPRINTF_S(tmp);
  482. if (strcmp(tmp, path) == 0) {
  483. free(tmp);
  484. return i;
  485. }
  486. free(tmp);
  487. }
  488. return 0;
  489. }
  490. void
  491. browse(const char *ipath, const char *ifilter)
  492. {
  493. struct entry *dents;
  494. int i, n, cur, r, fd;
  495. int nlines, odd;
  496. char *path = xstrdup(ipath);
  497. char *filter = xstrdup(ifilter);
  498. regex_t filter_re, re;
  499. char *cwd, *newpath, *oldpath = NULL;
  500. struct stat sb;
  501. char *name, *bin, *dir, *tmp, *run;
  502. int nowtyping = 0;
  503. begin:
  504. /* Path and filter should be malloc(3)-ed strings at all times */
  505. n = 0;
  506. dents = NULL;
  507. if (canopendir(path) == 0) {
  508. printwarn();
  509. goto nochange;
  510. }
  511. /* Search filter */
  512. r = setfilter(&filter_re, filter);
  513. if (r != 0)
  514. goto nochange;
  515. n = dentfill(path, &dents, visible, &filter_re);
  516. qsort(dents, n, sizeof(*dents), entrycmp);
  517. /* Find cur from history */
  518. cur = dentfind(dents, n, path, oldpath);
  519. free(oldpath);
  520. oldpath = NULL;
  521. for (;;) {
  522. nlines = MIN(LINES - 4, n);
  523. /* Clean screen */
  524. erase();
  525. /* Strip trailing slashes */
  526. for (i = strlen(path) - 1; i > 0; i--)
  527. if (path[i] == '/')
  528. path[i] = '\0';
  529. else
  530. break;
  531. DPRINTF_D(cur);
  532. DPRINTF_S(path);
  533. /* No text wrapping in cwd line */
  534. cwd = xmalloc(COLS * sizeof(char));
  535. strlcpy(cwd, path, COLS * sizeof(char));
  536. cwd[COLS - strlen(CWD) - 1] = '\0';
  537. printw(CWD "%s\n\n", cwd);
  538. /* Print listing */
  539. odd = ISODD(nlines);
  540. if (cur < nlines / 2) {
  541. for (i = 0; i < nlines; i++)
  542. printent(&dents[i], i == cur);
  543. } else if (cur >= n - nlines / 2) {
  544. for (i = n - nlines; i < n; i++)
  545. printent(&dents[i], i == cur);
  546. } else {
  547. for (i = cur - nlines / 2;
  548. i < cur + nlines / 2 + odd; i++)
  549. printent(&dents[i], i == cur);
  550. }
  551. /* Handle filter-as-you-type mode */
  552. if (nowtyping)
  553. goto moretyping;
  554. nochange:
  555. switch (nextsel(&run)) {
  556. case SEL_QUIT:
  557. free(path);
  558. free(filter);
  559. dentfree(dents, n);
  560. return;
  561. case SEL_BACK:
  562. /* There is no going back */
  563. if (strcmp(path, "/") == 0 ||
  564. strcmp(path, ".") == 0 ||
  565. strchr(path, '/') == NULL)
  566. goto nochange;
  567. if (canopendir(path) == 0) {
  568. printwarn();
  569. goto nochange;
  570. }
  571. dir = xdirname(path);
  572. /* Save history */
  573. oldpath = path;
  574. path = dir;
  575. /* Reset filter */
  576. free(filter);
  577. filter = xstrdup(ifilter);
  578. goto out;
  579. case SEL_GOIN:
  580. /* Cannot descend in empty directories */
  581. if (n == 0)
  582. goto nochange;
  583. name = dents[cur].name;
  584. newpath = makepath(path, name);
  585. DPRINTF_S(newpath);
  586. /* Get path info */
  587. fd = open(newpath, O_RDONLY | O_NONBLOCK);
  588. if (fd == -1) {
  589. printwarn();
  590. free(newpath);
  591. goto nochange;
  592. }
  593. r = fstat(fd, &sb);
  594. if (r == -1) {
  595. printwarn();
  596. close(fd);
  597. free(newpath);
  598. goto nochange;
  599. }
  600. close(fd);
  601. DPRINTF_U(sb.st_mode);
  602. switch (sb.st_mode & S_IFMT) {
  603. case S_IFDIR:
  604. if (canopendir(newpath) == 0) {
  605. printwarn();
  606. free(newpath);
  607. goto nochange;
  608. }
  609. free(path);
  610. path = newpath;
  611. /* Reset filter */
  612. free(filter);
  613. filter = xstrdup(ifilter);
  614. goto out;
  615. case S_IFREG:
  616. bin = openwith(newpath);
  617. if (bin == NULL) {
  618. printmsg("No association");
  619. free(newpath);
  620. goto nochange;
  621. }
  622. exitcurses();
  623. spawn(bin, newpath, NULL);
  624. initcurses();
  625. free(newpath);
  626. continue;
  627. default:
  628. printmsg("Unsupported file");
  629. goto nochange;
  630. }
  631. case SEL_FLTR:
  632. /* Read filter */
  633. printprompt("filter: ");
  634. tmp = readln();
  635. if (tmp == NULL)
  636. tmp = xstrdup(ifilter);
  637. /* Check and report regex errors */
  638. r = setfilter(&re, tmp);
  639. if (r != 0) {
  640. free(tmp);
  641. goto nochange;
  642. }
  643. free(filter);
  644. filter = tmp;
  645. DPRINTF_S(filter);
  646. /* Save current */
  647. if (n > 0)
  648. oldpath = makepath(path, dents[cur].name);
  649. goto out;
  650. case SEL_TYPE:
  651. nowtyping = 1;
  652. tmp = NULL;
  653. moretyping:
  654. printprompt("type: ");
  655. if (tmp != NULL)
  656. printw("%s", tmp);
  657. r = readmore(&tmp);
  658. DPRINTF_D(r);
  659. DPRINTF_S(tmp);
  660. if (r == 1)
  661. nowtyping = 0;
  662. /* Check regex errors */
  663. if (tmp != NULL) {
  664. r = setfilter(&re, tmp);
  665. if (r != 0)
  666. if (nowtyping) {
  667. goto moretyping;
  668. } else {
  669. free(tmp);
  670. goto nochange;
  671. }
  672. }
  673. /* Copy or reset filter */
  674. free(filter);
  675. if (tmp != NULL)
  676. filter = xstrdup(tmp);
  677. else
  678. filter = xstrdup(ifilter);
  679. /* Save current */
  680. if (n > 0)
  681. oldpath = makepath(path, dents[cur].name);
  682. if (!nowtyping)
  683. free(tmp);
  684. goto out;
  685. case SEL_NEXT:
  686. if (cur < n - 1)
  687. cur++;
  688. break;
  689. case SEL_PREV:
  690. if (cur > 0)
  691. cur--;
  692. break;
  693. case SEL_PGDN:
  694. if (cur < n - 1)
  695. cur += MIN((LINES - 4) / 2, n - 1 - cur);
  696. break;
  697. case SEL_PGUP:
  698. if (cur > 0)
  699. cur -= MIN((LINES - 4) / 2, cur);
  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. newpath = makepath(path, tmp);
  710. free(tmp);
  711. if (canopendir(newpath) == 0) {
  712. free(newpath);
  713. printwarn();
  714. goto nochange;
  715. }
  716. free(path);
  717. path = newpath;
  718. free(filter);
  719. filter = xstrdup(ifilter); /* Reset filter */
  720. DPRINTF_S(path);
  721. goto out;
  722. case SEL_MTIME:
  723. mtimeorder = !mtimeorder;
  724. goto out;
  725. case SEL_REDRAW:
  726. goto out;
  727. case SEL_RUN:
  728. exitcurses();
  729. spawn(run, NULL, path);
  730. initcurses();
  731. break;
  732. case SEL_RUNARG:
  733. name = dents[cur].name;
  734. exitcurses();
  735. spawn(run, name, path);
  736. initcurses();
  737. break;
  738. }
  739. }
  740. out:
  741. dentfree(dents, n);
  742. goto begin;
  743. }
  744. int
  745. main(int argc, char *argv[])
  746. {
  747. char cwd[PATH_MAX], *ipath;
  748. char *ifilter;
  749. /* Confirm we are in a terminal */
  750. if (!isatty(STDIN_FILENO))
  751. printerr(1, "isatty");
  752. if (getuid() == 0)
  753. ifilter = ".";
  754. else
  755. ifilter = "^[^.]"; /* Hide dotfiles */
  756. if (argv[1] != NULL) {
  757. ipath = argv[1];
  758. } else {
  759. ipath = getcwd(cwd, sizeof(cwd));
  760. if (ipath == NULL)
  761. ipath = "/";
  762. }
  763. /* Test initial path */
  764. if (canopendir(ipath) == 0)
  765. printerr(1, ipath);
  766. /* Set locale before curses setup */
  767. setlocale(LC_ALL, "");
  768. initcurses();
  769. browse(ipath, ifilter);
  770. exitcurses();
  771. return 0;
  772. }