My build of nnn with minor changes
 
 
 
 
 
 

865 lines
14 KiB

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