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`