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.
 
 
 
 
 
 

637 lines
11 KiB

  1. #include <sys/stat.h>
  2. #include <sys/types.h>
  3. #include <errno.h>
  4. #include <fcntl.h>
  5. #include <dirent.h>
  6. #include <curses.h>
  7. #include <libgen.h>
  8. #include <limits.h>
  9. #include <locale.h>
  10. #include <regex.h>
  11. #include <stdlib.h>
  12. #include <stdio.h>
  13. #include <signal.h>
  14. #include <string.h>
  15. #include <unistd.h>
  16. #ifdef LINUX
  17. #include <bsd/string.h>
  18. #endif
  19. #include "queue.h"
  20. #ifdef DEBUG
  21. #define DEBUG_FD 8
  22. #define DPRINTF_D(x) dprintf(DEBUG_FD, #x "=%d\n", x)
  23. #define DPRINTF_U(x) dprintf(DEBUG_FD, #x "=%u\n", x)
  24. #define DPRINTF_S(x) dprintf(DEBUG_FD, #x "=%s\n", x)
  25. #define DPRINTF_P(x) dprintf(DEBUG_FD, #x "=0x%p\n", x)
  26. #else
  27. #define DPRINTF_D(x)
  28. #define DPRINTF_U(x)
  29. #define DPRINTF_S(x)
  30. #define DPRINTF_P(x)
  31. #endif /* DEBUG */
  32. #define LEN(x) (sizeof(x) / sizeof(*(x)))
  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. #include "config.h"
  41. struct entry {
  42. char *name;
  43. mode_t mode;
  44. };
  45. struct history {
  46. int pos;
  47. SLIST_ENTRY(history) entry;
  48. };
  49. SLIST_HEAD(histhead, history) histhead = SLIST_HEAD_INITIALIZER(histhead);
  50. /*
  51. * Layout:
  52. * .---------
  53. * | cwd: /mnt/path
  54. * |
  55. * | file0
  56. * | file1
  57. * | > file2
  58. * | file3
  59. * | file4
  60. * ...
  61. * | filen
  62. * |
  63. * | Permission denied
  64. * '------
  65. */
  66. void printmsg(char *msg);
  67. void printwarn(void);
  68. void printerr(int ret, char *prefix);
  69. char *
  70. openwith(char *file)
  71. {
  72. regex_t regex;
  73. char *bin = NULL;
  74. int i;
  75. for (i = 0; i < LEN(assocs); i++) {
  76. if (regcomp(&regex, assocs[i].regex,
  77. REG_NOSUB | REG_EXTENDED) != 0)
  78. continue;
  79. if (regexec(&regex, file, 0, NULL, 0) != REG_NOMATCH) {
  80. bin = assocs[i].bin;
  81. break;
  82. }
  83. }
  84. DPRINTF_S(bin);
  85. return bin;
  86. }
  87. int
  88. setfilter(regex_t *regex, char *filter)
  89. {
  90. char *errbuf;
  91. int r;
  92. r = regcomp(regex, filter, REG_NOSUB | REG_EXTENDED);
  93. if (r != 0) {
  94. errbuf = malloc(COLS * sizeof(char));
  95. regerror(r, regex, errbuf, COLS * sizeof(char));
  96. printmsg(errbuf);
  97. free(errbuf);
  98. }
  99. return r;
  100. }
  101. int
  102. visible(regex_t *regex, char *file)
  103. {
  104. if (regexec(regex, file, 0, NULL, 0) != REG_NOMATCH)
  105. return 1;
  106. return 0;
  107. }
  108. int
  109. entrycmp(const void *va, const void *vb)
  110. {
  111. const struct entry *a, *b;
  112. a = (struct entry *)va;
  113. b = (struct entry *)vb;
  114. return strcmp(a->name, b->name);
  115. }
  116. void
  117. initcurses(void)
  118. {
  119. initscr();
  120. cbreak();
  121. noecho();
  122. nonl();
  123. intrflush(stdscr, FALSE);
  124. keypad(stdscr, TRUE);
  125. curs_set(FALSE); /* Hide cursor */
  126. }
  127. void
  128. exitcurses(void)
  129. {
  130. endwin(); /* Restore terminal */
  131. }
  132. /* Messages show up at the bottom */
  133. void
  134. printmsg(char *msg)
  135. {
  136. move(LINES - 1, 0);
  137. printw("%s\n", msg);
  138. }
  139. /* Display warning as a message */
  140. void
  141. printwarn(void)
  142. {
  143. printmsg(strerror(errno));
  144. }
  145. /* Kill curses and display error before exiting */
  146. void
  147. printerr(int ret, char *prefix)
  148. {
  149. exitcurses();
  150. printf("%s: %s\n", prefix, strerror(errno));
  151. exit(ret);
  152. }
  153. /*
  154. * Returns 0 normally
  155. * On movement it updates *cur
  156. * Returns SEL_{QUIT,BACK,GOIN,FLTR} otherwise
  157. */
  158. #define SEL_QUIT 1
  159. #define SEL_BACK 2
  160. #define SEL_GOIN 3
  161. #define SEL_FLTR 4
  162. int
  163. nextsel(int *cur, int max)
  164. {
  165. int c;
  166. c = getch();
  167. switch (c) {
  168. case 'q':
  169. return SEL_QUIT;
  170. /* Back */
  171. case KEY_BACKSPACE:
  172. case KEY_LEFT:
  173. case 'h':
  174. return SEL_BACK;
  175. /* Inside */
  176. case KEY_ENTER:
  177. case '\r':
  178. case KEY_RIGHT:
  179. case 'l':
  180. return SEL_GOIN;
  181. /* Filter */
  182. case '/':
  183. case '&':
  184. return SEL_FLTR;
  185. /* Next */
  186. case 'j':
  187. case KEY_DOWN:
  188. case CONTROL('N'):
  189. if (*cur < max - 1)
  190. (*cur)++;
  191. break;
  192. /* Previous */
  193. case 'k':
  194. case KEY_UP:
  195. case CONTROL('P'):
  196. if (*cur > 0)
  197. (*cur)--;
  198. break;
  199. /* Page down */
  200. case KEY_NPAGE:
  201. case CONTROL('D'):
  202. if (*cur < max -1)
  203. (*cur) += MIN((LINES - 4) / 2, max - 1 - *cur);
  204. break;
  205. /* Page up */
  206. case KEY_PPAGE:
  207. case CONTROL('U'):
  208. if (*cur > 0)
  209. (*cur) -= MIN((LINES - 4) / 2, *cur);
  210. break;
  211. }
  212. return 0;
  213. }
  214. char *
  215. readln(void)
  216. {
  217. int c;
  218. int i = 0;
  219. char *ln = NULL;
  220. int y, x, x0;
  221. echo();
  222. curs_set(TRUE);
  223. /* Starting point */
  224. getyx(stdscr, y, x);
  225. x0 = x;
  226. while (c = getch()) {
  227. if (c == KEY_ENTER || c == '\r')
  228. break;
  229. if (c == KEY_BACKSPACE) {
  230. getyx(stdscr, y, x);
  231. if (x >= x0) {
  232. ln = realloc(ln, (i - 1) * sizeof(*ln));
  233. i--;
  234. move(y, x);
  235. printw("%c", ' ');
  236. move(y, x);
  237. } else {
  238. move(y, x0);
  239. }
  240. continue;
  241. }
  242. ln = realloc(ln, (i + 1) * sizeof(*ln));
  243. ln[i] = c;
  244. i++;
  245. }
  246. if (ln != NULL) {
  247. ln = realloc(ln, (i + 1) * sizeof(*ln));
  248. ln[i] = '\0';
  249. }
  250. curs_set(FALSE);
  251. noecho();
  252. return ln;
  253. }
  254. int
  255. testopendir(char *path)
  256. {
  257. DIR *dirp;
  258. dirp = opendir(path);
  259. if (dirp == NULL) {
  260. return 0;
  261. } else {
  262. closedir(dirp);
  263. return 1;
  264. }
  265. }
  266. void
  267. printent(struct entry *ent, int active)
  268. {
  269. char *name;
  270. unsigned int maxlen = COLS - strlen(CURSR) - 1;
  271. char cm = 0;
  272. /* Copy name locally */
  273. name = strdup(ent->name);
  274. if (name == NULL)
  275. printerr(1, "strdup name");
  276. if (S_ISDIR(ent->mode)) {
  277. cm = '/';
  278. maxlen--;
  279. } else if (S_ISLNK(ent->mode)) {
  280. cm = '@';
  281. maxlen--;
  282. }
  283. /* No text wrapping in entries */
  284. if (strlen(name) > maxlen)
  285. name[maxlen] = '\0';
  286. if (cm == 0)
  287. printw("%s%s\n", active ? CURSR : EMPTY, name);
  288. else
  289. printw("%s%s%c\n", active ? CURSR : EMPTY, name, cm);
  290. free(name);
  291. }
  292. void
  293. browse(const char *ipath, const char *ifilter)
  294. {
  295. DIR *dirp;
  296. int dfd;
  297. struct dirent *dp;
  298. struct entry *dents;
  299. int i, n, cur;
  300. int r, ret;
  301. char *path = strdup(ipath);
  302. char *filter = strdup(ifilter);
  303. regex_t filter_re;
  304. char *cwd;
  305. struct stat sb;
  306. cur = 0;
  307. begin:
  308. /* Path and filter should be malloc(3)-ed strings at all times */
  309. n = 0;
  310. dents = NULL;
  311. dirp = opendir(path);
  312. if (dirp == NULL) {
  313. printwarn();
  314. goto nochange;
  315. }
  316. /* Search filter */
  317. r = setfilter(&filter_re, filter);
  318. if (r != 0)
  319. goto nochange;
  320. while ((dp = readdir(dirp)) != NULL) {
  321. char *name;
  322. /* Skip self and parent */
  323. if (strcmp(dp->d_name, ".") == 0
  324. || strcmp(dp->d_name, "..") == 0)
  325. continue;
  326. if (!visible(&filter_re, dp->d_name))
  327. continue;
  328. /* Deep copy because readdir(3) reuses the entries */
  329. dents = realloc(dents, (n + 1) * sizeof(*dents));
  330. if (dents == NULL)
  331. printerr(1, "realloc");
  332. dents[n].name = strdup(dp->d_name);
  333. if (dents[n].name == NULL)
  334. printerr(1, "strdup");
  335. /* Handle root case */
  336. if (strcmp(path, "/") == 0)
  337. asprintf(&name, "/%s", dents[n].name);
  338. else
  339. asprintf(&name, "%s/%s", path, dents[n].name);
  340. /* Get mode flags */
  341. r = lstat(name, &sb);
  342. free(name);
  343. if (r == -1)
  344. printerr(1, "stat");
  345. dents[n].mode = sb.st_mode;
  346. n++;
  347. }
  348. /* Make sure cur is in range */
  349. cur = MIN(cur, n - 1);
  350. qsort(dents, n, sizeof(*dents), entrycmp);
  351. for (;;) {
  352. int nlines;
  353. int maxlen;
  354. int odd;
  355. char *pathnew;
  356. char *name;
  357. char *bin;
  358. pid_t pid;
  359. int fd;
  360. char *dir;
  361. char *tmp;
  362. regex_t re;
  363. struct history *hist;
  364. int status;
  365. redraw:
  366. nlines = MIN(LINES - 4, n);
  367. /* Clean screen */
  368. erase();
  369. /* Strip trailing slashes */
  370. for (i = strlen(path) - 1; i > 0; i--)
  371. if (path[i] == '/')
  372. path[i] = '\0';
  373. else
  374. break;
  375. DPRINTF_D(cur);
  376. DPRINTF_S(path);
  377. /* No text wrapping in cwd line */
  378. cwd = malloc(COLS * sizeof(char));
  379. strlcpy(cwd, path, COLS * sizeof(char));
  380. cwd[COLS - strlen(CWD) - 1] = '\0';
  381. printw(CWD "%s\n\n", cwd);
  382. /* Print listing */
  383. odd = ISODD(nlines);
  384. if (cur < nlines / 2) {
  385. for (i = 0; i < nlines; i++)
  386. printent(&dents[i], i == cur);
  387. } else if (cur >= n - nlines / 2) {
  388. for (i = n - nlines; i < n; i++)
  389. printent(&dents[i], i == cur);
  390. } else {
  391. for (i = cur - nlines / 2;
  392. i < cur + nlines / 2 + odd; i++)
  393. printent(&dents[i], i == cur);
  394. }
  395. nochange:
  396. ret = nextsel(&cur, n);
  397. switch (ret) {
  398. case SEL_QUIT:
  399. free(path);
  400. free(filter);
  401. /* Forget history */
  402. while (!SLIST_EMPTY(&histhead)) {
  403. hist = SLIST_FIRST(&histhead);
  404. SLIST_REMOVE_HEAD(&histhead, entry);
  405. free(hist);
  406. }
  407. return;
  408. case SEL_BACK:
  409. /* There is no going back */
  410. if (strcmp(path, "/") == 0) {
  411. goto nochange;
  412. } else {
  413. dir = dirname(path);
  414. tmp = malloc(strlen(dir) + 1);
  415. strlcpy(tmp, dir, strlen(dir) + 1);
  416. free(path);
  417. path = tmp;
  418. free(filter);
  419. filter = strdup(ifilter); /* Reset filter */
  420. /* Recall history */
  421. hist = SLIST_FIRST(&histhead);
  422. if (hist != NULL) {
  423. cur = hist->pos;
  424. DPRINTF_D(hist->pos);
  425. SLIST_REMOVE_HEAD(&histhead, entry);
  426. free(hist);
  427. } else {
  428. cur = 0;
  429. }
  430. goto out;
  431. }
  432. case SEL_GOIN:
  433. /* Cannot descend in empty directories */
  434. if (n == 0)
  435. goto nochange;
  436. name = dents[cur].name;
  437. /* Handle root case */
  438. if (strcmp(path, "/") == 0)
  439. asprintf(&pathnew, "/%s", name);
  440. else
  441. asprintf(&pathnew, "%s/%s", path, name);
  442. DPRINTF_S(name);
  443. DPRINTF_S(pathnew);
  444. /* Get path info */
  445. fd = open(pathnew, O_RDONLY | O_NONBLOCK);
  446. if (fd == -1) {
  447. printwarn();
  448. free(pathnew);
  449. goto nochange;
  450. }
  451. r = fstat(fd, &sb);
  452. close(fd);
  453. if (r == -1) {
  454. printwarn();
  455. free(pathnew);
  456. goto nochange;
  457. }
  458. DPRINTF_U(sb.st_mode);
  459. /* Directory */
  460. if (S_ISDIR(sb.st_mode)) {
  461. free(path);
  462. path = pathnew;
  463. free(filter);
  464. filter = strdup(ifilter); /* Reset filter */
  465. /* Save history */
  466. hist = malloc(sizeof(struct history));
  467. hist->pos = cur;
  468. SLIST_INSERT_HEAD(&histhead, hist, entry);
  469. cur = 0;
  470. goto out;
  471. }
  472. /* Regular file */
  473. if (S_ISREG(sb.st_mode)) {
  474. /* Open with */
  475. bin = openwith(name);
  476. if (bin == NULL) {
  477. printmsg("No association");
  478. free(pathnew);
  479. goto nochange;
  480. }
  481. exitcurses();
  482. /* Run program */
  483. pid = fork();
  484. if (pid == 0) {
  485. execlp(bin, bin, pathnew, NULL);
  486. _exit(0);
  487. } else {
  488. /* Ignore interruptions */
  489. while (waitpid(pid, &status,
  490. 0) == -1)
  491. DPRINTF_D(status);
  492. DPRINTF_D(pid);
  493. }
  494. initcurses();
  495. free(pathnew);
  496. goto redraw;
  497. }
  498. /* All the rest */
  499. printmsg("Unsupported file");
  500. free(pathnew);
  501. goto nochange;
  502. case SEL_FLTR:
  503. /* Read filter */
  504. printmsg("");
  505. move(LINES - 1, 0);
  506. printw("filter: ");
  507. tmp = readln();
  508. if (tmp == NULL) {
  509. printmsg("");
  510. goto nochange;
  511. }
  512. r = setfilter(&re, tmp);
  513. if (r != 0) {
  514. free(tmp);
  515. goto nochange;
  516. }
  517. free(filter);
  518. filter = tmp;
  519. filter_re = re;
  520. DPRINTF_S(filter);
  521. cur = 0;
  522. goto out;
  523. }
  524. }
  525. out:
  526. for (i = 0; i < n; i++)
  527. free(dents[i].name);
  528. free(dents);
  529. /* Should never be null */
  530. r = closedir(dirp);
  531. if (r == -1)
  532. printerr(1, "closedir");
  533. goto begin;
  534. }
  535. int
  536. main(int argc, char *argv[])
  537. {
  538. char cwd[PATH_MAX], *ipath;
  539. char *ifilter = "^[^.].*"; /* Hide dotfiles */
  540. if (argv[1] != NULL) {
  541. ipath = argv[1];
  542. } else {
  543. ipath = getcwd(cwd, sizeof(cwd));
  544. if (ipath == NULL)
  545. ipath = "/";
  546. }
  547. /* Test initial path */
  548. if (!testopendir(ipath))
  549. printerr(1, ipath);
  550. /* Set locale before curses setup */
  551. setlocale(LC_ALL, "");
  552. initcurses();
  553. browse(ipath, ifilter);
  554. exitcurses();
  555. return 0;
  556. }