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.