- *navigate-as-you-type* (*search-as-you-type* enabled even on directory switch)
- check disk usage with number of files in current directory tree
- run desktop search utility (gnome-search-tool or catfish) in any directory
-- copy absolute file path to clipboard, spawn a terminal and use the file path
+- copy absolute file paths to clipboard, spawn a terminal and use the paths
- navigate instantly using shortcuts like `~`, `-`, `&` or handy bookmarks
- use `cd .....` at chdir prompt to go to a parent directory
- detailed file stats, media info, list and extract archives
- [add bookmarks](#add-bookmarks)
- [use cd .....](#use-cd-)
- [cd on quit](#cd-on-quit)
- - [copy file path to clipboard](#copy-file-path-to-clipboard)
+ - [copy file paths to clipboard](#copy-file-paths-to-clipboard)
- [change dir color](#change-dir-color)
- [file copy, move, delete](#file-copy-move-delete)
- [boost chdir prompt](#boost-chdir-prompt)
F | List archive
^F | Extract archive
^K | Invoke file path copier
+ ^Y | Toggle multi-copy mode
^L | Redraw, clear prompt
? | Help, settings
Q | Quit and cd
As you might notice, `nnn` uses the environment variable `NNN_TMPFILE` to write the last visited directory path. You can change it.
-#### copy file path to clipboard
+#### copy file paths to clipboard
-`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.
+`nnn` can pipe the absolute path of the current file or multiple files to a copier script. For example, you can use `xsel` on Linux or `pbcopy` on OS X.
Sample Linux copier script:
#!/bin/sh
+ # comment the next line to convert newlines to spaces
+ IFS=
+
echo -n $1 | xsel --clipboard --input
export `NNN_COPIER`:
export NNN_COPIER="/path/to/copier.sh"
-Start `nnn` and use <kbd>^K</kbd> to copy the absolute path (from `/`) of the file under the cursor to clipboard.
+Use <kbd>^K</kbd> to copy the absolute path (from `/`) of the file under the cursor to clipboard.
+
+To copy multiple file paths, switch to the multi-copy mode using <kbd>^Y</kbd>. In this mode you can
+
+- select multiple files one by one by pressing <kbd>^K</kbd> on each entry; or,
+- navigate to another file in the same directory to select a range of files.
+
+Pressing <kbd>^Y</kbd> again copies the paths to clipboard and exits the multi-copy mode.
#### change dir color
Extract archive in current directory
.It Ic ^K
Invoke file path copier
+.It Ic ^Y
+Toggle multiple file path copy mode
.It Ic ^L
Force a redraw, clear rename or filter prompt
.It Ic \&?
Filters support regexes to instantly (search-as-you-type) list the matching
entries in the current directory.
.Pp
-There are 3 ways to reset a filter: (1) pressing \fI^L\fR (at the new/rename
-prompt \fI^L\fR followed by \fIEnter\fR discards all changes and exits prompt),
-(2) a search with no matches or (3) an extra backspace at the filter prompt (like vi).
+There are 3 ways to reset a filter:
.Pp
-Common use cases: (1) To list all matches starting with the filter expression,
-start the expression with a '^' (caret) symbol. (2) Type '\\.mkv' to list all MKV files.
+(1) pressing \fI^L\fR (at the new/rename prompt \fI^L\fR followed by \fIEnter\fR
+discards all changes and exits prompt),
+.br
+(2) a search with no matches or
+.br
+(3) an extra backspace at the filter prompt (like vi).
+.Pp
+Common use cases:
+.Pp
+(1) To list all matches starting with the filter expression, start the expression
+with a '^' (caret) symbol.
+.br
+(2) Type '\\.mkv' to list all MKV files.
.Pp
If
.Nm
.Pp
In the \fInavigate-as-you-type\fR mode directories are opened in filter mode,
allowing continuous navigation. Works best with the \fBarrow keys\fR.
+.Sh MULTI-COPY MODE
+The absolute path of a single file can be copied to clipboard by pressing \fI^K\fR if
+NNN_COPIER is set (see ENVIRONMENT section below).
+.Pp
+To copy multiple file paths the multi-copy mode should be enabled using \fI^Y\fR.
+In this mode it's possible to
+.Pp
+(1) select multiple files one by one by pressing \fI^K\fR on each entry; or,
+.br
+(2) navigate to another file in the same directory to select a range of files.
+.Pp
+Pressing \fI^Y\fR again copies the paths to clipboard and exits the multi-copy mode.
.Sh ENVIRONMENT
The SHELL, EDITOR and PAGER environment variables take precedence
when dealing with the !, e and p commands respectively.
-------------------------------------
#!/bin/sh
+ # comment the next line to convert newlines to spaces
+ IFS=
echo -n $1 | xsel --clipboard --input
-------------------------------------
.Ed
#define F_SIGINT 0x08 /* restore default SIGINT handler */
#define F_NORMAL 0x80 /* spawn child process in non-curses regular CLI mode */
+/* CRC8 macros */
+#define WIDTH (8 * sizeof(unsigned char))
+#define TOPBIT (1 << (WIDTH - 1))
+#define POLYNOMIAL 0xD8 /* 11011 followed by 0's */
+
+/* Function macros */
#define exitcurses() endwin()
#define clearprompt() printmsg("")
#define printwarn() printmsg(strerror(errno))
ushort sizeorder : 1; /* Set to sort by file size */
ushort blkorder : 1; /* Set to sort by blocks used (disk usage) */
ushort showhidden : 1; /* Set to show hidden files */
+ ushort copymode : 1; /* Set when copying files */
ushort showdetail : 1; /* Clear to show fewer file info */
ushort showcolor : 1; /* Set to show dirs in blue */
ushort dircolor : 1; /* Current status of dir color */
/* GLOBALS */
/* Configuration */
-static settings cfg = {0, 0, 0, 0, 0, 1, 1, 0, 0, 4};
+static settings cfg = {0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 4};
static struct entry *dents;
-static char *pnamebuf;
+static char *pnamebuf, *pcopybuf;
static int ndents, cur, total_dents = ENTRY_INCR;
static uint idle;
-static uint idletimeout;
+static uint idletimeout, copybufpos, copybuflen;
static char *player;
static char *copier;
static char *editor;
static uint open_max;
static bm bookmark[BM_MAX];
+static uchar crc8table[256];
+static uchar g_crc;
+
#ifdef LINUX_INOTIFY
static int inotify_fd, inotify_wd = -1;
static uint INOTIFY_MASK = IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
static const char *STR_NOHOME = "HOME not set";
static const char *STR_INPUT = "No traversal delimiter allowed";
static const char *STR_INVBM = "Invalid bookmark";
+static const char *STR_COPY = "NNN_COPIER is not set";
static const char *STR_DATE = "%a %d %b %Y %T %z";
/* For use in functions which are isolated and don't return the buffer */
/* Functions */
+/*
+ * CRC8 source:
+ * https://barrgroup.com/Embedded-Systems/How-To/CRC-Calculation-C-Code
+ */
+static void
+crc8init()
+{
+ uchar remainder, bit;
+ uint dividend;
+
+ /* Compute the remainder of each possible dividend */
+ for (dividend = 0; dividend < 256; ++dividend)
+ {
+ /* Start with the dividend followed by zeros */
+ remainder = dividend << (WIDTH - 8);
+
+ /* Perform modulo-2 division, a bit at a time */
+ for (bit = 8; bit > 0; --bit)
+ {
+ /* Try to divide the current data bit */
+ if (remainder & TOPBIT)
+ remainder = (remainder << 1) ^ POLYNOMIAL;
+ else
+ remainder = (remainder << 1);
+ }
+
+ /* Store the result into the table */
+ crc8table[dividend] = remainder;
+ }
+}
+
+static uchar
+crc8fast(uchar const message[], size_t n)
+{
+ uchar data;
+ uchar remainder = 0;
+ size_t byte;
+
+
+ /* Divide the message by the polynomial, a byte at a time */
+ for (byte = 0; byte < n; ++byte)
+ {
+ data = message[byte] ^ (remainder >> (WIDTH - 8));
+ remainder = crc8table[data] ^ (remainder << 8);
+ }
+
+ /* The final remainder is the CRC */
+ return (remainder);
+
+}
+
/* Messages show up at the bottom */
static void
printmsg(const char *msg)
return limit;
}
+/*
+ * Wrapper to realloc()
+ * Frees current memory if realloc() fails and returns NULL.
+ *
+ * As per the docs, the *alloc() family is supposed to be memory aligned:
+ * Ubuntu: http://manpages.ubuntu.com/manpages/xenial/man3/malloc.3.html
+ * OS X: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man3/malloc.3.html
+ */
+static void *
+xrealloc(void *pcur, size_t len)
+{
+ static void *pmem;
+
+ pmem = realloc(pcur, len);
+ if (!pmem && pcur)
+ free(pcur);
+
+ return pmem;
+}
+
/*
* Custom xstrlen()
*/
return base ? base + 1 : path;
}
+static bool
+appendfilepath(const char *path, const size_t len)
+{
+ if ((copybufpos >= copybuflen) || (len > (copybuflen - (copybufpos + 1)))) {
+ copybuflen += PATH_MAX;
+ pcopybuf = xrealloc(pcopybuf, copybuflen);
+ if (!pcopybuf) {
+ printmsg("No memory!\n");
+ return FALSE;
+ }
+ }
+
+ if (copybufpos)
+ pcopybuf[copybufpos - 1] = '\n';
+
+ copybufpos += xstrlcpy(pcopybuf + copybufpos, path, len);
+ return TRUE;
+}
+
/*
* Return number of dots if all chars in a string are dots, else 0
*/
}
/*
- * Returns "dir/name or "/name"
+ * Updates out with "dir/name or "/name"
+ * Returns the number of bytes in out including the terminating NULL byte
*/
-static char *
+size_t
mkpath(char *dir, char *name, char *out, size_t n)
{
/* Handle absolute path */
if (name[0] == '/')
- xstrlcpy(out, name, n);
+ return xstrlcpy(out, name, n);
else {
/* Handle root case */
if (istopdir(dir))
- snprintf(out, n, "/%s", name);
+ return (snprintf(out, n, "/%s", name) + 1);
else
- snprintf(out, n, "%s/%s", dir, name);
+ return (snprintf(out, n, "%s/%s", dir, name) + 1);
}
- return out;
+
+ return 0;
}
static void
"eF | List archive\n"
"d^F | Extract archive\n"
"d^K | Invoke file path copier\n"
+ "d^Y | Toggle multi-copy mode\n"
"d^L | Redraw, clear prompt\n"
"e? | Help, settings\n"
"eQ | Quit and cd\n"
return 0;
}
-/*
- * Wrapper to realloc()
- * Frees current memory if realloc() fails and returns NULL.
- *
- * As per the docs, the *alloc() family is supposed to be memory aligned:
- * Ubuntu: http://manpages.ubuntu.com/manpages/xenial/man3/malloc.3.html
- * OS X: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man3/malloc.3.html
- */
-static void *
-xrealloc(void *pcur, size_t len)
-{
- static void *pmem;
-
- pmem = realloc(pcur, len);
- if (!pmem && pcur)
- free(pcur);
-
- return pmem;
-}
-
static int
dentfill(char *path, struct entry **dents,
int (*filter)(regex_t *, char *), regex_t *re)
/* Clean screen */
erase();
+ if (cfg.copymode)
+ if (g_crc != crc8fast((uchar *)dents, ndents * sizeof(struct entry))) {
+ cfg.copymode = 0;
+ DPRINTF_S("copymode off");
+ }
/* Fail redraw if < than 10 columns */
if (COLS < 10) {
static char oldname[NAME_MAX + 1] __attribute__ ((aligned));
char *dir, *tmp, *run = NULL, *env = NULL;
struct stat sb;
- int r, fd, presel;
+ int r, fd, presel, copystartid = 0, copyendid = 0;
enum action sel = SEL_RUNARG + 1;
bool dir_changed = FALSE;
cfg.sizeorder ^= 1;
cfg.mtimeorder = 0;
cfg.blkorder = 0;
+ cfg.copymode = 0;
/* Save current */
if (ndents > 0)
copycurname();
}
cfg.mtimeorder = 0;
cfg.sizeorder = 0;
+ cfg.copymode = 0;
/* Save current */
if (ndents > 0)
copycurname();
cfg.mtimeorder ^= 1;
cfg.sizeorder = 0;
cfg.blkorder = 0;
+ cfg.copymode = 0;
/* Save current */
if (ndents > 0)
copycurname();
goto begin;
case SEL_COPY:
if (copier && ndents) {
- mkpath(path, dents[cur].name, newpath, PATH_MAX);
- spawn(copier, newpath, NULL, NULL, F_NONE);
+ r = mkpath(path, dents[cur].name, newpath, PATH_MAX);
+ if (cfg.copymode) {
+ if (!appendfilepath(newpath, r))
+ goto nochange;
+ } else
+ spawn(copier, newpath, NULL, NULL, F_NONE);
printmsg(newpath);
} else if (!copier)
- printmsg("NNN_COPIER is not set");
+ printmsg(STR_COPY);
+ goto nochange;
+ case SEL_COPYMUL:
+ if (!copier) {
+ printmsg(STR_COPY);
+ goto nochange;
+ } else if (!ndents) {
+ goto nochange;
+ }
+
+ cfg.copymode ^= 1;
+ if (cfg.copymode) {
+ g_crc = crc8fast((uchar *)dents, ndents * sizeof(struct entry));
+ copystartid = cur;
+ copybufpos = 0;
+ DPRINTF_S("copymode on");
+ } else {
+ static size_t len;
+ len = 0;
+
+ /* Handle range selection */
+ if (copybufpos == 0) {
+
+ if (cur < copystartid) {
+ copyendid = copystartid;
+ copystartid = cur;
+ } else
+ copyendid = cur;
+
+ if (copystartid < copyendid) {
+ for (r = copystartid; r <= copyendid; ++r) {
+ len = mkpath(path, dents[r].name, newpath, PATH_MAX);
+ if (!appendfilepath(newpath, len))
+ goto nochange;;
+ }
+
+ sprintf(newpath, "%d files copied", copyendid - copystartid + 1);
+ printmsg(newpath);
+ }
+ }
+
+ if (copybufpos) {
+ spawn(copier, pcopybuf, NULL, NULL, F_NONE);
+ DPRINTF_S(pcopybuf);
+ if (!len)
+ printmsg("files copied");
+ }
+ }
goto nochange;
case SEL_OPEN:
- printprompt("open with: "); // fallthrough
+ printprompt("open with: "); // fallthrough
case SEL_NEW:
if (sel == SEL_NEW)
printprompt("name: ");
/* Set locale */
setlocale(LC_ALL, "");
+ crc8init();
+
#ifdef DEBUGMODE
enabledbg();
#endif