This is an automated email from the git hooks/post-receive script.

git pushed a commit to reference refs/pull/29/head
in repository terminology.

View the commit online.

commit 39bff5194cca5f16d6188982856127c08274961c
Author: [email protected] <[email protected]>
AuthorDate: Sat Mar 14 19:07:36 2026 -0600

    session: implement session_load with two-pass tree restore and fallback
    
    Adds _build_mux_cmd, _restore_stub, _restore_fill_tabs, _win_is_pristine,
    _session_file_free, and session_load. Uses two-pass approach: pass 1 builds
    the TC stub tree (deferring extra Tabs solos via Tabs_Pending list), pass 2
    fills deferred solos after win_tc_set installs the tree so tabs_tc->parent
    is valid. Includes pristine-window reuse, per-window and global fallback
    terminals, and version guard. Fixes eina_list_nth_data (non-existent API)
    to eina_list_nth, and replaces eet_data_descriptor_decode_free (absent in
    EET 1.28) with a manual _session_file_free walker.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 src/bin/session.c | 422 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 419 insertions(+), 3 deletions(-)

diff --git a/src/bin/session.c b/src/bin/session.c
index deba224d..f92835f5 100644
--- a/src/bin/session.c
+++ b/src/bin/session.c
@@ -584,9 +584,425 @@ cleanup:
    return ok;
 }
 
