commit 8775a527799aa9d023abb412ae537b79c52494b3
parent c42790aa189339a9562128b486ebdd82fed79f8c
Author: Arun Prakash Jana <engineerarun@gmail.com>
Date: Thu, 30 Mar 2017 23:55:30 +0530
Rename project to nnn (Noice is Not Noice)
Diffstat:
| M | Makefile | | | 22 | +++++++++++----------- |
| M | README.md | | | 56 | ++++++++++++++++++++++++++++---------------------------- |
| A | nnn.1 | | | 144 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | nnn.c | | | 985 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | noice.1 | | | 138 | ------------------------------------------------------------------------------- |
| D | noice.c | | | 985 | ------------------------------------------------------------------------------- |
6 files changed, 1168 insertions(+), 1162 deletions(-)
diff --git 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
@@ -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
@@ -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 <engineerarun@gmail.com>.
+.br
+More details: https://github.com/jarun/nnn
diff --git a/nnn.c b/nnn.c
@@ -0,0 +1,985 @@
+/* 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 <time.h>
+
+#include "util.h"
+
+#ifdef DEBUG
+#define DEBUG_FD 8
+#define DPRINTF_D(x) dprintf(DEBUG_FD, #x "=%d\n", x)
+#define DPRINTF_U(x) dprintf(DEBUG_FD, #x "=%u\n", x)
+#define DPRINTF_S(x) dprintf(DEBUG_FD, #x "=%s\n", x)
+#define DPRINTF_P(x) dprintf(DEBUG_FD, #x "=0x%p\n", x)
+#else
+#define DPRINTF_D(x)
+#define DPRINTF_U(x)
+#define DPRINTF_S(x)
+#define DPRINTF_P(x)
+#endif /* DEBUG */
+
+#define LEN(x) (sizeof(x) / sizeof(*(x)))
+#undef MIN
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define ISODD(x) ((x) & 1)
+#define CONTROL(c) ((c) ^ 0x40)
+#define 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
@@ -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 <engineerarun@gmail.com>.
-.br
-More details: https://github.com/jarun/noice
diff --git a/noice.c b/noice.c
@@ -1,985 +0,0 @@
-/* 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 <time.h>
-
-#include "util.h"
-
-#ifdef DEBUG
-#define DEBUG_FD 8
-#define DPRINTF_D(x) dprintf(DEBUG_FD, #x "=%d\n", x)
-#define DPRINTF_U(x) dprintf(DEBUG_FD, #x "=%u\n", x)
-#define DPRINTF_S(x) dprintf(DEBUG_FD, #x "=%s\n", x)
-#define DPRINTF_P(x) dprintf(DEBUG_FD, #x "=0x%p\n", x)
-#else
-#define DPRINTF_D(x)
-#define DPRINTF_U(x)
-#define DPRINTF_S(x)
-#define DPRINTF_P(x)
-#endif /* DEBUG */
-
-#define LEN(x) (sizeof(x) / sizeof(*(x)))
-#undef MIN
-#define MIN(x, y) ((x) < (y) ? (x) : (y))
-#define ISODD(x) ((x) & 1)
-#define CONTROL(c) ((c) ^ 0x40)
-#define 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);
-}