diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3379649d --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2014-2016 Lazaros Koromilas +Copyright (c) 2014-2016 Dimitris Papastamos +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 00000000..9035141d --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +VERSION = 0.5 + +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) $(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 00000000..258fd8f4 --- /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 +the freenode IRC network at #2f30. diff --git a/config.def.h b/config.def.h new file mode 100644 index 00000000..af9bcbb6 --- /dev/null +++ b/config.def.h @@ -0,0 +1,71 @@ +/* See LICENSE file for copyright and license details. */ +#define CWD "cwd: " +#define CURSR " > " +#define EMPTY " " + +int mtimeorder = 0; /* Set to 1 to sort by time modified */ +int idletimeout = 0; /* Screensaver timeout in seconds, 0 to disable */ +char *idlecmd = "rain"; /* The screensaver program */ + +struct assoc assocs[] = { + { "\\.(avi|mp4|mkv|mp3|ogg|flac|mov)$", "mplayer" }, + { "\\.(png|jpg|gif)$", "feh" }, + { "\\.(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 }, + { CONTROL('A'), SEL_HOME }, + { '^', SEL_HOME }, + /* End */ + { KEY_END, SEL_END }, + { CONTROL('E'), SEL_END }, + { '$', SEL_END }, + /* Change dir */ + { 'c', SEL_CD }, + { '~', SEL_CDHOME }, + /* Toggle hide .dot files */ + { '.', SEL_TOGGLEDOT }, + /* Toggle sort by time */ + { 't', SEL_MTIME }, + { 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/mktest.sh b/mktest.sh new file mode 100644 index 00000000..c808d387 --- /dev/null +++ b/mktest.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# Create test files and directories + +test -e test && { + echo "Remove test and try again" + exit 1 +} + +mkdir test && cd test + +echo 'It works!' > normal.txt +echo 'Με δουλέβει;' > 'κοινό.txt' +ln -s normal.txt ln-normal.txt +ln -s normal.txt ln-normal +mkdir normal-dir +ln -s normal-dir ln-normal-dir +ln -s nowhere ln-nowhere +mkfifo mk-fifo +touch no-access && chmod 000 no-access +mkdir no-access-dir && chmod 000 no-access-dir +ln -s ../normal.txt normal-dir/ln-normal.txt +ln -s ../normal.txt normal-dir/ln-normal +echo 'int main(void) { *((char *)0) = 0; }' > ill.c +make ill > /dev/null +echo 'test/ill' > ill.sh +mkdir empty-dir +mkdir cage +echo 'chmod 000 test/cage' > cage/lock.sh +echo 'chmod 755 test/cage' > cage-unlock.sh +mkdir cage/lion +echo 'chmod 000 test/cage' > cage/lion/lock.sh diff --git a/noice.1 b/noice.1 new file mode 100644 index 00000000..f9ac1afc --- /dev/null +++ b/noice.1 @@ -0,0 +1,124 @@ +.Dd February 25, 2016 +.Dt NOICE 1 +.Os +.Sh NAME +.Nm noice +.Nd small file browser +.Sh SYNOPSIS +.Nm noice +.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 C-a +Move to the first entry. +.It Ic [End], $ or C-e +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 HOME directory. +.It Ic \&. +Toggle hide .dot files. +.It Ic t +Toggle sort by time modified. +.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 'v' command in +.Xr less 1 to edit the file using the 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 SHELL, EDITOR and PAGER environment variables take precedence +when dealing with the !, e and 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)$", "mplayer" }, + { "\\.(png|jpg|gif)$", "feh" }, + { "\\.(html|svg)$", "firefox" }, + { "\\.pdf$", "mupdf" }, + { "\\.sh$", "sh" }, + { ".", "less" }, +}; +.Ed +.Sh KNOWN ISSUES +If you are using urxvt you might have to set backspacekey 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 00000000..a588a55c --- /dev/null +++ b/noice.c @@ -0,0 +1,824 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) + +struct assoc { + char *regex; /* Regex to match on filename */ + char *bin; /* Program */ +}; + +/* 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_MTIME, + 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(®ex, assocs[i].regex, + REG_NOSUB | REG_EXTENDED | REG_ICASE) != 0) + continue; + if (regexec(®ex, file, 0, NULL, 0) == 0) { + bin = assocs[i].bin; + break; + } + } + 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; +} + +int +visible(regex_t *regex, char *file) +{ + return regexec(regex, file, 0, NULL, 0) == 0; +} + +int +entrycmp(const void *va, const void *vb) +{ + const struct entry *a = va, *b = vb; + + if (mtimeorder) + return b->t - a->t; + return strcmp(a->name, b->name); +} + +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); + } + 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); +} + +/* 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 = getch(); + if (c == -1) + idle++; + else + idle = 0; + + 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 maxlen = COLS - strlen(CURSR) - 1; + char cm = 0; + + /* Copy name locally */ + strlcpy(name, ent->name, sizeof(name)); + + if (S_ISDIR(ent->mode)) { + cm = '/'; + maxlen--; + } else if (S_ISLNK(ent->mode)) { + cm = '@'; + maxlen--; + } else if (S_ISSOCK(ent->mode)) { + cm = '='; + maxlen--; + } else if (S_ISFIFO(ent->mode)) { + cm = '|'; + maxlen--; + } else if (ent->mode & S_IXUSR) { + cm = '*'; + maxlen--; + } + + /* No text wrapping in entries */ + if (strlen(name) > maxlen) + name[maxlen] = '\0'; + + if (cm == 0) + printw("%s%s\n", active ? CURSR : EMPTY, name); + else + printw("%s%s%c\n", active ? CURSR : EMPTY, name, cm); +} + +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); + + 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; + 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: + if (strcmp(fltr, ifilter) != 0) + strlcpy(fltr, ifilter, sizeof(fltr)); + else + strlcpy(fltr, ".", 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_REDRAW: + /* Save current */ + if (ndents > 0) + mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); + goto begin; + case SEL_RUN: + run = xgetenv(env, run); + exitcurses(); + spawn(run, NULL, path); + initcurses(); + break; + case SEL_RUNARG: + run = xgetenv(env, run); + exitcurses(); + spawn(run, dents[cur].name, path); + initcurses(); + break; + } + /* 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) + ifilter = "."; + else + ifilter = "^[^.]"; /* Hide dotfiles */ + + 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 00000000..bc523fb2 --- /dev/null +++ b/strlcat.c @@ -0,0 +1,55 @@ +/* + * Copyright (c) 1998, 2015 Todd C. Miller + * + * 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 +#include + +#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 00000000..0ec2b787 --- /dev/null +++ b/strlcpy.c @@ -0,0 +1,50 @@ +/* + * Copyright (c) 1998, 2015 Todd C. Miller + * + * 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 +#include + +#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 00000000..c4d1904b --- /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);