noice.c - noice - small file browser (mirror / fork from 2f30.org)
HTML git clone git://git.codemadness.org/noice
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
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 }