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

git pushed a commit to branch desktop-restore
in repository terminology.

View the commit online.

commit 33da88dedd29c19029ab5a410fccf7e1a6751c4b
Author: [email protected] <[email protected]>
AuthorDate: Fri Mar 20 10:26:16 2026 -0600

    feat: session metadata cache, lock files, focus and window role
    
    Session format:
    - Add is_focused to Session_Node (Solo: records which terminal had
      focus; Split: active_tab records last_focus child index 0/1)
    
    Session metadata cache:
    - Session_Meta struct with session name
    - Lazy cache built on first session_meta_list() call, invalidated
      on EIO monitor events and after session_save/session_del
    - session_meta_list() returns snapshot copies
    
    Session lock files:
    - .ses.lock files containing PID prevent loading a session twice
    - Stale locks (dead PID) cleaned up automatically
    - All locks removed on session_shutdown(); session_del() also unlocks
    - Lock only acquired when session genuinely loaded (not on fallback)
    
    Window role for Enlightenment remember:
    - Set WM_WINDOW_ROLE to "terminology-session:<name>" on windows
      created during session restore, enabling Enlightenment's window
      remember system to match and apply desktop placement rules
    
    Focus handling:
    - Remove premature wn->tc.is_focused = TRUE from win_new(); let WM
      focus_in event drive the entire focus chain with visual effects
    - Save/restore split->last_focus via win_split_set_last_focus()
    
    Deferred window show:
    - session_defer_show_begin/end batch evas_object_show() calls so
      multi-session restore doesn't trigger focus events mid-loop
---
 src/bin/session.c | 397 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 src/bin/session.h |  14 +-
 src/bin/win.c     |  15 ++-
 src/bin/win.h     |   1 +
 4 files changed, 404 insertions(+), 23 deletions(-)

diff --git a/src/bin/session.c b/src/bin/session.c
index 942481bd..9bbdc788 100644
--- a/src/bin/session.c
+++ b/src/bin/session.c
@@ -8,6 +8,8 @@
 #include <libgen.h>
 #include <limits.h>
 #include <string.h>
+#include <signal.h>
+#include <errno.h>
 
 #include <Elementary.h>
 #include <Efreet.h>
@@ -56,6 +58,11 @@ struct _Session_Node {
    Session_Node_Type type;
    int              active_tab;
    Eina_Bool        is_horizontal;
+   Eina_Bool        is_focused;  /* Solo only: this terminal had focus when
+                                  * saved.  Not applied on restore — focus is
+                                  * re-established by WM focus_in event.
+                                  * On SPLIT nodes, active_tab records the
+                                  * last_focus child index (0=tc1, 1=tc2). */
 };
 
 typedef struct {
@@ -73,8 +80,26 @@ static Eet_Data_Descriptor *_edd_file = NULL;
 static Eio_Monitor *_session_dir_monitor = NULL;
 /* }}} */
 
+/* {{{ Session metadata cache */
+static Eina_List *_meta_cache = NULL;
+static Eina_Bool  _meta_cache_valid = EINA_FALSE;
+/* }}} */
+
+/* {{{ Session lock files */
+static Eina_List *_locked_sessions = NULL; /* list of eina_stringshare names */
+/* }}} */
+
+/* {{{ Deferred window show */
+static Eina_List *_pending_show = NULL; /* list of Win* to show after all loads */
+static Eina_Bool  _defer_show = EINA_FALSE;
+/* }}} */
+
 static const char *_session_dir_get(void);
 static Eina_Bool _session_dir_ensure(void);
+static void _meta_cache_free(void);
+static Eina_Bool _session_is_locked(const char *name);
+static Eina_Bool _session_lock(const char *name);
+static void _session_unlock_all(void);
 
 void
 session_init(void)
@@ -110,6 +135,8 @@ session_init(void)
                                  active_tab, EET_T_INT);
    EET_DATA_DESCRIPTOR_ADD_BASIC(_edd_node, Session_Node, "is_horizontal",
                                  is_horizontal, EET_T_UCHAR);
