@@ -93,7 +93,7 @@ A curses library with wide char support (e.g. ncursesw), libreadline (optional) | |||||
| --- | --- | --- | | | --- | --- | --- | | ||||
| xdg-open (Linux), open(1) (macOS), cygstart (Cygwin) | base | desktop opener | | | xdg-open (Linux), open(1) (macOS), cygstart (Cygwin) | base | desktop opener | | ||||
| file, coreutils (cp, mv, rm), xargs | base | file type, copy, move and remove | | | file, coreutils (cp, mv, rm), xargs | base | file type, copy, move and remove | | ||||
| tar, (un)zip [atool/bsdtar for more formats] | base | create, list, extract tar, gzip, bzip2, zip | | | tar, (un)zip [atool/bsdtar for more formats] | base | create, list, extract bzip2, (g)zip, tar | | ||||
| archivemount, fusermount(3) | optional | mount, unmount archives | | | archivemount, fusermount(3) | optional | mount, unmount archives | | ||||
| sshfs, [rclone](https://rclone.org/), fusermount(3) | optional | mount, unmount remotes | | | sshfs, [rclone](https://rclone.org/), fusermount(3) | optional | mount, unmount remotes | | ||||
| trash-cli | optional | trash files (default action: rm) | | | trash-cli | optional | trash files (default action: rm) | | ||||
@@ -174,6 +174,13 @@ The minimum file size unit is byte (B). The rest are K, M, G, T, P, E, Z, Y (pow | |||||
The SHELL, EDITOR (VISUAL, if defined) and PAGER environment variables take precedence | The SHELL, EDITOR (VISUAL, if defined) and PAGER environment variables take precedence | ||||
when dealing with the !, e and p commands respectively. A single combination to arguments is supported for SHELL and PAGER. | when dealing with the !, e and p commands respectively. A single combination to arguments is supported for SHELL and PAGER. | ||||
.Pp | .Pp | ||||
\fBNNN_OPENER:\fR specify a custom file opener. | |||||
.Bd -literal | |||||
export NNN_OPENER=nuke | |||||
NOTE: `nuke` is a file opener available in plugin repository | |||||
.Ed | |||||
.Pp | |||||
\fBNNN_BMS:\fR bookmark string as \fIkey_char:location\fR pairs (max 10) separated by | \fBNNN_BMS:\fR bookmark string as \fIkey_char:location\fR pairs (max 10) separated by | ||||
\fI;\fR: | \fI;\fR: | ||||
.Bd -literal | .Bd -literal | ||||
@@ -187,7 +194,11 @@ when dealing with the !, e and p commands respectively. A single combination to | |||||
.Bd -literal | .Bd -literal | ||||
export NNN_PLUG='o:fzopen;p:mocplay;d:diffs;m:nmount;t:imgthumb;i:mediainf' | export NNN_PLUG='o:fzopen;p:mocplay;d:diffs;m:nmount;t:imgthumb;i:mediainf' | ||||
NOTE: To run a plugin directly, press \fI:\fR followed by the plugin key. | NOTES: | ||||
1. To run a plugin directly, press \fI;\fR followed by the plugin key | |||||
2. To skip directory refresh after running a plugin,prefix with \fB-\fR | |||||
export NNN_PLUG='m:-mediainfo' | |||||
.Ed | .Ed | ||||
.Pp | .Pp | ||||
To assign keys to arbitrary non-background non-shell-interpreted cli | To assign keys to arbitrary non-background non-shell-interpreted cli | ||||
@@ -198,7 +209,11 @@ when dealing with the !, e and p commands respectively. A single combination to | |||||
NOTES: | NOTES: | ||||
1. Use single quotes for $NNN_PLUG so $nnn is not interpreted | 1. Use single quotes for $NNN_PLUG so $nnn is not interpreted | ||||
2. $nnn should be the last argument (IF you want to pass the hovered file name) | 2. $nnn should be the last argument (IF you want to pass the hovered file name) | ||||
3. (Again) add \fI_\fR before the command | 3. (Again) add \fB_\fR before the command | ||||
4. To disable directory refresh after running a \fIcommand as plugin\fR, prefix the command with \fB-_\fR | |||||
5. To skip user confirmation after command execution, suffix with \fB*\fR | |||||
export NNN_PLUG='y:-_sync*' | |||||
.Ed | .Ed | ||||
.Pp | .Pp | ||||
\fBNNN_USE_EDITOR:\fR use VISUAL (else EDITOR, preferably CLI, fallback vi) to handle text files. | \fBNNN_USE_EDITOR:\fR use VISUAL (else EDITOR, preferably CLI, fallback vi) to handle text files. | ||||
@@ -227,13 +242,6 @@ when dealing with the !, e and p commands respectively. A single combination to | |||||
NOTE: The options must be preceded by `rclone` and max 5 flags are supported. | NOTE: The options must be preceded by `rclone` and max 5 flags are supported. | ||||
.Ed | .Ed | ||||
.Pp | .Pp | ||||
\fBNNN_OPENER:\fR specify a custom file opener. | |||||
.Bd -literal | |||||
export NNN_OPENER=nuke | |||||
NOTE: `nuke` is a file opener available in plugin repository | |||||
.Ed | |||||
.Pp | |||||
\fBNNN_IDLE_TIMEOUT:\fR set idle timeout (in seconds) to invoke terminal locker (default: disabled). | \fBNNN_IDLE_TIMEOUT:\fR set idle timeout (in seconds) to invoke terminal locker (default: disabled). | ||||
.Pp | .Pp | ||||
\fBNNN_TRASH:\fR trash (instead of \fIdelete\fR) files to desktop Trash. | \fBNNN_TRASH:\fR trash (instead of \fIdelete\fR) files to desktop Trash. | ||||
@@ -241,7 +249,7 @@ when dealing with the !, e and p commands respectively. A single combination to | |||||
export NNN_TRASH=1 | export NNN_TRASH=1 | ||||
.Ed | .Ed | ||||
.Pp | .Pp | ||||
\fBNNN:\fR this is a special variable set to the current entry before executing a command from the command prompt or spawning a shell. | \fBNNN:\fR this is a special variable set to the hovered entry before executing a command from the command prompt or spawning a shell. | ||||
.Sh KNOWN ISSUES | .Sh KNOWN ISSUES | ||||
.Nm | .Nm | ||||
may not handle keypresses correctly when used with tmux (see issue #104 for more details). Set \fBTERM=xterm-256color\fR to address it. | may not handle keypresses correctly when used with tmux (see issue #104 for more details). Set \fBTERM=xterm-256color\fR to address it. | ||||
@@ -81,7 +81,7 @@ Plugins are installed to `${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins`. You ca | |||||
`nnn` refreshes a directory after running a plugin by key (method 1 above) to reflect any changes by the plugin. To disable this (say while running the `mediainfo` plugin on some filtered files), add a `-` before the plugin name: | `nnn` refreshes a directory after running a plugin by key (method 1 above) to reflect any changes by the plugin. To disable this (say while running the `mediainfo` plugin on some filtered files), add a `-` before the plugin name: | ||||
export NNN_PLUG='o:fzopen;m:-mediainfo;p:mocplay; | export NNN_PLUG='m:-mediainfo' | ||||
Now `nnn` will not refresh the directory after running the `mediainfo` plugin. | Now `nnn` will not refresh the directory after running the `mediainfo` plugin. | ||||
@@ -99,7 +99,7 @@ Now <kbd>;x</kbd> can be used to make a file executable, <kbd>;g</kbd> can be us | |||||
`nnn` waits for user confirmation (the prompt `Press Enter to continue`) after it executes a command as plugin (unlike plugins which can add a `read` to wait). To skip this, add a `*` after the command. For example: | `nnn` waits for user confirmation (the prompt `Press Enter to continue`) after it executes a command as plugin (unlike plugins which can add a `read` to wait). To skip this, add a `*` after the command. For example: | ||||
export NNN_PLUG='x:_chmod +x $nnn;g:_git log;s:_smplayer $nnn*;o:fzopen' | export NNN_PLUG='s:_smplayer $nnn*' | ||||
Now there will be no prompt after <kbd>;s</kbd>. | Now there will be no prompt after <kbd>;s</kbd>. | ||||
@@ -108,7 +108,7 @@ Notes: | |||||
1. Use single quotes for `$NNN_PLUG` so `$nnn` is not interpreted | 1. Use single quotes for `$NNN_PLUG` so `$nnn` is not interpreted | ||||
2. `$nnn` should be the last argument (IF you want to pass the hovered file name) | 2. `$nnn` should be the last argument (IF you want to pass the hovered file name) | ||||
3. (_Again_) add `_` before the command | 3. (_Again_) add `_` before the command | ||||
4. To disable directory refresh after running a command as plugin prefix the command with `-_` | 4. To disable directory refresh after running a _command as plugin_, prefix the command with `-_` | ||||
## Access level of plugins | ## Access level of plugins | ||||
@@ -334,6 +334,7 @@ static kv bookmark[BM_MAX]; | |||||
static kv plug[PLUGIN_MAX]; | static kv plug[PLUGIN_MAX]; | ||||
static uchar g_tmpfplen; | static uchar g_tmpfplen; | ||||
static uchar blk_shift = BLK_SHIFT_512; | static uchar blk_shift = BLK_SHIFT_512; | ||||
static regex_t archive_re; | |||||
/* Retain old signal handlers */ | /* Retain old signal handlers */ | ||||
#ifdef __linux__ | #ifdef __linux__ | ||||
@@ -464,6 +465,7 @@ static char * const utils[] = { | |||||
#define MSG_ARCHIVE_OPTS 34 | #define MSG_ARCHIVE_OPTS 34 | ||||
#define MSG_PLUGIN_KEYS 35 | #define MSG_PLUGIN_KEYS 35 | ||||
#define MSG_BOOKMARK_KEYS 36 | #define MSG_BOOKMARK_KEYS 36 | ||||
#define MSG_INVALID_REG 37 | |||||
static const char * const messages[] = { | static const char * const messages[] = { | ||||
"no traversal", | "no traversal", | ||||
@@ -500,9 +502,10 @@ static const char * const messages[] = { | |||||
"'s'shfs / 'r'clone?", | "'s'shfs / 'r'clone?", | ||||
"may take a while, try refresh", | "may take a while, try refresh", | ||||
"app name: ", | "app name: ", | ||||
"e'x'tract / 'l'ist / 'm'ount?", | "'d'efault, e'x'tract / 'l'ist / 'm'ount?", | ||||
"plugin keys:", | "plugin keys:", | ||||
"bookmark keys:", | "bookmark keys:", | ||||
"invalid regex", | |||||
}; | }; | ||||
/* Supported configuration environment variables */ | /* Supported configuration environment variables */ | ||||
@@ -511,9 +514,10 @@ static const char * const messages[] = { | |||||
#define NNN_CONTEXT_COLORS 2 | #define NNN_CONTEXT_COLORS 2 | ||||
#define NNN_IDLE_TIMEOUT 3 | #define NNN_IDLE_TIMEOUT 3 | ||||
#define NNNLVL 4 | #define NNNLVL 4 | ||||
#define NNN_PIPE 5 /* strings end here */ | #define NNN_PIPE 5 | ||||
#define NNN_USE_EDITOR 6 /* flags begin here */ | #define NNN_ARCHIVE 6 /* strings end here */ | ||||
#define NNN_TRASH 7 | #define NNN_USE_EDITOR 7 /* flags begin here */ | ||||
#define NNN_TRASH 8 | |||||
static const char * const env_cfg[] = { | static const char * const env_cfg[] = { | ||||
"NNN_BMS", | "NNN_BMS", | ||||
@@ -522,6 +526,7 @@ static const char * const env_cfg[] = { | |||||
"NNN_IDLE_TIMEOUT", | "NNN_IDLE_TIMEOUT", | ||||
"NNNLVL", | "NNNLVL", | ||||
"NNN_PIPE", | "NNN_PIPE", | ||||
"NNN_ARCHIVE", | |||||
"NNN_USE_EDITOR", | "NNN_USE_EDITOR", | ||||
"NNN_TRASH", | "NNN_TRASH", | ||||
}; | }; | ||||
@@ -552,6 +557,7 @@ static char mv[] = "mv -i"; | |||||
static const char cpmvformatcmd[] = "sed -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s"; | static const char cpmvformatcmd[] = "sed -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s"; | ||||
static const char cpmvrenamecmd[] = "sed 's|^\\([^#][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' %s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'"; | static const char cpmvrenamecmd[] = "sed 's|^\\([^#][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' %s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'"; | ||||
static const char batchrenamecmd[] = "paste -d'\n' %s %s | sed 'N; /^\\(.*\\)\\n\\1$/!p;d' | tr '\n' '\\0' | xargs -0 -n2 mv 2>/dev/null"; | static const char batchrenamecmd[] = "paste -d'\n' %s %s | sed 'N; /^\\(.*\\)\\n\\1$/!p;d' | tr '\n' '\\0' | xargs -0 -n2 mv 2>/dev/null"; | ||||
static const char archive_regex[] ="\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$"; | |||||
/* Event handling */ | /* Event handling */ | ||||
#ifdef LINUX_INOTIFY | #ifdef LINUX_INOTIFY | ||||
@@ -599,7 +605,7 @@ static int dentfind(const char *fname, int n); | |||||
static void move_cursor(int target, int ignore_scrolloff); | static void move_cursor(int target, int ignore_scrolloff); | ||||
static inline bool getutil(char *util); | static inline bool getutil(char *util); | ||||
static size_t mkpath(const char *dir, const char *name, char *out); | static size_t mkpath(const char *dir, const char *name, char *out); | ||||
static char *xgetenv(const char *name, char *fallback); | static char *xgetenv(const char * const name, char *fallback); | ||||
static void plugscript(const char *plugin, char *newpath, uchar flags); | static void plugscript(const char *plugin, char *newpath, uchar flags); | ||||
/* Functions */ | /* Functions */ | ||||
@@ -1372,7 +1378,7 @@ static void prompt_run(char *cmd, const char *cur, const char *path) | |||||
} | } | ||||
/* Get program name from env var, else return fallback program */ | /* Get program name from env var, else return fallback program */ | ||||
static char *xgetenv(const char *name, char *fallback) | static char *xgetenv(const char * const name, char *fallback) | ||||
{ | { | ||||
char *value = getenv(name); | char *value = getenv(name); | ||||
@@ -2090,7 +2096,7 @@ static int filterentries(char *path, char *lastname) | |||||
case '=': // fallthrough /* Launch app */ | case '=': // fallthrough /* Launch app */ | ||||
case ']': // fallthorugh /*Prompt key */ | case ']': // fallthorugh /*Prompt key */ | ||||
case ';': // fallthrough /* Run plugin key */ | case ';': // fallthrough /* Run plugin key */ | ||||
case ',': // falltrough /* Pin CWD */ | case ',': // fallthrough /* Pin CWD */ | ||||
case '?': /* Help and config key, '?' is an invalid regex */ | case '?': /* Help and config key, '?' is an invalid regex */ | ||||
if (len == 1) | if (len == 1) | ||||
goto end; | goto end; | ||||
@@ -3480,8 +3486,8 @@ static void show_help(const char *path) | |||||
"cP Copy sel here%-10c^Y Edit sel\n" | "cP Copy sel here%-10c^Y Edit sel\n" | ||||
"cV Move sel here%-10c^V Copy/move sel as\n" | "cV Move sel here%-10c^V Copy/move sel as\n" | ||||
"cX Delete sel%-13c^X Delete entry\n" | "cX Delete sel%-13c^X Delete entry\n" | ||||
"cf Archive%-16c^F Archive ops\n" | |||||
"ce Edit in EDITOR%-10cp Open in PAGER\n" | "ce Edit in EDITOR%-10cp Open in PAGER\n" | ||||
"ci Archive entry%-0c\n" | |||||
"1ORDER TOGGLES\n" | "1ORDER TOGGLES\n" | ||||
"cS Disk usage%-14cA Apparent du\n" | "cS Disk usage%-14cA Apparent du\n" | ||||
"cz Size%-20ct Time\n" | "cz Size%-20ct Time\n" | ||||
@@ -4560,6 +4566,28 @@ nochange: | |||||
continue; | continue; | ||||
} | } | ||||
if (!regexec(&archive_re, dents[cur].name, 0, NULL, 0)) { | |||||
r = get_input(messages[MSG_ARCHIVE_OPTS]); | |||||
if (r == 'l' || r == 'x') { | |||||
mkpath(path, dents[cur].name, newpath); | |||||
handle_archive(newpath, path, r); | |||||
copycurname(); | |||||
goto begin; | |||||
} | |||||
fd = FALSE; | |||||
if (r == 'm') { | |||||
if (!archive_mount(dents[cur].name, path, newpath, &presel)) | |||||
fd = MSG_FAILED; | |||||
} else if (r != 'd') | |||||
fd = MSG_INVALID_KEY; | |||||
if (r != 'd') { | |||||
fd ? printwait(messages[fd], &presel) : clearprompt(); | |||||
goto nochange; | |||||
} | |||||
} | |||||
if (!sb.st_size) { | if (!sb.st_size) { | ||||
printwait(messages[MSG_EMPTY_FILE], &presel); | printwait(messages[MSG_EMPTY_FILE], &presel); | ||||
goto nochange; | goto nochange; | ||||
@@ -5324,28 +5352,6 @@ nochange: | |||||
copycurname(); | copycurname(); | ||||
/* Repopulate as directory content may have changed */ | /* Repopulate as directory content may have changed */ | ||||
goto begin; | goto begin; | ||||
case SEL_ARCHIVEOPS: | |||||
if (!ndents) | |||||
goto nochange; | |||||
r = get_input(messages[MSG_ARCHIVE_OPTS]); | |||||
if (r == 'l' || r == 'x') { | |||||
mkpath(path, dents[cur].name, newpath); | |||||
handle_archive(newpath, path, r); | |||||
copycurname(); | |||||
goto begin; | |||||
} | |||||
if (r != 'm') { | |||||
printwait(messages[MSG_INVALID_KEY], &presel); | |||||
goto nochange; | |||||
} | |||||
if (!archive_mount(dents[cur].name, path, newpath, &presel)) { | |||||
printwait(messages[MSG_FAILED], &presel); | |||||
goto nochange; | |||||
} | |||||
// fallthrough | |||||
case SEL_REMOTE: | case SEL_REMOTE: | ||||
if (sel == SEL_REMOTE && !remote_mount(newpath, &presel)) | if (sel == SEL_REMOTE && !remote_mount(newpath, &presel)) | ||||
goto nochange; | goto nochange; | ||||
@@ -5802,6 +5808,13 @@ int main(int argc, char *argv[]) | |||||
} | } | ||||
} | } | ||||
/* Set archive handling (enveditor used as tmp var) */ | |||||
enveditor = getenv(env_cfg[NNN_ARCHIVE]); | |||||
if (setfilter(&archive_re, (enveditor ? enveditor : archive_regex))) { | |||||
fprintf(stderr, "%s\n", messages[MSG_INVALID_REG]); | |||||
return _FAILURE; | |||||
} | |||||
/* Edit text in EDITOR if opted (and opener is not all-CLI) */ | /* Edit text in EDITOR if opted (and opener is not all-CLI) */ | ||||
if (!cfg.cliopener && xgetenv_set(env_cfg[NNN_USE_EDITOR])) | if (!cfg.cliopener && xgetenv_set(env_cfg[NNN_USE_EDITOR])) | ||||
cfg.useeditor = 1; | cfg.useeditor = 1; | ||||
@@ -5928,6 +5941,9 @@ int main(int argc, char *argv[]) | |||||
} else if (!cfg.picker && g_selpath) | } else if (!cfg.picker && g_selpath) | ||||
unlink(g_selpath); | unlink(g_selpath); | ||||
/* Free the regex */ | |||||
regfree(&archive_re); | |||||
/* Free the selection buffer */ | /* Free the selection buffer */ | ||||
free(pselbuf); | free(pselbuf); | ||||
@@ -87,7 +87,6 @@ enum action { | |||||
SEL_NEW, | SEL_NEW, | ||||
SEL_RENAME, | SEL_RENAME, | ||||
SEL_RENAMEMUL, | SEL_RENAMEMUL, | ||||
SEL_ARCHIVEOPS, | |||||
SEL_REMOTE, | SEL_REMOTE, | ||||
SEL_UMOUNT, | SEL_UMOUNT, | ||||
SEL_HELP, | SEL_HELP, | ||||
@@ -179,7 +178,7 @@ static struct key bindings[] = { | |||||
/* File details */ | /* File details */ | ||||
{ 'D', SEL_STATS }, | { 'D', SEL_STATS }, | ||||
/* Create archive */ | /* Create archive */ | ||||
{ 'f', SEL_ARCHIVE }, | { 'i', SEL_ARCHIVE }, | ||||
/* Toggle sort by size */ | /* Toggle sort by size */ | ||||
{ 'z', SEL_FSIZE }, | { 'z', SEL_FSIZE }, | ||||
/* Sort by apparent size including dir contents */ | /* Sort by apparent size including dir contents */ | ||||
@@ -226,8 +225,6 @@ static struct key bindings[] = { | |||||
{ KEY_F(2), SEL_RENAME }, | { KEY_F(2), SEL_RENAME }, | ||||
/* Rename contents of current dir */ | /* Rename contents of current dir */ | ||||
{ 'r', SEL_RENAMEMUL }, | { 'r', SEL_RENAMEMUL }, | ||||
/* Mount an archive */ | |||||
{ CONTROL('F'), SEL_ARCHIVEOPS }, | |||||
/* Connect to server over SSHFS */ | /* Connect to server over SSHFS */ | ||||
{ 'c', SEL_REMOTE }, | { 'c', SEL_REMOTE }, | ||||
/* Disconnect a SSHFS mount point */ | /* Disconnect a SSHFS mount point */ | ||||