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.

Reply via email to