+   EET_DATA_DESCRIPTOR_ADD_BASIC(_edd_node, Session_Node, "is_focused",
+                                 is_focused, EET_T_UCHAR);
 
    /* Session_File */
    eet_eina_stream_data_descriptor_class_set(&eddc, sizeof(eddc),
@@ -139,6 +166,10 @@ void
 session_shutdown(void)
 {
    if (_session_log_dom < 0) return;
+   _session_unlock_all();
+   _meta_cache_free();
+   eina_list_free(_pending_show);
+   _pending_show = NULL;
    if (_session_dir_monitor)
      {
         eio_monitor_del(_session_dir_monitor);
@@ -445,6 +476,9 @@ _save_node(Term_Container *tc)
            if (!node) return NULL;
            node->is_horizontal = split->is_horizontal;
            node->split_ratio = elm_panes_content_left_size_get(split->panes);
+           /* Record which child had focus (0=tc1, 1=tc2) so restore
+            * can set split->last_focus correctly. */
+           node->active_tab = (split->last_focus == split->tc1) ? 0 : 1;
            if (split->tc1)
              {
                 Session_Node *c = _save_node(split->tc1);
@@ -512,6 +546,7 @@ _save_node(Term_Container *tc)
               const char *utitle = termio_user_title_get(termio);
               node->tab_title = utitle ? eina_stringshare_add(utitle) : NULL;
            }
+           node->is_focused = tc->is_focused;
         }
         break;
 
@@ -524,11 +559,13 @@ _save_node(Term_Container *tc)
 Eina_Bool
 session_save(const char *name, Win *wn)
 {
-   Session_File sf = { NULL, SESSION_VERSION };
+   Session_File sf = {0};
    char path[PATH_MAX], tmp[PATH_MAX];
    Eet_File *ef;
    Eina_Bool ok = EINA_FALSE;
 
+   sf.version = SESSION_VERSION;
+
    if (!_session_name_valid(name))
      {
         ERR("session_save: invalid name '%s'", name);
@@ -738,8 +775,14 @@ _restore_stub(Session_Node *node, Win *wn,
                 return NULL;
              }
 
-           return win_split_new(tc1, tc2, node->split_ratio,
-                                node->is_horizontal);
+           {
+              Term_Container *split_tc = win_split_new(tc1, tc2,
+                                                       node->split_ratio,
+                                                       node->is_horizontal);
+              if (split_tc)
+                win_split_set_last_focus(split_tc, node->active_tab);
+              return split_tc;
+           }
         }
 
       case SESSION_NODE_TABS:
@@ -757,7 +800,6 @@ _restore_stub(Session_Node *node, Win *wn,
                 _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)
@@ -876,6 +918,12 @@ session_load(const char *name, Win *caller_wn)
         return EINA_FALSE;
      }
 
+   if (_session_is_locked(name))
+     {
+        WRN("session_load: session '%s' is already in use, skipping", name);
+        return EINA_FALSE;
+     }
+
    if (!_session_path_get(name, path, sizeof(path)))
      return EINA_FALSE;
 
@@ -921,19 +969,27 @@ session_load(const char *name, Win *caller_wn)
 
         if (win_node->type != SESSION_NODE_WIN) continue;
 
-        if (inject_wn)
-          {
-             wn = inject_wn;
-             win_clear_content(wn);
-             inject_wn = NULL;
-          }
-        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 a window role from the session name so Enlightenment's
+         * window remember system can match it for desktop placement. */
+        {
+           char role[128];
+           snprintf(role, sizeof(role), "terminology-session:%s", name);
+
+           if (inject_wn)
+             {
+                wn = inject_wn;
+                win_clear_content(wn);
+                elm_win_role_set(win_evas_object_get(wn), role);
+                inject_wn = NULL;
+             }
+           else
+             {
+                wn = win_new(NULL, role, 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 */
         {
@@ -948,6 +1004,14 @@ session_load(const char *name, Win *caller_wn)
         /* Install the tree into the window */
         win_tc_set(wn, root_tc);
 
+        /* Focus is NOT initialized here.  win_new() no longer sets
+         * is_focused, and _restore_stub does not copy is_focused flags.
+         * The entire tree starts with is_focused=FALSE.  The WM will
+         * deliver a focus_in event after evas_object_show, which drives
+         * the normal focus path with all visual effects (cursor blink,
+         * focus signals, termio_focus_in).  split->last_focus and
+         * tabs->current are already set correctly by _restore_stub. */
+
         /* Fill deferred Tabs solos (need live parent now) */
         _restore_fill_tabs(pending_tabs, wn, &success);
         pending_tabs = NULL;
@@ -962,7 +1026,10 @@ session_load(const char *name, Win *caller_wn)
 
         total_success += success;
 
-        evas_object_show(win_evas_object_get(wn));
+        if (_defer_show)
+          _pending_show = eina_list_append(_pending_show, wn);
+        else
+          evas_object_show(win_evas_object_get(wn));
         ok = EINA_TRUE;
         continue;
 
@@ -994,7 +1061,10 @@ win_fallback:
              ok = EINA_TRUE;
           }
         win_sizing_handle(wn);
-        evas_object_show(win_evas_object_get(wn));
+        if (_defer_show)
+          _pending_show = eina_list_append(_pending_show, wn);
+        else
+          evas_object_show(win_evas_object_get(wn));
      }
 
    /* Global fallback: zero terminals across all windows */
@@ -1014,11 +1084,21 @@ win_fallback:
                                 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));
+             if (_defer_show)
+               _pending_show = eina_list_append(_pending_show, fallback_wn);
+             else
+               evas_object_show(win_evas_object_get(fallback_wn));
           }
      }
 
    _session_file_free(sf);
