misc/auto-completion/bash/nnn-completion.bash | 3 +++ misc/auto-completion/fish/nnn.fish | 11 +++++++++-- nnn.1 | 18 ++++++++++++++++++ src/nnn.c | 202 ++++++++++++++++++++++++++++++++++++++++++++++++----- src/nnn.h | 2 ++ diff --git a/misc/auto-completion/bash/nnn-completion.bash b/misc/auto-completion/bash/nnn-completion.bash index d97b14497132339062380de2c55af5cd5edd6949..22180e51a760f06bdf12a1ededad6e48eeb446ae 100644 --- a/misc/auto-completion/bash/nnn-completion.bash +++ b/misc/auto-completion/bash/nnn-completion.bash @@ -34,6 +34,9 @@ local bookmarks=$(echo $NNN_BMS | awk -F: -v RS=\; '{print $1}') COMPREPLY=( $(compgen -W "$bookmarks" -- "$cur") ) elif [[ $prev == -p ]]; then COMPREPLY=( $(compgen -f -d -- "$cur") ) + elif [[ $prev == -e ]]; then + local sessions_dir=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/sessions + COMPREPLY=( $(compgen -W "$(ls $sessions_dir)" -- "$cur") ) elif [[ $cur == -* ]]; then COMPREPLY=( $(compgen -W "${opts[*]}" -- "$cur") ) else diff --git a/misc/auto-completion/fish/nnn.fish b/misc/auto-completion/fish/nnn.fish index ee3768a4268b5d51091237cf7ef6f46ecba74002..7119644b8e097cb59b0800ce9c44ba75a1b56905 100644 --- a/misc/auto-completion/fish/nnn.fish +++ b/misc/auto-completion/fish/nnn.fish @@ -5,17 +5,24 @@ # Author: # Arun Prakash Jana # +if test -n "$XDG_CONFIG_HOME" + set sessions_dir $XDG_CONFIG_HOME/.config/nnn/sessions +else + set sessions_dir $HOME/.config/nnn/sessions +end + complete -c nnn -s a -d 'use access time' -complete -c nnn -s b -r -d 'bookmark key to open' +complete -c nnn -s b -r -d 'bookmark key to open' -x -a '(echo $NNN_BMS | awk -F: -v RS=\; \'{print $1"\t"$2}\')' complete -c nnn -s c -d 'cli-only opener' complete -c nnn -s d -d 'start in detail mode' +complete -c nnn -s e -r -d 'load session by name' -x -a '@\t"last session" (ls $nnn_config)' complete -c nnn -s f -d 'run filter as cmd on prompt key' complete -c nnn -s H -d 'show hidden files' complete -c nnn -s i -d 'start in navigate-as-you-type mode' complete -c nnn -s K -d 'detect key collision' complete -c nnn -s n -d 'use version compare to sort files' complete -c nnn -s o -d 'open files only on Enter' -complete -c nnn -s p -r -d 'copy selection to file' +complete -c nnn -s p -r -d 'copy selection to file' -a '-\tstdout' complete -c nnn -s r -d 'show cp, mv progress (Linux-only)' complete -c nnn -s s -d 'use substring match for filters' complete -c nnn -s S -d 'start in disk usage analyzer mode' diff --git a/nnn.1 b/nnn.1 index 24b5eae74c1a858fb8efcead05d3f64ca17e12e2..1add6306bcadb560a3cbe378fd11dd621cb1d9ea 100644 --- a/nnn.1 +++ b/nnn.1 @@ -10,6 +10,7 @@ .Op Ar -a .Op Ar -b key .Op Ar -c .Op Ar -d +.Op Ar -e name .Op Ar -f .Op Ar -H .Op Ar -i @@ -51,6 +52,9 @@ opener opens files in cli utilities only .Pp .Fl d detail mode +.Pp +.Fl "e name" + Load a session by name .Pp .Fl f run filter as command when the prompt key is pressed @@ -108,6 +112,20 @@ .Pp On context creation, the state of the previous context is copied. Each context remembers its last visited directory. .Pp Each context can have its own directory color specified. See ENVIRONMENT section below. +.Sh SESSIONS +Sessions are a way to save and restore states of work. A session stores the settings and contexts. +.Pp +Sessions can be loaded dynamically from within a running +.Nm +instance, or with a flag to +.Nm . +.Pp +When a session is loaded dynamically, the last working session is saved automatically to a dedicated +-- "last session" -- session file. +.Pp +All the session files are located in \fBnnn\fR's +configuration directory under the "sessions" directory and named after the session. +"@" is the "last session" file. .Sh FILTERS Filters support regexes (default) to instantly (search-as-you-type) list the matching entries in the current directory. diff --git a/src/nnn.c b/src/nnn.c index 41ed548be42f6e997feb21e4ebdec4dc2fa92d25..504797cb0a4396a460c5ee874a2a69345365d476 100644 --- a/src/nnn.c +++ b/src/nnn.c @@ -105,6 +105,7 @@ /* Macro definitions */ #define VERSION "2.7" #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn" +#define SESSIONS_VERSION 0 #ifndef S_BLKSIZE #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */ @@ -254,6 +255,14 @@ settings c_cfg; /* Current configuration */ uint color; /* Color code for directories */ } context; +typedef struct { + size_t ver; + size_t pathln[CTX_MAX]; + size_t lastln[CTX_MAX]; + size_t nameln[CTX_MAX]; + size_t fltrln[CTX_MAX]; +} session_header_t; + /* GLOBALS */ /* Configuration, contexts */ @@ -305,6 +314,7 @@ static char *initpath; static char *cfgdir; static char *g_selpath; static char *plugindir; +static char *sessiondir; static char *pnamebuf, *pselbuf; static struct entry *dents; static blkcnt_t ent_blocks; @@ -2150,7 +2160,7 @@ /* * Updates out with "dir/name or "/name" * Returns the number of bytes copied including the terminating NULL byte */ -static size_t mkpath(char *dir, char *name, char *out) +static size_t mkpath(const char *dir, const char *name, char *out) { size_t len; @@ -2634,6 +2644,122 @@ *curcfg = cfg; } +static void save_session(bool last_session, int *presel) +{ + char session_path[PATH_MAX + 1]; + int status = _FAILURE; + int i; + session_header_t header; + char *session_name; + + header.ver = SESSIONS_VERSION; + + for (i = 0; i < CTX_MAX; ++i) { + if (!g_ctx[i].c_cfg.ctxactive) { + header.pathln[i] = header.nameln[i] + = header.lastln[i] = header.fltrln[i] = 0; + } else { + header.pathln[i] = strnlen(g_ctx[i].c_path, PATH_MAX) + 1; + header.nameln[i] = strnlen(g_ctx[i].c_name, NAME_MAX) + 1; + header.lastln[i] = strnlen(g_ctx[i].c_last, PATH_MAX) + 1; + header.fltrln[i] = strnlen(g_ctx[i].c_fltr, REGEX_MAX) + 1; + } + } + + session_name = !last_session ? xreadline("", "session name: ") : "@"; + if (session_name[0] != '\0') + mkpath(sessiondir, session_name, session_path); + else + return; + + FILE *fsession = fopen(session_path, "wb"); + if (!fsession) { + printwait("failed to open session file", presel); + return; + } + + if ((fwrite(&header, sizeof(header), 1, fsession) != 1) + || (fwrite(&cfg, sizeof(cfg), 1, fsession) != 1)) + goto END; + + for (i = 0; i < CTX_MAX; ++i) + if ((fwrite(&g_ctx[i].c_cfg, sizeof(settings), 1, fsession) != 1) + || (fwrite(&g_ctx[i].color, sizeof(uint), 1, fsession) != 1) + || (header.nameln[i] > 0 && fwrite(g_ctx[i].c_name, header.nameln[i], 1, fsession) != 1) + || (header.lastln[i] > 0 && fwrite(g_ctx[i].c_last, header.lastln[i], 1, fsession) != 1) + || (header.fltrln[i] > 0 && fwrite(g_ctx[i].c_fltr, header.fltrln[i], 1, fsession) != 1) + || (header.pathln[i] > 0 && fwrite(g_ctx[i].c_path, header.pathln[i], 1, fsession) != 1)) + goto END; + + status = _SUCCESS; + +END: + fclose(fsession); + + if (status == _FAILURE) + printwait("failed to write session data", presel); +} + +static bool load_session(const char *session_name, char **path, char **lastdir + , char **lastname, bool restore_session) { + char session_path[PATH_MAX + 1]; + int status = _FAILURE; + int i = 0; + session_header_t header; + bool has_loaded_dynamically = !(session_name || restore_session); + + if (!restore_session) { + session_name = session_name ? session_name : xreadline("", "session name: "); + if (session_name[0] != '\0') + mkpath(sessiondir, session_name ? session_name : xreadline("", "session name: "), session_path); + else + return _FAILURE; + } else + mkpath(sessiondir, "@", session_path); + + if (has_loaded_dynamically) + save_session(TRUE, NULL); + + FILE *fsession = fopen(session_path, "rb"); + if (!fsession) { + printmsg("failed to open session file"); + xdelay(); + return _FAILURE; + } + + if ((fread(&header, sizeof(header), 1, fsession) != 1) + || (header.ver != SESSIONS_VERSION) + || (fread(&cfg, sizeof(cfg), 1, fsession) != 1)) + goto END; + + g_ctx[cfg.curctx].c_name[0] = g_ctx[cfg.curctx].c_last[0] + = g_ctx[cfg.curctx].c_fltr[0] = g_ctx[cfg.curctx].c_fltr[1] = '\0'; + + for (; i < CTX_MAX; ++i) + if ((fread(&g_ctx[i].c_cfg, sizeof(settings), 1, fsession) != 1) + || (fread(&g_ctx[i].color, sizeof(uint), 1, fsession) != 1) + || (header.nameln[i] > 0 && fread(g_ctx[i].c_name, header.nameln[i], 1, fsession) != 1) + || (header.lastln[i] > 0 && fread(g_ctx[i].c_last, header.lastln[i], 1, fsession) != 1) + || (header.fltrln[i] > 0 && fread(g_ctx[i].c_fltr, header.fltrln[i], 1, fsession) != 1) + || (header.pathln[i] > 0 && fread(g_ctx[i].c_path, header.pathln[i], 1, fsession) != 1)) + goto END; + + *path = g_ctx[cfg.curctx].c_path; + *lastdir = g_ctx[cfg.curctx].c_last; + *lastname = g_ctx[cfg.curctx].c_name; + status = _SUCCESS; + +END: + fclose(fsession); + + if (status == _FAILURE) { + printmsg("failed to read session data"); + xdelay(); + } + + return status; +} + /* * Gets only a single line (that's what we need * for now) or shows full command output in pager. @@ -3078,8 +3204,9 @@ "1ORDER TOGGLES\n" "cA Apparent du S du\n" "cs Size E Extn t Time\n" "1MISC\n" - "9! ^] Shell = Launch C Execute entry\n" + "9! ^] Shell C Execute entry\n" "9R ^V Pick plugin :K xK Execute plugin K\n" + "cU Manage session = Launch\n" "cc SSHFS mount u Unmount\n" "b^P Prompt/run cmd L Lock\n"}; @@ -3620,7 +3747,7 @@ } else printmsg("0/0"); } -static void browse(char *ipath) +static void browse(char *ipath, const char *session) { char newpath[PATH_MAX] __attribute__ ((aligned)); char mark[PATH_MAX] __attribute__ ((aligned)); @@ -3639,17 +3766,23 @@ bool currentmouse = 1; atexit(dentfree); + xlines = LINES; + xcols = COLS; + /* setup first context */ - xstrlcpy(g_ctx[0].c_path, ipath, PATH_MAX); /* current directory */ - path = g_ctx[0].c_path; - g_ctx[0].c_last[0] = g_ctx[0].c_name[0] = newpath[0] = mark[0] = '\0'; - rundir[0] = runfile[0] = '\0'; - lastdir = g_ctx[0].c_last; /* last visited directory */ - lastname = g_ctx[0].c_name; /* last visited filename */ - g_ctx[0].c_fltr[0] = g_ctx[0].c_fltr[1] = '\0'; - g_ctx[0].c_cfg = cfg; /* current configuration */ + if (!session || load_session(session, &path, &lastdir, &lastname, FALSE) == _FAILURE) { + xstrlcpy(g_ctx[0].c_path, ipath, PATH_MAX); /* current directory */ + path = g_ctx[0].c_path; + g_ctx[0].c_last[0] = g_ctx[0].c_name[0] = '\0'; + lastdir = g_ctx[0].c_last; /* last visited directory */ + lastname = g_ctx[0].c_name; /* last visited filename */ + g_ctx[0].c_fltr[0] = g_ctx[0].c_fltr[1] = '\0'; + g_ctx[0].c_cfg = cfg; /* current configuration */ + } + + newpath[0] = rundir[0] = runfile[0] = mark[0] = '\0'; - cfg.filtermode ? (presel = FILTER) : (presel = 0); + presel = cfg.filtermode ? FILTER : 0; dents = xrealloc(dents, total_dents * sizeof(struct entry)); if (!dents) @@ -4878,6 +5011,22 @@ goto nochange; } } return; + case SEL_SESSIONS: + r = get_input("'s'(ave) / 'l'(oad) / 'r'(estore) session?"); + + if (r == 's') { + save_session(FALSE, &presel); + goto nochange; + } else if (r == 'l' || r == 'r') { + if (load_session(NULL, &path, &lastdir, &lastname, r == 'r') == _SUCCESS) { + setdirwatch(); + goto begin; + } + + presel = MSGWAIT; + goto nochange; + } + break; default: if (xlines != LINES || xcols != COLS) { idle = 0; @@ -4929,6 +5078,7 @@ " -a use access time\n" " -b key open bookmark key\n" " -c cli-only opener\n" " -d detail mode\n" + " -e name load session by name\n" " -f run filter as cmd on prompt key\n" " -H show hidden files\n" " -i nav-as-you-type mode\n" @@ -4966,16 +5116,17 @@ xerror(); return FALSE; } - len = strlen(xdgcfg) + 1 + 12; /* add length of "/nnn/plugins" */ + len = strlen(xdgcfg) + 1 + 13; /* add length of "/nnn/sessions" */ xdg = TRUE; } if (!xdg) - len = strlen(home) + 1 + 20; /* add length of "/.config/nnn/plugins" */ + len = strlen(home) + 1 + 21; /* add length of "/.config/nnn/sessions" */ cfgdir = (char *)malloc(len); plugindir = (char *)malloc(len); - if (!cfgdir || !plugindir) { + sessiondir = (char *)malloc(len); + if (!cfgdir || !plugindir || !sessiondir) { xerror(); return FALSE; } @@ -5017,6 +5168,18 @@ xerror(); return FALSE; } + /* Create ~/.config/nnn/sessions */ + xstrlcpy(cfgdir + r + 4 - 1, "/sessions", 10); + DPRINTF_S(cfgdir); + + xstrlcpy(sessiondir, cfgdir, len); + DPRINTF_S(sessiondir); + + if (!create_dir(cfgdir)) { + xerror(); + return FALSE; + } + /* Reset to config path */ cfgdir[r + 3] = '\0'; DPRINTF_S(cfgdir); @@ -5056,6 +5219,7 @@ static void cleanup(void) { free(g_selpath); free(plugindir); + free(sessiondir); free(cfgdir); free(initpath); free(bmstr); @@ -5070,12 +5234,13 @@ int main(int argc, char *argv[]) { mmask_t mask; char *arg = NULL; + char *session = NULL; int opt; #ifdef __linux__ bool progress = FALSE; #endif - while ((opt = getopt(argc, argv, "HSKiab:cdfnop:rstvh")) != -1) { + while ((opt = getopt(argc, argv, "HSKiab:cde:fnop:rstvh")) != -1) { switch (opt) { case 'S': cfg.blkorder = 1; @@ -5096,6 +5261,9 @@ arg = optarg; break; case 'c': cfg.cliopener = 1; + break; + case 'e': + session = optarg; break; case 'f': cfg.filtercmd = 1; @@ -5348,7 +5516,7 @@ if (!initcurses(&mask)) return _FAILURE; - browse(initpath); + browse(initpath, session); mousemask(mask, NULL); exitcurses(); diff --git a/src/nnn.h b/src/nnn.h index 88d9011c3d135993df0b70c7b5471c8d51015c18..9b38572750973828054ac1894eb6a9d9d3238654 100644 --- a/src/nnn.h +++ b/src/nnn.h @@ -106,6 +106,7 @@ SEL_QUITCTX, SEL_QUITCD, SEL_QUIT, SEL_CLICK, + SEL_SESSIONS, }; /* Associate a pressed key to an action */ @@ -269,4 +270,5 @@ /* Quit */ { 'Q', SEL_QUIT }, { CONTROL('Q'), SEL_QUIT }, { KEY_MOUSE, SEL_CLICK }, + { 'U', SEL_SESSIONS }, };