This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch tymux
in repository terminology.
View the commit online.
commit 5acf742992ffd4f616f61b26282fd144b682533a
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 22 10:31:23 2026 -0600
feat: implement daemon-side backlog buffer management
The tymux daemon now mirrors its Termsave scrollback into memfd-backed
shared-memory buffers. Each buffer holds up to 256 lines at a fixed
column width. New buffers are created when the head fills or the
terminal width changes. Oldest buffers are evicted when total lines
exceed the configured scrollback limit. The NOTIFY message now carries
backlog state so clients can track buffer availability.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
src/bin/termd_tymux.c | 231 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 229 insertions(+), 2 deletions(-)
diff --git a/src/bin/termd_tymux.c b/src/bin/termd_tymux.c
index 1d46f6db..37cc1b00 100644
--- a/src/bin/termd_tymux.c
+++ b/src/bin/termd_tymux.c
@@ -17,6 +17,7 @@
#include "termd_tymux.h"
#include "termpty.h"
#include "termsrv.h"
+#include "backlog.h"
/* ── log domain ─────────────────────────────────────────────────── */
@@ -62,6 +63,18 @@ struct _TermDTymux
int shm_fd;
TermShmHeader *shm;
Eina_List *clients;
+
+ /* backlog buffer chain */
+ struct {
+ int fd; /* memfd */
+ TermBacklogBufHeader *hdr; /* mmap'd header */
+ size_t total_size; /* mmap size */
+ } *backlog_bufs; /* dynamic array of buffers */
+ int backlog_buf_count; /* number of buffers in chain */
+ int backlog_buf_alloc; /* allocated array slots */
+ int oldest_buf_id; /* global id of oldest buffer */
+ int next_buf_id; /* next global id to assign */
+ size_t last_flushed_backpos; /* backpos at last flush */
};
/* ── synthetic Config for headless use ───────────────────────────── */
@@ -79,6 +92,193 @@ _default_config_new(void)
return cfg;
}
+/* ── backlog buffer helpers ──────────────────────────────────────── */
+
+static int
+_backlog_buf_new(TermDTymux *sess, int cols)
+{
+ char memfd_name[128];
+ size_t total_size;
+ int fd;
+ TermBacklogBufHeader *hdr;
+ int idx;
+
+ total_size = TERMSRV_BACKLOG_TOTAL_SIZE((unsigned)cols);
+
+ snprintf(memfd_name, sizeof(memfd_name),
+ "ty-backlog-%s-%d", sess->name, sess->next_buf_id);
+
+ fd = _memfd_create(memfd_name, MFD_CLOEXEC);
+ if (fd < 0)
+ {
+ ERR("memfd_create backlog failed: %s", strerror(errno));
+ return -1;
+ }
+
+ if (ftruncate(fd, (off_t)total_size) < 0)
+ {
+ ERR("ftruncate backlog failed: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ hdr = mmap(NULL, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (hdr == MAP_FAILED)
+ {
+ ERR("mmap backlog failed: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ /* grow array if needed */
+ if (sess->backlog_buf_count >= sess->backlog_buf_alloc)
+ {
+ int new_alloc = sess->backlog_buf_alloc ? sess->backlog_buf_alloc * 2 : 4;
+ void *tmp = realloc(sess->backlog_bufs,
+ (size_t)new_alloc * sizeof(*sess->backlog_bufs));
+ if (!tmp)
+ {
+ ERR("realloc backlog_bufs failed");
+ munmap(hdr, total_size);
+ close(fd);
+ return -1;
+ }
+ sess->backlog_bufs = tmp;
+ sess->backlog_buf_alloc = new_alloc;
+ }
+
+ idx = sess->backlog_buf_count;
+ sess->backlog_bufs[idx].fd = fd;
+ sess->backlog_bufs[idx].hdr = hdr;
+ sess->backlog_bufs[idx].total_size = total_size;
+ sess->backlog_buf_count++;
+
+ hdr->magic = TERMSRV_BACKLOG_MAGIC;
+ hdr->version = TERMSRV_BACKLOG_VERSION;
+ hdr->cols = (uint32_t)cols;
+ hdr->num_lines = 0;
+ hdr->max_lines = TERMSRV_BACKLOG_MAX_LINES;
+ atomic_store_explicit(&hdr->write_seq, 0, memory_order_relaxed);
+
+ sess->next_buf_id++;
+ return idx;
+}
+
+static void
+_backlog_buf_evict_oldest(TermDTymux *sess)
+{
+ if (sess->backlog_buf_count <= 0)
+ return;
+
+ munmap(sess->backlog_bufs[0].hdr, sess->backlog_bufs[0].total_size);
+ close(sess->backlog_bufs[0].fd);
+
+ int remaining = sess->backlog_buf_count - 1;
+ if (remaining > 0)
+ memmove(&sess->backlog_bufs[0], &sess->backlog_bufs[1],
+ (size_t)remaining * sizeof(*sess->backlog_bufs));
+
+ sess->backlog_buf_count--;
+ sess->oldest_buf_id++;
+}
+
+static void
+_flush_backlog_to_shm(TermDTymux *sess)
+{
+ Termpty *ty = sess->pty;
+ size_t cur_backpos = ty->backpos;
+ size_t old_backpos = sess->last_flushed_backpos;
+ size_t backsize = ty->backsize;
+ size_t new_lines;
+ size_t i;
+
+ if (cur_backpos == old_backpos)
+ return;
+
+ /* calculate how many new lines have been saved since last flush,
+ * handling the circular wrap of backpos */
+ if (cur_backpos >= old_backpos)
+ new_lines = cur_backpos - old_backpos;
+ else
+ new_lines = backsize - old_backpos + cur_backpos;
+
+ /* cap to backsize to avoid processing stale entries after full wrap */
+ if (new_lines > backsize)
+ new_lines = backsize;
+
+ /* ensure at least one head buffer exists */
+ if (sess->backlog_buf_count == 0)
+ {
+ if (_backlog_buf_new(sess, ty->w) < 0)
+ return;
+ }
+
+ /* iterate new lines from oldest to newest:
+ * BACKLOG_ROW_GET(ty, i) with i=new_lines gives oldest new line,
+ * i=1 gives the most recent */
+ for (i = new_lines; i >= 1; i--)
+ {
+ TermBacklogBufHeader *hdr;
+ Termcell *dst;
+ Termsave *ts;
+ uint32_t copy_cols;
+ uint32_t line_idx;
+
+ /* get or create a head buffer with matching cols */
+ hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+ if (hdr->num_lines >= hdr->max_lines || (int)hdr->cols != ty->w)
+ {
+ if (_backlog_buf_new(sess, ty->w) < 0)
+ break;
+ hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+ }
+
+ ts = BACKLOG_ROW_GET(ty, (int)i);
+ if (!ts || !ts->cells)
+ continue;
+
+ line_idx = hdr->num_lines;
+ dst = TERMSRV_BACKLOG_LINE(hdr, line_idx);
+ copy_cols = (ts->w < hdr->cols) ? (uint32_t)ts->w : hdr->cols;
+
+ memcpy(dst, ts->cells, (size_t)copy_cols * sizeof(Termcell));
+ if (copy_cols < hdr->cols)
+ memset(dst + copy_cols, 0,
+ (size_t)(hdr->cols - copy_cols) * sizeof(Termcell));
+
+ hdr->line_widths[line_idx] = copy_cols;
+ hdr->num_lines++;
+ }
+
+ /* evict oldest buffers while total lines exceed configured scrollback,
+ * but always keep at least one buffer */
+ {
+ int total_lines = 0;
+ int b;
+ for (b = 0; b < sess->backlog_buf_count; b++)
+ total_lines += (int)sess->backlog_bufs[b].hdr->num_lines;
+
+ while (total_lines > sess->config->scrollback &&
+ sess->backlog_buf_count > 1)
+ {
+ total_lines -= (int)sess->backlog_bufs[0].hdr->num_lines;
+ _backlog_buf_evict_oldest(sess);
+ }
+ }
+
+ /* bump head buffer write_seq with release semantics */
+ if (sess->backlog_buf_count > 0)
+ {
+ TermBacklogBufHeader *head =
+ sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+ uint64_t seq = atomic_load_explicit(&head->write_seq,
+ memory_order_relaxed);
+ atomic_store_explicit(&head->write_seq, seq + 1, memory_order_release);
+ }
+
+ sess->last_flushed_backpos = cur_backpos;
+}
+
/* ── shm flush ───────────────────────────────────────────────────── */
static void
@@ -140,9 +340,23 @@ _pty_change_cb(void *data)
TermDClient *c;
_flush_screen_to_shm(sess);
+ _flush_backlog_to_shm(sess);
+
+ pay.write_seq = atomic_load_explicit(&sess->shm->write_seq,
+ memory_order_acquire);
+ pay.backlog_buf_count = (uint32_t)sess->backlog_buf_count;
+ pay.oldest_buf_id = (uint32_t)sess->oldest_buf_id;
+ if (sess->backlog_buf_count > 0)
+ {
+ TermBacklogBufHeader *head =
+ sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+ pay.backlog_head_seq =
+ (uint32_t)atomic_load_explicit(&head->write_seq,
+ memory_order_acquire);
+ }
+ else
+ pay.backlog_head_seq = 0;
- pay.write_seq = atomic_load_explicit(&sess->shm->write_seq,
- memory_order_acquire);
EINA_LIST_FOREACH(sess->clients, l, c)
termsrv_msg_send(c->fd, TSRV_MSG_NOTIFY, &pay, sizeof(pay));
}
@@ -336,6 +550,8 @@ termd_tymux_new(const char *name, const char *cmd)
sess->pty->cb.exited.func = _pty_exit_cb;
sess->pty->cb.exited.data = ""
+ sess->last_flushed_backpos = sess->pty->backpos;
+
_flush_screen_to_shm(sess);
return sess;
@@ -374,6 +590,17 @@ termd_tymux_free(TermDTymux *sess)
free(c);
}
+ {
+ int b;
+ for (b = 0; b < sess->backlog_buf_count; b++)
+ {
+ munmap(sess->backlog_bufs[b].hdr, sess->backlog_bufs[b].total_size);
+ close(sess->backlog_bufs[b].fd);
+ }
+ free(sess->backlog_bufs);
+ sess->backlog_bufs = NULL;
+ }
+
if (sess->pty)
{
termpty_free(sess->pty);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.