README.md | 36 ++++++++++++++++++++++++++++++++++--
nnn.1 | 5 +++++
src/nnn.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++-------
src/nnn.h | 6 ++++++
diff --git a/README.md b/README.md
index 9e8d478fca9c7d4a9c1fbba90c1d6cff10d2bb59..f98e3b41c1fdbed2a3e214a8ae9be9f64a489005 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,7 @@ - [Filters](#filters)
- [Navigate-as-you-type](#navigate-as-you-type)
- [File indicators](#file-indicators)
- [Configuration](#configuration)
+ - [SSHFS mounts](#sshfs-mounts)
- [Help](#help)
- [Plugins](#plugins)
- [Troubleshooting](#troubleshooting)
@@ -103,7 +104,7 @@ - Copy, move, delete, archive, link selection
- FreeDesktop compliant trash (needs trash-cli)
- Show copy, move progress on Linux (needs avdcpmv)
- Plugin repository
- - Transfer files using lftp
+ - SSHFS mounts (needs sshfs)
- Batch rename (needs vidir)
- Per-context directory color (default: blue)
- Spawn a shell in the current directory
@@ -140,6 +141,8 @@ | fzy | app launcher with drop-down menu |
| vidir (from moreutils) | batch rename dir entries |
| vlock (Linux), bashlock (macOS), lock(1) (BSD) | terminal locker |
| advcpmv (Linux) ([integration](https://github.com/jarun/nnn/wiki/hacking-nnn#show-cp-mv-progress)) | copy, move progress |
+| sshfs | mount remote over SSHFS |
+| fusermount(3) | SSHFS unmount |
| $EDITOR (overridden by $VISUAL, if defined) | edit files (fallback vi) |
| $PAGER (less, most) | page through files (fallback less) |
| $SHELL | spawn a shell, run some commands (fallback sh) |
@@ -264,6 +267,7 @@ ^W Random s Size t Time modified
MISC
! ^] Spawn SHELL C Execute entry
R ^V Pick plugin L Lock terminal
+ c SSHFS mount u Unmount
^P Prompt ^N Note = Launcher
```
@@ -380,14 +384,42 @@ | `NNN_CONTEXT_COLORS='1234'` | specify per context color [default: '4444' (all blue)] |
| `NNN_IDLE_TIMEOUT=300` | idle seconds before locking terminal [default: disabled] |
| `NNN_COPIER='/absolute/path/to/copier'` | system clipboard copier script [default: none] |
| `NNN_PLUGIN_DIR=/home/user/nnn-plugins` | absolute path to plugins dir |
-| `NNN_NOTE=/home/user/Dropbox/Public/notes` | path to note file [default: none] |
+| `NNN_NOTE=/home/user/Dropbox/notes` | path to note file [default: none] |
| `NNN_TMPFILE=/tmp/nnn` | file to write current open dir path to for cd on quit |
+| `NNN_SSHFS_MNT_ROOT=/home/user/.netmnt` | absolute path to SSHFS mount point root |
| `NNN_USE_EDITOR=1` | Open text files in `$EDITOR` (`$VISUAL`, if defined; fallback vi) |
| `NNN_NO_AUTOSELECT=1` | do not auto-select matching dir in _nav-as-you-type_ mode |
| `NNN_RESTRICT_NAV_OPEN=1` | open files on ↵, not → or l |
| `NNN_RESTRICT_0B=1` | do not open 0-byte files |
| `NNN_TRASH=1` | trash files to the desktop Trash [default: delete] |
| `NNN_OPS_PROG=1` | show copy, move progress on Linux |
+
+#### SSHFS mounts
+
+To connect to and mount remote shares using SSHFS, `nnn` requires the following:
+
+1. ssh configuration file `~/.ssh/config` should have the host entries. sshfs reads this file.
+2. `NNN_SSHFS_MNT_ROOT` should be set to the **absolute path** to the directory under which `nnn` creates the mount point for a host. The mount point is the same as the host name.
+
+Example host entry for a Termux environment on Android device:
+
+```
+Host phone
+ HostName 192.168.0.102
+ User u0_a117
+ Port 8022
+```
+
+If `NNN_SSHFS_MNT_ROOT` is set to `/home/user/remotes`, the above host `phone` will be mounted at `/home/user/remotes/phone`. `nnn` creates the directory `phone` if it doesn't exist.
+
+To unmount a mount point highlight it in `nnn` (so that it's the current entry) and press the relevant keybind to unmount. It might be a good idea to bookmark `NNN_SSHFS_MNT_ROOT`.
+
+Notes:
+
+1. `nnn` places you inside the mount point after both mount and unmount. This is done so you can ensure the operation completed successfully. To jump back to the last directory, press the usual -.
+2. `nnn` doesn't delete the mount point on unmount. This is to prevent accidental data loss.
+
+More information on [SSHFS](https://wiki.archlinux.org/index.php/SSHFS)
#### Help
diff --git a/nnn.1 b/nnn.1
index 8621776af337087f6407b2016b717be4605b5625..f21ea14a6ee624d5b7dffafd45a279d63629c376 100644
--- a/nnn.1
+++ b/nnn.1
@@ -189,6 +189,11 @@ .Bd -literal
export NNN_TMPFILE=/tmp/nnn
.Ed
.Pp
+\fBNNN_SSHFS_MNT_ROOT:\fR absolute path to SSHFS mount point root. Mount points are created at this location.
+.Bd -literal
+ export NNN_SSHFS_MNT_ROOT=/home/user/.netmnt`
+.Ed
+.Pp
\fBNNN_USE_EDITOR:\fR use EDITOR (VISUAL takes preference, preferably CLI, fallback vi) to handle text
files.
.Bd -literal
diff --git a/src/nnn.c b/src/nnn.c
index 05d1a266dd3cdb24b22a2b21eeb69a6cb63a57a4..2f10bb2205ee8d0092e034497da5f3508a51f7e5 100644
--- a/src/nnn.c
+++ b/src/nnn.c
@@ -279,6 +279,7 @@ static char *editor;
static char *pager;
static char *shell;
static char *home;
+static char *sshfsmnt;
static blkcnt_t ent_blocks;
static blkcnt_t dir_blocks;
static ulong num_files;
@@ -388,15 +389,16 @@ #define NNN_COPIER 4
#define NNN_PLUGIN_DIR 5
#define NNN_NOTE 6
#define NNN_TMPFILE 7
-#define NNNLVL 8 /* strings end here */
-#define NNN_USE_EDITOR 9 /* flags begin here */
-#define NNN_NO_AUTOSELECT 10
-#define NNN_RESTRICT_NAV_OPEN 11
-#define NNN_RESTRICT_0B 12
-#define NNN_OPENER_DETACH 13
-#define NNN_TRASH 14
+#define NNN_SSHFS_MNT_ROOT 8
+#define NNNLVL 9 /* strings end here */
+#define NNN_USE_EDITOR 10 /* flags begin here */
+#define NNN_NO_AUTOSELECT 11
+#define NNN_RESTRICT_NAV_OPEN 12
+#define NNN_RESTRICT_0B 13
+#define NNN_OPENER_DETACH 14
+#define NNN_TRASH 15
#ifdef __linux__
-#define NNN_OPS_PROG 15
+#define NNN_OPS_PROG 16
#endif
static const char * const env_cfg[] = {
@@ -408,6 +410,7 @@ "NNN_COPIER",
"NNN_PLUGIN_DIR",
"NNN_NOTE",
"NNN_TMPFILE",
+ "NNN_SSHFS_MNT_ROOT",
"NNNLVL",
"NNN_USE_EDITOR",
"NNN_NO_AUTOSELECT",
@@ -2387,6 +2390,7 @@ "b^W Random s Size t Time modified\n"
"1MISC\n"
"9! ^] Spawn SHELL C Execute entry\n"
"9R ^V Pick plugin L Lock terminal\n"
+ "cc SSHFS mount u Unmount\n"
"b^P Prompt ^N Note = Launcher\n"};
if (g_tmpfpath[0])
@@ -2888,10 +2892,8 @@
/* Can fail when permissions change while browsing.
* It's assumed that path IS a directory when we are here.
*/
- if (access(path, R_OK) == -1) {
+ if (access(path, R_OK) == -1)
printwarn();
- goto nochange;
- }
populate(path, lastname);
if (interrupted) {
@@ -3862,6 +3864,73 @@ copycurname();
/* Repopulate as directory content may have changed */
goto begin;
+ case SEL_SSHFS:
+ if (!sshfsmnt) {
+ printwait("set NNN_SSHFS_MNT_ROOT", &presel);
+ goto nochange;
+ }
+
+ tmp = xreadline(NULL, "Host: ");
+ if (!tmp[0])
+ goto nochange;
+
+ /* Create the mount point */
+ mkpath(sshfsmnt, tmp, newpath);
+ r = mkdir(newpath, 0777);
+ if (r == -1 && errno != EEXIST) {
+ printwait(strerror(errno), &presel);
+ goto nochange;
+ }
+
+ /* Check if directory can be accessed */
+ if (!xdiraccess(newpath)) {
+ presel = MSGWAIT;
+ goto nochange;
+ }
+
+ if (!getutil("sshfs")) {
+ printwait("sshfs missing", &presel);
+ goto nochange;
+ }
+
+ /* Convert "Host" to "Host:" */
+ r = strlen(tmp);
+ tmp[r] = ':';
+ tmp[r + 1] = '\0';
+
+ /* Connect to remote */
+ spawn("sshfs", tmp, newpath, NULL, F_NORMAL); // fallthrough
+ case SEL_UMOUNT:
+ if (sel == SEL_UMOUNT) {
+ static char cmd[] = "fusermount3"; /* Arch Linux utility */
+ static bool found = FALSE;
+
+ /* On Ubuntu it's fusermount */
+ if (!found && !getutil(cmd))
+ cmd[10] = '\0';
+
+ if (!ndents)
+ goto nochange;
+
+ mkpath(path, dents[cur].name, newpath);
+ if (!xdiraccess(newpath)) {
+ presel = MSGWAIT;
+ goto nochange;
+ }
+
+ spawn(cmd, "-u", newpath, NULL, F_NORMAL);
+ }
+
+ lastname[0] = '\0';
+
+ /* Save last working directory */
+ xstrlcpy(lastdir, path, PATH_MAX);
+
+ /* Switch to mount point */
+ xstrlcpy(path, newpath, PATH_MAX);
+
+ setdirwatch();
+ goto begin;
case SEL_QUITCD: // fallthrough
case SEL_QUIT:
for (r = 0; r < CTX_MAX; ++r)
@@ -4168,6 +4237,9 @@ if (!cfg.picker && g_tmpfplen) {
xstrlcpy(g_cppath, g_tmpfpath, PATH_MAX);
xstrlcpy(g_cppath + g_tmpfplen - 1, "/.nnncp", PATH_MAX - g_tmpfplen);
}
+
+ /* Get SSHFS mountpoint */
+ sshfsmnt = getenv("NNN_SSHFS_MNT_ROOT");
/* Get the clipboard copier, if set */
copier = getenv(env_cfg[NNN_COPIER]);
diff --git a/src/nnn.h b/src/nnn.h
index dd21ef5b419a2e6217447c6b751c9209d74984cd..d47080b9fe7d3a56e1ca45af599ec82441d2fd3a 100644
--- a/src/nnn.h
+++ b/src/nnn.h
@@ -85,6 +85,8 @@ SEL_OPENWITH,
SEL_NEW,
SEL_RENAME,
SEL_RENAMEALL,
+ SEL_SSHFS,
+ SEL_UMOUNT,
SEL_HELP,
SEL_EXEC,
SEL_SHELL,
@@ -218,6 +220,10 @@ /* Show rename prompt */
{ CONTROL('R'), SEL_RENAME },
/* Rename contents of current dir */
{ 'r', SEL_RENAMEALL },
+ /* Connect to server over SSHFS */
+ { 'c', SEL_SSHFS },
+ /* Disconnect a SSHFS mount point */
+ { 'u', SEL_UMOUNT },
/* Show help */
{ '?', SEL_HELP },
/* Execute file */