diff options
author | John Ankarstr\xf6m <john@ankarstrom.se> | 2021-06-01 03:05:17 +0200 |
---|---|---|
committer | John Ankarstr\xf6m <john@ankarstrom.se> | 2021-06-01 03:05:17 +0200 |
commit | 64f2f6b907e40fa48ab1287ae60a800df7df3213 (patch) | |
tree | 132b5f4a3e25ce92a98a202257c126b05d900fec | |
download | noice-64f2f6b907e40fa48ab1287ae60a800df7df3213.tar.gz |
First commit (0.8)
-rw-r--r-- | LICENSE | 24 | ||||
-rw-r--r-- | Makefile | 45 | ||||
-rw-r--r-- | README | 62 | ||||
-rw-r--r-- | config.def.h | 99 | ||||
-rw-r--r-- | noice.1 | 149 | ||||
-rw-r--r-- | noice.c | 919 | ||||
-rw-r--r-- | strlcat.c | 55 | ||||
-rw-r--r-- | strlcpy.c | 50 | ||||
-rw-r--r-- | util.h | 5 |
9 files changed, 1408 insertions, 0 deletions
@@ -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 @@ -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" }, +}; @@ -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 . @@ -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(®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 <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 */ +} @@ -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); |