This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch tymux
in repository terminology.

View the commit online.

commit 030acfe8ce3e958f29bf79bf93f6872c38fc1c1d
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 15 22:00:14 2026 -0600

    feat: implement terminology client-side session attachment
    
    Add TermSession struct and client implementation for connecting to
    tymuxd daemon. When terminology --session=<name> is invoked, termsession_attach()
    connects to the daemon socket, receives the memfd via SCM_RIGHTS, maps it
    read-only, and creates a shadow Termpty whose write_cb hook forwards
    keyboard input to the daemon. An Ecore fd handler listens for NOTIFY,
    TITLE, BELL, and EXIT messages, invoking registered callbacks.
    
    The daemon is auto-spawned via _ensure_daemon if not already running.
    Also fixes meson.build source ordering: terminology_sources append was
    unreachable after executable() call.
    
    - termsession.h: public API (TermSession, termsession_attach/free/resize)
    - termsession.c: socket connection, memfd mmap, shadow Termpty, fd handler
    - meson.build: move terminology_sources append before executable() call
    - termsrv.c: add Ecore includes for terminology binary build
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 src/bin/meson.build   |  12 +-
 src/bin/termsession.c | 470 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/bin/termsession.h |  50 +++++-
 src/bin/termsrv.c     |   8 +
 4 files changed, 534 insertions(+), 6 deletions(-)

diff --git a/src/bin/meson.build b/src/bin/meson.build
index 874d3805..a84592ea 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -86,6 +86,13 @@ tytest_sources = ['termptyesc.c', 'termptyesc.h',
                   'tytest_common.c', 'tytest_common.h',
                   'tytest.c', 'tytest.h']
 