+
+   /* Only lock when at least one terminal was genuinely restored from the
+    * session data.  The global fallback (total_success == 0) creates a
+    * default terminal, which should not block future load attempts. */
+   if (ok && total_success > 0)
+     _session_lock(name);
+
    return ok;
 }
 
@@ -1044,6 +1124,143 @@ _session_path_get(const char *name, char *buf, size_t size)
    return EINA_TRUE;
 }
 
+/* {{{ Session lock helpers
+ * Lock files prevent loading a session that is already active in a running
+ * Terminology instance.  The lock is a plain text file containing the PID.
+ * A lock is considered stale if the owning PID no longer exists. */
+
+static Eina_Bool
+_session_lock_path_get(const char *name, char *buf, size_t size)
+{
+   /* Derive from _session_path_get so both share the same directory logic. */
+   char ses[PATH_MAX];
+   int n;
+
+   if (!_session_path_get(name, ses, sizeof(ses)))
+     {
+        buf[0] = '\0';
+        return EINA_FALSE;
+     }
+   n = snprintf(buf, size, "%s.lock", ses);
+   if (n < 0 || (size_t)n >= size)
+     {
+        buf[0] = '\0';
+        return EINA_FALSE;
+     }
+   return EINA_TRUE;
+}
+
+static Eina_Bool
+_session_is_locked(const char *name)
+{
+   char path[PATH_MAX];
+   FILE *fp;
+   pid_t pid = 0;
+
+   if (!_session_lock_path_get(name, path, sizeof(path)))
+     return EINA_FALSE;
+
+   fp = fopen(path, "r");
+   if (!fp)
+     {
+        if (errno != ENOENT)
+          WRN("session '%s': cannot read lock file '%s': %s",
+              name, path, strerror(errno));
+        return EINA_FALSE;
+     }
+
+   if (fscanf(fp, "%d", &pid) != 1 || pid <= 0)
+     {
+        fclose(fp);
+        /* Corrupt lock — treat as stale */
+        ecore_file_unlink(path);
+        return EINA_FALSE;
+     }
+   fclose(fp);
+
+   /* Check if the process is still alive */
+   if (kill(pid, 0) == 0 || errno == EPERM)
+     {
+        /* Process exists (or we lack permission to signal it — still alive) */
+        if (pid == getpid())
+          {
+             /* We already own this lock */
+             INF("session '%s': already loaded in this instance (pid %d)",
+                 name, (int)pid);
+             return EINA_TRUE;
+          }
+        INF("session '%s': locked by pid %d", name, (int)pid);
+        return EINA_TRUE;
+     }
+
+   /* Process is dead — stale lock */
+   INF("session '%s': removing stale lock (pid %d)", name, (int)pid);
+   ecore_file_unlink(path);
+   return EINA_FALSE;
+}
+
+static Eina_Bool
+_session_lock(const char *name)
+{
+   char path[PATH_MAX];
+   FILE *fp;
+
+   if (!_session_lock_path_get(name, path, sizeof(path)))
+     return EINA_FALSE;
+
+   fp = fopen(path, "w");
+   if (!fp)
+     {
+        ERR("session '%s': cannot create lock file: %s", name, path);
+        return EINA_FALSE;
+     }
+   fprintf(fp, "%d\n", (int)getpid());
+   fclose(fp);
+
+   _locked_sessions = eina_list_append(_locked_sessions,
+                                        eina_stringshare_add(name));
+   return EINA_TRUE;
+}
+
+static void
+_session_unlock(const char *name)
+{
+   char path[PATH_MAX];
+   Eina_List *l;
+   const char *s;
+
+   if (!_session_lock_path_get(name, path, sizeof(path)))
+     return;
+
+   ecore_file_unlink(path);
+
+   EINA_LIST_FOREACH(_locked_sessions, l, s)
+     {
+        if (!strcmp(s, name))
+          {
+             eina_stringshare_del(s);
+             _locked_sessions = eina_list_remove_list(_locked_sessions, l);
+             break;
+          }
+     }
+}
+
+static void
+_session_unlock_all(void)
+{
+   const char *name;
+   char path[PATH_MAX];
+
+   EINA_LIST_FREE(_locked_sessions, name)
+     {
+        if (_session_lock_path_get(name, path, sizeof(path)))
+          ecore_file_unlink(path);
+        eina_stringshare_del(name);
+     }
+   _locked_sessions = NULL;
+}
+/* }}} */
+
 /* Used by session_save (Task 7) */
 static Eina_Bool
 _session_dir_ensure(void)
