From a40d29ba9f5673523dfdf28e17d448a3cb8da1f8 Mon Sep 17 00:00:00 2001 From: Arun Prakash Jana Date: Sun, 14 Jan 2018 01:43:30 +0530 Subject: [PATCH] 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. --- README.md | 21 +++- nnn.1 | 35 ++++++- nnn.c | 213 ++++++++++++++++++++++++++++++++------- nnn.h | 3 + scripts/copier/copier.sh | 2 + 5 files changed, 230 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 2c6f76c..703f289 100644 --- a/README.md +++ b/README.md @@ -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 ^K to copy the absolute path (from `/`) of the file under the cursor to clipboard. +Use ^K 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 ^Y. In this mode you can + +- select multiple files one by one by pressing ^K on each entry; or, +- navigate to another file in the same directory to select a range of files. + +Pressing ^Y again copies the paths to clipboard and exits the multi-copy mode. #### change dir color diff --git a/nnn.1 b/nnn.1 index 4c2a3cb..e1bbfee 100644 --- a/nnn.1 +++ b/nnn.1 @@ -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 diff --git a/nnn.c b/nnn.c index 46d89dd..d6a1927 100644 --- a/nnn.c +++ b/nnn.c @@ -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 diff --git a/nnn.h b/nnn.h index 9582420..4027c25 100644 --- a/nnn.h +++ b/nnn.h @@ -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 */ diff --git a/scripts/copier/copier.sh b/scripts/copier/copier.sh index 90d2a26..5629301 100755 --- a/scripts/copier/copier.sh +++ b/scripts/copier/copier.sh @@ -1,3 +1,5 @@ #!/bin/sh +# comment the next line to convert newlines to spaces +IFS= echo -n $1 | `xsel --clipboard --input`