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 01dc82cce11c65046e7fc2292b8fc99a6207b48c
Author: [email protected] <[email protected]>
AuthorDate: Wed Jun 3 20:42:02 2026 -0600

    feat: add tytab CLI tool and behavior toggle
    
    Add the tytab command-line tool, which talks to the running terminology
    over $TERMINOLOGY_IPC to create a new tab or split (optionally running a
    command, optionally targeting/naming a panel), and to set a panel name
    or freeze tytab in the current terminal. Add the Settings -> Behavior
    toggle to enable the feature.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 src/bin/meson.build        |   7 +
 src/bin/options_behavior.c |   2 +
 src/bin/tytab.c            | 310 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 319 insertions(+)

diff --git a/src/bin/meson.build b/src/bin/meson.build
index c775b4a0..67f2d743 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -49,6 +49,8 @@ tyq_sources = ['tycommon.c', 'tycommon.h', 'tyq.c']
 tycat_sources = ['tycommon.c', 'tycommon.h', 'tycat.c', 'extns.c', 'extns.h']
 tyls_sources = ['extns.c', 'extns.h', 'tyls.c', 'tycommon.c', 'tycommon.h']
 tysend_sources = ['tycommon.c', 'tycommon.h', 'tysend.c']
+tytab_sources = ['tycommon.c', 'tycommon.h', 'tytab.c',
+                  'tytab_ipc.c', 'tytab_ipc.h']
 tyfuzz_sources = ['termptyesc.c', 'termptyesc.h',
                   'backlog.c', 'backlog.h',
                   'termptyops.c', 'termptyops.h',
@@ -128,6 +130,11 @@ executable('tysend',
            install: true,
            include_directories: config_dir,
            dependencies: terminology_dependencies)
+executable('tytab',
+           tytab_sources,
+           install: true,
+           include_directories: config_dir,
+           dependencies: terminology_dependencies)
 
 if fuzzing
   executable('tyfuzz',
diff --git a/src/bin/options_behavior.c b/src/bin/options_behavior.c
index 433e75f0..821ecea8 100644
--- a/src/bin/options_behavior.c
+++ b/src/bin/options_behavior.c
@@ -38,6 +38,7 @@ OPTIONS_CB(Behavior_Ctx, login_shell, 0);
 OPTIONS_CB(Behavior_Ctx, show_tabs,  0);
 OPTIONS_CB(Behavior_Ctx, mv_always_show, 0);
 OPTIONS_CB(Behavior_Ctx, ty_escapes, 0);
+OPTIONS_CB(Behavior_Ctx, tytab, 0);
 OPTIONS_CB(Behavior_Ctx, selection_escapes, 0);
 OPTIONS_CB(Behavior_Ctx, changedir_to_current, 0);
 OPTIONS_CB(Behavior_Ctx, emoji_dbl_width, 0);
@@ -415,6 +416,7 @@ options_behavior(Evas_Object *opbox, Evas_Object *term)
    OPTIONS_CX(_("Open new terminals in current working directory"), changedir_to_current, 0);
    OPTIONS_CX(_("Always show miniview"), mv_always_show, 0);
    OPTIONS_CX(_("Enable special Terminology escape codes"), ty_escapes, 0);
+   OPTIONS_CX(_("Enable tytab escape codes for tab/split management"), tytab, 0);
    OPTIONS_CX(_("Enable escape codes manipulating selections"), selection_escapes, 0);
    OPTIONS_CX(_("Always treat Emojis as double-width characters"), emoji_dbl_width, 0);
    OPTIONS_CX(_("When grouping input, do it on all terminals and not just the visible ones"), group_all, 0);
diff --git a/src/bin/tytab.c b/src/bin/tytab.c
new file mode 100644
index 00000000..a803f400
--- /dev/null
+++ b/src/bin/tytab.c
@@ -0,0 +1,310 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <Ecore.h>
+#include <Ecore_Ipc.h>
+#include <Eet.h>
+#include "private.h"
+#include "tycommon.h"
+#include "tytab_ipc.h"
+
+static void
+print_usage(const char *argv0)
+{
+   printf("Usage: %s " HELP_ARGUMENT_SHORT " name [NAME]\n"
+          "       %s " HELP_ARGUMENT_SHORT " new [NAME DIR] [-- CMD...]\n"
+          "       %s " HELP_ARGUMENT_SHORT " freeze\n"
+          "\n"
+          "  Manage tabs and splits in Terminology.\n"
+          "\n"
+          "  Commands:\n"
+          "    name [NAME]              Set panel name (omit NAME to clear)\n"
+          "    new [NAME DIR] [-- CMD]  Create a new tab\n"
+          "    freeze                   Permanently disable tytab in this terminal\n"
+          "\n"
+          "  NAME       Target panel name\n"
+          "  DIR        Split direction: left, right, top, bottom (or l, r, t, b)\n"
+          "  CMD        Command to run (default: shell)\n"
+          "\n"
+          HELP_ARGUMENT_DOC "\n"
+          "\n",
+          argv0, argv0, argv0);
+}
+
+static char
+_parse_direction(const char *s)
+{
+   if (!strcmp(s, "l") || !strcmp(s, "left"))   return 'l';
+   if (!strcmp(s, "r") || !strcmp(s, "right"))  return 'r';
+   if (!strcmp(s, "t") || !strcmp(s, "top"))    return 't';
+   if (!strcmp(s, "b") || !strcmp(s, "bottom")) return 'b';
+   return 0;
+}
+
+static int
+_find_separator(int argc, char **argv, int start)
+{
+   int i;
+   for (i = start; i < argc; i++)
+     {
+        if (!strcmp(argv[i], "--"))
+          return i;
+     }
+   return -1;
+}
+
+static void
+_join_args(char *buf, size_t bufsz, int argc, char **argv, int start)
+{
+   size_t pos = 0;
+   int i;
+   for (i = start; i < argc && pos < bufsz - 1; i++)
+     {
+        if (i > start && pos < bufsz - 1)
+          buf[pos++] = ' ';
+        size_t len = strlen(argv[i]);
+        if (pos + len >= bufsz) len = bufsz - 1 - pos;
+        memcpy(buf + pos, argv[i], len);
+        pos += len;
+     }
+   buf[pos] = '\0';
+}
+
+/* IPC reply state */
+static Eina_Bool _reply_ok = EINA_FALSE;
+static char *_reply_error = NULL;
+static Eina_Bool _reply_received = EINA_FALSE;
+
+static Eina_Bool
+_cb_server_data(void *_data EINA_UNUSED,
+                int _type EINA_UNUSED,
+                void *event)
+{
+   Ecore_Ipc_Event_Server_Data *e = event;
+
+   if ((e->major == TY_IPC_MAJOR_TAB) &&
+       (e->minor == TY_IPC_MINOR_TAB) &&
+       (e->data) && (e->size > 0))
+     {
+        Ipc_Tab_Reply *rep;
+
+        rep = eet_data_descriptor_decode(tytab_ipc_rep_edd_get(), e->data, e->size);
+        if (rep)
+          {
+             _reply_ok = (rep->ok != 0);
+             if (!_reply_ok && rep->error)
+               _reply_error = strdup(rep->error);
+             free(rep);
+          }
+     }
+   _reply_received = EINA_TRUE;
+   ecore_main_loop_quit();
+   return ECORE_CALLBACK_DONE;
+}
+
+static Eina_Bool
+_cb_server_del(void *_data EINA_UNUSED,
+               int _type EINA_UNUSED,
+               void *_event EINA_UNUSED)
+{
+   _reply_received = EINA_TRUE;
+   ecore_main_loop_quit();
+   return ECORE_CALLBACK_DONE;
+}
+
+static Eina_Bool
+_cb_timeout(void *_data EINA_UNUSED)
+{
+   _reply_received = EINA_TRUE;
+   ecore_main_loop_quit();
+   return ECORE_CALLBACK_CANCEL;
+}
+
+static Eina_Bool
+_request_new(unsigned int term_id, const char *name,
+             char dir, const char *cmd_str)
+{
+   char *sock;
+   Ecore_Ipc_Server *srv;
+   Ecore_Event_Handler *hnd_data, *hnd_del;
+   Ecore_Timer *timer;
+   Ipc_Tab_Request req = { 0 };
+   char dirbuf[2] = { 0, 0 };
+   void *data;
+   int size;
+
+   sock = getenv("TERMINOLOGY_IPC");
+   if (!sock)
+     {
+        fprintf(stderr, "TERMINOLOGY_IPC not set (need recent terminology)\n");
+        return EINA_FALSE;
+     }
+
+   srv = ecore_ipc_server_connect(ECORE_IPC_LOCAL_USER, sock, 0, NULL);
+   if (!srv)
+     {
+        fprintf(stderr, "failed to connect to terminology IPC\n");
+        return EINA_FALSE;
+     }
+
+   req.term_id = term_id;
+   req.name = (char *)name;
+   if (dir)
+     {
+        dirbuf[0] = dir;
+        req.direction = dirbuf;
+     }
+   else
+     req.direction = "";
+   req.cmd = (char *)cmd_str;
+
+   data = "" &req, &size);
+   if (!data)
+     {
+        ecore_ipc_server_del(srv);
+        return EINA_FALSE;
+     }
+
+   ecore_ipc_server_send(srv, TY_IPC_MAJOR_TAB, TY_IPC_MINOR_TAB,
+                          0, 0, 0, data, size);
+   ecore_ipc_server_flush(srv);
+   free(data);
+
+   hnd_data = ecore_event_handler_add(ECORE_IPC_EVENT_SERVER_DATA,
+                                      _cb_server_data, NULL);
+   hnd_del = ecore_event_handler_add(ECORE_IPC_EVENT_SERVER_DEL,
+                                     _cb_server_del, NULL);
+   timer = ecore_timer_add(0.5, _cb_timeout, NULL);
+
+   if (!_reply_received)
+     ecore_main_loop_begin();
+
+   ecore_event_handler_del(hnd_data);
+   ecore_event_handler_del(hnd_del);
+   if (timer) ecore_timer_del(timer);
+   ecore_ipc_server_del(srv);
+
+   return _reply_ok;
+}
+
+int
+main(int argc, char **argv)
+{
+   char tbuf[8192];
+
+   ON_NOT_RUNNING_IN_TERMINOLOGY_EXIT_1();
+   ARGUMENT_ENTRY_CHECK(argc, argv, print_usage);
+
+   if (argc < 2)
+     {
+        print_usage(argv[0]);
+        return EXIT_FAILURE;
+     }
+
+   if (!strcmp(argv[1], "name"))
+     {
+        /* tytab name [NAME] — direct escape, no IPC needed */
+        if (argc >= 3)
+          {
+             if (strchr(argv[2], ';'))
+               {
+                  fprintf(stderr, "NAME must not contain ';'\n");
+                  return EXIT_FAILURE;
+               }
+             snprintf(tbuf, sizeof(tbuf), "%c}tn;%s", 0x1b, argv[2]);
+          }
+        else
+          snprintf(tbuf, sizeof(tbuf), "%c}tn;", 0x1b);
+
+        if (ty_write(1, tbuf, strlen(tbuf) + 1) != (signed)(strlen(tbuf) + 1))
+          perror("write");
+     }
+   else if (!strcmp(argv[1], "freeze"))
+     {
+        /* tytab freeze — direct escape, no IPC needed */
+        snprintf(tbuf, sizeof(tbuf), "%c}tf", 0x1b);
+        if (ty_write(1, tbuf, strlen(tbuf) + 1) != (signed)(strlen(tbuf) + 1))
+          perror("write");
+     }
+   else if (!strcmp(argv[1], "new"))
+     {
+        /* tytab new [NAME DIR] [-- CMD...] — single IPC round-trip */
+        int sep = _find_separator(argc, argv, 2);
+        int positional_end = (sep >= 0) ? sep : argc;
+        int positional_count = positional_end - 2;
+        const char *name = NULL;
+        const char *term_id_str;
+        unsigned int term_id;
+        char dir = 0;
+        char cmdbuf[4096];
+        Eina_Bool ok;
+
+        cmdbuf[0] = '\0';
+
+        if (positional_count >= 2)
+          {
+             name = argv[2];
+             if (strchr(name, ';'))
+               {
+                  fprintf(stderr, "NAME must not contain ';'\n");
+                  return EXIT_FAILURE;
+               }
+             dir = _parse_direction(argv[3]);
+             if (!dir)
+               {
+                  fprintf(stderr, "invalid direction: %s\n", argv[3]);
+                  return EXIT_FAILURE;
+               }
+          }
+        else if (positional_count == 1)
+          {
+             fprintf(stderr, "NAME requires a DIR argument\n");
+             print_usage(argv[0]);
+             return EXIT_FAILURE;
+          }
+
+        if (sep >= 0 && sep + 1 < argc)
+          _join_args(cmdbuf, sizeof(cmdbuf), argc, argv, sep + 1);
+
+        term_id_str = getenv("TERMINOLOGY_ID");
+        if (!term_id_str)
+          {
+             fprintf(stderr, "TERMINOLOGY_ID not set "
+                     "(need recent terminology)\n");
+             return EXIT_FAILURE;
+          }
+        term_id = (unsigned int)strtoul(term_id_str, NULL, 10);
+
+        /* Send the request over IPC; terminology acts on it directly. */
+        ecore_init();
+        ecore_ipc_init();
+        eet_init();
+        tytab_ipc_eet_init();
+
+        ok = _request_new(term_id, name ? name : "",
+                          dir, cmdbuf[0] ? cmdbuf : "");
+
+        tytab_ipc_eet_shutdown();
+        eet_shutdown();
+        ecore_ipc_shutdown();
+        ecore_shutdown();
+
+        if (!ok)
+          {
+             if (_reply_error)
+               fprintf(stderr, "tytab: %s\n", _reply_error);
+             else
+               fprintf(stderr, "tytab: request rejected by terminology\n");
+             free(_reply_error);
+             return EXIT_FAILURE;
+          }
+     }
+   else
+     {
+        fprintf(stderr, "unknown command: %s\n", argv[1]);
+        print_usage(argv[0]);
+        return EXIT_FAILURE;
+     }
+
+   return EXIT_SUCCESS;
+}

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

Reply via email to