From 60dac94a5e3727d7c8bf0f02f3d6f8b53f8d1d02 Mon Sep 17 00:00:00 2001 From: Anna Arad <4895022+annagrram@users.noreply.github.com> Date: Sat, 19 Oct 2019 01:11:39 +0300 Subject: [PATCH] Implementing sessions support (#360) * Initial commit of sessions implementation * Reduce code duplication * Move load session to program flag -e * Fix context initialization problem when loading session * Add pinned directory to session and reduce session file size * Make load_session print an error if exists and few minor adjustments * Refactor session's file structure * Initialize required structures in load_session before loading * Add load session dynamically, restore last session, and extra fixes * Fix indentation * Add sessions documentation to man page * Update fish completions with sessions and make some improvements * Move to single keybinding session management and add help info * ESC when asked to insert session name behaves better * Add sessions completion for bash * Remove pinned dir from session and minor code refactors --- misc/auto-completion/bash/nnn-completion.bash | 3 + misc/auto-completion/fish/nnn.fish | 11 +- nnn.1 | 18 ++ src/nnn.c | 204 ++++++++++++++++-- src/nnn.h | 2 + 5 files changed, 218 insertions(+), 20 deletions(-) diff --git a/misc/auto-completion/bash/nnn-completion.bash b/misc/auto-completion/bash/nnn-completion.bash index d97b1449..22180e51 100644 --- a/misc/auto-completion/bash/nnn-completion.bash +++ b/misc/auto-completion/bash/nnn-completion.bash @@ -34,6 +34,9 @@ _nnn () { 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 ee3768a4..7119644b 100644 --- a/misc/auto-completion/fish/nnn.fish +++ b/misc/auto-completion/fish/nnn.fish @@ -5,17 +5,24 @@ # 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 24b5eae7..1add6306 100644 --- a/nnn.1 +++ b/nnn.1 @@ -10,6 +10,7 @@ .Op Ar -b key .Op Ar -c .Op Ar -d +.Op Ar -e name .Op Ar -f .Op Ar -H .Op Ar -i @@ -52,6 +53,9 @@ supports the following options: .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 .Pp @@ -108,6 +112,20 @@ are available. The status of the contexts are shown in the top left corner: 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 41ed548b..504797cb 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 @@ typedef struct { 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 @@ static char *getreadline(char *prompt, char *path, char *curpath, int *presel) * 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 @@ static void savecurctx(settings *curcfg, char *path, char *curname, int r /* nex *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 @@ static void show_help(const char *path) "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 @@ static void redraw(char *path) 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 @@ static void browse(char *ipath) atexit(dentfree); - /* 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 */ + xlines = LINES; + xcols = COLS; - cfg.filtermode ? (presel = FILTER) : (presel = 0); + /* setup first context */ + 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'; + + presel = cfg.filtermode ? FILTER : 0; dents = xrealloc(dents, total_dents * sizeof(struct entry)); if (!dents) @@ -4878,6 +5011,22 @@ 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 @@ static void usage(void) " -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 @@ static bool setup_config(void) 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 @@ static bool setup_config(void) 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; @@ -5097,6 +5262,9 @@ int main(int argc, char *argv[]) case 'c': cfg.cliopener = 1; break; + case 'e': + session = optarg; + break; case 'f': cfg.filtercmd = 1; break; @@ -5348,7 +5516,7 @@ int main(int argc, char *argv[]) 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 88d9011c..9b385727 100644 --- a/src/nnn.h +++ b/src/nnn.h @@ -106,6 +106,7 @@ enum action { SEL_QUITCD, SEL_QUIT, SEL_CLICK, + SEL_SESSIONS, }; /* Associate a pressed key to an action */ @@ -269,4 +270,5 @@ static struct key bindings[] = { { 'Q', SEL_QUIT }, { CONTROL('Q'), SEL_QUIT }, { KEY_MOUSE, SEL_CLICK }, + { 'U', SEL_SESSIONS }, };