@@ -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); |