aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Ankarstr\xf6m <john@ankarstrom.se>2021-06-01 03:05:17 +0200
committerJohn Ankarstr\xf6m <john@ankarstrom.se>2021-06-01 03:05:17 +0200
commit64f2f6b907e40fa48ab1287ae60a800df7df3213 (patch)
tree132b5f4a3e25ce92a98a202257c126b05d900fec
downloadnoice-64f2f6b907e40fa48ab1287ae60a800df7df3213.tar.gz
First commit (0.8)
-rw-r--r--LICENSE24
-rw-r--r--Makefile45
-rw-r--r--README62
-rw-r--r--config.def.h99
-rw-r--r--noice.1149
-rw-r--r--noice.c919
-rw-r--r--strlcat.c55
-rw-r--r--strlcpy.c50
-rw-r--r--util.h5
9 files changed, 1408 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0b771db
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2014-2019 Lazaros Koromilas <lostd@2f30.org>
+Copyright (c) 2014-2019 Dimitris Papastamos <sin@2f30.org>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bb84dd0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,45 @@
+VERSION = 0.8
+
+PREFIX = /usr/local
+MANPREFIX = $(PREFIX)/man
+
+#CPPFLAGS = -DDEBUG
+#CFLAGS = -g
+LDLIBS = -lcurses
+
+DISTFILES = noice.c strlcat.c strlcpy.c util.h config.def.h\
+ noice.1 Makefile README LICENSE
+OBJ = noice.o strlcat.o strlcpy.o
+BIN = noice
+
+all: $(BIN)
+
+$(BIN): $(OBJ)
+ $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS)
+
+noice.o: util.h config.h
+strlcat.o: util.h
+strlcpy.o: util.h
+
+config.h:
+ cp config.def.h $@
+
+install: all
+ mkdir -p $(DESTDIR)$(PREFIX)/bin
+ cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
+ mkdir -p $(DESTDIR)$(MANPREFIX)/man1
+ cp -f $(BIN).1 $(DESTDIR)$(MANPREFIX)/man1
+
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN)
+ rm -f $(DESTDIR)$(MANPREFIX)/man1/$(BIN).1
+
+dist:
+ mkdir -p noice-$(VERSION)
+ cp $(DISTFILES) noice-$(VERSION)
+ tar -cf noice-$(VERSION).tar noice-$(VERSION)
+ gzip noice-$(VERSION).tar
+ rm -rf noice-$(VERSION)
+
+clean:
+ rm -f $(BIN) $(OBJ) noice-$(VERSION).tar.gz
diff --git a/README b/README
new file mode 100644
index 0000000..457e927
--- /dev/null
+++ b/README
@@ -0,0 +1,62 @@
+ __
+ ___ ___ /\_\ ___ __
+/' _ `\ / __`\/\ \ /'___\ /'__`\
+/\ \/\ \/\ \L\ \ \ \/\ \__//\ __/
+\ \_\ \_\ \____/\ \_\ \____\ \____\
+ \/_/\/_/\/___/ \/_/\/____/\/____/
+ -- by lostd and sin
+=======================================================
+
+
+What is it?
+===========
+
+noice is a small curses-based file browser.
+It was first developed to be used with a TV remote control for a media
+center solution.
+
+
+Getting started
+===============
+
+Get the latest version from the git-repository; build and install it. Run
+noice in a directory to display its content in the form of a list, where
+each line is a file or directory. The currently selected item will be
+preceded with a " > " by default.
+
+For more information refer to the manpage.
+
+
+Building
+========
+
+To build noice you need a curses implementation available. In most
+cases you just do:
+
+ make
+
+It is known to work on OpenBSD, NetBSD, FreeBSD, DragonFly BSD, Linux, OSX,
+IRIX 6.5, Haiku and Solaris 9. Some notes for building on certain systems
+follow.
+
+ * IRIX 6.5:
+ Tested with gcc from http://freeware.sgi.com/.
+
+ make CC="gcc" LDLIBS="-lgen -lcurses"
+
+ * Haiku:
+
+ make LDLIBS="-lncurses"
+
+ * Solaris 9:
+ Tested with gcc from http://www.opencsw.org/.
+
+ export PATH=/usr/ccs/bin:/opt/csw/bin:$PATH
+ make CC="gcc"
+
+
+Contact
+=======
+
+To report bugs and/or submit patches, you can reach us through
+irc.2f30.org at #2f30.
diff --git a/config.def.h b/config.def.h
new file mode 100644
index 0000000..f60227b
--- /dev/null
+++ b/config.def.h
@@ -0,0 +1,99 @@
+/* See LICENSE file for copyright and license details. */
+#define CWD "cwd: "
+#define CURSR " > "
+#define EMPTY " "
+
+int dirorder = 0; /* Set to 1 to sort by directory first */
+int mtimeorder = 0; /* Set to 1 to sort by time modified */
+int icaseorder = 0; /* Set to 1 to sort by ignoring case */
+int idletimeout = 0; /* Screensaver timeout in seconds, 0 to disable */
+int showhidden = 0; /* Set to 1 to show hidden files by default */
+int usecolor = 0; /* Set to 1 to enable color attributes */
+char *idlecmd = "rain"; /* The screensaver program */
+
+/* See curs_attr(3) for valid video attributes */
+#define CURSR_ATTR A_NORMAL
+#define DIR_ATTR A_NORMAL | COLOR_PAIR(4)
+#define LINK_ATTR A_NORMAL | COLOR_PAIR(6)
+#define SOCK_ATTR A_NORMAL | COLOR_PAIR(1)
+#define FIFO_ATTR A_NORMAL | COLOR_PAIR(5)
+#define EXEC_ATTR A_NORMAL | COLOR_PAIR(2)
+
+/* Colors to use with COLOR_PAIR(n) as attributes */
+struct cpair pairs[] = {
+ { .fg = 0, .bg = 0 },
+ /* pairs start at 1 */
+ { COLOR_RED, -1 },
+ { COLOR_GREEN, -1 },
+ { COLOR_YELLOW, -1 },
+ { COLOR_BLUE, -1 },
+ { COLOR_MAGENTA, -1 },
+ { COLOR_CYAN, -1 },
+};
+
+struct assoc assocs[] = {
+ { "\\.(avi|mp4|mkv|mp3|ogg|flac|mov)$", "mpv" },
+ { "\\.(png|jpg|gif)$", "sxiv" },
+ { "\\.(html|svg)$", "firefox" },
+ { "\\.pdf$", "mupdf" },
+ { "\\.sh$", "sh" },
+ { ".", "less" },
+};
+
+struct key bindings[] = {
+ /* Quit */
+ { 'q', SEL_QUIT },
+ /* Back */
+ { KEY_BACKSPACE, SEL_BACK },
+ { KEY_LEFT, SEL_BACK },
+ { 'h', SEL_BACK },
+ { CONTROL('H'), SEL_BACK },
+ /* Inside */
+ { KEY_ENTER, SEL_GOIN },
+ { '\r', SEL_GOIN },
+ { KEY_RIGHT, SEL_GOIN },
+ { 'l', SEL_GOIN },
+ /* Filter */
+ { '/', SEL_FLTR },
+ { '&', SEL_FLTR },
+ /* Next */
+ { 'j', SEL_NEXT },
+ { KEY_DOWN, SEL_NEXT },
+ { CONTROL('N'), SEL_NEXT },
+ /* Previous */
+ { 'k', SEL_PREV },
+ { KEY_UP, SEL_PREV },
+ { CONTROL('P'), SEL_PREV },
+ /* Page down */
+ { KEY_NPAGE, SEL_PGDN },
+ { CONTROL('D'), SEL_PGDN },
+ /* Page up */
+ { KEY_PPAGE, SEL_PGUP },
+ { CONTROL('U'), SEL_PGUP },
+ /* Home */
+ { KEY_HOME, SEL_HOME },
+ { META('<'), SEL_HOME },
+ { '^', SEL_HOME },
+ /* End */
+ { KEY_END, SEL_END },
+ { META('>'), SEL_END },
+ { '$', SEL_END },
+ /* Change dir */
+ { 'c', SEL_CD },
+ { '~', SEL_CDHOME },
+ /* Toggle hide .dot files */
+ { '.', SEL_TOGGLEDOT },
+ /* Toggle sort by directory first */
+ { 'd', SEL_DSORT },
+ /* Toggle sort by time */
+ { 't', SEL_MTIME },
+ /* Toggle case sensitivity */
+ { 'i', SEL_ICASE },
+ { CONTROL('L'), SEL_REDRAW },
+ /* Run command */
+ { 'z', SEL_RUN, "top" },
+ { '!', SEL_RUN, "sh", "SHELL" },
+ /* Run command with argument */
+ { 'e', SEL_RUNARG, "vi", "EDITOR" },
+ { 'p', SEL_RUNARG, "less", "PAGER" },
+};
diff --git a/noice.1 b/noice.1
new file mode 100644
index 0000000..dddd302
--- /dev/null
+++ b/noice.1
@@ -0,0 +1,149 @@
+.Dd Jan 19, 2019
+.Dt NOICE 1
+.Os
+.Sh NAME
+.Nm noice
+.Nd small file browser
+.Sh SYNOPSIS
+.Nm
+.Op Ar dir
+.Sh DESCRIPTION
+.Nm
+is a simple and efficient file browser that gets out of your way
+as much as possible.
+It was initially implemented to be controlled with a TV remote control.
+.Pp
+.Nm
+defaults to the current directory if
+.Ar dir
+is not specified.
+As an extra feature, if
+.Ar dir
+is a relative path,
+.Nm
+will not go back beyond the first component of the path using standard
+navigation key presses.
+.Pp
+.Nm
+supports both vi-like and emacs-like key bindings in the default
+configuration.
+The default key bindings are described below;
+their functionality is described in more detail later.
+.Pp
+.Bl -tag -width "l, [Right], [Return] or C-mXXXX" -offset indent -compact
+.It Ic k, [Up] or C-p
+Move to previous entry.
+.It Ic j, [Down] or C-n
+Move to next entry.
+.It Ic [Pgup] or C-u
+Scroll up half a page.
+.It Ic [Pgdown] or C-d
+Scroll down half a page.
+.It Ic [Home], ^ or M-<
+Move to the first entry.
+.It Ic [End], $ or M->
+Move to the last entry.
+.It Ic l, [Right], [Return] or C-m
+Open file or enter directory.
+.It Ic h, C-h, [Left] or [Backspace]
+Back up one directory level.
+.It Ic / or &
+Change filter (see below for more information).
+.It Ic c
+Change into the given directory.
+.It Ic ~
+Change to the
+.Ev HOME
+directory.
+.It Ic \&.
+Toggle hidden .dot files.
+.It Ic d
+Toggle sort by directory first.
+.It Ic t
+Toggle sort by time modified.
+.It Ic i
+Toggle case sensitive sort.
+.It Ic C-l
+Force a redraw.
+.It Ic \&!
+Spawn a shell in current directory.
+.It Ic z
+Run the system top utility.
+.It Ic e
+Open selected entry with the vi editor.
+.It Ic p
+Open selected entry with the less pager.
+.It Ic q
+Quit.
+.El
+.Pp
+Backing up one directory level will set the cursor position at the
+directory you came out of.
+.Sh CONFIGURATION
+.Nm
+is configured by modifying
+.Pa config.h
+and recompiling the code.
+.Pp
+The file associations are specified by regexes
+matching on the currently selected filename.
+If a match is found the associated program is executed
+with the filename passed in as the argument.
+If no match is found the program
+.Xr less 1
+is invoked.
+This is useful for editing text files as one can use the
+.Ic v
+command in
+.Xr less 1
+to edit the file using the
+.Ev EDITOR
+environment variable.
+.Pp
+See the examples section below for more information.
+.Sh FILTERS
+Filters allow you to use regexes to display only the matched
+entries in the current directory view.
+This effectively allows searching through the directory tree
+for a particular entry.
+.Pp
+Filters do not stack on top of each other.
+They are applied anew every time.
+.Pp
+To reset the filter you can input an empty filter expression.
+.Pp
+If
+.Nm
+is invoked as root the default filter will also match hidden files.
+.Sh ENVIRONMENT
+The
+.Ev SHELL ,
+.Ev EDITOR
+and
+.Ev PAGER
+environment variables take precedence when dealing with the
+.Ic \&! ,
+.Ic e
+and
+.Ic p
+commands respectively.
+.Sh EXAMPLES
+The following example shows one possible configuration for
+file associations which is also the default:
+.Bd -literal
+struct assoc assocs[] = {
+ { "\\.(avi|mp4|mkv|mp3|ogg|flac|mov)$", "mpv" },
+ { "\\.(png|jpg|gif)$", "sxiv" },
+ { "\\.(html|svg)$", "firefox" },
+ { "\\.pdf$", "mupdf" },
+ { "\\.sh$", "sh" },
+ { ".", "less" },
+};
+.Ed
+.Sh KNOWN ISSUES
+If you are using
+.Xr urxvt 1
+you might have to set backspace key to DEC.
+.Sh AUTHORS
+.An Lazaros Koromilas Aq Mt lostd@2f30.org ,
+.An Dimitris Papastamos Aq Mt sin@2f30.org .
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);
+}
diff --git a/strlcat.c b/strlcat.c
new file mode 100644
index 0000000..bc523fb
--- /dev/null
+++ b/strlcat.c
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <string.h>
+
+#include "util.h"
+
+/*
+ * Appends src to string dst of size dsize (unlike strncat, dsize is the
+ * full size of dst, not space left). At most dsize-1 characters
+ * will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
+ * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
+ * If retval >= dsize, truncation occurred.
+ */
+size_t
+strlcat(char *dst, const char *src, size_t dsize)
+{
+ const char *odst = dst;
+ const char *osrc = src;
+ size_t n = dsize;
+ size_t dlen;
+
+ /* Find the end of dst and adjust bytes left but don't go past end. */
+ while (n-- != 0 && *dst != '\0')
+ dst++;
+ dlen = dst - odst;
+ n = dsize - dlen;
+
+ if (n-- == 0)
+ return(dlen + strlen(src));
+ while (*src != '\0') {
+ if (n != 0) {
+ *dst++ = *src;
+ n--;
+ }
+ src++;
+ }
+ *dst = '\0';
+
+ return(dlen + (src - osrc)); /* count does not include NUL */
+}
diff --git a/strlcpy.c b/strlcpy.c
new file mode 100644
index 0000000..0ec2b78
--- /dev/null
+++ b/strlcpy.c
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <string.h>
+
+#include "util.h"
+
+/*
+ * Copy string src to buffer dst of size dsize. At most dsize-1
+ * chars will be copied. Always NUL terminates (unless dsize == 0).
+ * Returns strlen(src); if retval >= dsize, truncation occurred.
+ */
+size_t
+strlcpy(char *dst, const char *src, size_t dsize)
+{
+ const char *osrc = src;
+ size_t nleft = dsize;
+
+ /* Copy as many bytes as will fit. */
+ if (nleft != 0) {
+ while (--nleft != 0) {
+ if ((*dst++ = *src++) == '\0')
+ break;
+ }
+ }
+
+ /* Not enough room in dst, add NUL and traverse rest of src. */
+ if (nleft == 0) {
+ if (dsize != 0)
+ *dst = '\0'; /* NUL-terminate dst */
+ while (*src++)
+ ;
+ }
+
+ return(src - osrc - 1); /* count does not include NUL */
+}
diff --git a/util.h b/util.h
new file mode 100644
index 0000000..c4d1904
--- /dev/null
+++ b/util.h
@@ -0,0 +1,5 @@
+/* See LICENSE file for copyright and license details. */
+#undef strlcat
+size_t strlcat(char *, const char *, size_t);
+#undef strlcpy
+size_t strlcpy(char *, const char *, size_t);