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)
- 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
@ -67,7 +67,7 @@ Have fun with it! PRs are welcome. Check out [#1](https://github.com/jarun/nnn/i
- [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)
@ -246,6 +246,7 @@ optional arguments:
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
@ -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.
#### 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

35
nnn.1
View File

@ -102,6 +102,8 @@ List files in archive
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 \&?
@ -171,12 +173,21 @@ instructions.
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
@ -184,6 +195,18 @@ is invoked as root the default filter will also match hidden files.
.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.
@ -214,6 +237,8 @@ screensaver.
-------------------------------------
#!/bin/sh
# comment the next line to convert newlines to spaces
IFS=
echo -n $1 | xsel --clipboard --input
-------------------------------------
.Ed

213
nnn.c
View File

@ -169,6 +169,12 @@ disabledbg()
#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))
@ -217,6 +223,7 @@ typedef struct {
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 */
@ -227,13 +234,13 @@ typedef struct {
/* 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;
@ -245,6 +252,9 @@ static ulong num_files;
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;
@ -274,6 +284,7 @@ static const char *STR_ATROOT = "You are at /";
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 */
@ -284,6 +295,57 @@ static void redraw(char *path);
/* 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)
@ -334,6 +396,26 @@ max_openfds()
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()
*/
@ -523,6 +605,25 @@ xbasename(char *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
*/
@ -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)
{
/* 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
@ -1726,6 +1829,7 @@ show_help(char *path)
"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"
@ -1796,26 +1900,6 @@ sum_bsizes(const char *fpath, const struct stat *sb,
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)
@ -2057,6 +2141,11 @@ redraw(char *path)
/* 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) {
@ -2170,7 +2259,7 @@ browse(char *ipath, char *ifilter)
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;
@ -2683,6 +2772,7 @@ nochange:
cfg.sizeorder ^= 1;
cfg.mtimeorder = 0;
cfg.blkorder = 0;
cfg.copymode = 0;
/* Save current */
if (ndents > 0)
copycurname();
@ -2695,6 +2785,7 @@ nochange:
}
cfg.mtimeorder = 0;
cfg.sizeorder = 0;
cfg.copymode = 0;
/* Save current */
if (ndents > 0)
copycurname();
@ -2703,6 +2794,7 @@ nochange:
cfg.mtimeorder ^= 1;
cfg.sizeorder = 0;
cfg.blkorder = 0;
cfg.copymode = 0;
/* Save current */
if (ndents > 0)
copycurname();
@ -2714,14 +2806,65 @@ nochange:
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: ");
@ -3034,6 +3177,8 @@ main(int argc, char *argv[])
/* Set locale */
setlocale(LC_ALL, "");
crc8init();
#ifdef DEBUGMODE
enabledbg();
#endif

3
nnn.h
View File

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

View File

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