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.