This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/36/head
in repository terminology.
View the commit online.
commit 893717e85b2e9bb55e0663ccb2b48e16b8faf5c7
Author: [email protected] <[email protected]>
AuthorDate: Wed Mar 18 22:34:17 2026 -0600
feat: rewrite tymux attach as inline terminal proxy
Previously, `tymux attach` spawned a new Terminology window, which broke
user expectations — tmux and similar tools attach in the current terminal.
This rewrite makes `tymux attach` work from any terminal emulator by
implementing it as a raw terminal proxy:
• Connects to the tymuxd socket and receives the session memfd via SCM_RIGHTS
• Maps the shared memory read-only and enters raw terminal mode
• Runs a poll loop proxying stdin to INPUT messages and rendering NOTIFY
frames as ANSI escape sequences to the screen
• Handles SIGWINCH for terminal resize and Ctrl+\ for clean detach
• Parses and displays TITLE, BELL, and EXIT protocol messages
• Zero EFL dependencies — all protocol marshalling is inline
This allows users to attach to persistent tymux sessions from any terminal,
matching standard tmux/screen behavior while reusing the session engine.
---
src/bin/tymux.c | 816 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 810 insertions(+), 6 deletions(-)
diff --git a/src/bin/tymux.c b/src/bin/tymux.c
index 44933765..b0bd6893 100644
--- a/src/bin/tymux.c
+++ b/src/bin/tymux.c
@@ -8,7 +8,7 @@
* Commands:
* tymux list – list active sessions
* tymux new [name] – start daemon + attach (default name: "default")
- * tymux attach <name> – attach to existing session
+ * tymux attach <name> – take over the current terminal and attach inline
*/
#include <stdio.h>
@@ -22,6 +22,12 @@
#include <fcntl.h>
#include <time.h>
#include <sys/wait.h>
+#include <stdint.h>
+#include <poll.h>
+#include <signal.h>
+#include <termios.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
/* ── path helpers (mirrors termsrv.c, no EFL) ───────────────────────── */
@@ -82,6 +88,553 @@ _session_is_live(const char *sock_path)
return live;
}
+/* ── inline protocol types (mirrors termsrv.h without EFL deps) ─────── */
+
+typedef enum {
+ _TSRV_MSG_ATTACH = 1,
+ _TSRV_MSG_ATTACHED = 2,
+ _TSRV_MSG_DETACH = 3,
+ _TSRV_MSG_INPUT = 4,
+ _TSRV_MSG_RESIZE = 5,
+ _TSRV_MSG_NOTIFY = 6,
+ _TSRV_MSG_TITLE = 7,
+ _TSRV_MSG_BELL = 8,
+ _TSRV_MSG_EXIT = 9,
+} _TsrvMsgType;
+
+typedef struct {
+ uint32_t type;
+ uint32_t size;
+} _TsrvMsgHdr;
+
+typedef struct {
+ uint32_t cols;
+ uint32_t rows;
+} _TsrvMsgAttached;
+
+typedef struct {
+ uint32_t cols;
+ uint32_t rows;
+} _TsrvMsgResize;
+
+typedef struct {
+ uint64_t write_seq;
+} _TsrvMsgNotify;
+
+typedef struct {
+ int32_t exit_code;
+} _TsrvMsgExit;
+
+/* Shared-memory layout constants (must match TERMSRV_SHM_* in termsrv.h) */
+#define _SHM_HEADER_SIZE 4096u
+#define _SHM_MAX_COLS 500
+#define _SHM_MAX_ROWS 200
+
+/*
+ * Mirror of TermShmHeader — only the fields we actually read.
+ * The struct is padded to fill 4096 bytes on the daemon side but we only
+ * access the first few fields, so a partial mirror is fine.
+ */
+typedef struct {
+ uint32_t magic;
+ uint32_t version;
+ uint32_t cols;
+ uint32_t rows;
+ uint32_t cursor_x;
+ uint32_t cursor_y;
+ uint8_t cursor_visible;
+ uint8_t _pad[7];
+ /* _Atomic uint64_t write_seq follows — read with __atomic built-in */
+} _ShmHdr;
+
+/*
+ * Mirror of Termatt bitfield. Must match the exact layout in termpty.h.
+ * We only need fg/bg/bold/underline/italic/strike/inverse/fg256/bg256/
+ * fgintense/bgintense.
+ */
+typedef struct {
+ uint8_t fg, bg;
+ unsigned short bold : 1;
+ unsigned short faint : 1;
+ unsigned short italic : 1;
+ unsigned short dblwidth : 1;
+ unsigned short underline : 1;
+ unsigned short blink : 1;
+ unsigned short blink2 : 1;
+ unsigned short inverse : 1;
+ unsigned short invisible : 1;
+ unsigned short strike : 1;
+ unsigned short fg256 : 1;
+ unsigned short bg256 : 1;
+ unsigned short fgintense : 1;
+ unsigned short bgintense : 1;
+ unsigned short autowrapped : 1;
+ unsigned short newline : 1;
+ unsigned short fraktur : 1;
+ unsigned short framed : 1;
+ unsigned short encircled : 1;
+ unsigned short overlined : 1;
+ unsigned short tab_inserted: 1;
+ unsigned short tab_last : 1;
+ /* bit_padding + link_id — not accessed */
+ unsigned short bit_padding : 10;
+ uint16_t link_id;
+} _Termatt;
+
+typedef struct {
+ uint32_t codepoint; /* Eina_Unicode ≡ uint32_t */
+ _Termatt att;
+} _Termcell;
+
+/* Total shm mapping size */
+static size_t
+_shm_total_size(void)
+{
+ return _SHM_HEADER_SIZE +
+ (size_t)_SHM_MAX_COLS * _SHM_MAX_ROWS * sizeof(_Termcell);
+}
+
+/* ── inline protocol I/O ─────────────────────────────────────────────── */
+
+static int
+_proto_send(int fd, _TsrvMsgType type, const void *payload, uint32_t size)
+{
+ _TsrvMsgHdr hdr = { .type = (uint32_t)type, .size = size };
+ struct iovec iov[2] = {
+ { .iov_base = &hdr, .iov_len = sizeof(hdr) },
+ { .iov_base = (void *)payload, .iov_len = size },
+ };
+ int iovcnt = (size && payload) ? 2 : 1;
+ struct msghdr msg = { .msg_iov = iov, .msg_iovlen = (size_t)iovcnt };
+ ssize_t expected = (ssize_t)(sizeof(hdr) + ((payload && size) ? size : 0));
+ ssize_t n = sendmsg(fd, &msg, MSG_NOSIGNAL);
+ return (n == expected) ? 0 : -1;
+}
+
+static int
+_recv_exact(int fd, void *buf, size_t need)
+{
+ size_t got = 0;
+ int retries = 0;
+
+ while (got < need)
+ {
+ ssize_t r = recv(fd, (char *)buf + got, need - got, 0);
+ if (r > 0)
+ {
+ got += (size_t)r;
+ retries = 0;
+ }
+ else if (r == 0)
+ {
+ return -1; /* EOF */
+ }
+ else
+ {
+ if (errno == EINTR) continue;
+ if ((errno == EAGAIN || errno == EWOULDBLOCK) && retries < 100)
+ {
+ retries++;
+ continue;
+ }
+ return -1;
+ }
+ }
+ return 0;
+}
+
+/*
+ * Receive one message. Returns msg type (>0) on success.
+ * Returns 0 if EAGAIN (no data available yet, non-blocking fd).
+ * Returns -1 on EOF or protocol error.
+ * *payload_out is malloc'd; caller frees. NULL when no payload.
+ */
+static int
+_proto_recv(int fd, void **payload_out, uint32_t *size_out)
+{
+ _TsrvMsgHdr hdr;
+ *payload_out = NULL;
+ *size_out = 0;
+
+ ssize_t r = recv(fd, &hdr, sizeof(hdr), MSG_DONTWAIT);
+ if (r == 0) return -1;
+ if (r < 0)
+ {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
+ if (errno == EINTR) return 0;
+ return -1;
+ }
+ if (r != (ssize_t)sizeof(hdr))
+ {
+ if (_recv_exact(fd, (char *)&hdr + r, sizeof(hdr) - (size_t)r) < 0)
+ return -1;
+ }
+
+ if (hdr.type == 0 || hdr.type > 9) return -1;
+ if (hdr.size > (1u << 20)) return -1;
+
+ if (hdr.size > 0)
+ {
+ void *buf = malloc(hdr.size);
+ if (!buf) return -1;
+ if (_recv_exact(fd, buf, hdr.size) < 0)
+ { free(buf); return -1; }
+ *payload_out = buf;
+ }
+
+ *size_out = hdr.size;
+ return (int)hdr.type;
+}
+
+/*
+ * Receive the ATTACHED message and extract the memfd via SCM_RIGHTS.
+ * Mirrors termsrv_recv_attached() without EFL deps.
+ */
+static int
+_proto_recv_attached(int sock_fd, int *shm_fd_out,
+ uint32_t *cols_out, uint32_t *rows_out)
+{
+ _TsrvMsgHdr hdr;
+ _TsrvMsgAttached pl;
+ char cmsgbuf[CMSG_SPACE(sizeof(int))];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+ struct iovec iov[2] = {
+ { &hdr, sizeof(hdr) },
+ { &pl, sizeof(pl) }
+ };
+ struct msghdr msg = {
+ .msg_iov = iov,
+ .msg_iovlen = 2,
+ .msg_control = cmsgbuf,
+ .msg_controllen = sizeof(cmsgbuf)
+ };
+
+ *shm_fd_out = -1;
+ ssize_t n = recvmsg(sock_fd, &msg, MSG_WAITALL);
+ if (n < (ssize_t)(sizeof(hdr) + sizeof(pl))) return -1;
+ if (msg.msg_flags & MSG_CTRUNC) return -1;
+ if (hdr.type != _TSRV_MSG_ATTACHED) return -1;
+
+ struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
+ if (!cm || cm->cmsg_type != SCM_RIGHTS) return -1;
+ memcpy(shm_fd_out, CMSG_DATA(cm), sizeof(int));
+
+ *cols_out = pl.cols;
+ *rows_out = pl.rows;
+ return 0;
+}
+
+/* ── terminal raw mode ───────────────────────────────────────────────── */
+
+static struct termios _orig_termios;
+static int _raw_mode_active = 0;
+
+static void
+_term_raw_mode(void)
+{
+ struct termios t;
+ if (tcgetattr(STDIN_FILENO, &_orig_termios) < 0) return;
+ t = _orig_termios;
+ t.c_lflag &= (tcflag_t)~(ECHO | ICANON | ISIG | IEXTEN);
+ t.c_iflag &= (tcflag_t)~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);
+ t.c_oflag &= (tcflag_t)~OPOST;
+ t.c_cc[VMIN] = 1;
+ t.c_cc[VTIME] = 0;
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &t);
+ _raw_mode_active = 1;
+}
+
+static void
+_term_restore(void)
+{
+ if (_raw_mode_active)
+ {
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &_orig_termios);
+ _raw_mode_active = 0;
+ }
+}
+
+/* ── SIGWINCH / signal handling ──────────────────────────────────────── */
+
+static volatile sig_atomic_t _got_winch = 0;
+static volatile sig_atomic_t _got_int = 0;
+
+static void
+_sigwinch(int sig __attribute__((unused)))
+{
+ _got_winch = 1;
+}
+
+static void
+_sigint(int sig __attribute__((unused)))
+{
+ _got_int = 1;
+}
+
+/* ── 256-colour palette (xterm-compatible) ───────────────────────────── */
+
+/*
+ * Map a Termatt colour index to an RGB triple using the standard
+ * xterm-256colour cube / grayscale ramp. Colours 0-15 map to the
+ * 16 ANSI/intense pairs; 16-231 map to the 6x6x6 colour cube;
+ * 232-255 map to a 24-step grayscale.
+ */
+typedef struct { uint8_t r, g, b; } _RGB;
+
+static _RGB
+_colour256(uint8_t idx)
+{
+ /* 0-7: standard ANSI colours (de-facto xterm defaults) */
+ static const _RGB ansi16[16] = {
+ { 0, 0, 0 }, { 128, 0, 0 }, { 0, 128, 0 }, { 128, 128, 0 },
+ { 0, 0, 128 }, { 128, 0, 128 }, { 0, 128, 128 }, { 192, 192, 192 },
+ { 128, 128, 128 }, { 255, 0, 0 }, { 0, 255, 0 }, { 255, 255, 0 },
+ { 0, 0, 255 }, { 255, 0, 255 }, { 0, 255, 255 }, { 255, 255, 255 },
+ };
+ if (idx < 16) return ansi16[idx];
+ if (idx < 232)
+ {
+ uint8_t n = idx - 16;
+ uint8_t bi = n % 6; n /= 6;
+ uint8_t gi = n % 6; n /= 6;
+ uint8_t ri = n;
+ static const uint8_t lv[6] = { 0, 95, 135, 175, 215, 255 };
+ return (_RGB){ lv[ri], lv[gi], lv[bi] };
+ }
+ /* 232-255: 24-step grayscale */
+ uint8_t v = (uint8_t)(8 + (idx - 232) * 10);
+ return (_RGB){ v, v, v };
+}
+
+/* Write a UTF-8 sequence for codepoint cp to buf[].
+ * Returns number of bytes written (at most 4).
+ * Caller must ensure buf has at least 4 bytes available. */
+static int
+_utf8_encode(uint32_t cp, char *buf)
+{
+ if (cp < 0x80)
+ {
+ buf[0] = (char)cp;
+ return 1;
+ }
+ if (cp < 0x800)
+ {
+ buf[0] = (char)(0xC0 | (cp >> 6));
+ buf[1] = (char)(0x80 | (cp & 0x3F));
+ return 2;
+ }
+ if (cp < 0x10000)
+ {
+ buf[0] = (char)(0xE0 | (cp >> 12));
+ buf[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
+ buf[2] = (char)(0x80 | (cp & 0x3F));
+ return 3;
+ }
+ /* Supplementary plane */
+ buf[0] = (char)(0xF0 | (cp >> 18));
+ buf[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
+ buf[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
+ buf[3] = (char)(0x80 | (cp & 0x3F));
+ return 4;
+}
+
+/* ── screen rendering ────────────────────────────────────────────────── */
+
+/*
+ * Render the shared-memory screen to stdout using ANSI escape sequences.
+ * We read the header for geometry and the Termcell array for cell data.
+ * A full repaint is done each time (simple but correct); for large
+ * terminals this is a few tens of KB — well within the line-buffer budget.
+ */
+static void
+_render_screen(const void *shm, uint32_t cols, uint32_t rows)
+{
+ const _ShmHdr *hdr = (const _ShmHdr *)shm;
+ const _Termcell *cells = (const _Termcell *)((const uint8_t *)shm +
+ _SHM_HEADER_SIZE);
+
+ /* Clamp to what the shm actually advertises. */
+ uint32_t scols = hdr->cols ? hdr->cols : cols;
+ uint32_t srows = hdr->rows ? hdr->rows : rows;
+ if (scols > _SHM_MAX_COLS) scols = _SHM_MAX_COLS;
+ if (srows > _SHM_MAX_ROWS) srows = _SHM_MAX_ROWS;
+
+ /* Output buffer — worst case ~6 bytes/cell (UTF-8) + ~40 bytes ANSI/cell.
+ * We heap-alloc to avoid large stack frames. */
+ size_t bufsz = (size_t)(scols * srows) * 50 + 256;
+ char *out = malloc(bufsz);
+ if (!out) return;
+
+ size_t pos = 0;
+
+/* Helper macro: append a formatted string to out[] */
+#define EMIT(...) \
+ do { \
+ int _n = snprintf(out + pos, bufsz - pos, __VA_ARGS__); \
+ if (_n > 0) pos += (size_t)_n; \
+ } while (0)
+
+ /* Hide cursor during repaint to reduce flicker. */
+ EMIT("\033[?25l");
+ /* Move to top-left. */
+ EMIT("\033[H");
+
+ uint8_t cur_fg_idx = 0xFF; /* sentinel: no colour set yet */
+ uint8_t cur_bg_idx = 0xFF;
+ int cur_bold = -1;
+ int cur_uline = -1;
+ int cur_italic = -1;
+ int cur_strike = -1;
+ int cur_inv = -1;
+ int cur_fg256 = -1;
+ int cur_bg256 = -1;
+
+ for (uint32_t row = 0; row < srows; row++)
+ {
+ for (uint32_t col = 0; col < scols; col++)
+ {
+ const _Termcell *c = &cells[row * _SHM_MAX_COLS + col];
+
+ /* Attributes changed? Emit SGR. */
+ int need_reset = 0;
+ if ((int)c->att.bold != cur_bold ||
+ (int)c->att.italic != cur_italic ||
+ (int)c->att.strike != cur_strike ||
+ (int)c->att.inverse != cur_inv)
+ need_reset = 1;
+
+ if (need_reset)
+ {
+ EMIT("\033[0");
+ if (c->att.bold) EMIT(";1");
+ if (c->att.italic) EMIT(";3");
+ if (c->att.underline) EMIT(";4");
+ if (c->att.strike) EMIT(";9");
+ if (c->att.inverse) EMIT(";7");
+ EMIT("m");
+ /* Force colour re-emit after reset */
+ cur_fg_idx = 0xFF;
+ cur_bg_idx = 0xFF;
+ cur_fg256 = -1;
+ cur_bg256 = -1;
+ cur_bold = (int)c->att.bold;
+ cur_uline = (int)c->att.underline;
+ cur_italic = (int)c->att.italic;
+ cur_strike = (int)c->att.strike;
+ cur_inv = (int)c->att.inverse;
+ }
+ else if ((int)c->att.underline != cur_uline)
+ {
+ EMIT("\033[%sm", c->att.underline ? "4" : "24");
+ cur_uline = (int)c->att.underline;
+ }
+
+ /* Foreground colour */
+ if ((int)c->att.fg256 != cur_fg256 ||
+ c->att.fg != cur_fg_idx)
+ {
+ if (c->att.fg256)
+ {
+ _RGB rgb = _colour256(c->att.fg);
+ EMIT("\033[38;2;%u;%u;%um", rgb.r, rgb.g, rgb.b);
+ }
+ else
+ {
+ /* Standard ANSI: 0=default fg, 1..8=colours 30..37,
+ * fgintense shifts to 90..97 range. */
+ if (c->att.fg == 0)
+ EMIT("\033[39m"); /* default fg */
+ else
+ {
+ int base = c->att.fgintense ? 90 : 30;
+ EMIT("\033[%dm", base + (c->att.fg - 1));
+ }
+ }
+ cur_fg_idx = c->att.fg;
+ cur_fg256 = (int)c->att.fg256;
+ }
+
+ /* Background colour */
+ if ((int)c->att.bg256 != cur_bg256 ||
+ c->att.bg != cur_bg_idx)
+ {
+ if (c->att.bg256)
+ {
+ _RGB rgb = _colour256(c->att.bg);
+ EMIT("\033[48;2;%u;%u;%um", rgb.r, rgb.g, rgb.b);
+ }
+ else
+ {
+ if (c->att.bg == 0)
+ EMIT("\033[49m"); /* default bg */
+ else
+ {
+ int base = c->att.bgintense ? 100 : 40;
+ EMIT("\033[%dm", base + (c->att.bg - 1));
+ }
+ }
+ cur_bg_idx = c->att.bg;
+ cur_bg256 = (int)c->att.bg256;
+ }
+
+ /* Output the glyph (or space for empty / control cells) */
+ uint32_t cp = c->codepoint;
+ if (cp == 0 || cp < 0x20)
+ {
+ if (pos < bufsz - 1) out[pos++] = ' ';
+ }
+ else
+ {
+ char utf8[4];
+ int ulen = _utf8_encode(cp, utf8);
+ if (pos + (size_t)ulen < bufsz)
+ {
+ memcpy(out + pos, utf8, (size_t)ulen);
+ pos += (size_t)ulen;
+ }
+ }
+ }
+
+ /* Move to next line with CR+LF (raw mode output, no OPOST). */
+ if (row + 1 < srows)
+ {
+ if (pos + 2 < bufsz) { out[pos++] = '\r'; out[pos++] = '\n'; }
+ }
+ }
+
+ /* Restore attributes and position cursor. */
+ EMIT("\033[0m");
+
+ /* Render cursor if visible */
+ if (hdr->cursor_visible &&
+ hdr->cursor_x < scols && hdr->cursor_y < srows)
+ {
+ EMIT("\033[%u;%uH", hdr->cursor_y + 1, hdr->cursor_x + 1);
+ EMIT("\033[?25h");
+ }
+ else
+ {
+ EMIT("\033[?25l");
+ }
+
+#undef EMIT
+
+ /* Single write to minimize flicker. */
+ size_t written = 0;
+ while (written < pos)
+ {
+ ssize_t w = write(STDOUT_FILENO, out + written, pos - written);
+ if (w < 0)
+ {
+ if (errno == EINTR) continue;
+ break;
+ }
+ written += (size_t)w;
+ }
+
+ free(out);
+}
+
/* ── commands ────────────────────────────────────────────────────────── */
static int
@@ -237,6 +790,14 @@ cmd_new(int argc, char **argv)
return 1;
}
+/*
+ * cmd_attach — inline terminal client.
+ *
+ * Connects to tymuxd, sends ATTACH, receives ATTACHED + memfd, then takes
+ * over the current terminal: puts it in raw mode, polls stdin and the
+ * daemon socket in a tight loop, renders shm cells on NOTIFY, and forwards
+ * stdin bytes as TSRV_MSG_INPUT. Detach key: Ctrl+\ (0x1C).
+ */
static int
cmd_attach(int argc, char **argv)
{
@@ -269,12 +830,253 @@ cmd_attach(int argc, char **argv)
return 1;
}
- /* Advertise the session name so nested tymux invocations can warn. */
+ /* ── Connect ── */
+ int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock < 0) { perror("tymux: socket"); return 1; }
+
+ 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);
+
+ if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
+ {
+ perror("tymux: connect");
+ close(sock);
+ return 1;
+ }
+
+ /* ── Send ATTACH ── */
+ if (_proto_send(sock, _TSRV_MSG_ATTACH, NULL, 0) < 0)
+ {
+ fprintf(stderr, "tymux: failed to send ATTACH\n");
+ close(sock);
+ return 1;
+ }
+
+ /* ── Receive ATTACHED + memfd ── */
+ int shm_fd = -1;
+ uint32_t srv_cols = 0, srv_rows = 0;
+ if (_proto_recv_attached(sock, &shm_fd, &srv_cols, &srv_rows) < 0)
+ {
+ fprintf(stderr, "tymux: did not receive valid ATTACHED message\n");
+ close(sock);
+ return 1;
+ }
+
+ /* ── mmap the shared memory ── */
+ size_t shm_sz = _shm_total_size();
+ void *shm = mmap(NULL, shm_sz, PROT_READ, MAP_SHARED, shm_fd, 0);
+ close(shm_fd); shm_fd = -1;
+ if (shm == MAP_FAILED)
+ {
+ perror("tymux: mmap shm");
+ close(sock);
+ return 1;
+ }
+
+ /* ── Get current terminal size and send initial RESIZE ── */
+ struct winsize ws;
+ memset(&ws, 0, sizeof(ws));
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0 || ws.ws_col == 0)
+ {
+ /* Fallback to daemon-reported size */
+ ws.ws_col = (unsigned short)srv_cols;
+ ws.ws_row = (unsigned short)srv_rows;
+ }
+
+ {
+ _TsrvMsgResize rpl = {
+ .cols = ws.ws_col,
+ .rows = ws.ws_row,
+ };
+ _proto_send(sock, _TSRV_MSG_RESIZE, &rpl, sizeof(rpl));
+ }
+
+ /* ── Install signal handlers ── */
+ struct sigaction sa_winch;
+ memset(&sa_winch, 0, sizeof(sa_winch));
+ sa_winch.sa_handler = _sigwinch;
+ sigemptyset(&sa_winch.sa_mask);
+ sa_winch.sa_flags = SA_RESTART;
+ sigaction(SIGWINCH, &sa_winch, NULL);
+
+ struct sigaction sa_int;
+ memset(&sa_int, 0, sizeof(sa_int));
+ sa_int.sa_handler = _sigint;
+ sigemptyset(&sa_int.sa_mask);
+ sa_int.sa_flags = 0;
+ sigaction(SIGINT, &sa_int, NULL);
+
+ /* Ignore SIGPIPE — connection loss is handled via send() return value */
+ signal(SIGPIPE, SIG_IGN);
+
+ /* ── Enter raw mode and clear screen ── */
+ _term_raw_mode();
+
+ /* Clear screen and paint the initial state. */
+ write(STDOUT_FILENO, "\033[2J\033[H", 7);
+ _render_screen(shm, srv_cols, srv_rows);
+
setenv("TYMUX_SESSION", name, 1);
- execlp("terminology", "terminology", "--session", name, (char *)NULL);
- perror("tymux: exec terminology");
- return 1;
+ /* ── Main I/O loop ── */
+ int running = 1;
+ int exit_code = 0;
+
+ while (running)
+ {
+ /* Handle SIGWINCH before polling so we don't miss a resize that
+ * arrived while we were in _render_screen(). */
+ if (_got_winch)
+ {
+ _got_winch = 0;
+ memset(&ws, 0, sizeof(ws));
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 &&
+ ws.ws_col > 0 && ws.ws_row > 0)
+ {
+ _TsrvMsgResize rpl = { .cols = ws.ws_col, .rows = ws.ws_row };
+ if (_proto_send(sock, _TSRV_MSG_RESIZE, &rpl, sizeof(rpl)) < 0)
+ running = 0;
+ }
+ }
+
+ if (_got_int)
+ {
+ /* User pressed Ctrl+C — detach cleanly. */
+ _got_int = 0;
+ _proto_send(sock, _TSRV_MSG_DETACH, NULL, 0);
+ running = 0;
+ break;
+ }
+
+ struct pollfd fds[2];
+ fds[0].fd = STDIN_FILENO;
+ fds[0].events = POLLIN;
+ fds[1].fd = sock;
+ fds[1].events = POLLIN;
+
+ int nready = poll(fds, 2, 200); /* 200 ms timeout → re-check signals */
+ if (nready < 0)
+ {
+ if (errno == EINTR) continue; /* signal → re-check flags */
+ break;
+ }
+
+ /* ── stdin → INPUT ── */
+ if (fds[0].revents & POLLIN)
+ {
+ char ibuf[4096];
+ ssize_t nr = read(STDIN_FILENO, ibuf, sizeof(ibuf));
+ if (nr < 0)
+ {
+ if (errno == EINTR) continue;
+ running = 0;
+ break;
+ }
+ if (nr == 0) { running = 0; break; }
+
+ /* Scan for detach key: Ctrl+\ (0x1C, like GNU screen) */
+ for (ssize_t i = 0; i < nr; i++)
+ {
+ if ((unsigned char)ibuf[i] == 0x1C)
+ {
+ /* Detach: send DETACH then exit loop */
+ _proto_send(sock, _TSRV_MSG_DETACH, NULL, 0);
+ running = 0;
+ goto detach_exit;
+ }
+ }
+
+ if (_proto_send(sock, _TSRV_MSG_INPUT, ibuf, (uint32_t)nr) < 0)
+ {
+ running = 0;
+ break;
+ }
+ }
+
+ /* ── daemon → handle messages ── */
+ if (fds[1].revents & (POLLIN | POLLHUP | POLLERR))
+ {
+ void *payload = NULL;
+ uint32_t psize = 0;
+ int mtype = _proto_recv(sock, &payload, &psize);
+
+ if (mtype < 0)
+ {
+ /* Connection closed or protocol error */
+ free(payload);
+ running = 0;
+ break;
+ }
+ if (mtype == 0)
+ {
+ /* EAGAIN — no full message yet */
+ free(payload);
+ continue;
+ }
+
+ switch ((_TsrvMsgType)mtype)
+ {
+ case _TSRV_MSG_NOTIFY:
+ _render_screen(shm, srv_cols, srv_rows);
+ break;
+
+ case _TSRV_MSG_TITLE:
+ if (payload && psize > 0)
+ {
+ /* OSC 2 — set window title. Null-terminate safely. */
+ char titlebuf[512];
+ size_t tlen = psize < sizeof(titlebuf) - 1
+ ? psize : sizeof(titlebuf) - 1;
+ memcpy(titlebuf, payload, tlen);
+ titlebuf[tlen] = '\0';
+ /* ESC ] 2 ; <title> BEL */
+ char esc[528];
+ int en = snprintf(esc, sizeof(esc),
+ "\033]2;%s\007", titlebuf);
+ if (en > 0)
+ write(STDOUT_FILENO, esc, (size_t)en);
+ }
+ break;
+
+ case _TSRV_MSG_BELL:
+ write(STDOUT_FILENO, "\007", 1);
+ break;
+
+ case _TSRV_MSG_EXIT:
+ if (payload && psize >= sizeof(_TsrvMsgExit))
+ {
+ _TsrvMsgExit ep;
+ memcpy(&ep, payload, sizeof(ep));
+ exit_code = (int)ep.exit_code;
+ }
+ running = 0;
+ break;
+
+ default:
+ /* Unknown or unexpected message — ignore */
+ break;
+ }
+
+ free(payload);
+ }
+ }
+
+detach_exit:
+ /* ── Cleanup ── */
+ _term_restore();
+
+ /* Restore SIGINT to default so the process is kill-able normally. */
+ signal(SIGINT, SIG_DFL);
+
+ munmap(shm, shm_sz);
+ close(sock);
+
+ /* Move cursor to a clean line after the restored terminal. */
+ write(STDOUT_FILENO, "\r\n", 2);
+
+ return exit_code;
}
/* ── usage ───────────────────────────────────────────────────────────── */
@@ -288,7 +1090,9 @@ usage(const char *prog)
" 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"
+ " attach <name> Attach to an existing session inline\n"
+ " (takes over the current terminal, like tmux attach)\n"
+ " Detach with Ctrl+\\ (Ctrl-backslash)\n"
"\n"
"Environment:\n"
" XDG_RUNTIME_DIR Base directory for session sockets\n"
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.