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 6b0d0b236d4e097a4336b8e09e2607751e93a68c
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 15 21:41:33 2026 -0600
feat: implement tymux CLI for managing persistent terminal sessions
Replace stub with full user-facing CLI that discovers active tymuxd
daemons by scanning $XDG_RUNTIME_DIR/terminology/tymux/*.sock and
probing liveness via connect(). Supports three operations:
- list: directory scan + liveness probe for each socket
- new: fork/setsid/exec tymuxd daemon, poll for socket, then exec
terminology to attach
- attach: verify session exists and exec terminology to connect
Kept pure POSIX (no EFL dependencies) and implemented path helpers
inline to avoid pulling in termsrv.h, which has EFL type requirements
via termpty.h that would make tymux unnecessarily heavyweight.
On ECONNREFUSED during liveness checks, stale socket files are
unlinked automatically.
---
src/bin/tymux.c | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 309 insertions(+), 4 deletions(-)
diff --git a/src/bin/tymux.c b/src/bin/tymux.c
index 30c488ad..ce485c64 100644
--- a/src/bin/tymux.c
+++ b/src/bin/tymux.c
@@ -1,7 +1,312 @@
+/*
+ * tymux — lightweight CLI front-end for tymuxd session management.
+ *
+ * Pure POSIX, no EFL dependency. Path helpers are duplicated inline to
+ * keep the binary self-contained (termsrv.h pulls in EFL types via
+ * termpty.h and cannot be included here).
+ *
+ * Commands:
+ * tymux list – list active sessions
+ * tymux new [name] – start daemon + attach (default name: "default")
+ * tymux attach <name> – attach to existing session
+ */
+
#include <stdio.h>
-int main(int argc, char **argv)
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <dirent.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <fcntl.h>
+#include <time.h>
+
+/* ── path helpers (mirrors termsrv.c, no EFL) ───────────────────────── */
+
+#define TYMUX_MAX_SESSION_NAME 64 /* must match TSRV_MAX_SESSION_NAME in termsrv.h */
+
+static const char *
+_xdg(void)
{
- (void)argc; (void)argv;
- fprintf(stderr, "tymux: not yet implemented\n");
- return 1;
+ const char *d = getenv("XDG_RUNTIME_DIR");
+ return (d && *d) ? d : "/tmp";
+}
+
+static int
+_socket_path(const char *name, char *buf, size_t bufsz)
+{
+ if (!name || name[0] == '\0' || strchr(name, '/')) return -1;
+ int n = snprintf(buf, bufsz, "%s/terminology/tymux/%s.sock", _xdg(), name);
+ return (n > 0 && (size_t)n < bufsz) ? 0 : -1;
+}
+
+static int
+_sessions_dir(char *buf, size_t bufsz)
+{
+ int n = snprintf(buf, bufsz, "%s/terminology/tymux", _xdg());
+ return (n > 0 && (size_t)n < bufsz) ? 0 : -1;
+}
+
+/* ── live-socket probe ───────────────────────────────────────────────── */
+
+/*
+ * Returns 1 if a tymuxd is actively listening on sock_path.
+ * On ECONNREFUSED the socket file is stale — unlink it and return 0.
+ */
+static int
+_session_is_live(const char *sock_path)
+{
+ int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (fd < 0) return 0;
+
+ struct sockaddr_un sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sun_family = AF_UNIX;
+ strncpy(sa.sun_path, sock_path, sizeof(sa.sun_path) - 1);
+
+ int live = 0;
+ if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) == 0)
+ {
+ live = 1;
+ }
+ else if (errno == ECONNREFUSED)
+ {
+ /* Stale socket left over from a crashed daemon. */
+ unlink(sock_path);
+ }
+ /* ENOENT, EACCES, etc. → not live, don't unlink */
+
+ close(fd);
+ return live;
+}
+
+/* ── commands ────────────────────────────────────────────────────────── */
+
+static int
+cmd_list(void)
+{
+ char dir[512];
+ if (_sessions_dir(dir, sizeof(dir)) != 0)
+ {
+ fprintf(stderr, "tymux: failed to build sessions dir path\n");
+ return 1;
+ }
+
+ DIR *d = opendir(dir);
+ if (!d)
+ {
+ /* Directory not existing just means no sessions yet. */
+ printf("No active sessions.\n");
+ return 0;
+ }
+
+ int found = 0;
+ struct dirent *ent;
+ while ((ent = readdir(d)) != NULL)
+ {
+ const char *name = ent->d_name;
+ size_t len = strlen(name);
+ if (len < 6) continue; /* shorter than "x.sock" */
+ if (strcmp(name + len - 5, ".sock") != 0) continue;
+
+ /* Build full socket path. */
+ char sock_path[600];
+ int n = snprintf(sock_path, sizeof(sock_path), "%s/%s", dir, name);
+ if (n <= 0 || (size_t)n >= sizeof(sock_path)) continue;
+
+ if (!_session_is_live(sock_path)) continue;
+
+ if (!found)
+ {
+ printf("Active sessions:\n");
+ found = 1;
+ }
+
+ /* Print session name (strip the ".sock" suffix). */
+ printf(" %.*s\n", (int)(len - 5), name);
+ }
+ closedir(d);
+
+ if (!found)
+ printf("No active sessions.\n");
+
+ return 0;
+}
+
+static int
+cmd_new(int argc, char **argv)
+{
+ const char *name = "default";
+ if (argc >= 3)
+ name = argv[2];
+
+ if (strlen(name) > TYMUX_MAX_SESSION_NAME)
+ {
+ fprintf(stderr, "tymux: session name too long (max %d chars)\n",
+ TYMUX_MAX_SESSION_NAME);
+ return 1;
+ }
+ if (strchr(name, '/'))
+ {
+ fprintf(stderr, "tymux: session name must not contain '/'\n");
+ return 1;
+ }
+
+ char sock_path[512];
+ if (_socket_path(name, sock_path, sizeof(sock_path)) != 0)
+ {
+ fprintf(stderr, "tymux: failed to build socket path\n");
+ return 1;
+ }
+
+ if (_session_is_live(sock_path))
+ {
+ fprintf(stderr, "tymux: session '%s' is already active.\n"
+ "Use: tymux attach %s\n", name, name);
+ return 1;
+ }
+
+ /* Fork a detached tymuxd. */
+ pid_t pid = fork();
+ if (pid < 0)
+ {
+ perror("tymux: fork");
+ return 1;
+ }
+
+ if (pid == 0)
+ {
+ /* Child: become session leader and redirect stdio. */
+ setsid();
+
+ int devnull = open("/dev/null", O_RDWR);
+ if (devnull >= 0)
+ {
+ dup2(devnull, STDIN_FILENO);
+ dup2(devnull, STDOUT_FILENO);
+ dup2(devnull, STDERR_FILENO);
+ if (devnull > STDERR_FILENO)
+ close(devnull);
+ }
+
+ execlp("tymuxd", "tymuxd", name, (char *)NULL);
+ /* If exec fails there is nothing useful we can report — the
+ * parent will time out waiting for the socket. */
+ _exit(127);
+ }
+
+ /* Parent: poll for the socket to appear (up to 3 s, 10 ms intervals). */
+ struct timespec ts = { 0, 10 * 1000 * 1000 }; /* 10 ms */
+ int attempts = 300; /* 3 s total */
+ int ready = 0;
+ while (attempts-- > 0)
+ {
+ nanosleep(&ts, NULL);
+ if (_session_is_live(sock_path)) { ready = 1; break; }
+ }
+
+ if (!ready)
+ {
+ fprintf(stderr, "tymux: tymuxd did not start in time for session '%s'\n",
+ name);
+ return 1;
+ }
+
+ printf("Session '%s' created.\n", name);
+
+ /* Replace this process with terminology, connecting to the new session. */
+ execlp("terminology", "terminology", "--session", name, (char *)NULL);
+ perror("tymux: exec terminology");
+ return 1;
+}
+
+static int
+cmd_attach(int argc, char **argv)
+{
+ if (argc < 3)
+ {
+ fprintf(stderr, "tymux attach: session name required\n");
+ fprintf(stderr, "Usage: tymux attach <session-name>\n");
+ return 1;
+ }
+
+ const char *name = argv[2];
+
+ /* Warn if we are already inside a tymux session. */
+ if (getenv("TYMUX_SESSION"))
+ fprintf(stderr,
+ "tymux: warning: already inside tymux session '%s'; "
+ "nesting may cause issues\n",
+ getenv("TYMUX_SESSION"));
+
+ char sock_path[512];
+ if (_socket_path(name, sock_path, sizeof(sock_path)) != 0)
+ {
+ fprintf(stderr, "tymux: failed to build socket path\n");
+ return 1;
+ }
+
+ if (!_session_is_live(sock_path))
+ {
+ fprintf(stderr, "tymux: no active session named '%s'\n", name);
+ return 1;
+ }
+
+ execlp("terminology", "terminology", "--session", name, (char *)NULL);
+ perror("tymux: exec terminology");
+ return 1;
+}
+
+/* ── usage ───────────────────────────────────────────────────────────── */
+
+static void
+usage(const char *prog)
+{
+ printf("Usage: %s <command> [args]\n"
+ "\n"
+ "Commands:\n"
+ " list List all active tymux sessions\n"
+ " new [name] Start a new session (default name: \"default\")\n"
+ " then launch terminology attached to it\n"
+ " attach <name> Attach to an existing session in terminology\n"
+ "\n"
+ "Environment:\n"
+ " XDG_RUNTIME_DIR Base directory for session sockets\n"
+ " (default: /tmp)\n"
+ " TYMUX_SESSION Set by tymux to the active session name;\n"
+ " used to warn about nested sessions\n",
+ prog);
+}
+
+/* ── main ────────────────────────────────────────────────────────────── */
+
+int
+main(int argc, char **argv)
+{
+ if (argc < 2)
+ {
+ usage(argv[0]);
+ return 1;
+ }
+
+ const char *cmd = argv[1];
+
+ if (strcmp(cmd, "list") == 0)
+ return cmd_list();
+ else if (strcmp(cmd, "new") == 0)
+ return cmd_new(argc, argv);
+ else if (strcmp(cmd, "attach") == 0)
+ return cmd_attach(argc, argv);
+ else if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0)
+ {
+ usage(argv[0]);
+ return 0;
+ }
+ else
+ {
+ fprintf(stderr, "tymux: unknown command '%s'\n", cmd);
+ usage(argv[0]);
+ return 1;
+ }
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.