From 8775a527799aa9d023abb412ae537b79c52494b3 Mon Sep 17 00:00:00 2001 From: Arun Prakash Jana Date: Thu, 30 Mar 2017 23:55:30 +0530 Subject: Rename project to nnn (Noice is Not Noice) --- Makefile | 22 +- README.md | 56 ++-- nnn.1 | 144 +++++++++ nnn.c | 985 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ noice.1 | 138 --------- noice.c | 985 -------------------------------------------------------------- 6 files changed, 1168 insertions(+), 1162 deletions(-) create mode 100644 nnn.1 create mode 100644 nnn.c delete mode 100644 noice.1 delete mode 100644 noice.c diff --git a/Makefile b/Makefile index bd67f31..aa0dd28 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,10 @@ MANPREFIX = $(PREFIX)/man CFLAGS = -O3 -march=native 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 +DISTFILES = nnn.c strlcat.c strlcpy.c util.h config.def.h\ + nnn.1 Makefile README.md LICENSE +OBJ = nnn.o strlcat.o strlcpy.o +BIN = nnn all: $(BIN) @@ -19,7 +19,7 @@ $(BIN): $(OBJ) $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) strip $(BIN) -noice.o: util.h config.h +nnn.o: util.h config.h strlcat.o: util.h strlcpy.o: util.h @@ -37,11 +37,11 @@ uninstall: 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) + mkdir -p nnn-$(VERSION) + cp $(DISTFILES) nnn-$(VERSION) + tar -cf nnn-$(VERSION).tar nnn-$(VERSION) + gzip nnn-$(VERSION).tar + rm -rf nnn-$(VERSION) clean: - rm -f config.h $(BIN) $(OBJ) noice-$(VERSION).tar.gz + rm -f config.h $(BIN) $(OBJ) nnn-$(VERSION).tar.gz diff --git a/README.md b/README.md index 5912a98..dbb5f8c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -## noice +## nnn -A fork of the [noice](http://git.2f30.org/noice/) file browser to make it more friendly towards major distros (which `suck more` by some standards). +Noice is Not Noice, a noicer fork... ### Table of Contents - [Introduction](#introduction) - [Why fork?](#why-fork) -- [Default features](#default-features) -- [Fork toppings](#fork-toppings) +- [Original features](#original-features) +- [nnn toppings](#nnn-toppings) - [Installation](#installation) - [Usage](#usage) - [Keyboard shortcuts](#keyboard-shortcuts) @@ -18,19 +18,19 @@ A fork of the [noice](http://git.2f30.org/noice/) file browser to make it more f ### Introduction -noice is a blazing-fast terminal file browser with easy keyboard shortcuts for navigation, opening files and running tasks. noice is developed with terminal based systems in mind. However, the incredible user-friendliness and speed make it a perfect utility on modern distros. Navigate to `/usr/bin` from your regular file browser and noice to feel the difference. +nnn is a fork of [noice](http://git.2f30.org/noice/), a blazing-fast terminal file browser with easy keyboard shortcuts for navigation, opening files and running tasks. It is developed with terminal based systems in mind. However, the incredible user-friendliness and speed make it a perfect utility on modern distros. -The only issue with noice is hard-coded file association. There is no config file (better performance and simpler to maintain) and you have to modify the source to change associations (see [how to change file associations](#change-file-associations)). This fork solves the problem by adding the flexibility of using the default desktop opener at runtime. There are several other improvements too (see [fork-toppings](#fork-toppings)). +The only issue with noice is hard-coded file associations. There is no config file (better performance and simpler to maintain) and one has to modify the source to change associations (see [how to change file associations](#change-file-associations)). nnn solves the problem by adding the flexibility of using the default desktop opener at runtime. There are several other improvements too (see [fork-toppings](#fork-toppings)). -Have fun with it! PRs are welcome. Check out [#1](https://github.com/jarun/noice/issues/1). +Have fun with it! PRs are welcome. Check out [#1](https://github.com/jarun/nnn/issues/1). ### Why fork? -I chose to fork noice because: +I chose to fork because: - one can argue my approach deviates from the goal of the original project - keep the utility `suckless`. In my opinion evolution is the taste of time. - I would like to have a bit of control on what features are added in the name of desktop integration. A feature-bloat is the last thing in my mind. -### Default features +### Original features - Super-easy navigation - Open files with default-associated programs @@ -42,7 +42,7 @@ I chose to fork noice because: - Run `top` - Open a file with `vim` or `less` -### Fork toppings +### nnn toppings - Behaviour and navigation - Detail view (default: disabled) with: @@ -55,20 +55,20 @@ I chose to fork noice because: - Case-insensitive alphabetic content listing instead of upper case first - Roll over at the first and last entries of a directory (with Up/Down keys) - Sort entries by file size (largest to smallest) - - Shortcut to invoke file name copier (set using environment variable `NOICE_COPIER`) + - Shortcut to invoke file name copier (set using environment variable `NNN_COPIER`) - File associations - - Environment variable `NOICE_OPENER` to override all associations and open all files with your desktop environment's default file opener. Examples: + - Environment variable `NNN_OPENER` to override all associations and open all files with your desktop environment's default file opener. Examples: - export NOICE_OPENER=xdg-open - export NOICE_OPENER=gnome-open - export NOICE_OPENER=gvfs-open - - Selective file associations (ignored if `NOICE_OPENER` is set): + export NNN_OPENER=xdg-open + export NNN_OPENER=gnome-open + export NNN_OPENER=gvfs-open + - Selective file associations (ignored if `NNN_OPENER` is set): - Associate plain text files with vim (using `file` command) - Remove video file associations (to each his own favourite video player) - Associate common audio mimes with lightweight [fmedia](http://fmedia.firmdev.com/) - Associate PDF files with [zathura](https://pwmt.org/projects/zathura/) - Removed `less` as default file opener - - Use environment variable `NOICE_FALLBACK_OPENER` to open other non-associated files + - Use environment variable `NNN_FALLBACK_OPENER` to open other non-associated files - Compilation - Use `-O3` for compilation, fixed warnings - Added compilation flag `-march=native` @@ -77,9 +77,9 @@ I chose to fork noice because: ### Installation -noice needs a curses implementation and standard libc. +nnn needs a curses implementation and standard libc. -Download the [latest master](https://github.com/jarun/noice/archive/master.zip) or clone this repository. Compile and install: +Download the [latest master](https://github.com/jarun/nnn/archive/master.zip) or clone this repository. Compile and install: $ make $ sudo make install @@ -87,9 +87,9 @@ No plans of packaging at the time. ### Usage -Start noice (default: current directory): +Start nnn (default: current directory): - $ noice [path_to_dir] + $ nnn [path_to_dir] `>` indicates the currently selected entry. ### Keyboard shortcuts @@ -117,7 +117,7 @@ Start noice (default: current directory): | `z` | run `top` | | `Ctrl-k` | invoke file name copier | | `Ctrl-l` | redraw window | -| `q` | quit noice | +| `q` | quit | ### File type abbreviations @@ -135,11 +135,11 @@ The following abbreviations are used in the detail view: ### Help - $ man noice + $ man nnn ### Copy current file path to clipboard -noice can pipe the absolute path of the current file to a copier script. For example, you can use `xsel` on Linux or `pbcopy` on OS X. +nnn can pipe the absolute path of the current file to a copier script. For example, you can use `xsel` on Linux or `pbcopy` on OS X. Sample Linux copier script: @@ -147,12 +147,12 @@ Sample Linux copier script: echo -n $1 | xsel --clipboard --input -export `NOICE_OPENER`: +export `NNN_OPENER`: - export NOICE_COPIER="/home/vaio/copier.sh" + export NNN_COPIER="/home/vaio/copier.sh" -Start noice and use `Ctrl-k` to copy the absolute path (from `/`) of the file under the cursor to clipboard. +Start nnn and use `Ctrl-k` to copy the absolute path (from `/`) of the file under the cursor to clipboard. ### Change file associations -If you want to set custom applications for certain mime types, or change the ones set already (e.g. vim, fmedia, zathura), modify the `assocs` structure in [config.def.h](https://github.com/jarun/noice/blob/master/config.def.h) (it's easy). Then re-compile and install. +If you want to set custom applications for certain mime types, or change the ones set already (e.g. vim, fmedia, zathura), modify the `assocs` structure in [config.def.h](https://github.com/jarun/nnn/blob/master/config.def.h) (it's easy). Then re-compile and install. diff --git a/nnn.1 b/nnn.1 new file mode 100644 index 0000000..1010d10 --- /dev/null +++ b/nnn.1 @@ -0,0 +1,144 @@ +.Dd August 21, 2016 +.Dt NNN 1 +.Os +.Sh NAME +.Nm nnn +.Nd small file browser +.Sh SYNOPSIS +.Nm nnn +.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. +.Pp +\fBNNN_OPENER:\fR set to your desktop environment's default +mime opener to override all custom mime associations. +.br +Examples: xdg-open, gnome-open, gvfs-open. +.Pp +\fBNNN_FALLBACK_OPENER:\fR set to your desktop environment's default +mime opener to use as a fallback when no association is set for a file +type. Custom associations are listed in the EXAMPLES section below. +.Pp +\fBNNN_COPIER:\fR set to a clipboard copier script. For example, on Linux: +.Bd -literal + #!/bin/sh + + echo -n $1 | xsel --clipboard --input +.Sh EXAMPLES +The following example shows one possible configuration for +file associations which is also the default if environment +variable NNN_OPENER is not set: +.Bd -literal + struct assoc assocs[] = { + { "\\.(c|cpp|h|txt|log)$", "vim" }, + { "\\.pdf$", "zathura" }, + { "\\.sh$", "sh" }, + }; +Plain text files are opened with vim. +.br +Any other file types are opened with the 'xdg-open' command. +.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 . +.Pp +** The current non-mainstream version is a modified one patched by Arun Prakash Jana . +.br +More details: https://github.com/jarun/nnn diff --git a/nnn.c b/nnn.c new file mode 100644 index 0000000..dc19656 --- /dev/null +++ b/nnn.c @@ -0,0 +1,985 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 TOUPPER(ch) \ + (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch)) +#define MAX_LEN 1024 +#define cur(flag) (flag ? CURSR : EMPTY) + +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_DETAIL, + SEL_FSIZE, + SEL_MTIME, + SEL_REDRAW, + SEL_COPY, + 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; + off_t size; +}; + +/* Global context */ +struct entry *dents; +int ndents, cur; +int idle; +char *opener = NULL; +char *fallback_opener = NULL; +char *copier = NULL; +char size_buf[12]; /* Buffer to hold human readable size */ +const char* size_units[] = {"B", "K", "M", "G", "T", "P", "E", "Z", "Y"}; + +/* + * Layout: + * .--------- + * | cwd: /mnt/path + * | + * | file0 + * | file1 + * | > file2 + * | file3 + * | file4 + * ... + * | filen + * | + * | Permission denied + * '------ + */ + +void (*printptr)(struct entry *ent, int active); +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) + r = 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) + status = 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; +} + +int +xstricmp(const char *s1, const char *s2) +{ + while (*s2 != 0 && TOUPPER(*s1) == TOUPPER(*s2)) + s1++, s2++; + + /* In case of alphabetically same names, make sure + lower case one comes before upper case one */ + if (!*s1 && !*s2) + return 1; + return (int) (TOUPPER(*s1) - TOUPPER(*s2)); +} + +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; +} + +void +initfilter(int dot, char **ifilter) +{ + *ifilter = dot ? "." : "^[^.]"; +} + +int +visible(regex_t *regex, char *file) +{ + return regexec(regex, file, 0, NULL, 0) == 0; +} + +int +entrycmp(const void *va, const void *vb) +{ + if (mtimeorder) + return ((struct entry *)vb)->t - ((struct entry *)va)->t; + + if (sizeorder) + return ((struct entry *)vb)->size - ((struct entry *)va)->size; + + return xstricmp(((struct entry *)va)->name, ((struct entry *)vb)->name); +} + +void +initcurses(void) +{ + if (initscr() == NULL) { + char *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) + snprintf(out, n, "/%s", name); + else + snprintf(out, n, "%s/%s", dir, name); + } + return out; +} + +void +printent(struct entry *ent, int active) +{ + if (S_ISDIR(ent->mode)) + printw("%s%s/\n", active ? CURSR : EMPTY, ent->name); + else if (S_ISLNK(ent->mode)) + printw("%s%s@\n", active ? CURSR : EMPTY, ent->name); + else if (S_ISSOCK(ent->mode)) + printw("%s%s=\n", active ? CURSR : EMPTY, ent->name); + else if (S_ISFIFO(ent->mode)) + printw("%s%s|\n", active ? CURSR : EMPTY, ent->name); + else if (ent->mode & S_IXUSR) + printw("%s%s*\n", active ? CURSR : EMPTY, ent->name); + else + printw("%s%s\n", active ? CURSR : EMPTY, ent->name); +} + +char* +coolsize(off_t size) +{ + int i = 0; + long double fsize = (double)size; + + while (fsize > 1024) { + fsize /= 1024; + i++; + } + + snprintf(size_buf, 12, "%.*Lf%s", i, fsize, size_units[i]); + return size_buf; +} + +void +printent_long(struct entry *ent, int active) +{ + static char buf[18]; + const static struct tm *p; + static char name[PATH_MAX + 2]; + + p = localtime(&ent->t); + strftime(buf, 18, "%b %d %H:%M %Y", p); + + if (active) + attron(A_REVERSE); + + if (S_ISDIR(ent->mode)) { + sprintf(name, "%s/", ent->name); + printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); + } else if (S_ISLNK(ent->mode)) { + sprintf(name, "%s@", ent->name); + printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); + } else if (S_ISSOCK(ent->mode)) { + sprintf(name, "%s=", ent->name); + printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); + } else if (S_ISFIFO(ent->mode)) { + sprintf(name, "%s|", ent->name); + printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); + } else if (S_ISBLK(ent->mode)) + printw("%s%-32.32s b %-18.18s\n", cur(active), ent->name, buf); + else if (S_ISCHR(ent->mode)) + printw("%s%-32.32s c %-18.18s\n", cur(active), ent->name, buf); + else if (ent->mode & S_IXUSR) { + sprintf(name, "%s*", ent->name); + printw("%s%-32.32s %-18.18s %s\n", cur(active), name, + buf, coolsize(ent->size)); + } else + printw("%s%-32.32s %-18.18s %s\n", cur(active), ent->name, + buf, coolsize(ent->size)); + + if (active) + attroff(A_REVERSE); +} + +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; + (*dents)[n].size = sb.st_size; + 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'; + if (!realpath(path, cwdresolved)) { + printmsg("Cannot resolve path"); + return; + } + + printw(CWD "%s\n\n", cwdresolved); + + /* Print listing */ + odd = ISODD(nlines); + if (cur < (nlines >> 1)) { + for (i = 0; i < nlines; i++) + printptr(&dents[i], i == cur); + } else if (cur >= ndents - (nlines >> 1)) { + for (i = ndents - nlines; i < ndents; i++) + printptr(&dents[i], i == cur); + } else { + nlines >>= 1; + for (i = cur - nlines; i < cur + nlines + odd; i++) + printptr(&dents[i], i == cur); + } + + if (showdetail) { + if (ndents) { + sprintf(cwd, "%d items [%s]", ndents, dents[cur].name); + printmsg(cwd); + } else + printmsg("0 items"); + } +} + +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: + /* If default mime opener is set, use it */ + if (opener) { + char cmd[MAX_LEN]; + int status; + + snprintf(cmd, MAX_LEN, "%s \"%s\" > /dev/null 2>&1", + opener, newpath); + status = system(cmd); + continue; + } + + /* Try custom applications */ + bin = openwith(newpath); + char *execvim = "vim"; + + if (bin == NULL) { + /* If a custom handler application is not set, open + plain text files with vim, then try fallback_opener */ + FILE *fp; + char cmd[MAX_LEN]; + int status; + + snprintf(cmd, MAX_LEN, "file \"%s\"", newpath); + fp = popen(cmd, "r"); + if (fp == NULL) + goto nochange; + if (fgets(cmd, MAX_LEN, fp) == NULL) { + pclose(fp); + goto nochange; + } + pclose(fp); + + if (strstr(cmd, "ASCII text") != NULL) + bin = execvim; + else if (fallback_opener) { + snprintf(cmd, MAX_LEN, "%s \"%s\" > /dev/null 2>&1", + fallback_opener, newpath); + status = system(cmd); + continue; + } else { + 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++; + else if (ndents) + /* Roll over, set cursor to first entry */ + cur = 0; + break; + case SEL_PREV: + if (cur > 0) + cur--; + else if (ndents) + /* Roll over, set cursor to last entry */ + cur = ndents - 1; + 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_DETAIL: + showdetail = !showdetail; + showdetail ? (printptr = &printent_long) : (printptr = &printent); + /* Save current */ + if (ndents > 0) + mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); + goto begin; + case SEL_FSIZE: + sizeorder = !sizeorder; + mtimeorder = 0; + /* Save current */ + if (ndents > 0) + mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); + goto begin; + case SEL_MTIME: + mtimeorder = !mtimeorder; + sizeorder = 0; + /* 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_COPY: + if (copier && ndents) { + char abspath[PATH_MAX]; + + if (strcmp(path, "/") == 0) + snprintf(abspath, PATH_MAX, "/%s", dents[cur].name); + else + snprintf(abspath, PATH_MAX, "%s/%s", path, dents[cur].name); + spawn(copier, abspath, NULL); + printmsg(abspath); + } else if (!copier) + printmsg("NNN_COPIER is not set"); + goto nochange; + case SEL_RUN: + run = xgetenv(env, run); + exitcurses(); + spawn(run, NULL, path); + initcurses(); + /* Re-populate as directory content may have changed */ + goto begin; + 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) + showhidden = 1; + initfilter(showhidden, &ifilter); + + printptr = &printent; + + if (argv[1] != NULL) { + ipath = argv[1]; + } else { + ipath = getcwd(cwd, sizeof(cwd)); + if (ipath == NULL) + ipath = "/"; + } + + /* Get the default desktop mime opener, if set */ + opener = getenv("NNN_OPENER"); + + /* Get the fallback desktop mime opener, if set */ + fallback_opener = getenv("NNN_FALLBACK_OPENER"); + + /* Get the default copier, if set */ + copier = getenv("NNN_COPIER"); + + 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/noice.1 b/noice.1 deleted file mode 100644 index d39be8f..0000000 --- a/noice.1 +++ /dev/null @@ -1,138 +0,0 @@ -.Dd August 21, 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. -.Pp -\fBNOICE_OPENER:\fR set to your desktop environment's default -mime opener to override all custom mime associations. -.br -Examples: xdg-open, gnome-open, gvfs-open. -.Pp -\fBNOICE_FALLBACK_OPENER:\fR set to your desktop environment's default -mime opener to use as a fallback when no association is set for a file -type. Custom associations are listed in the EXAMPLES section below. -.Sh EXAMPLES -The following example shows one possible configuration for -file associations which is also the default if environment -variable NOICE_OPENER is not set: -.Bd -literal -struct assoc assocs[] = { - { "\\.(c|cpp|h|txt|log)$", "vim" }, - { "\\.pdf$", "zathura" }, - { "\\.sh$", "sh" }, -}; -Plain text files are opened with vim. -.br -Any other file types are opened with the 'xdg-open' command. -.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 . -.Pp -** The current non-mainstream version is a modified one patched by Arun Prakash Jana . -.br -More details: https://github.com/jarun/noice diff --git a/noice.c b/noice.c deleted file mode 100644 index 6f64b5e..0000000 --- a/noice.c +++ /dev/null @@ -1,985 +0,0 @@ -/* See LICENSE file for copyright and license details. */ -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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 TOUPPER(ch) \ - (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch)) -#define MAX_LEN 1024 -#define cur(flag) (flag ? CURSR : EMPTY) - -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_DETAIL, - SEL_FSIZE, - SEL_MTIME, - SEL_REDRAW, - SEL_COPY, - 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; - off_t size; -}; - -/* Global context */ -struct entry *dents; -int ndents, cur; -int idle; -char *opener = NULL; -char *fallback_opener = NULL; -char *copier = NULL; -char size_buf[12]; /* Buffer to hold human readable size */ -const char* size_units[] = {"B", "K", "M", "G", "T", "P", "E", "Z", "Y"}; - -/* - * Layout: - * .--------- - * | cwd: /mnt/path - * | - * | file0 - * | file1 - * | > file2 - * | file3 - * | file4 - * ... - * | filen - * | - * | Permission denied - * '------ - */ - -void (*printptr)(struct entry *ent, int active); -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) - r = 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) - status = 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; -} - -int -xstricmp(const char *s1, const char *s2) -{ - while (*s2 != 0 && TOUPPER(*s1) == TOUPPER(*s2)) - s1++, s2++; - - /* In case of alphabetically same names, make sure - lower case one comes before upper case one */ - if (!*s1 && !*s2) - return 1; - return (int) (TOUPPER(*s1) - TOUPPER(*s2)); -} - -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; -} - -void -initfilter(int dot, char **ifilter) -{ - *ifilter = dot ? "." : "^[^.]"; -} - -int -visible(regex_t *regex, char *file) -{ - return regexec(regex, file, 0, NULL, 0) == 0; -} - -int -entrycmp(const void *va, const void *vb) -{ - if (mtimeorder) - return ((struct entry *)vb)->t - ((struct entry *)va)->t; - - if (sizeorder) - return ((struct entry *)vb)->size - ((struct entry *)va)->size; - - return xstricmp(((struct entry *)va)->name, ((struct entry *)vb)->name); -} - -void -initcurses(void) -{ - if (initscr() == NULL) { - char *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) - snprintf(out, n, "/%s", name); - else - snprintf(out, n, "%s/%s", dir, name); - } - return out; -} - -void -printent(struct entry *ent, int active) -{ - if (S_ISDIR(ent->mode)) - printw("%s%s/\n", active ? CURSR : EMPTY, ent->name); - else if (S_ISLNK(ent->mode)) - printw("%s%s@\n", active ? CURSR : EMPTY, ent->name); - else if (S_ISSOCK(ent->mode)) - printw("%s%s=\n", active ? CURSR : EMPTY, ent->name); - else if (S_ISFIFO(ent->mode)) - printw("%s%s|\n", active ? CURSR : EMPTY, ent->name); - else if (ent->mode & S_IXUSR) - printw("%s%s*\n", active ? CURSR : EMPTY, ent->name); - else - printw("%s%s\n", active ? CURSR : EMPTY, ent->name); -} - -char* -coolsize(off_t size) -{ - int i = 0; - long double fsize = (double)size; - - while (fsize > 1024) { - fsize /= 1024; - i++; - } - - snprintf(size_buf, 12, "%.*Lf%s", i, fsize, size_units[i]); - return size_buf; -} - -void -printent_long(struct entry *ent, int active) -{ - static char buf[18]; - const static struct tm *p; - static char name[PATH_MAX + 2]; - - p = localtime(&ent->t); - strftime(buf, 18, "%b %d %H:%M %Y", p); - - if (active) - attron(A_REVERSE); - - if (S_ISDIR(ent->mode)) { - sprintf(name, "%s/", ent->name); - printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); - } else if (S_ISLNK(ent->mode)) { - sprintf(name, "%s@", ent->name); - printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); - } else if (S_ISSOCK(ent->mode)) { - sprintf(name, "%s=", ent->name); - printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); - } else if (S_ISFIFO(ent->mode)) { - sprintf(name, "%s|", ent->name); - printw("%s%-32.32s %-18.18s\n", cur(active), name, buf); - } else if (S_ISBLK(ent->mode)) - printw("%s%-32.32s b %-18.18s\n", cur(active), ent->name, buf); - else if (S_ISCHR(ent->mode)) - printw("%s%-32.32s c %-18.18s\n", cur(active), ent->name, buf); - else if (ent->mode & S_IXUSR) { - sprintf(name, "%s*", ent->name); - printw("%s%-32.32s %-18.18s %s\n", cur(active), name, - buf, coolsize(ent->size)); - } else - printw("%s%-32.32s %-18.18s %s\n", cur(active), ent->name, - buf, coolsize(ent->size)); - - if (active) - attroff(A_REVERSE); -} - -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; - (*dents)[n].size = sb.st_size; - 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'; - if (!realpath(path, cwdresolved)) { - printmsg("Cannot resolve path"); - return; - } - - printw(CWD "%s\n\n", cwdresolved); - - /* Print listing */ - odd = ISODD(nlines); - if (cur < (nlines >> 1)) { - for (i = 0; i < nlines; i++) - printptr(&dents[i], i == cur); - } else if (cur >= ndents - (nlines >> 1)) { - for (i = ndents - nlines; i < ndents; i++) - printptr(&dents[i], i == cur); - } else { - nlines >>= 1; - for (i = cur - nlines; i < cur + nlines + odd; i++) - printptr(&dents[i], i == cur); - } - - if (showdetail) { - if (ndents) { - sprintf(cwd, "%d items [%s]", ndents, dents[cur].name); - printmsg(cwd); - } else - printmsg("0 items"); - } -} - -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: - /* If default mime opener is set, use it */ - if (opener) { - char cmd[MAX_LEN]; - int status; - - snprintf(cmd, MAX_LEN, "%s \"%s\" > /dev/null 2>&1", - opener, newpath); - status = system(cmd); - continue; - } - - /* Try custom applications */ - bin = openwith(newpath); - char *execvim = "vim"; - - if (bin == NULL) { - /* If a custom handler application is not set, open - plain text files with vim, then try fallback_opener */ - FILE *fp; - char cmd[MAX_LEN]; - int status; - - snprintf(cmd, MAX_LEN, "file \"%s\"", newpath); - fp = popen(cmd, "r"); - if (fp == NULL) - goto nochange; - if (fgets(cmd, MAX_LEN, fp) == NULL) { - pclose(fp); - goto nochange; - } - pclose(fp); - - if (strstr(cmd, "ASCII text") != NULL) - bin = execvim; - else if (fallback_opener) { - snprintf(cmd, MAX_LEN, "%s \"%s\" > /dev/null 2>&1", - fallback_opener, newpath); - status = system(cmd); - continue; - } else { - 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++; - else if (ndents) - /* Roll over, set cursor to first entry */ - cur = 0; - break; - case SEL_PREV: - if (cur > 0) - cur--; - else if (ndents) - /* Roll over, set cursor to last entry */ - cur = ndents - 1; - 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_DETAIL: - showdetail = !showdetail; - showdetail ? (printptr = &printent_long) : (printptr = &printent); - /* Save current */ - if (ndents > 0) - mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); - goto begin; - case SEL_FSIZE: - sizeorder = !sizeorder; - mtimeorder = 0; - /* Save current */ - if (ndents > 0) - mkpath(path, dents[cur].name, oldpath, sizeof(oldpath)); - goto begin; - case SEL_MTIME: - mtimeorder = !mtimeorder; - sizeorder = 0; - /* 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_COPY: - if (copier && ndents) { - char abspath[PATH_MAX]; - - if (strcmp(path, "/") == 0) - snprintf(abspath, PATH_MAX, "/%s", dents[cur].name); - else - snprintf(abspath, PATH_MAX, "%s/%s", path, dents[cur].name); - spawn(copier, abspath, NULL); - printmsg(abspath); - } else if (!copier) - printmsg("NOICE_COPIER is not set"); - goto nochange; - case SEL_RUN: - run = xgetenv(env, run); - exitcurses(); - spawn(run, NULL, path); - initcurses(); - /* Re-populate as directory content may have changed */ - goto begin; - 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) - showhidden = 1; - initfilter(showhidden, &ifilter); - - printptr = &printent; - - if (argv[1] != NULL) { - ipath = argv[1]; - } else { - ipath = getcwd(cwd, sizeof(cwd)); - if (ipath == NULL) - ipath = "/"; - } - - /* Get the default desktop mime opener, if set */ - opener = getenv("NOICE_OPENER"); - - /* Get the fallback desktop mime opener, if set */ - fallback_opener = getenv("NOICE_FALLBACK_OPENER"); - - /* Get the default copier, if set */ - copier = getenv("NOICE_COPIER"); - - 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); -} -- cgit v1.2.3-70-g09d2