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 09c4e9d9162ea9349af6b3d4df62abfd66d1c96a
Author: [email protected] <[email protected]>
AuthorDate: Thu Mar 19 23:03:04 2026 -0600

    fix: address all remaining review issues and add delta rendering
    
    Must-fix:
    - Sequential realloc in _sync_shadow to prevent size-inconsistent
      screen buffers on partial OOM
    - Validate hdr.size in termsrv_recv_attached against expected payload
    - Add usleep(1000) to _recv_exact EAGAIN retry loop (both termsrv.c
      and tymux.c) to avoid busy-spinning
    
    Should-fix:
    - Wire test_termsrv.c into meson build (new src/tests/meson.build)
    - Add termpty_init/shutdown calls in tymuxd.c for proper log domain
    - Increase termsession.c sock_path buffer from 256 to 512 bytes
    - Add nested session warning to tymux cmd_new (matching cmd_attach)
    - Remove dead !ts condition in termsession.c fail path
    - Delete empty tymuxd.h (no declarations, serves no purpose)
    - Update comment /* --session */ to /* --tymux */ in main.c
    
    Nice-to-have:
    - Add SIGTERM handler in tymux attach (same as SIGINT: detach)
    - Print "[tymux: session ended]" message on EXIT in tymux attach
    - Add explanatory comment for session_name field in termiointernals.h
    
    Delta rendering for tymux attach:
    - Keep a previous-frame cell buffer; on each NOTIFY compare cells
      with memcmp and only emit ANSI escapes for changed cells
    - Full repaint on first render and dimension changes
    - Track cursor pen position to suppress redundant move escapes
    - Track attribute state (fg/bg/bold/etc) to suppress redundant SGR
    - Use 8KB stack buffer with inline flushes instead of per-frame malloc
    - Typical shell prompt update: ~100 bytes output instead of ~50KB
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 meson.build               |   3 +
 src/bin/main.c            |   2 +-
 src/bin/meson.build       |   2 +-
 src/bin/termiointernals.h |   2 +
 src/bin/termsession.c     |  29 ++-
 src/bin/termsrv.c         |   4 +-
 src/bin/tymux.c           | 500 ++++++++++++++++++++++++++++++++--------------
 src/bin/tymuxd.c          |  13 +-
 src/bin/tymuxd.h          |   4 -
 src/tests/meson.build     |  13 ++
 src/tests/test_termsrv.c  |   3 +
 11 files changed, 412 insertions(+), 163 deletions(-)

diff --git a/meson.build b/meson.build
index 7e11504d..b8802ba6 100644
--- a/meson.build
+++ b/meson.build
@@ -176,4 +176,7 @@ config_dir = include_directories('.')
 subdir('data')
 subdir('man')
 subdir('src/bin')
+if host_os == 'linux'
+  subdir('src/tests')
+endif
 
diff --git a/src/bin/main.c b/src/bin/main.c
index c618ac5e..b647be98 100644
--- a/src/bin/main.c
+++ b/src/bin/main.c
@@ -872,7 +872,7 @@ elm_main(int argc, char **argv)
      ECORE_GETOPT_VALUE_BOOL(instance.active_links),    /* --active-links */
      ECORE_GETOPT_VALUE_BOOL(no_wizard),                /* --no-wizard */
 #ifdef HAVE_TYMUX
-     ECORE_GETOPT_VALUE_STR(instance.session),          /* --session */
+     ECORE_GETOPT_VALUE_STR(instance.session),          /* --tymux */
 #endif
 
      ECORE_GETOPT_VALUE_BOOL(quit_option),              /* -v, --version */
