* Implement plugins control of nnn + plugins * Refactor plugins control code and fix getplugs to recognize hidden files * Fix bug when going to dir on non-current context from plugin * Fix some plugins to work on openbsd and freebsd * Renamings * Switch to -R flag in cp instead of -r; BSDs complain * Change braces of function location * Rewrite plugin creation in README and add new plugins to the table * Update the fzcd script to include fzy or fzf * Change plugin name resolve-link-dir -> lncd * Fixing plugins README table * Remove some cd plugins but add them as examples to plugins READMEmaster
@@ -0,0 +1,33 @@ | |||||
#!/usr/bin/env sh | |||||
# Description: Helper script for plugins | |||||
# | |||||
# Shell: POSIX compliant | |||||
# Author: Anna Arad | |||||
selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection | |||||
## Ask nnn to switch to directory $1 in context $2. | |||||
## If $2 is not provided, the function asks explicitly. | |||||
nnn_cd () { | |||||
dir=$1 | |||||
if [ -z "$NNN_PIPE" ]; then | |||||
echo "No pipe file found" 1>&2 | |||||
return | |||||
fi | |||||
if [ -n "$2" ]; then | |||||
context=$2 | |||||
else | |||||
echo -n "Choose context 1-4 (blank for current): " | |||||
read context | |||||
fi | |||||
echo -n ${context:-0}$dir > $NNN_PIPE | |||||
} | |||||
cmd_exists () { | |||||
which "$1" > /dev/null 2>&1 | |||||
echo $? | |||||
} |
@@ -14,6 +14,7 @@ The currently available plugins are listed below. | |||||
| checksum | sh | md5sum,<br>sha256sum | Create and verify checksums | | | checksum | sh | md5sum,<br>sha256sum | Create and verify checksums | | ||||
| drag-file | sh | [dragon](https://github.com/mwh/dragon) | Drag and drop files from nnn | | | drag-file | sh | [dragon](https://github.com/mwh/dragon) | Drag and drop files from nnn | | ||||
| drop-file | sh | [dragon](https://github.com/mwh/dragon) | Drag and drop files into nnn | | | drop-file | sh | [dragon](https://github.com/mwh/dragon) | Drag and drop files into nnn | | ||||
| fzcd | sh | fzy/fzf<br>(optional fd) | Change to the directory of a file/directory selected by fzy/fzf | | |||||
| fzy-open | sh | fzy, xdg-open | Fuzzy find a file in dir subtree and edit or xdg-open | | | fzy-open | sh | fzy, xdg-open | Fuzzy find a file in dir subtree and edit or xdg-open | | ||||
| getplugs | sh | curl | Update plugins | | | getplugs | sh | curl | Update plugins | | ||||
| gutenread | sh | curl, unzip, w3m<br>[epr](https://github.com/wustho/epr) (optional)| Browse, download, read from Project Gutenberg | | | gutenread | sh | curl, unzip, w3m<br>[epr](https://github.com/wustho/epr) (optional)| Browse, download, read from Project Gutenberg | | ||||
@@ -67,28 +68,68 @@ With this, plugin `fzy-open` can be run with the keybind <kbd>:o</kbd>, `mocplay | |||||
**Method 2:** Use the _pick plugin_ shortcut to visit the plugin directory and execute a plugin. Repeating the same shortcut cancels the operation and puts you back in the original directory. | **Method 2:** Use the _pick plugin_ shortcut to visit the plugin directory and execute a plugin. Repeating the same shortcut cancels the operation and puts you back in the original directory. | ||||
## File access from plugins | ## Create your own plugins | ||||
Plugins are a powerful yet easy way to extend the capabilities of `nnn`. | |||||
Plugins can access: | Plugins are scripts that can be written in any scripting language. However, POSIX-compliant shell scripts runnable in `sh` are preferred. | ||||
- all files in the directory (`nnn` switches to the dir where the plugin is to be run so the dir is `$PWD` for the plugin) | |||||
- the current file under the cursor (the file name is passed as the first argument to a plugin) | |||||
- the traversed path where plugin is invoked (this is the second argument to the plugin; for all practical purposes this is the same as `$PWD` except paths with symlinks) | |||||
- the current selection (by reading the file `.selection` in config dir, see the plugin `ndiff`) | |||||
Each script has a _Description_ section which provides more details on what the script does, if applicable. | Each script has a _Description_ section which provides more details on what the script does, if applicable. | ||||
## Create your own plugins | The plugins reside in `${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins`. | ||||
When `nnn` executes a plugin, it does the following: | |||||
- Change to the directory where the plugin is to be run (`$PWD` pointing to the active directory) | |||||
- Passes two arguments to the script: | |||||
1. The hovered file's name | |||||
2. The working directory (might differ from `$PWD` in case of symlinked paths; non-canonical) | |||||
- Sets the environment variable `NNN_PIPE` used to control `nnn` active directory. | |||||
Plugins can also access the current selections by reading the `.selections` file in the config directory (See the `ndiff` plugin for example). | |||||
Plugins are scripts and all scripting languages should work. However, POSIX-compliant shell scripts runnable in `sh` are preferred. If that's too rudimentary for your use case, use Python, Perl or Ruby. | #### Controlling `nnn`'s active directory | ||||
`nnn` provides a mechanism for plugins to control its active directory. | |||||
The way to do so is by writing to the pipe pointed by the environment variable `NNN_PIPE`. | |||||
The plugin should write a single string in the format `<number><path>` without a newline at the end. For example, `1/etc`. | |||||
The number indicates the context to change the active directory of (0 is used to indicate the current context). | |||||
You can create your own plugins by putting them in `${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins`. | For convenience, we provided a helper script named `.nnn-plugin-helper` and a function named `nnn_cd` to ease this process. `nnn_cd` receives the path to change to as the first argument, and the context as an optional second argument. | ||||
If a context is not provided, it is asked for explicitly. | |||||
Usage examples can be found in the Examples section below. | |||||
For example, you could create a executable shell script `git-changes`: | #### Examples | ||||
There are many plugins provided by `nnn` which can be used as examples. Here are a few simple selected examples. | |||||
- Show the git log of changes to the particular file along with the code for a quick and easy review. | |||||
```sh | |||||
#!/usr/bin/env sh | #!/usr/bin/env sh | ||||
git log -p -- "$@" | git log -p -- "$1" | ||||
``` | |||||
- Change to directory in clipboard using helper script | |||||
```sh | |||||
#!/usr/bin/env sh | |||||
. $(dirname $0/.nnn-plugin-helper) | |||||
nnn_cd "$(xsel -ob)" | |||||
``` | |||||
And then trigger it by hitting the pick plugin key and selecting `git-changes` which will conveniently show the git log of changes to the particular file along with the code for a quick and easy review. | - Change direcory to the location of a link using helper script with specific context (current) | ||||
```sh | |||||
#!/usr/bin/env sh | |||||
. $(dirname $0/.nnn-plugin-helper) | |||||
nnn_cd "$(dirname $(readlink -fn $1))" 0 | |||||
``` | |||||
- Change to arbitrary directory without helper script | |||||
```sh | |||||
#!/usr/bin/env sh | |||||
echo -n "cd to: " | |||||
read dir | |||||
echo -n "0$dir" > $NNN_PIPE | |||||
``` | |||||
## Contributing plugins | ## Contributing plugins | ||||
@@ -0,0 +1,32 @@ | |||||
#!/usr/bin/env sh | |||||
# Description: Run fzf and go to the directory of the file selected | |||||
# | |||||
# Shell: POSIX compliant | |||||
# Author: Anna Arad | |||||
. $(dirname $0)/.nnn-plugin-helper | |||||
if [ "$(cmd_exists fzy)" -eq "0" ]; then | |||||
if [ "$(cmd_exists fd)" -eq "0" ]; then | |||||
fd=fd | |||||
elif [ "$(cmd_exists fdfind)" -eq "0" ]; then | |||||
fd=fdfind | |||||
else | |||||
fd=find | |||||
fi | |||||
sel=$($fd | fzy) | |||||
elif [ "$(cmd_exists fzf)" -eq "0" ]; then | |||||
sel=$(fzf --print0) | |||||
else | |||||
exit 1 | |||||
fi | |||||
if [ "$?" -eq "0" ]; then | |||||
case "$(file -bi "$sel")" in | |||||
*directory*) ;; | |||||
*) sel=$(dirname $sel) ;; | |||||
esac | |||||
nnn_cd "$PWD/$sel" | |||||
fi |
@@ -8,15 +8,27 @@ | |||||
CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/ | CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/ | ||||
PLUGIN_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins | PLUGIN_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins | ||||
is_cmd_exists () { | |||||
which "$1" > /dev/null 2>&1 | |||||
echo $? | |||||
} | |||||
if [ "$(is_cmd_exists sudo)" == "0" ]; then | |||||
sucmd=sudo | |||||
elif [ "$(is_cmd_exists doas)" == "0" ]; then | |||||
sucmd=doas | |||||
else | |||||
sucmd=: # noop | |||||
fi | |||||
# backup any earlier plugins | # backup any earlier plugins | ||||
if [ -d $PLUGIN_DIR ]; then | if [ -d $PLUGIN_DIR ]; then | ||||
tar -C $CONFIG_DIR -cf $CONFIG_DIR"plugins-$(date '+%Y%m%d%H%M').tar.bz2" plugins/ | tar -C $CONFIG_DIR -czf $CONFIG_DIR"plugins-$(date '+%Y%m%d%H%M').tar.gz" plugins/ | ||||
fi | fi | ||||
mkdir -p $PLUGIN_DIR | cd $CONFIG_DIR | ||||
cd $PLUGIN_DIR | |||||
curl -Ls -O https://github.com/jarun/nnn/archive/master.tar.gz | curl -Ls -O https://github.com/jarun/nnn/archive/master.tar.gz | ||||
tar -xf master.tar.gz | tar -zxf master.tar.gz | ||||
cp -vf nnn-master/plugins/* . | cp -vRf nnn-master/plugins . | ||||
sudo mv -vf nnn-master/misc/nlaunch/nlaunch /usr/local/bin/ | $sucmd mv -vf nnn-master/misc/nlaunch/nlaunch /usr/local/bin/ | ||||
rm -rf nnn-master/ master.tar.gz README.md | rm -rf nnn-master/ master.tar.gz README.md |
@@ -342,6 +342,11 @@ static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned)); | |||||
/* Buffer to store tmp file path to show selection, file stats and help */ | /* Buffer to store tmp file path to show selection, file stats and help */ | ||||
static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned)); | static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned)); | ||||
/* Buffer to store plugins control pipe location */ | |||||
static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned)); | |||||
static bool g_plinit = FALSE; | |||||
/* Replace-str for xargs on different platforms */ | /* Replace-str for xargs on different platforms */ | ||||
#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) | #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) | ||||
#define REPLACE_STR 'J' | #define REPLACE_STR 'J' | ||||
@@ -429,9 +434,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 NNN_COPIER 4 | #define NNN_COPIER 4 | ||||
#define NNNLVL 5 /* strings end here */ | #define NNNLVL 5 | ||||
#define NNN_USE_EDITOR 6 /* flags begin here */ | #define NNN_PIPE 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", | ||||
@@ -440,6 +446,7 @@ static const char * const env_cfg[] = { | |||||
"NNN_IDLE_TIMEOUT", | "NNN_IDLE_TIMEOUT", | ||||
"NNN_COPIER", | "NNN_COPIER", | ||||
"NNNLVL", | "NNNLVL", | ||||
"NNN_PIPE", | |||||
"NNN_USE_EDITOR", | "NNN_USE_EDITOR", | ||||
"NNN_TRASH", | "NNN_TRASH", | ||||
}; | }; | ||||
@@ -499,7 +506,7 @@ static int spawn(char *file, char *arg1, char *arg2, const char *dir, uchar flag | |||||
static int (*nftw_fn)(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); | static int (*nftw_fn)(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); | ||||
static int dentfind(const char *fname, int n); | 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 bool getutil(char *util); | static inline bool getutil(char *util); | ||||
/* Functions */ | /* Functions */ | ||||
@@ -3340,24 +3347,68 @@ static void show_help(const char *path) | |||||
unlink(g_tmpfpath); | unlink(g_tmpfpath); | ||||
} | } | ||||
static bool run_selected_plugin(char *path, const char *file, char *newpath, char *rundir, char *runfile, char *lastname) | static bool plctrl_init() | ||||
{ | |||||
snprintf(g_buf, CMD_LEN_MAX, "nnn-pipe.%d", getpid()); | |||||
mkpath(g_tmpfpath, g_buf, g_pipepath); | |||||
unlink(g_pipepath); | |||||
if (mkfifo(g_pipepath, 0600) != 0) | |||||
return _FAILURE; | |||||
setenv(env_cfg[NNN_PIPE], g_pipepath, TRUE); | |||||
return _SUCCESS; | |||||
} | |||||
static bool run_selected_plugin(char **path, const char *file, char *newpath, char *rundir, char *runfile, char **lastname, char **lastdir) | |||||
{ | { | ||||
if (!g_plinit) { | |||||
plctrl_init(); | |||||
g_plinit = TRUE; | |||||
} | |||||
if ((cfg.runctx != cfg.curctx) | if ((cfg.runctx != cfg.curctx) | ||||
/* Must be in plugin directory to select plugin */ | /* Must be in plugin directory to select plugin */ | ||||
|| (strcmp(path, plugindir) != 0)) | || (strcmp(*path, plugindir) != 0)) | ||||
return FALSE; | return FALSE; | ||||
mkpath(path, file, newpath); | int fd = open(g_pipepath, O_RDONLY | O_NONBLOCK); | ||||
if (fd == -1) | |||||
return FALSE; | |||||
mkpath(*path, file, newpath); | |||||
/* Copy to path so we can return back to earlier dir */ | /* Copy to path so we can return back to earlier dir */ | ||||
xstrlcpy(path, rundir, PATH_MAX); | xstrlcpy(*path, rundir, PATH_MAX); | ||||
if (runfile[0]) { | if (runfile[0]) { | ||||
xstrlcpy(lastname, runfile, NAME_MAX); | xstrlcpy(*lastname, runfile, NAME_MAX); | ||||
spawn(newpath, lastname, path, path, F_NORMAL); | spawn(newpath, *lastname, *path, *path, F_NORMAL); | ||||
runfile[0] = '\0'; | runfile[0] = '\0'; | ||||
} else | } else | ||||
spawn(newpath, NULL, path, path, F_NORMAL); | spawn(newpath, NULL, *path, *path, F_NORMAL); | ||||
rundir[0] = '\0'; | rundir[0] = '\0'; | ||||
cfg.runplugin = 0; | cfg.runplugin = 0; | ||||
size_t len = read(fd, g_buf, PATH_MAX); | |||||
g_buf[len] = '\0'; | |||||
close(fd); | |||||
if (len > 1) { | |||||
int ctx = g_buf[0] - '0'; | |||||
if (ctx == 0) { | |||||
xstrlcpy(*lastdir, *path, PATH_MAX); | |||||
xstrlcpy(*path, g_buf + 1, PATH_MAX); | |||||
} else if (ctx >= 1 && ctx <= CTX_MAX) { | |||||
int r = ctx - 1; | |||||
g_ctx[r].c_cfg.ctxactive = 0; | |||||
savecurctx(&cfg, g_buf + 1, dents[cur].name, r); | |||||
*path = g_ctx[r].c_path; | |||||
*lastdir = g_ctx[r].c_last; | |||||
*lastname = g_ctx[r].c_name; | |||||
} | |||||
} | |||||
return TRUE; | return TRUE; | ||||
} | } | ||||
@@ -4146,8 +4197,9 @@ nochange: | |||||
/* Handle plugin selection mode */ | /* Handle plugin selection mode */ | ||||
if (cfg.runplugin) { | if (cfg.runplugin) { | ||||
if (!run_selected_plugin(path, dents[cur].name, newpath, rundir, runfile, lastname)) | if (!run_selected_plugin(&path, dents[cur].name, newpath, rundir, runfile, &lastname, &lastdir)) | ||||
continue; | continue; | ||||
setdirwatch(); | setdirwatch(); | ||||
goto begin; | goto begin; | ||||
} | } | ||||
@@ -5244,6 +5296,8 @@ static void cleanup(void) | |||||
free(bmstr); | free(bmstr); | ||||
free(pluginstr); | free(pluginstr); | ||||
unlink(g_pipepath); | |||||
#ifdef DBGMODE | #ifdef DBGMODE | ||||
disabledbg(); | disabledbg(); | ||||
#endif | #endif | ||||