+if host_os == 'linux'
+  terminology_sources += [
+      'termsrv.c', 'termsrv.h',
+      'termsession.c', 'termsession.h',
+  ]
+endif
+
 executable('terminology',
            terminology_sources,
            edj_targets,
@@ -129,11 +136,6 @@ executable('tysend',
            dependencies: terminology_dependencies)
 
 if host_os == 'linux'
-  terminology_sources += [
-      'termsrv.c', 'termsrv.h',
-      'termsession.c', 'termsession.h',
-  ]
-
   tymuxd_sources = [
       'tymuxd.c', 'tymuxd.h',
       'tymux_session.c', 'tymux_session.h',
diff --git a/src/bin/termsession.c b/src/bin/termsession.c
index 5cd51c5c..918a8e19 100644
--- a/src/bin/termsession.c
+++ b/src/bin/termsession.c
@@ -1,2 +1,472 @@
 #include "private.h"
 #include "termsession.h"
+#include "termsrv.h"
+
+#ifdef HAVE_TYMUX
+
+#include <Ecore.h>
+#include <Eina.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <stdatomic.h>
+#include <time.h>
+#include <sys/stat.h>
+
+/* ── write_cb: forward input bytes to daemon via TSRV_MSG_INPUT ─────── */
+
+static void
+_session_write_cb(const char *buf, int len, void *data)
+{
+   TermSession *ts = data;
+
+   if (!ts || ts->sock_fd < 0 || len <= 0)
+     return;
+
+   if (termsrv_msg_send(ts->sock_fd, TSRV_MSG_INPUT,
+                        buf, (uint32_t)len) < 0)
+     ERR("termsession: failed to send INPUT: %s", strerror(errno));
+}
+
+/* ── shadow Termpty ─────────────────────────────────────────────────── */
+
+static Termpty *
+_shadow_pty_new(TermSession *ts, int cols, int rows)
+{
+   Termpty *ty;
+
+   ty = calloc(1, sizeof(Termpty));
+   if (!ty)
+     return NULL;
+
+   ty->fd      = -1;
+   ty->slavefd = -1;
+   ty->pid     = -1;
+   ty->w       = cols;
+   ty->h       = rows;
+
+   ty->screen = calloc((size_t)(cols * rows), sizeof(Termcell));
+   if (!ty->screen)
+     {
+        free(ty);
+        return NULL;
+     }
+
+   ty->screen2 = calloc((size_t)(cols * rows), sizeof(Termcell));
+   if (!ty->screen2)
+     {
+        free(ty->screen);
+        free(ty);
+        return NULL;
+     }
+
+   ty->write_cb.func = _session_write_cb;
+   ty->write_cb.data = ""
+
+   return ty;
+}
+
+static void
+_shadow_pty_free(Termpty *ty)
+{
+   if (!ty)
+     return;
+   free(ty->screen);
+   free(ty->screen2);
+   free(ty);
+}
+
+/* ── sync: copy shm cells into shadow pty ───────────────────────────── */
+
+static void
+_sync_shadow(TermSession *ts)
+{
+   TermShmHeader *shm = ts->shm;
+   Termpty       *ty  = ts->shadow_pty;
+   uint32_t       cols, rows;
+
+   cols = shm->cols;
+   rows = shm->rows;
+
+   /* Clamp to maximum dimensions defined in termsrv.h */
+   if (cols > TERMSRV_MAX_COLS) cols = TERMSRV_MAX_COLS;
+   if (rows > TERMSRV_MAX_ROWS) rows = TERMSRV_MAX_ROWS;
+   if (cols == 0 || rows == 0) return;
+
+   /* Resize screen buffers if dimensions changed */
+   if ((int)cols != ty->w || (int)rows != ty->h)
+     {
+        Termcell *s1, *s2;
+
+        s1 = realloc(ty->screen,  (size_t)(cols * rows) * sizeof(Termcell));
+        s2 = realloc(ty->screen2, (size_t)(cols * rows) * sizeof(Termcell));
+        if (!s1 || !s2)
+          {
+             ERR("termsession: realloc screen failed");
+             if (s1) ty->screen  = s1;
+             if (s2) ty->screen2 = s2;
+             return;
+          }
+        ty->screen  = s1;
+        ty->screen2 = s2;
+        ty->w = (int)cols;
+        ty->h = (int)rows;
+     }
+
+   /* Copy display-order cells from shm */
+   memcpy(ty->screen, TERMSRV_SHM_CELLS(shm),
+          (size_t)(cols * rows) * sizeof(Termcell));
+
+   /* Cursor position */
+   ty->cursor_state.cx = (int)shm->cursor_x;
+   ty->cursor_state.cy = (int)shm->cursor_y;
+   ty->termstate.hide_cursor = shm->cursor_visible ? 0 : 1;
+
+   /* Server writes cells in display order — no rotation needed */
+   ty->circular_offset = 0;
+}
+
+/* ── fd handler: receive messages from daemon ───────────────────────── */
+
+static Eina_Bool
+_cb_session_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
+{
+   TermSession *ts = data;
+   void        *payload = NULL;
+   uint32_t     size    = 0;
+   int          type;
+
+   type = termsrv_msg_recv(ts->sock_fd, &payload, &size);
+   if (type < 0)
+     {
+        ERR("termsession: recv error, assuming daemon exit");
+        if (ts->cb_exit.func)
+          ts->cb_exit.func(ts->cb_exit.data);
+        free(payload);
+        return ECORE_CALLBACK_CANCEL;
+     }
+
+   switch (type)
+     {
+      case TSRV_MSG_NOTIFY:
+        {
+           uint64_t seq;
+
+           /* Read write_seq from shm with acquire semantics */
+           seq = atomic_load_explicit(&ts->shm->write_seq,
+                                      memory_order_acquire);
+           if (seq != ts->last_seq)
+             {
+                ts->last_seq = seq;
+                _sync_shadow(ts);
+                if (ts->cb_change.func)
+                  ts->cb_change.func(ts->cb_change.data);
+             }
+           break;
+        }
+
+      case TSRV_MSG_TITLE:
+        if (ts->cb_title.func)
+          ts->cb_title.func(ts->cb_title.data);
+        break;
+
+      case TSRV_MSG_BELL:
+        if (ts->cb_bell.func)
+          ts->cb_bell.func(ts->cb_bell.data);
+        break;
+
+      case TSRV_MSG_EXIT:
+        if (ts->cb_exit.func)
+          ts->cb_exit.func(ts->cb_exit.data);
+        free(payload);
+        return ECORE_CALLBACK_CANCEL;
+
+      default:
+        WRN("termsession: unexpected message type %d", type);
+        break;
+     }
+
+   free(payload);
+   return ECORE_CALLBACK_RENEW;
+}
+
+/* ── daemon spawn helper ─────────────────────────────────────────────── */
+
+/*
+ * Fork, setsid, redirect stdio to /dev/null, and exec tymuxd with the
+ * session name.  Then poll for the socket to appear (up to 3 s).
+ * Returns 0 once the socket is reachable, -1 on timeout.
+ */
+static int
+_ensure_daemon(const char *session_name, const char *sock_path)
+{
+   struct timespec ts_sleep;
+   int             i;
+   pid_t           child;
+   struct stat     st;
+
+   /* If socket already exists, nothing to do */
+   if (stat(sock_path, &st) == 0)
+     return 0;
+
+   child = fork();
+   if (child < 0)
+     {
+        ERR("termsession: fork failed: %s", strerror(errno));
+        return -1;
+     }
+
+   if (child == 0)
+     {
+        /* Child: become session leader, redirect stdio, exec daemon */
+        int fd;
+
+        setsid();
+
+        fd = open("/dev/null", O_RDWR);
+        if (fd >= 0)
+          {
+             dup2(fd, STDIN_FILENO);
+             dup2(fd, STDOUT_FILENO);
+             dup2(fd, STDERR_FILENO);
+             if (fd > STDERR_FILENO)
+               close(fd);
+          }
+
+        execlp("tymuxd", "tymuxd", session_name, (char *)NULL);
+        /* If exec fails, exit without running atexit handlers */
+        _exit(127);
+     }
+
+   /* Parent: poll for socket with 10 ms intervals, 3 s total */
+   ts_sleep.tv_sec  = 0;
+   ts_sleep.tv_nsec = 10 * 1000 * 1000; /* 10 ms */
+
+   for (i = 0; i < 300; i++)
+     {
+        nanosleep(&ts_sleep, NULL);
+        if (stat(sock_path, &st) == 0)
+          return 0;
+     }
+
+   ERR("termsession: daemon did not create socket within 3 s: %s", sock_path);
+   return -1;
+}
+
+/* ── public API ─────────────────────────────────────────────────────── */
+
+TermSession *
+termsession_attach(const char *session_name,
+                   int cols, int rows,
+                   Termpty **shadow_pty_out)
+{
+   char              sock_path[256];
+   struct sockaddr_un addr;
+   int               sock_fd = -1;
+   int               shm_fd  = -1;
+   uint32_t          srv_cols, srv_rows;
+   TermShmHeader    *shm  = MAP_FAILED;
+   TermSession      *ts   = NULL;
+   Termpty          *ty   = NULL;
+
+   if (!session_name || !shadow_pty_out)
+     return NULL;
+
+   if (termsrv_socket_path(session_name, sock_path, sizeof(sock_path)) < 0)
+     {
+        ERR("termsession: session name too long or invalid: %s", session_name);
+        return NULL;
+     }
+
+   /* Create socket */
+   sock_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+   if (sock_fd < 0)
+     {
+        ERR("termsession: socket(): %s", strerror(errno));
+        return NULL;
+     }
+
+   memset(&addr, 0, sizeof(addr));
+   addr.sun_family = AF_UNIX;
+   strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1);
+
+   /* Try to connect; if refused, spawn daemon and retry */
+   if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
+     {
+        if (errno == ENOENT || errno == ECONNREFUSED)
+          {
+             if (_ensure_daemon(session_name, sock_path) < 0)
+               goto fail;
+
+             if (connect(sock_fd, (struct sockaddr *)&addr,
+                         sizeof(addr)) < 0)
+               {
+                  ERR("termsession: connect after daemon start: %s",
+                      strerror(errno));
+                  goto fail;
+               }
+          }
+        else
+          {
+             ERR("termsession: connect(%s): %s", sock_path, strerror(errno));
+             goto fail;
+          }
+     }
+
+   /* Send ATTACH with session name as payload */
+   if (termsrv_msg_send(sock_fd, TSRV_MSG_ATTACH,
+                        session_name,
+                        (uint32_t)strlen(session_name)) < 0)
+     {
+        ERR("termsession: send ATTACH: %s", strerror(errno));
+        goto fail;
+     }
+
+   /* Receive ATTACHED + memfd */
+   if (termsrv_recv_attached(sock_fd, &shm_fd,
+                             &srv_cols, &srv_rows) < 0)
+     {
+        ERR("termsession: recv ATTACHED failed");
+        goto fail;
+     }
+
+   /* Map shared memory read-only */
+   shm = mmap(NULL, TERMSRV_SHM_TOTAL_SIZE,
+              PROT_READ, MAP_SHARED, shm_fd, 0);
+   close(shm_fd);
+   shm_fd = -1;
+   if (shm == MAP_FAILED)
+     {
+        ERR("termsession: mmap shm: %s", strerror(errno));
+        goto fail;
+     }
+
+   /* Validate header */
+   if (shm->magic != TERMSRV_SHM_MAGIC ||
+       shm->version != TERMSRV_SHM_VERSION)
+     {
+        ERR("termsession: shm magic/version mismatch");
+        goto fail;
+     }
+
+   /* Use requested cols/rows for the shadow pty; _sync_shadow will
+    * adjust to whatever the server reports in the header. */
+   ty = _shadow_pty_new(NULL, cols, rows);
+   if (!ty)
+     {
+        ERR("termsession: failed to allocate shadow pty");
+        goto fail;
+     }
+
+   ts = calloc(1, sizeof(TermSession));
+   if (!ts)
+     {
+        ERR("termsession: calloc TermSession");
+        goto fail;
+     }
+
+   ts->sock_fd    = sock_fd;
+   ts->shm        = shm;
+   ts->shm_size   = TERMSRV_SHM_TOTAL_SIZE;
+   ts->shadow_pty = ty;
+   ts->last_seq   = 0;
+
+   /* Now that ts is ready, wire the write_cb */
+   ty->write_cb.func = _session_write_cb;
+   ty->write_cb.data = ""
+
+   /* Initial sync from shm */
+   _sync_shadow(ts);
+   ts->last_seq = atomic_load_explicit(&shm->write_seq, memory_order_acquire);
+
+   /* Register Ecore fd handler for incoming messages */
+   ts->handler = ecore_main_fd_handler_add(sock_fd,
+                                           ECORE_FD_READ | ECORE_FD_ERROR,
+                                           _cb_session_data, ts,
+                                           NULL, NULL);
+   if (!ts->handler)
+     {
+        ERR("termsession: ecore_main_fd_handler_add failed");
+        goto fail;
+     }
+
+   *shadow_pty_out = ty;
+   return ts;
+
+fail:
+   if (ts)
+     {
+        if (ts->handler)
+          ecore_main_fd_handler_del(ts->handler);
+        free(ts);
+        ts = NULL;
+     }
+   if (ty && !ts)
+     _shadow_pty_free(ty);
+   if (shm != MAP_FAILED)
+     munmap(shm, TERMSRV_SHM_TOTAL_SIZE);
+   if (shm_fd >= 0)
+     close(shm_fd);
+   if (sock_fd >= 0)
+     close(sock_fd);
+   return NULL;
+}
+
+void
+termsession_free(TermSession *ts)
+{
+   if (!ts)
+     return;
+
+   /* Send clean disconnect */
+   if (ts->sock_fd >= 0)
+     termsrv_msg_send(ts->sock_fd, TSRV_MSG_DETACH, NULL, 0);
+
+   /* Remove fd handler before closing socket */
+   if (ts->handler)
+     {
+        ecore_main_fd_handler_del(ts->handler);
+        ts->handler = NULL;
+     }
+
+   if (ts->shm && ts->shm != MAP_FAILED)
+     {
+        munmap(ts->shm, ts->shm_size);
+        ts->shm = NULL;
+     }
+
+   if (ts->sock_fd >= 0)
+     {
+        close(ts->sock_fd);
+        ts->sock_fd = -1;
+     }
+
+   _shadow_pty_free(ts->shadow_pty);
+   ts->shadow_pty = NULL;
+
+   free(ts);
+}
+
+void
+termsession_resize(TermSession *ts, int cols, int rows)
+{
+   TermSrvMsgResize payload;
+
+   if (!ts || ts->sock_fd < 0)
+     return;
+
+   payload.cols = (uint32_t)cols;
+   payload.rows = (uint32_t)rows;
+
+   if (termsrv_msg_send(ts->sock_fd, TSRV_MSG_RESIZE,
+                        &payload, sizeof(payload)) < 0)
+     ERR("termsession: send RESIZE: %s", strerror(errno));
+}
+
+#endif /* HAVE_TYMUX */
diff --git a/src/bin/termsession.h b/src/bin/termsession.h
index fb1c6e22..1df9148e 100644
--- a/src/bin/termsession.h
+++ b/src/bin/termsession.h
@@ -1,3 +1,51 @@
 #ifndef TERMINOLOGY_TERMSESSION_H_
 #define TERMINOLOGY_TERMSESSION_H_
-#endif
+
+#include "config.h"
+
+#ifdef HAVE_TYMUX
+
+#include <Eina.h>
+#include <Ecore.h>
+#include <Ecore_Evas_Types.h>
+#include "termpty.h"
+#include "termsrv.h"
+
+typedef struct _TermSession TermSession;
+
+struct _TermSession {
+   int            sock_fd;
+   TermShmHeader *shm;
+   size_t         shm_size;
+   Termpty       *shadow_pty;
+   uint64_t       last_seq;
+
+   Ecore_Fd_Handler *handler;
+
+   /* Callbacks — set these after termsession_attach() returns,
+    * before the Ecore main loop fires. */
+   struct {
+      void (*func)(void *data);
+      void *data;
+   } cb_change, cb_title, cb_bell, cb_exit;
+};
+
+/*
+ * Connect to tymuxd for the named session.
+ * If the daemon is not running, spawns it and waits up to 3 s.
+ * On success *shadow_pty_out is ready for rendering.
+ * Do NOT call termpty_free() on it — use termsession_free().
+ * Returns NULL on failure.
+ */
+TermSession *termsession_attach(const char *session_name,
+                                int cols, int rows,
+                                Termpty **shadow_pty_out);
+
+/* Send DETACH, munmap, close socket, free shadow Termpty. */
+void termsession_free(TermSession *ts);
+
+/* Send a resize event to the daemon. */
+void termsession_resize(TermSession *ts, int cols, int rows);
+
+#endif /* HAVE_TYMUX */
+#endif /* TERMINOLOGY_TERMSESSION_H_ */
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
index c3ac3845..0f65e1de 100644
--- a/src/bin/termsrv.c
+++ b/src/bin/termsrv.c
@@ -2,6 +2,14 @@
 
 #ifdef HAVE_TYMUX
 
+/* termpty.h (pulled in by termsrv.h) needs Ecore types.  In the tymuxd
+ * build private.h already includes them; in the terminology build we must
+ * include them explicitly before the termpty.h include chain fires. */
+#ifndef BINARY_TYMUXD
+# include <Ecore.h>
+# include <Ecore_Evas.h>
+#endif
+
 #include "termsrv.h"
 
 /* Sanity cap on incoming message payload size.  No tymux message should

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

Reply via email to