@@ -1064,6 +1281,95 @@ _session_dir_ensure(void)
    return EINA_TRUE;
 }
 
+static void
+_meta_cache_free(void)
+{
+   Session_Meta *m;
+   EINA_LIST_FREE(_meta_cache, m)
+     {
+        eina_stringshare_del(m->name);
+        free(m);
+     }
+   _meta_cache = NULL;
+   _meta_cache_valid = EINA_FALSE;
+}
+
+static void
+_meta_cache_build(void)
+{
+   char dir[PATH_MAX];
+   Eina_List *files;
+   char *file;
+
+   _meta_cache_free();
+
+   int n = snprintf(dir, sizeof(dir), "%s/terminology/sessions",
+                    _session_dir_get());
+   if (n < 0 || (size_t)n >= sizeof(dir))
+     {
+        _meta_cache_valid = EINA_TRUE;
+        return;
+     }
+
+   if (!ecore_file_is_dir(dir))
+     {
+        _meta_cache_valid = EINA_TRUE;
+        return;
+     }
+
+   files = ecore_file_ls(dir);
+   EINA_LIST_FREE(files, file)
+     {
+        size_t len = strlen(file);
+        if (len > 4 && !strcmp(file + len - 4, ".ses"))
+          {
+             char path[PATH_MAX];
+             int pn = snprintf(path, sizeof(path), "%s/%s", dir, file);
+             if (pn >= 0 && (size_t)pn < sizeof(path))
+               {
+                  Eet_File *ef = eet_open(path, EET_FILE_MODE_READ);
+                  if (ef)
+                    {
+                       Session_File *sf = eet_data_read(ef, _edd_file,
+                                                        "session");
+                       if (sf)
+                         {
+                            Session_Meta *m = calloc(1, sizeof(*m));
+                            if (m)
+                              {
+                                 char *name = strndup(file, len - 4);
+                                 if (name)
+                                   {
+                                      m->name = eina_stringshare_add(name);
+                                      free(name);
+                                   }
+                                 if (!m->name)
+                                   {
+                                      free(m);
+                                   }
+                                 else
+                                   {
+                                      _meta_cache = eina_list_append(_meta_cache, m);
+                                   }
+                              }
+                            _session_file_free(sf);
+                         }
+                       eet_close(ef);
+                    }
+               }
+          }
+        free(file);
+     }
+
+   _meta_cache_valid = EINA_TRUE;
+}
+
+void
+session_meta_cache_invalidate(void)
+{
+   _meta_cache_valid = EINA_FALSE;
+}
+
 Eina_List *
 session_list(void)
 {
@@ -1106,6 +1412,56 @@ session_list_free(Eina_List *list)
      eina_stringshare_del(name);
 }
 
