From 64f2f6b907e40fa48ab1287ae60a800df7df3213 Mon Sep 17 00:00:00 2001 From: "John Ankarstr\\xf6m" Date: Tue, 1 Jun 2021 03:05:17 +0200 Subject: First commit (0.8) --- LICENSE | 24 ++ Makefile | 45 +++ README | 62 ++++ config.def.h | 99 +++++++ noice.1 | 149 ++++++++++ noice.c | 919 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ strlcat.c | 55 ++++ strlcpy.c | 50 ++++ util.h | 5 + 9 files changed, 1408 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 config.def.h create mode 100644 noice.1 create mode 100644 noice.c create mode 100644 strlcat.c create mode 100644 strlcpy.c create mode 100644 util.h 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 +Copyright (c) 2014-2019 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 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 +#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) +#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(®ex, assocs[i].regex, + REG_NOSUB | REG_EXTENDED | REG_ICASE) != 0) + continue; + if (regexec(®ex, file, 0, NULL, 0) == 0) { + bin = assocs[i].bin; + regfree(®ex); + break; + } + regfree(®ex); + } + 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 + * + * 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 0000000..0ec2b78 --- /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 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); -- cgit v1.2.3