Support multiple file path copy

Design overview:

We are introducing multiple file path copy as a mode which can be toggled using
the keybind `^Y`. `^K` works as the individual entry selector. If the user wants
to select a range, (s)he can press `^Y` on the first entry and `^Y` on the last
entry.

We subscribe to notifications, so we need a fail-proof way to detect changes in
the directory contents. For example, if a file is deleted, it becomes difficult
to get the names of all the files in a range containing that file. If the file
is on a range boundary it would lead to wrong calculations. To handle this the
right way we use CRC8 checksum of all the visible entries in the directory. The
checksum is calculated based on the file information buffer. If the CRC changes
on a redraw(), we reset the multi-select mode.

New line (`\n`) works as the delimiter between file paths. Note that you may have
to disable IFS in the `NNN_COPIER` script to show file paths separated by spaces.
This commit is contained in:
Arun Prakash Jana 2018-01-14 01:43:30 +05:30
parent 4800250814
commit a40d29ba9f
No known key found for this signature in database
GPG key ID: A75979F35C080412
5 changed files with 230 additions and 44 deletions

View file

@ -25,7 +25,7 @@ Cool things you can do with `nnn`:
- *navigate-as-you-type* (*search-as-you-type* enabled even on directory switch) - *navigate-as-you-type* (*search-as-you-type* enabled even on directory switch)
- check disk usage with number of files in current directory tree - check disk usage with number of files in current directory tree
- run desktop search utility (gnome-search-tool or catfish) in any directory - 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 - navigate instantly using shortcuts like `~`, `-`, `&` or handy bookmarks
- use `cd .....` at chdir prompt to go to a parent directory - use `cd .....` at chdir prompt to go to a parent directory
- detailed file stats, media info, list and extract archives - detailed file stats, media info, list and extract archives
@ -67,7 +67,7 @@ Have fun with it! PRs are welcome. Check out [#1](https://github.com/jarun/nnn/i
- [add bookmarks](#add-bookmarks) - [add bookmarks](#add-bookmarks)
- [use cd .....](#use-cd-) - [use cd .....](#use-cd-)
- [cd on quit](#cd-on-quit) - [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) - [change dir color](#change-dir-color)
- [file copy, move, delete](#file-copy-move-delete) - [file copy, move, delete](#file-copy-move-delete)
- [boost chdir prompt](#boost-chdir-prompt) - [boost chdir prompt](#boost-chdir-prompt)
@ -246,6 +246,7 @@ optional arguments:
F | List archive F | List archive
^F | Extract archive ^F | Extract archive
^K | Invoke file path copier ^K | Invoke file path copier
^Y | Toggle multi-copy mode
^L | Redraw, clear prompt ^L | Redraw, clear prompt
? | Help, settings ? | Help, settings
Q | Quit and cd Q | Quit and cd
@ -342,21 +343,31 @@ Pick the appropriate file for your shell from [`scripts/quitcd`](scripts/quitcd)
As you might notice, `nnn` uses the environment variable `NNN_TMPFILE` to write the last visited directory path. You can change it. 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: Sample Linux copier script:
#!/bin/sh #!/bin/sh
# comment the next line to convert newlines to spaces
IFS=
echo -n $1 | xsel --clipboard --input echo -n $1 | xsel --clipboard --input
export `NNN_COPIER`: export `NNN_COPIER`:
export NNN_COPIER="/path/to/copier.sh" 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 #### change dir color

35
nnn.1
View file

@ -102,6 +102,8 @@ List files in archive
Extract archive in current directory Extract archive in current directory
.It Ic ^K .It Ic ^K
Invoke file path copier Invoke file path copier
.It Ic ^Y
Toggle multiple file path copy mode
.It Ic ^L .It Ic ^L
Force a redraw, clear rename or filter prompt Force a redraw, clear rename or filter prompt
.It Ic \&? .It Ic \&?
@ -171,12 +173,21 @@ instructions.
Filters support regexes to instantly (search-as-you-type) list the matching Filters support regexes to instantly (search-as-you-type) list the matching
entries in the current directory. entries in the current directory.
.Pp .Pp
There are 3 ways to reset a filter: (1) pressing \fI^L\fR (at the new/rename There are 3 ways to reset a filter:
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).
.Pp .Pp
Common use cases: (1) To list all matches starting with the filter expression, (1) pressing \fI^L\fR (at the new/rename prompt \fI^L\fR followed by \fIEnter\fR
start the expression with a '^' (caret) symbol. (2) Type '\\.mkv' to list all MKV files. 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 .Pp
If If
.Nm .Nm
@ -184,6 +195,18 @@ is invoked as root the default filter will also match hidden files.
.Pp .Pp
In the \fInavigate-as-you-type\fR mode directories are opened in filter mode, In the \fInavigate-as-you-type\fR mode directories are opened in filter mode,
allowing continuous navigation. Works best with the \fBarrow keys\fR. 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 .Sh ENVIRONMENT
The SHELL, EDITOR and PAGER environment variables take precedence The SHELL, EDITOR and PAGER environment variables take precedence
when dealing with the !, e and p commands respectively. when dealing with the !, e and p commands respectively.
@ -214,6 +237,8 @@ screensaver.
------------------------------------- -------------------------------------
#!/bin/sh #!/bin/sh
# comment the next line to convert newlines to spaces
IFS=
echo -n $1 | xsel --clipboard --input echo -n $1 | xsel --clipboard --input
------------------------------------- -------------------------------------
.Ed .Ed

213
nnn.c
View file

@ -169,6 +169,12 @@ disabledbg()
#define F_SIGINT 0x08 /* restore default SIGINT handler */ #define F_SIGINT 0x08 /* restore default SIGINT handler */
#define F_NORMAL 0x80 /* spawn child process in non-curses regular CLI mode */ #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 exitcurses() endwin()
#define clearprompt() printmsg("") #define clearprompt() printmsg("")
#define printwarn() printmsg(strerror(errno)) #define printwarn() printmsg(strerror(errno))
@ -217,6 +223,7 @@ typedef struct {
ushort sizeorder : 1; /* Set to sort by file size */ ushort sizeorder : 1; /* Set to sort by file size */
ushort blkorder : 1; /* Set to sort by blocks used (disk usage) */ ushort blkorder : 1; /* Set to sort by blocks used (disk usage) */
ushort showhidden : 1; /* Set to show hidden files */ 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 showdetail : 1; /* Clear to show fewer file info */
ushort showcolor : 1; /* Set to show dirs in blue */ ushort showcolor : 1; /* Set to show dirs in blue */
ushort dircolor : 1; /* Current status of dir color */ ushort dircolor : 1; /* Current status of dir color */
@ -227,13 +234,13 @@ typedef struct {
/* GLOBALS */ /* GLOBALS */
/* Configuration */ /* 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 struct entry *dents;
static char *pnamebuf; static char *pnamebuf, *pcopybuf;
static int ndents, cur, total_dents = ENTRY_INCR; static int ndents, cur, total_dents = ENTRY_INCR;
static uint idle; static uint idle;
static uint idletimeout; static uint idletimeout, copybufpos, copybuflen;
static char *player; static char *player;
static char *copier; static char *copier;
static char *editor; static char *editor;
@ -245,6 +252,9 @@ static ulong num_files;
static uint open_max; static uint open_max;
static bm bookmark[BM_MAX]; static bm bookmark[BM_MAX];
static uchar crc8table[256];
static uchar g_crc;
#ifdef LINUX_INOTIFY #ifdef LINUX_INOTIFY
static int inotify_fd, inotify_wd = -1; 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 uint INOTIFY_MASK = IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
@ -274,6 +284,7 @@ static const char *STR_ATROOT = "You are at /";
static const char *STR_NOHOME = "HOME not set"; static const char *STR_NOHOME = "HOME not set";
static const char *STR_INPUT = "No traversal delimiter allowed"; static const char *STR_INPUT = "No traversal delimiter allowed";
static const char *STR_INVBM = "Invalid bookmark"; 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"; static const char *STR_DATE = "%a %d %b %Y %T %z";
/* For use in functions which are isolated and don't return the buffer */ /* For use in functions which are isolated and don't return the buffer */
@ -284,6 +295,57 @@ static void redraw(char *path);
/* Functions */ /* 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 */ /* Messages show up at the bottom */
static void static void
printmsg(const char *msg) printmsg(const char *msg)
@ -334,6 +396,26 @@ max_openfds()
return limit; 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() * Custom xstrlen()
*/ */
@ -523,6 +605,25 @@ xbasename(char *path)
return base ? base + 1 : path; 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 * Return number of dots if all chars in a string are dots, else 0
*/ */
@ -1128,22 +1229,24 @@ readinput(void)
} }
/* /*
* 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) mkpath(char *dir, char *name, char *out, size_t n)
{ {
/* Handle absolute path */ /* Handle absolute path */
if (name[0] == '/') if (name[0] == '/')
xstrlcpy(out, name, n); return xstrlcpy(out, name, n);
else { else {
/* Handle root case */ /* Handle root case */
if (istopdir(dir)) if (istopdir(dir))
snprintf(out, n, "/%s", name); return (snprintf(out, n, "/%s", name) + 1);
else else
snprintf(out, n, "%s/%s", dir, name); return (snprintf(out, n, "%s/%s", dir, name) + 1);
} }
return out;
return 0;
} }
static void static void
@ -1726,6 +1829,7 @@ show_help(char *path)
"eF | List archive\n" "eF | List archive\n"
"d^F | Extract archive\n" "d^F | Extract archive\n"
"d^K | Invoke file path copier\n" "d^K | Invoke file path copier\n"
"d^Y | Toggle multi-copy mode\n"
"d^L | Redraw, clear prompt\n" "d^L | Redraw, clear prompt\n"
"e? | Help, settings\n" "e? | Help, settings\n"
"eQ | Quit and cd\n" "eQ | Quit and cd\n"
@ -1796,26 +1900,6 @@ sum_bsizes(const char *fpath, const struct stat *sb,
return 0; 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 static int
dentfill(char *path, struct entry **dents, dentfill(char *path, struct entry **dents,
int (*filter)(regex_t *, char *), regex_t *re) int (*filter)(regex_t *, char *), regex_t *re)
@ -2057,6 +2141,11 @@ redraw(char *path)
/* Clean screen */ /* Clean screen */
erase(); 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 */ /* Fail redraw if < than 10 columns */
if (COLS < 10) { if (COLS < 10) {
@ -2170,7 +2259,7 @@ browse(char *ipath, char *ifilter)
static char oldname[NAME_MAX + 1] __attribute__ ((aligned)); static char oldname[NAME_MAX + 1] __attribute__ ((aligned));
char *dir, *tmp, *run = NULL, *env = NULL; char *dir, *tmp, *run = NULL, *env = NULL;
struct stat sb; struct stat sb;
int r, fd, presel; int r, fd, presel, copystartid = 0, copyendid = 0;
enum action sel = SEL_RUNARG + 1; enum action sel = SEL_RUNARG + 1;
bool dir_changed = FALSE; bool dir_changed = FALSE;
@ -2683,6 +2772,7 @@ nochange:
cfg.sizeorder ^= 1; cfg.sizeorder ^= 1;
cfg.mtimeorder = 0; cfg.mtimeorder = 0;
cfg.blkorder = 0; cfg.blkorder = 0;
cfg.copymode = 0;
/* Save current */ /* Save current */
if (ndents > 0) if (ndents > 0)
copycurname(); copycurname();
@ -2695,6 +2785,7 @@ nochange:
} }
cfg.mtimeorder = 0; cfg.mtimeorder = 0;
cfg.sizeorder = 0; cfg.sizeorder = 0;
cfg.copymode = 0;
/* Save current */ /* Save current */
if (ndents > 0) if (ndents > 0)
copycurname(); copycurname();
@ -2703,6 +2794,7 @@ nochange:
cfg.mtimeorder ^= 1; cfg.mtimeorder ^= 1;
cfg.sizeorder = 0; cfg.sizeorder = 0;
cfg.blkorder = 0; cfg.blkorder = 0;
cfg.copymode = 0;
/* Save current */ /* Save current */
if (ndents > 0) if (ndents > 0)
copycurname(); copycurname();
@ -2714,14 +2806,65 @@ nochange:
goto begin; goto begin;
case SEL_COPY: case SEL_COPY:
if (copier && ndents) { if (copier && ndents) {
mkpath(path, dents[cur].name, newpath, PATH_MAX); r = mkpath(path, dents[cur].name, newpath, PATH_MAX);
spawn(copier, newpath, NULL, NULL, F_NONE); if (cfg.copymode) {
if (!appendfilepath(newpath, r))
goto nochange;
} else
spawn(copier, newpath, NULL, NULL, F_NONE);
printmsg(newpath); printmsg(newpath);
} else if (!copier) } 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; goto nochange;
case SEL_OPEN: case SEL_OPEN:
printprompt("open with: "); // fallthrough printprompt("open with: "); // fallthrough
case SEL_NEW: case SEL_NEW:
if (sel == SEL_NEW) if (sel == SEL_NEW)
printprompt("name: "); printprompt("name: ");
@ -3034,6 +3177,8 @@ main(int argc, char *argv[])
/* Set locale */ /* Set locale */
setlocale(LC_ALL, ""); setlocale(LC_ALL, "");
crc8init();
#ifdef DEBUGMODE #ifdef DEBUGMODE
enabledbg(); enabledbg();
#endif #endif

3
nnn.h
View file

@ -34,6 +34,7 @@ enum action {
SEL_MTIME, SEL_MTIME,
SEL_REDRAW, SEL_REDRAW,
SEL_COPY, SEL_COPY,
SEL_COPYMUL,
SEL_OPEN, SEL_OPEN,
SEL_NEW, SEL_NEW,
SEL_RENAME, SEL_RENAME,
@ -145,6 +146,8 @@ static struct key bindings[] = {
{ KEY_F(5), SEL_REDRAW, "", "" }, /* Undocumented */ { KEY_F(5), SEL_REDRAW, "", "" }, /* Undocumented */
/* Copy currently selected file path */ /* Copy currently selected file path */
{ CONTROL('K'), SEL_COPY, "", "" }, { CONTROL('K'), SEL_COPY, "", "" },
/* Toggle copy multiple file paths */
{ CONTROL('Y'), SEL_COPYMUL, "", "" },
/* Open in a custom application */ /* Open in a custom application */
{ CONTROL('O'), SEL_OPEN, "", "" }, { CONTROL('O'), SEL_OPEN, "", "" },
/* Create a new file */ /* Create a new file */

View file

@ -1,3 +1,5 @@
#!/bin/sh #!/bin/sh
# comment the next line to convert newlines to spaces
IFS=
echo -n $1 | `xsel --clipboard --input` echo -n $1 | `xsel --clipboard --input`