@@ -0,0 +1,24 @@ | |||||
Copyright (c) 2014-2016 Lazaros Koromilas <lostd@2f30.org> | |||||
Copyright (c) 2014-2016 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. |
@@ -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 |
@@ -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. |
@@ -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" }, | |||||
}; |
@@ -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 |
@@ -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 . |
@@ -0,0 +1,824 @@ | |||||
/* 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) | |||||
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); | |||||
} |
@@ -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 */ | |||||
} |
@@ -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); |