This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/34/head
in repository terminology.
View the commit online.
commit 604290ad760425889497cc00b472a95083377ebb
Author: [email protected] <[email protected]>
AuthorDate: Sat Mar 28 13:19:18 2026 -0600
feat: add escape sequence commands for named panel targeting and dynamic splits
Implement two new escape sequences that enable dynamic terminal layout
manipulation from within running applications:
1. \033}tn;NAME\0 — Assigns a name to the current panel (container).
Names are stored as Eina_Stringshare for O(1) lookup. Using steal
semantics: assigning the same name to multiple panels clears it from
previous containers (last writer wins).
2. \033}tt;NAME;DIR[;CMD]\0 — Creates a tab in a named panel, or creates
a split if the target panel doesn't exist. Parameters: NAME is the
target panel name, DIR is split direction (l/r/t/b), and CMD is an
optional shell command to run in the new terminal.
Anti-bombing protection prevents abuse: tab creation requires a key
press within the last 7 seconds, and each key press allows exactly one
tab to be created.
Implementation includes recursive container tree search, keypress
timestamp tracking, and proper resource cleanup on container destruction.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
src/bin/term_container.h | 1 +
src/bin/win.c | 285 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 286 insertions(+)
diff --git a/src/bin/term_container.h b/src/bin/term_container.h
index 08b15134..29e24d5d 100644
--- a/src/bin/term_container.h
+++ b/src/bin/term_container.h
@@ -43,6 +43,7 @@ struct tag_Term_Container {
Evas_Object *selector_img;
Eina_Bool is_focused;
const char *title;
+ Eina_Stringshare *name; /* optional panel name for escape sequence targeting */
Term *(*term_next)(const Term_Container *tc, const Term_Container *child);
Term *(*term_prev)(const Term_Container *tc, const Term_Container *child);
diff --git a/src/bin/win.c b/src/bin/win.c
index 7ed9f58a..2d7465ad 100644
--- a/src/bin/win.c
+++ b/src/bin/win.c
@@ -140,6 +140,7 @@ struct tag_Term
struct {
int x, y;
} down;
+ double last_keypress_time; /* for anti-bombing, updated on key-down */
int refcnt;
unsigned char hold : 1;
unsigned char unswallowed : 1;
@@ -527,6 +528,7 @@ _solo_close(Term_Container *tc,
tc->parent->close(tc->parent, tc);
eina_stringshare_del(tc->title);
+ eina_stringshare_del(tc->name);
term = solo->term;
term->container = NULL;
@@ -1387,6 +1389,7 @@ _win_close(Term_Container *tc,
return;
}
eina_stringshare_del(tc->title);
+ eina_stringshare_del(tc->name);
win_free(wn);
}
@@ -2052,6 +2055,9 @@ end:
if (term)
termio_key_down(term->termio, ev, done);
}
+ /* Update anti-bombing timestamp for escape tab/split creation */
+ if (term && !key_is_modifier(ev->key))
+ term->last_keypress_time = ecore_time_get();
}
static void
@@ -2773,6 +2779,7 @@ _split_close(Term_Container *tc, Term_Container *child)
evas_object_del(split->panes);
eina_stringshare_del(tc->title);
+ eina_stringshare_del(tc->name);
free(tc);
}
@@ -4708,6 +4715,7 @@ _tabs_close(Term_Container *tc, Term_Container *child)
_solo_tab_show(next_child);
eina_stringshare_del(tc->title);
+ eina_stringshare_del(tc->name);
tc_parent->swallow(tc_parent, tc, next_child);
if (tc->is_focused)
@@ -6534,6 +6542,49 @@ _sendfile_request(Term *term, const char *path)
elm_object_focus_set(o, EINA_TRUE);
}
+static Term_Container *
+_container_find_by_name(Term_Container *tc, Eina_Stringshare *name)
+{
+ if (!tc || !name)
+ return NULL;
+ if (tc->name == name)
+ return tc;
+
+ switch (tc->type)
+ {
+ case TERM_CONTAINER_TYPE_SOLO:
+ return NULL;
+ case TERM_CONTAINER_TYPE_WIN:
+ {
+ Win *wn = (Win *)tc;
+ return _container_find_by_name((Term_Container *)wn->child, name);
+ }
+ case TERM_CONTAINER_TYPE_SPLIT:
+ {
+ Split *sp = (Split *)tc;
+ Term_Container *found;
+ found = _container_find_by_name((Term_Container *)sp->tc1, name);
+ if (found) return found;
+ return _container_find_by_name((Term_Container *)sp->tc2, name);
+ }
+ case TERM_CONTAINER_TYPE_TABS:
+ {
+ Tabs *tabs = (Tabs *)tc;
+ Eina_List *l;
+ Tab_Item *tab;
+ EINA_LIST_FOREACH(tabs->tabs, l, tab)
+ {
+ Term_Container *found;
+ found = _container_find_by_name(tab->tc, name);
+ if (found) return found;
+ }
+ return NULL;
+ }
+ default:
+ return NULL;
+ }
+}
+
static void
_cb_command(void *data,
Evas_Object *_obj EINA_UNUSED,
@@ -6607,6 +6658,240 @@ _cb_command(void *data,
{
}
}
+ else if (cmd[0] == 't') // tab/split escape commands
+ {
+ if (cmd[1] == 'n') // name panel: tn;NAME
+ {
+ Term_Container *tc = term->container;
+ const char *name_str = (cmd[2] == ';') ? cmd + 3 : NULL;
+
+ if (name_str && name_str[0])
+ {
+ Eina_Stringshare *name = eina_stringshare_add(name_str);
+ Term_Container *existing;
+
+ /* Steal: remove name from any existing container */
+ existing = _container_find_by_name(
+ (Term_Container *)tc->wn, name);
+ if (existing)
+ eina_stringshare_replace(&existing->name, NULL);
+
+ eina_stringshare_replace(&tc->name, name);
+ eina_stringshare_del(name);
+ }
+ else
+ {
+ /* Empty name: clear */
+ eina_stringshare_replace(&tc->name, NULL);
+ }
+ }
+ else if (cmd[1] == 't') // new tab: tt[;NAME;DIR[;CMD]]
+ {
+ const char *p = cmd + 2;
+ const char *name_str = NULL, *cmd_str = NULL;
+ const char *name_end = NULL;
+ char direction = 0;
+ Eina_Stringshare *name = NULL;
+ Term_Container *tc = term->container;
+ Win *wn = tc->wn;
+ double now;
+ char buf[PATH_MAX];
+ char *wdir = NULL;
+
+ /* Parse: ;NAME;DIR[;CMD] — all optional */
+ if (*p == ';')
+ {
+ p++; /* skip first ; */
+ name_str = p;
+
+ name_end = strchr(p, ';');
+ if (!name_end) return; /* malformed: need at least ;NAME;DIR */
+ p = name_end + 1;
+
+ /* Parse direction (may be empty) */
+ if (*p == 'l' || *p == 'r' || *p == 't' || *p == 'b')
+ {
+ direction = *p;
+ p++;
+ }
+
+ /* Optional CMD after ; */
+ if (*p == ';' && *(p + 1))
+ cmd_str = p + 1;
+ }
+ /* else: no arguments at all — current panel, default shell */
+
+ /* Anti-bombing check */
+ now = ecore_time_get();
+ if (term->last_keypress_time == 0.0 ||
+ (now - term->last_keypress_time) > 7.0)
+ {
+ WRN("tab creation blocked: no recent keypress "
+ "(anti-bombing protection)");
+ return;
+ }
+ term->last_keypress_time = 0.0;
+
+ /* Always inherit CWD from requesting terminal */
+ if (termio_cwd_get(term->termio, buf, sizeof(buf)))
+ wdir = buf;
+
+ /* Build name if provided */
+ if (name_str && name_end && name_end > name_str)
+ name = eina_stringshare_add_length(name_str, name_end - name_str);
+
+ if (name && direction)
+ {
+ /* Full form: find named panel or create split */
+ Term_Container *target;
+ target = _container_find_by_name((Term_Container *)wn, name);
+ if (target)
+ {
+ /* Panel exists: find attachment point */
+ Term *tm_new;
+ Term_Container *tc_new, *tc_attach = NULL;
+
+ if (target->type == TERM_CONTAINER_TYPE_SOLO)
+ {
+ if (target->parent->type != TERM_CONTAINER_TYPE_TABS)
+ _tabs_new(target, target->parent);
+ tc_attach = target->parent;
+ }
+ else if (target->type == TERM_CONTAINER_TYPE_TABS)
+ {
+ tc_attach = target;
+ }
+ else
+ {
+ Term *tm_first = target->term_first(target);
+ if (tm_first)
+ {
+ Term_Container *tc_first = tm_first->container;
+ if (tc_first->parent->type !=
+ TERM_CONTAINER_TYPE_TABS)
+ _tabs_new(tc_first, tc_first->parent);
+ tc_attach = tc_first->parent;
+ }
+ }
+
+ if (!tc_attach)
+ {
+ eina_stringshare_del(name);
+ return;
+ }
+
+ tm_new = term_new(wn, wn->config,
+ cmd_str, wn->config->login_shell,
+ wdir, 80, 24, EINA_FALSE, NULL);
+ tc_new = _solo_new(tm_new, wn);
+ evas_object_data_set(tm_new->termio, "sizedone",
+ tm_new->termio);
+ _tabs_attach(tc_attach, tc_new);
+ }
+ else
+ {
+ /* Panel not found: split and create */
+ Split_Direction dir;
+ Term *tm_new;
+ Term_Container *tc_new;
+
+ switch (direction)
+ {
+ case 'l': dir = SPLIT_DIRECTION_LEFT; break;
+ case 'r': dir = SPLIT_DIRECTION_RIGHT; break;
+ case 't': dir = SPLIT_DIRECTION_TOP; break;
+ case 'b': dir = SPLIT_DIRECTION_BOTTOM; break;
+ default: eina_stringshare_del(name); return;
+ }
+
+ if (tc->type != TERM_CONTAINER_TYPE_SOLO)
+ {
+ WRN("tt: requesting terminal is not in a solo "
+ "container");
+ eina_stringshare_del(name);
+ return;
+ }
+
+ tm_new = term_new(wn, wn->config,
+ cmd_str, wn->config->login_shell,
+ wdir, 80, 24, EINA_FALSE, NULL);
+ tc_new = _solo_new(tm_new, wn);
+ evas_object_data_set(tm_new->termio, "sizedone",
+ tm_new->termio);
+ tc->parent->split_direction(tc->parent, tc, tc_new, dir);
+ eina_stringshare_replace(&tc_new->name, name);
+ }
+ }
+ else if (name && !direction)
+ {
+ /* Name provided but no direction: find panel, add tab (no split) */
+ Term_Container *target;
+ target = _container_find_by_name((Term_Container *)wn, name);
+ if (target)
+ {
+ Term *tm_new;
+ Term_Container *tc_new, *tc_attach = NULL;
+
+ if (target->type == TERM_CONTAINER_TYPE_SOLO)
+ {
+ if (target->parent->type != TERM_CONTAINER_TYPE_TABS)
+ _tabs_new(target, target->parent);
+ tc_attach = target->parent;
+ }
+ else if (target->type == TERM_CONTAINER_TYPE_TABS)
+ {
+ tc_attach = target;
+ }
+ else
+ {
+ Term *tm_first = target->term_first(target);
+ if (tm_first)
+ {
+ Term_Container *tc_first = tm_first->container;
+ if (tc_first->parent->type !=
+ TERM_CONTAINER_TYPE_TABS)
+ _tabs_new(tc_first, tc_first->parent);
+ tc_attach = tc_first->parent;
+ }
+ }
+
+ if (!tc_attach)
+ {
+ eina_stringshare_del(name);
+ return;
+ }
+
+ tm_new = term_new(wn, wn->config,
+ cmd_str, wn->config->login_shell,
+ wdir, 80, 24, EINA_FALSE, NULL);
+ tc_new = _solo_new(tm_new, wn);
+ evas_object_data_set(tm_new->termio, "sizedone",
+ tm_new->termio);
+ _tabs_attach(tc_attach, tc_new);
+ }
+ /* else: named panel not found, no direction to create — ignore */
+ }
+ else
+ {
+ /* No name, no direction: add tab to current panel */
+ Term *tm_new;
+ Term_Container *tc_new;
+
+ if (tc->parent->type != TERM_CONTAINER_TYPE_TABS)
+ _tabs_new(tc, tc->parent);
+
+ tm_new = term_new(wn, wn->config,
+ cmd_str, wn->config->login_shell, wdir,
+ 80, 24, EINA_FALSE, NULL);
+ tc_new = _solo_new(tm_new, wn);
+ evas_object_data_set(tm_new->termio, "sizedone",
+ tm_new->termio);
+ _tabs_attach(tc->parent, tc_new);
+ }
+
+ eina_stringshare_del(name);
+ }
+ }
}
static void
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.