diff --git a/src/bin/meson.build b/src/bin/meson.build
index a84592ea..fdf8d53c 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -137,7 +137,7 @@ executable('tysend',
 
 if host_os == 'linux'
   tymuxd_sources = [
-      'tymuxd.c', 'tymuxd.h',
+      'tymuxd.c',
       'tymux_session.c', 'tymux_session.h',
       'termsrv.c', 'termsrv.h',
       'termpty.c', 'termpty.h',
diff --git a/src/bin/termiointernals.h b/src/bin/termiointernals.h
index f727388e..143b6f69 100644
--- a/src/bin/termiointernals.h
+++ b/src/bin/termiointernals.h
@@ -78,6 +78,8 @@ struct tag_Termio
    Termpty *pty;
 #ifdef HAVE_TYMUX
    TermSession *session;        /* non-NULL in session mode */
+   /* session_name: tymux session name (eina_stringshare).  Kept for display
+    * purposes (e.g. window title) even after the session fd is open. */
    const char  *session_name;   /* eina_stringshare */
 #endif
    Ecore_Animator *anim;
diff --git a/src/bin/termsession.c b/src/bin/termsession.c
index 81438614..348086d5 100644
--- a/src/bin/termsession.c
+++ b/src/bin/termsession.c
@@ -150,21 +150,32 @@ _sync_shadow(TermSession *ts)
    if (rows > TERMSRV_MAX_ROWS) rows = TERMSRV_MAX_ROWS;
    if (cols == 0 || rows == 0) return;
 
-   /* Resize screen buffers if dimensions changed */
+   /* Resize screen buffers if dimensions changed.
+    * Realloc sequentially: update ty->screen before attempting screen2 so
+    * that if screen2 fails we still have a valid (possibly larger) screen
+    * buffer at the old dimensions — both buffers always cover at least
+    * ty->w * ty->h cells. */
    if ((int)cols != ty->w || (int)rows != ty->h)
      {
         Termcell *s1, *s2;
+        size_t    new_sz = (size_t)(cols * rows) * sizeof(Termcell);
 
-        s1 = realloc(ty->screen,  (size_t)(cols * rows) * sizeof(Termcell));
-        s2 = realloc(ty->screen2, (size_t)(cols * rows) * sizeof(Termcell));
-        if (!s1 || !s2)
+        s1 = realloc(ty->screen, new_sz);
+        if (!s1)
           {
              ERR("termsession: realloc screen failed");
-             if (s1) ty->screen  = s1;
-             if (s2) ty->screen2 = s2;
              return;
           }
-        ty->screen  = s1;
+        ty->screen = s1;
+
+        s2 = realloc(ty->screen2, new_sz);
+        if (!s2)
+          {
+             ERR("termsession: realloc screen2 failed");
+             /* ty->screen is the new (larger) allocation but ty->w/ty->h
+              * remain at old values, so both buffers are >= old dimensions. */
+             return;
+          }
         ty->screen2 = s2;
         ty->w = (int)cols;
         ty->h = (int)rows;
@@ -373,7 +384,7 @@ termsession_attach(const char *session_name,
                    Config *config,
                    Termpty **shadow_pty_out)
 {
-   char              sock_path[256];
+   char              sock_path[512];
    struct sockaddr_un addr;
    int               sock_fd = -1;
    int               shm_fd  = -1;
@@ -524,7 +535,7 @@ fail:
         free(ts);
         ts = NULL;
      }
-   if (ty && !ts)
+   if (ty)
      _shadow_pty_free(ty);
    if (shm != MAP_FAILED)
      munmap(shm, TERMSRV_SHM_TOTAL_SIZE);
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
index 6e85599b..47df5a10 100644
--- a/src/bin/termsrv.c
+++ b/src/bin/termsrv.c
@@ -107,8 +107,9 @@ _recv_exact(int fd, void *buf, size_t need)
               if (errno == EINTR) continue;
               if ((errno == EAGAIN || errno == EWOULDBLOCK) && retries < 100)
                 {
-                   /* Payload bytes haven't arrived yet — spin briefly */
+                   /* Payload bytes haven't arrived yet — yield briefly */
                    retries++;
+                   usleep(1000); /* 1 ms */
                    continue;
                 }
               return -1;
@@ -223,6 +224,7 @@ termsrv_recv_attached(int sock_fd, int *shm_fd_out,
     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;
+    if (hdr.size != sizeof(TermSrvMsgAttached)) return -1;
 
     struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
     if (!cm || cm->cmsg_type != SCM_RIGHTS ||
diff --git a/src/bin/tymux.c b/src/bin/tymux.c
index c4e425c1..25b6736e 100644
--- a/src/bin/tymux.c
+++ b/src/bin/tymux.c
@@ -235,6 +235,7 @@ _recv_exact(int fd, void *buf, size_t need)
              if ((errno == EAGAIN || errno == EWOULDBLOCK) && retries < 100)
                {
                   retries++;
+                  usleep(1000); /* 1 ms — yield before retrying */
                   continue;
                }
              return -1;
@@ -442,53 +443,180 @@ _utf8_encode(uint32_t cp, char *buf)
 /* ── 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.
+ * _flush_buf — write all of buf[0..pos) to STDOUT_FILENO, tolerating EINTR.
  */
 static void
-_render_screen(const void *shm, uint32_t cols, uint32_t rows)
+_flush_buf(const char *buf, size_t pos)
+{
+   size_t written = 0;
+   while (written < pos)
+     {
+        ssize_t w = write(STDOUT_FILENO, buf + written, pos - written);
+        if (w < 0)
+          {
+             if (errno == EINTR) continue;
+             break;
+          }
+        written += (size_t)w;
+     }
+}
+
+/*
+ * _emit_cell — write ANSI escapes + glyph for cell @c into buf[].
+ *
+ * Tracks attribute/colour state via the cur_* pointers; only emits SGR when
+ * something changed.  @pos is advanced; buf must have EMIT_CELL_MAX spare
+ * bytes (conservatively 80 bytes covers the worst-case SGR + UTF-8 + move).
+ */
+#define EMIT_CELL_MAX  128
+
+static size_t
+_emit_cell(char *buf, size_t pos, size_t bufsz,
+           const _Termcell *c,
+           uint8_t *cur_fg_idx, uint8_t *cur_bg_idx,
+           int *cur_bold, int *cur_uline,
+           int *cur_italic, int *cur_strike, int *cur_inv,
+           int *cur_fg256, int *cur_bg256)
+{
+#define EMIT(...) \
+   do { \
+      int _n = snprintf(buf + pos, bufsz - pos, __VA_ARGS__); \
+      if (_n > 0) pos += (size_t)_n; \
+   } while (0)
+
+   /* Attributes: reset+reapply when bold/italic/strike/inverse change. */
+   int need_reset = ((int)c->att.bold    != *cur_bold   ||
+                     (int)c->att.italic  != *cur_italic ||
+                     (int)c->att.strike  != *cur_strike ||
+                     (int)c->att.inverse != *cur_inv);
+
+   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
+          {
+             if (c->att.fg == 0 || c->att.fg > 8)
+               EMIT("\033[39m");
+             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 || c->att.bg > 8)
+               EMIT("\033[49m");
+             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;
+     }
+
+   /* Glyph */
+   uint32_t cp = c->codepoint;
+   if (c->att.invisible || cp == 0 || cp < 0x20)
+     {
+        if (pos < bufsz - 1) buf[pos++] = ' ';
+     }
+   else
+     {
+        char utf8[4];
+        int ulen = _utf8_encode(cp, utf8);
+        if (pos + (size_t)ulen < bufsz)
+          {
+             memcpy(buf + pos, utf8, (size_t)ulen);
+             pos += (size_t)ulen;
+          }
+     }
+
+#undef EMIT
+   return pos;
+}
+
+/*
+ * _render_full — repaint the entire screen unconditionally.
+ *
+ * Used on first render and whenever terminal dimensions change.
+ * Updates prev[] to match the current shm cells so subsequent
+ * _render_delta calls can skip unchanged cells.
+ */
+static void
+_render_full(const void *shm, _Termcell *prev,
+             uint32_t scols, uint32_t srows)
 {
    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. */
+   /* Heap-allocate output buffer: worst case ~50 bytes/cell + cursor escapes */
    size_t bufsz = (size_t)scols * (size_t)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");
+   EMIT("\033[?25l"); /* hide cursor during repaint */
+   EMIT("\033[H");    /* move to top-left */
 
-   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;
+   uint8_t cur_fg_idx = 0xFF;
+   uint8_t cur_bg_idx = 0xFF;
+   int cur_bold    = -1, cur_uline = -1, cur_italic = -1;
+   int cur_strike  = -1, cur_inv   = -1;
+   int cur_fg256   = -1, cur_bg256 = -1;
 
    for (uint32_t row = 0; row < srows; row++)
      {
@@ -496,122 +624,28 @@ _render_screen(const void *shm, uint32_t cols, uint32_t rows)
           {
              const _Termcell *c = &cells[row * scols + 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)
+             /* Flush before each cell if headroom is low */
+             if (pos + EMIT_CELL_MAX >= bufsz)
                {
-                  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;
+                  _flush_buf(out, pos);
+                  pos = 0;
                }
 
-             /* 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.
-                        * Values >8 (COL_INVIS=9, COL_INVERSE=10,
-                        * COL_INVERSEBG=11) are Terminology-internal and
-                        * have no ANSI mapping — fall back to default fg. */
-                       if (c->att.fg == 0 || c->att.fg > 8)
-                         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
-                    {
-                       /* Values >8 are Terminology-internal (COL_INVIS etc.)
-                        * with no ANSI mapping — fall back to default bg. */
-                       if (c->att.bg == 0 || c->att.bg > 8)
-                         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 / invisible cells) */
-             uint32_t cp = c->codepoint;
-             if (c->att.invisible || 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;
-                    }
-               }
+             pos = _emit_cell(out, pos, bufsz, c,
+                              &cur_fg_idx, &cur_bg_idx,
+                              &cur_bold, &cur_uline, &cur_italic,
+                              &cur_strike, &cur_inv,
+                              &cur_fg256, &cur_bg256);
           }
 
-        /* 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)
      {
@@ -625,20 +659,169 @@ _render_screen(const void *shm, uint32_t cols, uint32_t rows)
 
 #undef EMIT
 
-   /* Single write to minimize flicker. */
-   size_t written = 0;
-   while (written < pos)
+   _flush_buf(out, pos);
+   free(out);
+
+   /* Update prev to current state */
+   memcpy(prev, cells, (size_t)scols * (size_t)srows * sizeof(_Termcell));
+}
+
+/*
+ * _render_delta — emit only the cells that changed since the last render.
+ *
+ * Scans cell-by-cell with memcmp; skips identical cells.  Emits a cursor
+ * positioning escape before the first changed cell in a run and whenever
+ * the cell is not the immediate successor of the previous one (a "jump").
+ * Uses an 8 KB stack buffer flushed inline to avoid large heap allocations
+ * on the hot path.
+ *
+ * After writing, prev[] is updated with the current cell values.
+ */
+#define _DELTA_BUF 8192
+
+static void
+_render_delta(const void *shm, _Termcell *prev,
+              uint32_t scols, uint32_t srows)
+{
+   const _ShmHdr   *hdr   = (const _ShmHdr *)shm;
+   const _Termcell *cells = (const _Termcell *)((const uint8_t *)shm +
+                             _SHM_HEADER_SIZE);
+
+   char   buf[_DELTA_BUF];
+   size_t pos = 0;
+
+#define EMIT(...) \
+   do { \
+      int _n = snprintf(buf + pos, sizeof(buf) - pos, __VA_ARGS__); \
+      if (_n > 0) pos += (size_t)_n; \
+   } while (0)
+#define FLUSH_IF_NEEDED() \
+   do { \
+      if (pos + EMIT_CELL_MAX + 32 >= sizeof(buf)) \
+        { _flush_buf(buf, pos); pos = 0; } \
+   } while (0)
+
+   EMIT("\033[?25l"); /* hide cursor while writing */
+
+   uint8_t cur_fg_idx = 0xFF;
+   uint8_t cur_bg_idx = 0xFF;
+   int cur_bold    = -1, cur_uline = -1, cur_italic = -1;
+   int cur_strike  = -1, cur_inv   = -1;
+   int cur_fg256   = -1, cur_bg256 = -1;
+
+   /* Track where the terminal cursor currently is (after our last emit).
+    * (-1,-1) means unknown — will always emit a move for the first changed cell. */
+   int32_t pen_row = -1, pen_col = -1;
+
+   int any_change = 0;
+
+   for (uint32_t row = 0; row < srows; row++)
      {
-        ssize_t w = write(STDOUT_FILENO, out + written, pos - written);
-        if (w < 0)
+        for (uint32_t col = 0; col < scols; col++)
           {
-             if (errno == EINTR) continue;
-             break;
+             const _Termcell *c = &cells[row * scols + col];
+             _Termcell       *p = &prev[row * scols + col];
+
+             if (memcmp(c, p, sizeof(_Termcell)) == 0)
+               continue;
+
+             any_change = 1;
+             FLUSH_IF_NEEDED();
+
+             /* Move cursor if not immediately following previous cell */
+             if (pen_row != (int32_t)row || pen_col != (int32_t)col)
+               {
+                  EMIT("\033[%u;%uH", row + 1, col + 1);
+                  pen_row = (int32_t)row;
+                  pen_col = (int32_t)col;
+               }
+
+             pos = _emit_cell(buf, pos, sizeof(buf), c,
+                              &cur_fg_idx, &cur_bg_idx,
+                              &cur_bold, &cur_uline, &cur_italic,
+                              &cur_strike, &cur_inv,
+                              &cur_fg256, &cur_bg256);
+
+             /* Cursor advances one column after writing a character */
+             pen_col++;
+
+             *p = *c;
           }
-        written += (size_t)w;
      }
 
-   free(out);
+   if (any_change)
+     EMIT("\033[0m");
+
+   /* Reposition cursor */
+   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 FLUSH_IF_NEEDED
+#undef EMIT
+
+   if (pos > 0)
+     _flush_buf(buf, pos);
+}
+
+/*
+ * _render_screen — entry point for each NOTIFY event.
+ *
+ * On first call (prev == NULL or dimension change) does a full repaint and
+ * seeds prev[].  On subsequent calls with the same dimensions, does a delta
+ * update that only emits changed cells.
+ *
+ * @prev_cells_io:  pointer to the heap-allocated previous-frame buffer
+ *                  (may point to a NULL pointer on first call).
+ * @prev_cols_io:   previous render width  (updated on dimension change)
+ * @prev_rows_io:   previous render height (updated on dimension change)
+ */
+static void
+_render_screen(const void *shm,
+               uint32_t hint_cols, uint32_t hint_rows,
+               _Termcell **prev_cells_io,
+               uint32_t *prev_cols_io, uint32_t *prev_rows_io)
+{
+   const _ShmHdr *hdr = (const _ShmHdr *)shm;
+
+   uint32_t scols = hdr->cols ? hdr->cols : hint_cols;
+   uint32_t srows = hdr->rows ? hdr->rows : hint_rows;
+   if (scols > _SHM_MAX_COLS) scols = _SHM_MAX_COLS;
+   if (srows > _SHM_MAX_ROWS) srows = _SHM_MAX_ROWS;
+   if (scols == 0 || srows == 0) return;
+
+   int dim_changed = (scols != *prev_cols_io || srows != *prev_rows_io);
+
+   /* Allocate / reallocate the previous-frame buffer when dimensions change. */
+   if (dim_changed || !*prev_cells_io)
+     {
+        size_t sz = (size_t)scols * (size_t)srows * sizeof(_Termcell);
+        _Termcell *nb = realloc(*prev_cells_io, sz);
+        if (!nb) return; /* keep old state on OOM */
+        if (dim_changed)
+          memset(nb, 0, sz); /* force full repaint by zeroing prev */
+        *prev_cells_io = nb;
+        *prev_cols_io  = scols;
+        *prev_rows_io  = srows;
+     }
+
+   if (dim_changed)
+     {
+        /* Clear screen before full repaint on resize */
+        write(STDOUT_FILENO, "\033[2J", 4);
+        _render_full(shm, *prev_cells_io, scols, srows);
+     }
+   else
+     {
+        _render_delta(shm, *prev_cells_io, scols, srows);
+     }
 }
 
 /* ── commands ────────────────────────────────────────────────────────── */
@@ -701,6 +884,13 @@ cmd_new(int argc, char **argv)
    if (argc >= 3)
      name = argv[2];
 
+   /* Warn if we are already inside a tymux session (same check as cmd_attach). */
+   {
+      const char *current = getenv("TYMUX_SESSION");
+      if (current)
+        fprintf(stderr, "tymux: warning: already inside session '%s'\n", current);
+   }
+
    if (strlen(name) > TYMUX_MAX_SESSION_NAME)
      {
         fprintf(stderr, "tymux: session name too long (max %d chars)\n",
@@ -920,16 +1110,22 @@ cmd_attach(int argc, char **argv)
    sigemptyset(&sa_int.sa_mask);
    sa_int.sa_flags = 0;
    sigaction(SIGINT, &sa_int, NULL);
+   sigaction(SIGTERM, &sa_int, NULL); /* same handler: set flag and detach */
 
    /* Ignore SIGPIPE — connection loss is handled via send() return value */
    signal(SIGPIPE, SIG_IGN);
 
-   /* ── Enter raw mode and clear screen ── */
+   /* ── Delta rendering state ── */
+   _Termcell *prev_cells = NULL;   /* previous-frame buffer (heap, NULL = not yet alloc'd) */
+   uint32_t   prev_cols  = 0;
+   uint32_t   prev_rows  = 0;
+
+   /* ── Enter raw mode and paint the initial state ── */
    _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);
+   /* Initial full repaint (prev_cells is NULL → _render_screen forces full) */
+   _render_screen(shm, srv_cols, srv_rows,
+                  &prev_cells, &prev_cols, &prev_rows);
 
    setenv("TYMUX_SESSION", name, 1);
 
@@ -1035,7 +1231,8 @@ cmd_attach(int argc, char **argv)
              switch ((_TsrvMsgType)mtype)
                {
                 case _TSRV_MSG_NOTIFY:
-                   _render_screen(shm, srv_cols, srv_rows);
+                   _render_screen(shm, srv_cols, srv_rows,
+                                  &prev_cells, &prev_cols, &prev_rows);
                    break;
 
                 case _TSRV_MSG_TITLE:
@@ -1067,6 +1264,13 @@ cmd_attach(int argc, char **argv)
                         memcpy(&ep, payload, sizeof(ep));
                         exit_code = (int)ep.exit_code;
                      }
+                   {
+                      char exitmsg[64];
+                      int len = snprintf(exitmsg, sizeof(exitmsg),
+                                         "\r\n[tymux: session ended]\r\n");
+                      if (len > 0)
+                        write(STDOUT_FILENO, exitmsg, (size_t)len);
+                   }
                    running = 0;
                    break;
 
@@ -1083,8 +1287,12 @@ detach_exit:
    /* ── Cleanup ── */
    _term_restore();
 
-   /* Restore SIGINT to default so the process is kill-able normally. */
-   signal(SIGINT, SIG_DFL);
+   /* Restore SIGINT/SIGTERM to default so the process is kill-able normally. */
+   signal(SIGINT,  SIG_DFL);
+   signal(SIGTERM, SIG_DFL);
+
+   free(prev_cells);
+   prev_cells = NULL;
 
    munmap(shm, shm_sz);
    close(sock);
diff --git a/src/bin/tymuxd.c b/src/bin/tymuxd.c
index 89b7cab5..a6c3acc8 100644
--- a/src/bin/tymuxd.c
+++ b/src/bin/tymuxd.c
@@ -1,6 +1,6 @@
 #include "private.h"
-#include "tymuxd.h"
 #include "tymux_session.h"
+#include "termpty.h"
 #include "termsrv.h"
 
 #include <Ecore.h>
@@ -105,6 +105,7 @@ main(int argc, char **argv)
 
    eina_init();
    ecore_init();
+   termpty_init();
 
    _log_domain = eina_log_domain_register("tymuxd", NULL);
    if (_log_domain < 0)
@@ -118,6 +119,7 @@ main(int argc, char **argv)
    if (termsrv_ensure_sessions_dir() < 0)
      {
         ERR("could not create sessions directory: %s", strerror(errno));
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -126,6 +128,7 @@ main(int argc, char **argv)
    if (termsrv_socket_path(name, _sock_path, sizeof(_sock_path)) < 0)
      {
         ERR("socket path too long for session '%s'", name);
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -137,6 +140,7 @@ main(int argc, char **argv)
    if (probe_fd < 0)
      {
         ERR("socket (probe): %s", strerror(errno));
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -151,6 +155,7 @@ main(int argc, char **argv)
         /* A live daemon is already accepting on this socket */
         close(probe_fd);
         fprintf(stderr, "tymuxd: session '%s' already running\n", name);
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -167,6 +172,7 @@ main(int argc, char **argv)
    if (_listen_fd < 0)
      {
         ERR("socket (listen): %s", strerror(errno));
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -181,6 +187,7 @@ main(int argc, char **argv)
         ERR("bind '%s': %s", _sock_path, strerror(errno));
         close(_listen_fd);
         _listen_fd = -1;
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -192,6 +199,7 @@ main(int argc, char **argv)
         unlink(_sock_path);
         close(_listen_fd);
         _listen_fd = -1;
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -206,6 +214,7 @@ main(int argc, char **argv)
         unlink(_sock_path);
         close(_listen_fd);
         _listen_fd = -1;
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -224,6 +233,7 @@ main(int argc, char **argv)
         unlink(_sock_path);
         close(_listen_fd);
         _listen_fd = -1;
+        termpty_shutdown();
         ecore_shutdown();
         eina_shutdown();
         return 1;
@@ -254,6 +264,7 @@ main(int argc, char **argv)
    termd_session_free(_session);
    _session = NULL;
 
+   termpty_shutdown();
    ecore_shutdown();
    eina_shutdown();
 
diff --git a/src/bin/tymuxd.h b/src/bin/tymuxd.h
deleted file mode 100644
index cd8be4c2..00000000
--- a/src/bin/tymuxd.h
+++ /dev/null
@@ -1,4 +0,0 @@
-#ifndef TERMINOLOGY_TYMUXD_H_
-#define TERMINOLOGY_TYMUXD_H_
-/* Usage: tymuxd <session-name> */
-#endif /* TERMINOLOGY_TYMUXD_H_ */
diff --git a/src/tests/meson.build b/src/tests/meson.build
new file mode 100644
index 00000000..2a0f67be
--- /dev/null
+++ b/src/tests/meson.build
@@ -0,0 +1,13 @@
+if host_os == 'linux'
+  # termsrv unit tests: compile termsrv.c alongside the test and link with
+  # tymuxd_dependencies (eina + ecore are needed because termsrv.c uses
+  # Eina_Log indirectly via private.h, and termpty.h references Ecore types).
+  test_termsrv = executable('test_termsrv',
+    ['test_termsrv.c',
+     '../bin/termsrv.c',
+    ],
+    c_args: ['-DHAVE_TYMUX=1', '-DBINARY_TYMUXD=1'],
+    include_directories: [config_dir, include_directories('../bin')],
+    dependencies: tymuxd_dependencies)
+  test('termsrv', test_termsrv)
+endif
diff --git a/src/tests/test_termsrv.c b/src/tests/test_termsrv.c
index 62d15297..a6008289 100644
--- a/src/tests/test_termsrv.c
+++ b/src/tests/test_termsrv.c
@@ -4,6 +4,9 @@
 #include <assert.h>
 #include <unistd.h>
 #include <sys/socket.h>
+/* termsrv.h pulls in termpty.h which needs Ecore types including Ecore_Window */
+#include <Ecore.h>
+#include <Ecore_Evas.h>
 #include "termsrv.h"
 
 static void

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

Reply via email to