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.

Reply via email to