My build of nnn with minor changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

843 lines
15 KiB

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