aboutsummaryrefslogtreecommitdiff
path: root/noice.c
diff options
context:
space:
mode:
Diffstat (limited to 'noice.c')
-rw-r--r--noice.c919
1 files changed, 919 insertions, 0 deletions
diff --git a/noice.c b/noice.c
new file mode 100644
index 0000000..3b8068e
--- /dev/null
+++ b/noice.c
@@ -0,0 +1,919 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <curses.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <locale.h>
+#include <regex.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+#ifdef DEBUG
+#define DEBUG_FD 8
+#define DPRINTF_D(x) dprintf(DEBUG_FD, #x "=%d\n", x)
+#define DPRINTF_U(x) dprintf(DEBUG_FD, #x "=%u\n", x)
+#define DPRINTF_S(x) dprintf(DEBUG_FD, #x "=%s\n", x)
+#define DPRINTF_P(x) dprintf(DEBUG_FD, #x "=0x%p\n", x)
+#else
+#define DPRINTF_D(x)
+#define DPRINTF_U(x)
+#define DPRINTF_S(x)
+#define DPRINTF_P(x)
+#endif /* DEBUG */
+
+#define LEN(x) (sizeof(x) / sizeof(*(x)))
+#undef MIN
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define ISODD(x) ((x) & 1)
+#define CONTROL(c) ((c) ^ 0x40)
+#define META(c) ((c) ^ 0x80)
+
+struct assoc {
+ char *regex; /* Regex to match on filename */
+ char *bin; /* Program */
+};
+
+struct cpair {
+ int fg;
+ int bg;
+};
+
+/* Supported actions */
+enum action {
+ SEL_QUIT = 1,
+ SEL_BACK,
+ SEL_GOIN,
+ SEL_FLTR,
+ SEL_NEXT,
+ SEL_PREV,
+ SEL_PGDN,
+ SEL_PGUP,
+ SEL_HOME,
+ SEL_END,
+ SEL_CD,
+ SEL_CDHOME,
+ SEL_TOGGLEDOT,
+ SEL_DSORT,
+ SEL_MTIME,
+ SEL_ICASE,
+ SEL_REDRAW,
+ SEL_RUN,
+ SEL_RUNARG,
+};
+
+struct key {
+ int sym; /* Key pressed */
+ enum action act; /* Action */
+ char *run; /* Program to run */
+ char *env; /* Environment variable to run */
+};
+
+#include "config.h"
+
+struct entry {
+ char name[PATH_MAX];
+ mode_t mode;
+ time_t t;
+};
+
+/* Global context */
+struct entry *dents;
+int ndents, cur;
+int idle;
+
+/*
+ * Layout:
+ * .---------
+ * | cwd: /mnt/path
+ * |
+ * | file0
+ * | file1
+ * | > file2
+ * | file3
+ * | file4
+ * ...
+ * | filen
+ * |
+ * | Permission denied
+ * '------
+ */
+
+void printmsg(char *);
+void printwarn(void);
+void printerr(int, char *);
+
+#undef dprintf
+int
+dprintf(int fd, const char *fmt, ...)
+{
+ char buf[BUFSIZ];
+ int r;
+ va_list ap;
+
+ va_start(ap, fmt);
+ r = vsnprintf(buf, sizeof(buf), fmt, ap);
+ if (r > 0)
+ write(fd, buf, r);
+ va_end(ap);
+ return r;
+}
+
+void *
+xmalloc(size_t size)
+{
+ void *p;
+
+ p = malloc(size);
+ if (p == NULL)
+ printerr(1, "malloc");
+ return p;
+}
+
+void *
+xrealloc(void *p, size_t size)
+{
+ p = realloc(p, size);
+ if (p == NULL)
+ printerr(1, "realloc");
+ return p;
+}
+
+char *
+xstrdup(const char *s)
+{
+ char *p;
+
+ p = strdup(s);
+ if (p == NULL)
+ printerr(1, "strdup");
+ return p;
+}
+
+/* Some implementations of dirname(3) may modify `path' and some
+ * return a pointer inside `path'. */
+char *
+xdirname(const char *path)
+{
+ static char out[PATH_MAX];
+ char tmp[PATH_MAX], *p;
+
+ strlcpy(tmp, path, sizeof(tmp));
+ p = dirname(tmp);
+ if (p == NULL)
+ printerr(1, "dirname");
+ strlcpy(out, p, sizeof(out));
+ return out;
+}
+
+void
+spawn(char *file, char *arg, char *dir)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ if (pid == 0) {
+ if (dir != NULL)
+ chdir(dir);
+ execlp(file, file, arg, NULL);
+ _exit(1);
+ } else {
+ /* Ignore interruptions */
+ while (waitpid(pid, &status, 0) == -1)
+ DPRINTF_D(status);
+ DPRINTF_D(pid);
+ }
+}
+
+char *
+xgetenv(char *name, char *fallback)
+{
+ char *value;
+
+ if (name == NULL)
+ return fallback;
+ value = getenv(name);
+ return value && value[0] ? value : fallback;
+}
+
+char *
+openwith(char *file)
+{
+ regex_t regex;
+ char *bin = NULL;
+ int i;
+
+ for (i = 0; i < LEN(assocs); i++) {
+ if (regcomp(&regex, assocs[i].regex,
+ REG_NOSUB | REG_EXTENDED | REG_ICASE) != 0)
+ continue;
+ if (regexec(&regex, file, 0, NULL, 0) == 0) {
+ bin = assocs[i].bin;
+ regfree(&regex);
+ break;
+ }
+ regfree(&regex);
+ }
+ DPRINTF_S(bin);
+ return bin;
+}
+
+int
+setfilter(regex_t *regex, char *filter)
+{
+ char errbuf[LINE_MAX];
+ size_t len;
+ int r;
+
+ r = regcomp(regex, filter, REG_NOSUB | REG_EXTENDED | REG_ICASE);
+ if (r != 0) {
+ len = COLS;
+ if (len > sizeof(errbuf))
+ len = sizeof(errbuf);
+ regerror(r, regex, errbuf, len);
+ printmsg(errbuf);
+ }
+ return r;
+}
+
+void
+freefilter(regex_t *regex)
+{
+ regfree(regex);
+}
+
+void
+initfilter(int dot, char **ifilter)
+{
+ *ifilter = dot ? "." : "^[^.]";
+}
+
+int
+visible(regex_t *regex, char *file)
+{
+ return regexec(regex, file, 0, NULL, 0) == 0;
+}
+
+int
+dircmp(mode_t a, mode_t b)
+{
+ if (S_ISDIR(a) && S_ISDIR(b))
+ return 0;
+ if (!S_ISDIR(a) && !S_ISDIR(b))
+ return 0;
+ if (S_ISDIR(a))
+ return -1;
+ else
+ return 1;
+}
+
+int
+entrycmp(const void *va, const void *vb)
+{
+ const struct entry *a = va, *b = vb;
+
+ if (dirorder) {
+ if (dircmp(a->mode, b->mode) != 0)
+ return dircmp(a->mode, b->mode);
+ }
+
+ if (mtimeorder)
+ return b->t - a->t;
+ if (icaseorder)
+ return strcasecmp(a->name, b->name);
+ else
+ return strcmp(a->name, b->name);
+}
+
+void
+initcolor(void)
+{
+ int i;
+
+ start_color();
+ use_default_colors();
+ for (i = 1; i < LEN(pairs); i++)
+ init_pair(i, pairs[i].fg, pairs[i].bg);
+}
+
+void
+initcurses(void)
+{
+ char *term;
+
+ if (initscr() == NULL) {
+ term = getenv("TERM");
+ if (term != NULL)
+ fprintf(stderr, "error opening terminal: %s\n", term);
+ else
+ fprintf(stderr, "failed to initialize curses\n");
+ exit(1);
+ }
+ if (usecolor && has_colors())
+ initcolor();
+ cbreak();
+ noecho();
+ nonl();
+ intrflush(stdscr, FALSE);
+ keypad(stdscr, TRUE);
+ curs_set(FALSE); /* Hide cursor */
+ timeout(1000); /* One second */
+}
+
+void
+exitcurses(void)
+{
+ endwin(); /* Restore terminal */
+}
+
+/* Messages show up at the bottom */
+void
+printmsg(char *msg)
+{
+ move(LINES - 1, 0);
+ printw("%s\n", msg);
+}
+
+/* Display warning as a message */
+void
+printwarn(void)
+{
+ printmsg(strerror(errno));
+}
+
+/* Kill curses and display error before exiting */
+void
+printerr(int ret, char *prefix)
+{
+ exitcurses();
+ fprintf(stderr, "%s: %s\n", prefix, strerror(errno));
+ exit(ret);
+}
+
+/* Clear the last line */
+void
+clearprompt(void)
+{
+ printmsg("");
+}
+
+/* Print prompt on the last line */
+void
+printprompt(char *str)
+{
+ clearprompt();
+ printw(str);
+}
+
+int
+xgetch(void)
+{
+ int c;
+
+ c = getch();
+ if (c == -1)
+ idle++;
+ else
+ idle = 0;
+ return c;
+}
+
+/* Returns SEL_* if key is bound and 0 otherwise.
+ * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}) */
+int
+nextsel(char **run, char **env)
+{
+ int c, i;
+
+ c = xgetch();
+ if (c == 033)
+ c = META(xgetch());
+
+ for (i = 0; i < LEN(bindings); i++)
+ if (c == bindings[i].sym) {
+ *run = bindings[i].run;
+ *env = bindings[i].env;
+ return bindings[i].act;
+ }
+ return 0;
+}
+
+char *
+readln(void)
+{
+ static char ln[LINE_MAX];
+
+ timeout(-1);
+ echo();
+ curs_set(TRUE);
+ memset(ln, 0, sizeof(ln));
+ wgetnstr(stdscr, ln, sizeof(ln) - 1);
+ noecho();
+ curs_set(FALSE);
+ timeout(1000);
+ return ln[0] ? ln : NULL;
+}
+
+int
+canopendir(char *path)
+{
+ DIR *dirp;
+
+ dirp = opendir(path);
+ if (dirp == NULL)
+ return 0;
+ closedir(dirp);
+ return 1;
+}
+
+char *
+mkpath(char *dir, char *name, char *out, size_t n)
+{
+ /* Handle absolute path */
+ if (name[0] == '/') {
+ strlcpy(out, name, n);
+ } else {
+ /* Handle root case */
+ if (strcmp(dir, "/") == 0) {
+ strlcpy(out, "/", n);
+ strlcat(out, name, n);
+ } else {
+ strlcpy(out, dir, n);
+ strlcat(out, "/", n);
+ strlcat(out, name, n);
+ }
+ }
+ return out;
+}
+
+void
+printent(struct entry *ent, int active)
+{
+ char name[PATH_MAX];
+ unsigned int len = COLS - strlen(CURSR) - 1;
+ char cm = 0;
+ int attr = 0;
+
+ /* Copy name locally */
+ strlcpy(name, ent->name, sizeof(name));
+
+ /* No text wrapping in entries */
+ if (strlen(name) < len)
+ len = strlen(name) + 1;
+
+ if (S_ISDIR(ent->mode)) {
+ cm = '/';
+ attr |= DIR_ATTR;
+ } else if (S_ISLNK(ent->mode)) {
+ cm = '@';
+ attr |= LINK_ATTR;
+ } else if (S_ISSOCK(ent->mode)) {
+ cm = '=';
+ attr |= SOCK_ATTR;
+ } else if (S_ISFIFO(ent->mode)) {
+ cm = '|';
+ attr |= FIFO_ATTR;
+ } else if (ent->mode & S_IXUSR) {
+ cm = '*';
+ attr |= EXEC_ATTR;
+ }
+
+ if (active)
+ attr |= CURSR_ATTR;
+
+ if (cm) {
+ name[len - 1] = cm;
+ name[len] = '\0';
+ }
+
+ attron(attr);
+ printw("%s%s\n", active ? CURSR : EMPTY, name);
+ attroff(attr);
+}
+
+int
+dentfill(char *path, struct entry **dents,
+ int (*filter)(regex_t *, char *), regex_t *re)
+{
+ char newpath[PATH_MAX];
+ DIR *dirp;
+ struct dirent *dp;
+ struct stat sb;
+ int r, n = 0;
+
+ dirp = opendir(path);
+ if (dirp == NULL)
+ return 0;
+
+ while ((dp = readdir(dirp)) != NULL) {
+ /* Skip self and parent */
+ if (strcmp(dp->d_name, ".") == 0 ||
+ strcmp(dp->d_name, "..") == 0)
+ continue;
+ if (filter(re, dp->d_name) == 0)
+ continue;
+ *dents = xrealloc(*dents, (n + 1) * sizeof(**dents));
+ strlcpy((*dents)[n].name, dp->d_name, sizeof((*dents)[n].name));
+ /* Get mode flags */
+ mkpath(path, dp->d_name, newpath, sizeof(newpath));
+ r = lstat(newpath, &sb);
+ if (r == -1)
+ printerr(1, "lstat");
+ (*dents)[n].mode = sb.st_mode;
+ (*dents)[n].t = sb.st_mtime;
+ n++;
+ }
+
+ /* Should never be null */
+ r = closedir(dirp);
+ if (r == -1)
+ printerr(1, "closedir");
+ return n;
+}
+
+void
+dentfree(struct entry *dents)
+{
+ free(dents);
+}
+
+/* Return the position of the matching entry or 0 otherwise */
+int
+dentfind(struct entry *dents, int n, char *cwd, char *path)
+{
+ char tmp[PATH_MAX];
+ int i;
+
+ if (path == NULL)
+ return 0;
+ for (i = 0; i < n; i++) {
+ mkpath(cwd, dents[i].name, tmp, sizeof(tmp));
+ DPRINTF_S(path);
+ DPRINTF_S(tmp);
+ if (strcmp(tmp, path) == 0)
+ return i;
+ }
+ return 0;
+}
+
+int
+populate(char *path, char *oldpath, char *fltr)
+{
+ regex_t re;
+ int r;
+
+ /* Can fail when permissions change while browsing */
+ if (canopendir(path) == 0)
+ return -1;
+
+ /* Search filter */
+ r = setfilter(&re, fltr);
+ if (r != 0)
+ return -1;
+
+ dentfree(dents);
+
+ ndents = 0;
+ dents = NULL;
+
+ ndents = dentfill(path, &dents, visible, &re);
+ freefilter(&re);
+ if (ndents == 0)
+ return 0; /* Empty result */
+
+ qsort(dents, ndents, sizeof(*dents), entrycmp);
+
+ /* Find cur from history */
+ cur = dentfind(dents, ndents, path, oldpath);
+ return 0;
+}
+
+void
+redraw(char *path)
+{
+ char cwd[PATH_MAX], cwdresolved[PATH_MAX];
+ size_t ncols;
+ int nlines, odd;
+ int i;
+
+ nlines = MIN(LINES - 4, ndents);
+
+ /* Clean screen */
+ erase();
+
+ /* Strip trailing slashes */
+ for (i = strlen(path) - 1; i > 0; i--)
+ if (path[i] == '/')
+ path[i] = '\0';
+ else
+ break;
+
+ DPRINTF_D(cur);
+ DPRINTF_S(path);
+
+ /* No text wrapping in cwd line */
+ ncols = COLS;
+ if (ncols > PATH_MAX)
+ ncols = PATH_MAX;
+ strlcpy(cwd, path, ncols);
+ cwd[ncols - strlen(CWD) - 1] = '\0';
+ realpath(cwd, cwdresolved);
+
+ printw(CWD "%s\n\n", cwdresolved);
+
+ /* Print listing */
+ odd = ISODD(nlines);
+ if (cur < nlines / 2) {
+ for (i = 0; i < nlines; i++)
+ printent(&dents[i], i == cur);
+ } else if (cur >= ndents - nlines / 2) {
+ for (i = ndents - nlines; i < ndents; i++)
+ printent(&dents[i], i == cur);
+ } else {
+ for (i = cur - nlines / 2;
+ i < cur + nlines / 2 + odd; i++)
+ printent(&dents[i], i == cur);
+ }
+}
+
+void
+browse(char *ipath, char *ifilter)
+{
+ char path[PATH_MAX], oldpath[PATH_MAX], newpath[PATH_MAX];
+ char fltr[LINE_MAX];
+ char *bin, *dir, *tmp, *run, *env;
+ struct stat sb;
+ regex_t re;
+ int r, fd;
+
+ strlcpy(path, ipath, sizeof(path));
+ strlcpy(fltr, ifilter, sizeof(fltr));
+ oldpath[0] = '\0';
+begin:
+ r = populate(path, oldpath, fltr);
+ if (r == -1) {
+ printwarn();
+ goto nochange;
+ }
+
+ for (;;) {
+ redraw(path);
+nochange:
+ switch (nextsel(&run, &env)) {
+ case SEL_QUIT:
+ dentfree(dents);
+ return;
+ case SEL_BACK:
+ /* There is no going back */
+ if (strcmp(path, "/") == 0 ||
+ strcmp(path, ".") == 0 ||
+ strchr(path, '/') == NULL)
+ goto nochange;
+ dir = xdirname(path);
+ if (canopendir(dir) == 0) {
+ printwarn();
+ goto nochange;
+ }
+ /* Save history */
+ strlcpy(oldpath, path, sizeof(oldpath));
+ strlcpy(path, dir, sizeof(path));
+ /* Reset filter */
+ strlcpy(fltr, ifilter, sizeof(fltr));
+ goto begin;
+ case SEL_GOIN:
+ /* Cannot descend in empty directories */
+ if (ndents == 0)
+ goto nochange;
+
+ mkpath(path, dents[cur].name, newpath, sizeof(newpath));
+ DPRINTF_S(newpath);
+
+ /* Get path info */
+ fd = open(newpath, O_RDONLY | O_NONBLOCK);
+ if (fd == -1) {
+ printwarn();
+ goto nochange;
+ }
+ r = fstat(fd, &sb);
+ if (r == -1) {
+ printwarn();
+ close(fd);
+ goto nochange;
+ }
+ close(fd);
+ DPRINTF_U(sb.st_mode);
+
+ switch (sb.st_mode & S_IFMT) {
+ case S_IFDIR:
+ if (canopendir(newpath) == 0) {
+ printwarn();
+ goto nochange;
+ }
+ strlcpy(path, newpath, sizeof(path));
+ /* Reset filter */
+ strlcpy(fltr, ifilter, sizeof(fltr));
+ goto begin;
+ case S_IFREG:
+ bin = openwith(newpath);
+ if (bin == NULL) {
+ printmsg("No association");
+ goto nochange;
+ }
+ exitcurses();
+ spawn(bin, newpath, NULL);
+ initcurses();
+ continue;
+ default:
+ printmsg("Unsupported file");
+ goto nochange;
+ }
+ case SEL_FLTR:
+ /* Read filter */
+ printprompt("filter: ");
+ tmp = readln();
+ if (tmp == NULL)
+ tmp = ifilter;
+ /* Check and report regex errors */
+ r = setfilter(&re, tmp);
+ if (r != 0)
+ goto nochange;
+ freefilter(&re);
+ strlcpy(fltr, tmp, sizeof(fltr));
+ DPRINTF_S(fltr);
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ goto begin;
+ case SEL_NEXT:
+ if (cur < ndents - 1)
+ cur++;
+ break;
+ case SEL_PREV:
+ if (cur > 0)
+ cur--;
+ break;
+ case SEL_PGDN:
+ if (cur < ndents - 1)
+ cur += MIN((LINES - 4) / 2, ndents - 1 - cur);
+ break;
+ case SEL_PGUP:
+ if (cur > 0)
+ cur -= MIN((LINES - 4) / 2, cur);
+ break;
+ case SEL_HOME:
+ cur = 0;
+ break;
+ case SEL_END:
+ cur = ndents - 1;
+ break;
+ case SEL_CD:
+ /* Read target dir */
+ printprompt("chdir: ");
+ tmp = readln();
+ if (tmp == NULL) {
+ clearprompt();
+ goto nochange;
+ }
+ mkpath(path, tmp, newpath, sizeof(newpath));
+ if (canopendir(newpath) == 0) {
+ printwarn();
+ goto nochange;
+ }
+ strlcpy(path, newpath, sizeof(path));
+ /* Reset filter */
+ strlcpy(fltr, ifilter, sizeof(fltr));
+ DPRINTF_S(path);
+ goto begin;
+ case SEL_CDHOME:
+ tmp = getenv("HOME");
+ if (tmp == NULL) {
+ clearprompt();
+ goto nochange;
+ }
+ if (canopendir(tmp) == 0) {
+ printwarn();
+ goto nochange;
+ }
+ strlcpy(path, tmp, sizeof(path));
+ /* Reset filter */
+ strlcpy(fltr, ifilter, sizeof(fltr));
+ DPRINTF_S(path);
+ goto begin;
+ case SEL_TOGGLEDOT:
+ showhidden ^= 1;
+ initfilter(showhidden, &ifilter);
+ strlcpy(fltr, ifilter, sizeof(fltr));
+ goto begin;
+ case SEL_MTIME:
+ mtimeorder = !mtimeorder;
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ goto begin;
+ case SEL_DSORT:
+ dirorder = !dirorder;
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ goto begin;
+ case SEL_ICASE:
+ icaseorder = !icaseorder;
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ goto begin;
+ case SEL_REDRAW:
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ goto begin;
+ case SEL_RUN:
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ run = xgetenv(env, run);
+ exitcurses();
+ spawn(run, NULL, path);
+ initcurses();
+ goto begin;
+ case SEL_RUNARG:
+ /* Save current */
+ if (ndents > 0)
+ mkpath(path, dents[cur].name, oldpath, sizeof(oldpath));
+ run = xgetenv(env, run);
+ exitcurses();
+ spawn(run, dents[cur].name, path);
+ initcurses();
+ goto begin;
+ }
+ /* Screensaver */
+ if (idletimeout != 0 && idle == idletimeout) {
+ idle = 0;
+ exitcurses();
+ spawn(idlecmd, NULL, NULL);
+ initcurses();
+ }
+ }
+}
+
+void
+usage(char *argv0)
+{
+ fprintf(stderr, "usage: %s [dir]\n", argv0);
+ exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char cwd[PATH_MAX], *ipath;
+ char *ifilter;
+
+ if (argc > 2)
+ usage(argv[0]);
+
+ /* Confirm we are in a terminal */
+ if (!isatty(0) || !isatty(1)) {
+ fprintf(stderr, "stdin or stdout is not a tty\n");
+ exit(1);
+ }
+
+ if (getuid() == 0)
+ showhidden = 1;
+ initfilter(showhidden, &ifilter);
+
+ if (argv[1] != NULL) {
+ ipath = argv[1];
+ } else {
+ ipath = getcwd(cwd, sizeof(cwd));
+ if (ipath == NULL)
+ ipath = "/";
+ }
+
+ signal(SIGINT, SIG_IGN);
+
+ /* Test initial path */
+ if (canopendir(ipath) == 0) {
+ fprintf(stderr, "%s: %s\n", ipath, strerror(errno));
+ exit(1);
+ }
+
+ /* Set locale before curses setup */
+ setlocale(LC_ALL, "");
+ initcurses();
+ browse(ipath, ifilter);
+ exitcurses();
+ exit(0);
+}