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.