-Eina_Bool session_load(const char *name EINA_UNUSED,
-                       Win *wn EINA_UNUSED)
-{ return EINA_FALSE; }
+/* Free a Session_File decoded by eet_data_read.
+ * EET uses eina_stringshare for EET_T_STRING fields, so _node_free handles
+ * them correctly.  The Session_File shell itself is plain malloc. */
+static void
+_session_file_free(Session_File *sf)
+{
+   Session_Node *n;
+   if (!sf) return;
+   EINA_LIST_FREE(sf->windows, n)
+     _node_free(n);
+   free(sf);
+}
+
+/* Returns a malloc'd command string or NULL for plain shell.
+ * Caller must free(). */
+static char *
+_build_mux_cmd(const Session_Node *solo)
+{
+   char buf[512];
+
+   if (!solo->mux_type || !solo->mux_session)
+     return NULL;
+
+   /* Validate mux_session to prevent shell injection from crafted files */
+   if (!_session_name_valid(solo->mux_session))
+     {
+        WRN("session_load: mux_session '%s' fails validation, skipping mux attach",
+            solo->mux_session);
+        return NULL;
+     }
+
+   if (!strcmp(solo->mux_type, "tmux"))
+     {
+        int n = snprintf(buf, sizeof(buf),
+                         "tmux new-session -A -s %s", solo->mux_session);
+        if (n < 0 || (size_t)n >= sizeof(buf)) return NULL;
+        return strdup(buf);
+     }
+   else if (!strcmp(solo->mux_type, "screen"))
+     {
+        int n = snprintf(buf, sizeof(buf),
+                         "screen -R %s", solo->mux_session);
+        if (n < 0 || (size_t)n >= sizeof(buf)) return NULL;
+        return strdup(buf);
+     }
+   return NULL;
+}
+
+typedef struct {
+   Session_Node    *node;
+   Term_Container  *tc;
+} Tabs_Pending;
+
+/* Close a partially-built TC tree and all its terms.
+ * Called when _restore_stub fails after creating child TCs. */
+static void
+_tc_tree_close(Term_Container *tc, Win *wn)
+{
+   if (!tc) return;
+
+   switch (tc->type)
+     {
+      case TERM_CONTAINER_TYPE_SOLO:
+        {
+           Solo *solo = (Solo*)tc;
+           if (solo->term)
+             term_close(win_evas_object_get(wn),
+                        term_termio_get(solo->term), EINA_FALSE);
+        }
+        break;
+
+      case TERM_CONTAINER_TYPE_SPLIT:
+        {
+           Split *split = (Split*)tc;
+           _tc_tree_close(split->tc1, wn);
+           _tc_tree_close(split->tc2, wn);
+        }
+        break;
+
+      case TERM_CONTAINER_TYPE_TABS:
+        {
+           Tabs *tabs = (Tabs*)tc;
+           Tab_Item *tab_item;
+           Eina_List *l;
+           EINA_LIST_FOREACH(tabs->tabs, l, tab_item)
+             {
+                if (tab_item && tab_item->tc)
+                  _tc_tree_close(tab_item->tc, wn);
+             }
+        }
+        break;
+
+      default:
+        break;
+     }
+}
+
+/* Builds the full TC stub tree. For TABS, only the first Solo is added;
+ * additional Solos are deferred via pending_tabs list.
+ * Returns the root Term_Container* or NULL on complete failure. */
+static Term_Container *
+_restore_stub(Session_Node *node, Win *wn,
+              Eina_List **pending_tabs, int *success)
+{
+   switch (node->type)
+     {
+      case SESSION_NODE_SPLIT:
+        {
+           Term_Container *tc1 = NULL, *tc2 = NULL;
+           Session_Node *child1, *child2;
+
+           child1 = eina_list_data_get(node->children);
+           child2 = eina_list_nth(node->children, 1);
+           if (!child1 || !child2) return NULL;
+
+           tc1 = _restore_stub(child1, wn, pending_tabs, success);
+           if (!tc1) return NULL;
+           tc2 = _restore_stub(child2, wn, pending_tabs, success);
+           if (!tc2)
+             {
+                _tc_tree_close(tc1, wn);
+                return NULL;
+             }
+
+           return win_split_new(tc1, tc2, node->split_ratio,
+                                node->is_horizontal);
+        }
+
+      case SESSION_NODE_TABS:
+        {
+           Session_Node *first = eina_list_data_get(node->children);
+           if (!first) return NULL;
+
+           Term_Container *first_solo = _restore_stub(first, wn,
+                                                      pending_tabs, success);
+           if (!first_solo) return NULL;
+
+           Term_Container *tabs_tc = win_tabs_create(first_solo);
+           if (!tabs_tc)
+             {
+                _tc_tree_close(first_solo, wn);
+                return NULL;
+             }
+
+           /* Defer the remaining solos — they need tabs_tc to be
+            * installed in the window before _tabs_attach can run */
+           if (eina_list_count(node->children) > 1)
+             {
+                Tabs_Pending *tp = malloc(sizeof(Tabs_Pending));
+                if (tp)
+                  {
+                     tp->node = node;
+                     tp->tc   = tabs_tc;
+                     *pending_tabs = eina_list_append(*pending_tabs, tp);
+                  }
+             }
+           return tabs_tc;
+        }
+
+      case SESSION_NODE_SOLO:
+        {
+           char *cmd = _build_mux_cmd(node);
+           Config *cfg = main_config_get();
+           Term *term;
+
+           term = term_new(wn, cfg, cmd,
+                           cfg->login_shell,
+                           node->cwd,
+                           cfg->cg_width  > 0 ? cfg->cg_width  : 80,
+                           cfg->cg_height > 0 ? cfg->cg_height : 24,
+                           EINA_FALSE, NULL);
+           free(cmd);
+
+           if (!term) return NULL;
+           (*success)++;
+
+           /* Set tab title */
+           {
+              const char *title = node->mux_session
+                                  ? node->mux_session
+                                  : node->tab_title;
+              if (title)
+                termio_user_title_set(term_termio_get(term), title);
+           }
+
+           return win_solo_new(term, wn);
+        }
+
+      default:
+        return NULL;
+     }
+}
+
+/* Second pass: fill remaining Solos into Tabs nodes now that the
+ * Tabs container is installed in the window (has a valid parent). */
+static void
+_restore_fill_tabs(Eina_List *pending_tabs, Win *wn, int *success)
+{
+   Tabs_Pending *tp;
+   EINA_LIST_FREE(pending_tabs, tp)
+     {
+        Session_Node *child;
+        Eina_List *l;
+        int i = 0;
+
+        EINA_LIST_FOREACH(tp->node->children, l, child)
+          {
+             if (i++ == 0) continue; /* first already done in _restore_stub */
+             char *cmd = _build_mux_cmd(child);
+             Config *cfg = main_config_get();
+             Term *term = term_new(wn, cfg, cmd,
+                                   cfg->login_shell,
+                                   child->cwd,
+                                   cfg->cg_width  > 0 ? cfg->cg_width  : 80,
+                                   cfg->cg_height > 0 ? cfg->cg_height : 24,
+                                   EINA_FALSE, NULL);
+             free(cmd);
+             if (!term) continue;
+             (*success)++;
+
+             const char *title = child->mux_session
+                                 ? child->mux_session
+                                 : child->tab_title;
+             if (title)
+               termio_user_title_set(term_termio_get(term), title);
+
+             win_tabs_term_append(tp->tc, term);
+          }
+        win_tabs_focus_index(tp->tc, tp->node->active_tab);
+        free(tp);
+     }
+}
+
+/* Returns EINA_TRUE if the window has a single shell terminal at prompt
+ * with no active child process (best-effort). */
+static Eina_Bool
+_win_is_pristine(Win *wn)
+{
+   Eina_List *terms;
+   Term *term;
+   Evas_Object *termio;
+   pid_t shell_pid, fg_pid;
+   int pty_fd;
+
+   terms = win_terms_get(wn);
+   if (!terms || eina_list_count(terms) != 1) return EINA_FALSE;
+   term = eina_list_data_get(terms);
+   if (!term) return EINA_FALSE;
+
+   termio = term_termio_get(term);
+   shell_pid = termio_pid_get(termio);
+   pty_fd = termpty_fd_get(termio_pty_get(termio));
+   if (pty_fd < 0) return EINA_FALSE;
+
+   fg_pid = tcgetpgrp(pty_fd);
+   if (fg_pid < 0) return EINA_FALSE; /* conservative: non-pristine */
+
+   return (fg_pid == shell_pid) ? EINA_TRUE : EINA_FALSE;
+}
+
+Eina_Bool
+session_load(const char *name, Win *caller_wn)
+{
+   char path[PATH_MAX];
+   Eet_File *ef;
+   Session_File *sf = NULL;
+   Session_Node *win_node;
+   Eina_List *l;
+   int total_success = 0;
+   Eina_Bool ok = EINA_FALSE;
+   Eina_Bool pristine = EINA_FALSE;
+
+   if (!_session_name_valid(name))
+     {
+        ERR("session_load: invalid name '%s'", name);
+        return EINA_FALSE;
+     }
+
+   if (!_session_path_get(name, path, sizeof(path)))
+     return EINA_FALSE;
+
+   ef = eet_open(path, EET_FILE_MODE_READ);
+   if (!ef)
+     {
+        ERR("session_load: cannot open '%s'", path);
+        return EINA_FALSE;
+     }
+
+   sf = eet_data_read(ef, _edd_file, "session");
+   eet_close(ef);
+
+   if (!sf)
+     {
+        ERR("session_load: EET read failed for '%s'", path);
+        return EINA_FALSE;
+     }
+
+   if (sf->version > SESSION_VERSION)
+     {
+        ERR("session_load: version %d > compiled %d, rejecting",
+            sf->version, SESSION_VERSION);
+        _session_file_free(sf);
+        return EINA_FALSE;
+     }
+   if (sf->version < SESSION_VERSION)
+     WRN("session_load: file version %d < compiled %d, some data may be missing",
+         sf->version, SESSION_VERSION);
+
+   /* Determine pristine-replace path */
+   if (caller_wn && _win_is_pristine(caller_wn))
+     pristine = EINA_TRUE;
+
+   EINA_LIST_FOREACH(sf->windows, l, win_node)
+     {
+        Win *wn;
+        Term_Container *root_tc = NULL;
+        Eina_List *pending_tabs = NULL;
+        int success = 0;
+
+        if (win_node->type != SESSION_NODE_WIN) continue;
+
+        if (pristine && caller_wn)
+          {
+             /* Reuse the existing window — close its current terminal */
+             Eina_List *terms = eina_list_clone(win_terms_get(caller_wn));
+             Term *t;
+             EINA_LIST_FREE(terms, t)
+               term_close(win_evas_object_get(caller_wn),
+                          term_termio_get(t), EINA_FALSE);
+             wn = caller_wn;
+             pristine = EINA_FALSE; /* only first win replaces */
+          }
+        else
+          {
+             wn = win_new(NULL, NULL, NULL, NULL, main_config_get(),
+                          EINA_FALSE, EINA_FALSE, EINA_FALSE, EINA_FALSE,
+                          EINA_FALSE);
+             if (!wn) continue;
+          }
+
+        /* Build stub tree from the WIN node's child */
+        {
+           Session_Node *child = eina_list_data_get(win_node->children);
+           if (child)
+             root_tc = _restore_stub(child, wn, &pending_tabs, &success);
+        }
+
+        if (!root_tc)
+          goto win_fallback;
+
+        /* Install the tree into the window */
+        win_tc_set(wn, root_tc);
+
+        /* Fill deferred Tabs solos (need live parent now) */
+        _restore_fill_tabs(pending_tabs, wn, &success);
+        pending_tabs = NULL;
+
+        total_success += success;
+
+        win_sizing_handle(wn);
+        evas_object_show(win_evas_object_get(wn));
+        ok = EINA_TRUE;
+        continue;
+
+win_fallback:
+        {
+           /* Clean up any pending_tabs */
+           Tabs_Pending *tp;
+           EINA_LIST_FREE(pending_tabs, tp) free(tp);
+        }
+        if (success == 0)
+          {
+             /* No terminal opened at all — create one default terminal */
+             ERR("session '%s': restore failed for a window, "
+                 "falling back to default terminal", name);
+             Term *fallback = term_new(wn, main_config_get(), NULL,
+                                       main_config_get()->login_shell,
+                                       NULL, 80, 24, EINA_FALSE, NULL);
+             if (fallback)
+               {
+                  win_term_set(wn, fallback);
+                  total_success++;
+                  ok = EINA_TRUE;
+               }
+          }
+        else
+          {
+             /* Partial success — some terms were created */
+             total_success += success;
+             ok = EINA_TRUE;
+          }
+        win_sizing_handle(wn);
+        evas_object_show(win_evas_object_get(wn));
+     }
+
+   /* Global fallback: zero terminals across all windows */
+   if (total_success == 0)
+     {
+        ERR("session '%s': complete restore failure, "
+            "creating default terminal", name);
+        Win *fallback_wn = caller_wn;
+        if (!fallback_wn)
+          fallback_wn = win_new(NULL, NULL, NULL, NULL, main_config_get(),
+                                EINA_FALSE, EINA_FALSE, EINA_FALSE,
+                                EINA_FALSE, EINA_FALSE);
+        if (fallback_wn)
+          {
+             Term *t = term_new(fallback_wn, main_config_get(), NULL,
+                                main_config_get()->login_shell,
+                                NULL, 80, 24, EINA_FALSE, NULL);
+             if (t) win_term_set(fallback_wn, t);
+             win_sizing_handle(fallback_wn);
+             evas_object_show(win_evas_object_get(fallback_wn));
+          }
+     }
+
+   _session_file_free(sf);
+   return ok;
+}
 
 static const char *
 _session_dir_get(void)

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to