+Eina_List *
+session_meta_list(void)
+{
+   Eina_List *result = NULL;
+   const Eina_List *l;
+   Session_Meta *m;
+
+   if (!_meta_cache_valid)
+     _meta_cache_build();
+
+   EINA_LIST_FOREACH(_meta_cache, l, m)
+     {
+        Session_Meta *copy = calloc(1, sizeof(*copy));
+        if (copy)
+          {
+             copy->name = eina_stringshare_add(m->name);
+             result = eina_list_append(result, copy);
+          }
+     }
+   return result;
+}
+
+void
+session_meta_list_free(Eina_List *list)
+{
+   Session_Meta *m;
+   EINA_LIST_FREE(list, m)
+     {
+        eina_stringshare_del(m->name);
+        free(m);
+     }
+}
+
+/* Not re-entrant: must not be called while already deferred. */
+void
+session_defer_show_begin(void)
+{
+   _defer_show = EINA_TRUE;
+}
+
+void
+session_defer_show_end(void)
+{
+   Win *wn;
+
+   _defer_show = EINA_FALSE;
+   EINA_LIST_FREE(_pending_show, wn)
+     evas_object_show(win_evas_object_get(wn));
+}
+
 Eina_Bool
 session_del(const char *name)
 {
@@ -1128,6 +1484,7 @@ session_del(const char *name)
         ERR("session_del: failed to delete: %s", path);
         return EINA_FALSE;
      }
+   _session_unlock(name);
    return EINA_TRUE;
 }
 
diff --git a/src/bin/session.h b/src/bin/session.h
index b05df682..eb8f0622 100644
--- a/src/bin/session.h
+++ b/src/bin/session.h
@@ -6,7 +6,7 @@
 #include <Eio.h>
 #include "win.h"
 
-#define SESSION_VERSION 1
+#define SESSION_VERSION 2
 
 void session_init(void);
 void session_shutdown(void);
@@ -21,6 +21,14 @@ Eina_List  *session_list(void);
 void        session_list_free(Eina_List *list);
 Eina_Bool   session_del(const char *name);
 
+typedef struct {
+   const char *name;    /* eina_stringshare */
+} Session_Meta;
+
+Eina_List  *session_meta_list(void);
+void        session_meta_list_free(Eina_List *list);
+void        session_meta_cache_invalidate(void);
+
 void session_popup_save_show(Evas_Object *win, Evas_Object *base,
                              Evas_Object *bg, Evas_Object *term,
                              void (*donecb)(void *), void *donedata);
@@ -30,5 +38,7 @@ void session_popup_load_show(Evas_Object *win, Evas_Object *base,
 
 Eio_Monitor *session_monitor_get(void);
 
-#endif
+void session_defer_show_begin(void);
+void session_defer_show_end(void);
 
+#endif
diff --git a/src/bin/win.c b/src/bin/win.c
index cd790351..a042da17 100644
--- a/src/bin/win.c
+++ b/src/bin/win.c
@@ -2402,7 +2402,11 @@ imf_done:
 
    wins = eina_list_append(wins, wn);
 
-   wn->tc.is_focused = EINA_TRUE;
+   /* Do NOT set wn->tc.is_focused here.  The WM will deliver a focus_in
+    * event after the window is shown, which drives _cb_win_focus_in →
+    * _win_focus → child focus chain with all visual side effects (cursor
+    * blink, focus signals, termio_focus_in).  Setting it prematurely
+    * creates an inconsistent state (Win focused, children not). */
 
    wn->config_elm_handler = ecore_event_handler_add
      (ELM_EVENT_CONFIG_ALL_CHANGED, _cb_elm_config_change, wn);
@@ -3173,6 +3177,15 @@ win_split_new(Term_Container *tc1, Term_Container *tc2,
    return _split_new(tc1, tc2, ratio, is_horizontal);
 }
 
+void
+win_split_set_last_focus(Term_Container *split_tc, int child_idx)
+{
+   Split *split;
+   if (!split_tc || split_tc->type != TERM_CONTAINER_TYPE_SPLIT) return;
+   split = (Split *)split_tc;
+   split->last_focus = (child_idx == 0) ? split->tc1 : split->tc2;
+}
+
 static void
 _size_job(void *data)
 {
diff --git a/src/bin/win.h b/src/bin/win.h
index 4728dc6a..92c719ed 100644
--- a/src/bin/win.h
+++ b/src/bin/win.h
@@ -99,6 +99,7 @@ struct tag_Solo {
 Term_Container *win_solo_new(Term *term, Win *wn);
 Term_Container *win_split_new(Term_Container *tc1, Term_Container *tc2,
                               double ratio, Eina_Bool is_horizontal);
+void            win_split_set_last_focus(Term_Container *split_tc, int child_idx);
 Term_Container *win_tabs_create(Term_Container *first_solo);
 void            win_tabs_term_append(Term_Container *tabs_tc, Term *term);
 void            win_tabs_focus_index(Term_Container *tabs_tc, int index);

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

Reply via email to