Implement plugins control of nnn + plugins (#364)

* 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 README
This commit is contained in:
Anna Arad 2019-10-23 13:04:12 +03:00 committed by Mischievous Meerkat
parent 0144f44060
commit 9afd7cf3bf
5 changed files with 204 additions and 32 deletions

View file

@ -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 $?
}

View file

@ -14,6 +14,7 @@ The currently available plugins are listed below.
| checksum | sh | md5sum,<br>sha256sum | Create and verify checksums |
| 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 |
| 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 |
| getplugs | sh | curl | Update plugins |
| 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.
## File access from plugins
## Create your own plugins
Plugins can access:
- 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`)
Plugins are a powerful yet easy way to extend the capabilities of `nnn`.
Plugins are scripts that can be written in any scripting language. However, POSIX-compliant shell scripts runnable in `sh` are preferred.
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`.
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.
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.
You can create your own plugins by putting them in `${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins`.
Plugins can also access the current selections by reading the `.selections` file in the config directory (See the `ndiff` plugin for example).
For example, you could create a executable shell script `git-changes`:
#### 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).
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.
#### 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
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)
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.
nnn_cd "$(xsel -ob)"
```
- 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

32
plugins/fzcd Executable file
View file

@ -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

View file

@ -8,15 +8,27 @@
CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/
PLUGIN_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins
# backup any earlier plugins
if [ -d $PLUGIN_DIR ]; then
tar -C $CONFIG_DIR -cf $CONFIG_DIR"plugins-$(date '+%Y%m%d%H%M').tar.bz2" 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
mkdir -p $PLUGIN_DIR
cd $PLUGIN_DIR
# backup any earlier plugins
if [ -d $PLUGIN_DIR ]; then
tar -C $CONFIG_DIR -czf $CONFIG_DIR"plugins-$(date '+%Y%m%d%H%M').tar.gz" plugins/
fi
cd $CONFIG_DIR
curl -Ls -O https://github.com/jarun/nnn/archive/master.tar.gz
tar -xf master.tar.gz
cp -vf nnn-master/plugins/* .
sudo mv -vf nnn-master/misc/nlaunch/nlaunch /usr/local/bin/
tar -zxf master.tar.gz
cp -vRf nnn-master/plugins .
$sucmd mv -vf nnn-master/misc/nlaunch/nlaunch /usr/local/bin/
rm -rf nnn-master/ master.tar.gz README.md

View file

@ -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 */
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 */
#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
#define REPLACE_STR 'J'
@ -429,9 +434,10 @@ static const char * const messages[] = {
#define NNN_CONTEXT_COLORS 2
#define NNN_IDLE_TIMEOUT 3
#define NNN_COPIER 4
#define NNNLVL 5 /* strings end here */
#define NNN_USE_EDITOR 6 /* flags begin here */
#define NNN_TRASH 7
#define NNNLVL 5
#define NNN_PIPE 6 /* strings end here */
#define NNN_USE_EDITOR 7 /* flags begin here */
#define NNN_TRASH 8
static const char * const env_cfg[] = {
"NNN_BMS",
@ -440,6 +446,7 @@ static const char * const env_cfg[] = {
"NNN_IDLE_TIMEOUT",
"NNN_COPIER",
"NNNLVL",
"NNN_PIPE",
"NNN_USE_EDITOR",
"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 dentfind(const char *fname, int n);
static void move_cursor(int target, int ignore_scrolloff);
static bool getutil(char *util);
static inline bool getutil(char *util);
/* Functions */
@ -3340,24 +3347,68 @@ static void show_help(const char *path)
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)
/* Must be in plugin directory to select plugin */
|| (strcmp(path, plugindir) != 0))
|| (strcmp(*path, plugindir) != 0))
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 */
xstrlcpy(path, rundir, PATH_MAX);
xstrlcpy(*path, rundir, PATH_MAX);
if (runfile[0]) {
xstrlcpy(lastname, runfile, NAME_MAX);
spawn(newpath, lastname, path, path, F_NORMAL);
xstrlcpy(*lastname, runfile, NAME_MAX);
spawn(newpath, *lastname, *path, *path, F_NORMAL);
runfile[0] = '\0';
} else
spawn(newpath, NULL, path, path, F_NORMAL);
spawn(newpath, NULL, *path, *path, F_NORMAL);
rundir[0] = '\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;
}
@ -4146,8 +4197,9 @@ nochange:
/* Handle plugin selection mode */
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;
setdirwatch();
goto begin;
}
@ -5244,6 +5296,8 @@ static void cleanup(void)
free(bmstr);
free(pluginstr);
unlink(g_pipepath);
#ifdef DBGMODE
disabledbg();
#endif