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.