From 9afd7cf3bf8badb7e704fb1e1fbdb8031572c722 Mon Sep 17 00:00:00 2001 From: Anna Arad <4895022+annagrram@users.noreply.github.com> Date: Wed, 23 Oct 2019 13:04:12 +0300 Subject: [PATCH] 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 --- plugins/.nnn-plugin-helper | 33 ++++++++++++++++ plugins/README.md | 65 +++++++++++++++++++++++++------ plugins/fzcd | 32 ++++++++++++++++ plugins/getplugs | 28 ++++++++++---- src/nnn.c | 78 ++++++++++++++++++++++++++++++++------ 5 files changed, 204 insertions(+), 32 deletions(-) create mode 100644 plugins/.nnn-plugin-helper create mode 100755 plugins/fzcd diff --git a/plugins/.nnn-plugin-helper b/plugins/.nnn-plugin-helper new file mode 100644 index 00000000..ec66a29d --- /dev/null +++ b/plugins/.nnn-plugin-helper @@ -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 $? +} diff --git a/plugins/README.md b/plugins/README.md index 85550ca0..9d48ec30 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -14,6 +14,7 @@ The currently available plugins are listed below. | checksum | sh | md5sum,
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
(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
[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 :o, `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 `` 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 diff --git a/plugins/fzcd b/plugins/fzcd new file mode 100755 index 00000000..443ac2da --- /dev/null +++ b/plugins/fzcd @@ -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 diff --git a/plugins/getplugs b/plugins/getplugs index e144be39..42d5400d 100755 --- a/plugins/getplugs +++ b/plugins/getplugs @@ -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 diff --git a/src/nnn.c b/src/nnn.c index da0a6475..2b506470 100644 --- a/src/nnn.c +++ b/src/nnn.c @@ -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