]> Sergey Matveev's repositories - nnn.git/commitdiff
Implementing sessions support (#360)
authorAnna Arad <4895022+annagrram@users.noreply.github.com>
Fri, 18 Oct 2019 22:11:39 +0000 (01:11 +0300)
committerMischievous Meerkat <engineerarun@gmail.com>
Fri, 18 Oct 2019 22:11:39 +0000 (03:41 +0530)
* 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
misc/auto-completion/fish/nnn.fish
nnn.1
src/nnn.c
src/nnn.h

index d97b14497132339062380de2c55af5cd5edd6949..22180e51a760f06bdf12a1ededad6e48eeb446ae 100644 (file)
@@ -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
index ee3768a4268b5d51091237cf7ef6f46ecba74002..7119644b8e097cb59b0800ce9c44ba75a1b56905 100644 (file)
@@ -5,17 +5,24 @@
 #   Arun Prakash Jana <engineerarun@gmail.com>
 #
 
+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 (file)
--- 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.
index 41ed548be42f6e997feb21e4ebdec4dc2fa92d25..504797cb0a4396a460c5ee874a2a69345365d476 100644 (file)
--- a/src/nnn.c
+++ b/src/nnn.c
 /* 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);
 
+       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 @@ 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();
 
index 88d9011c3d135993df0b70c7b5471c8d51015c18..9b38572750973828054ac1894eb6a9d9d3238654 100644 (file)
--- 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 },
 };