summaryrefslogtreecommitdiff
path: root/src/snapshot.c
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /src/snapshot.c
term: move the term core to subprojects/term
The core now builds, tests, and benchmarks alone, as a Meson subproject with no dependency on the main tree. - include/term/ holds term.h and the header-only unicode/. src/ holds the core and its private headers. The main tree gets only include/term through term_dep, so it cannot reach a private header. - The term tests, the term benchmarks, the Kitty parser suite, and tools/amalgamate.py and tools/generate_unicode.py move with it. The tests run as term:<name>. - The main build passes its -march (arch_flag) and b_ndebug to the subproject. A directory configured before this commit needs meson configure -Dterm:arch_flag=-march=... once. - The term tests compile with -UNDEBUG. They do work inside assert. - The subproject exports term_test_dep with term_api.h, for the main tree's render, thumbnail, color, and selection tests. - The Kitty bridge used struct vector2i and has not compiled since the public API redesign. It builds again. 2 of its 14 tests fail: REP after a tab, and a cursor report after a resize. - dirgraph.py drops the term and unicode layers. Its self-test uses pty. - The symbols test ignores the __odr_asan symbols that ASan adds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/snapshot.c')
-rw-r--r--src/snapshot.c346
1 files changed, 346 insertions, 0 deletions
diff --git a/src/snapshot.c b/src/snapshot.c
new file mode 100644
index 0000000..dcd1dc0
--- /dev/null
+++ b/src/snapshot.c
@@ -0,0 +1,346 @@
+#include "compat.h"
+#include "screen.h"
+#include "unicode/grapheme.h"
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * A snapshot keeps one converted copy of each viewport row. An update
+ * copies a row again only when the terminal reports a new key for it.
+ * Cluster cells refer to an arena of [length, code points...] records that
+ * the snapshot owns, so a renderer never reads the terminal's cluster table.
+ */
+struct term_snapshot {
+ struct term_view view;
+ uint32_t palette[TERM_PALETTE_SIZE];
+ uint64_t source; /* serial of the terminal it copied */
+
+ struct term_cell *cells; /* rows * cols */
+ uint64_t *keys; /* per row */
+ uint32_t *row_clusters; /* arena records each row holds */
+ struct term_span *selection; /* per row */
+ size_t cell_cap;
+ int row_cap;
+
+ uint32_t *arena;
+ uint32_t arena_len, arena_cap, arena_dead;
+};
+
+struct term_snapshot *term_snapshot_new(void)
+{
+ struct term_snapshot *s = calloc(1, sizeof(*s));
+ if (s)
+ s->view.palette = s->palette;
+ return s;
+}
+
+void term_snapshot_free(struct term_snapshot *s)
+{
+ if (!s)
+ return;
+ free(s->cells);
+ free(s->keys);
+ free(s->row_clusters);
+ free(s->selection);
+ free(s->arena);
+ free(s);
+}
+
+static bool reserve(struct term_snapshot *s, int cols, int rows)
+{
+ size_t cells = (size_t)cols * (size_t)rows;
+ if (cells > s->cell_cap) {
+ struct term_cell *c = realloc(s->cells, cells * sizeof(*c));
+ if (!c)
+ return false;
+ s->cells = c;
+ s->cell_cap = cells;
+ }
+ if (rows > s->row_cap) {
+ uint64_t *k = realloc(s->keys, (size_t)rows * sizeof(*k));
+ if (k)
+ s->keys = k;
+ uint32_t *rc = realloc(s->row_clusters, (size_t)rows * sizeof(*rc));
+ if (rc)
+ s->row_clusters = rc;
+ struct term_span *sel = realloc(s->selection, (size_t)rows * sizeof(*sel));
+ if (sel)
+ s->selection = sel;
+ if (!k || !rc || !sel)
+ return false;
+ s->row_cap = rows;
+ }
+ return true;
+}
+
+/* Attribute contributions of the low and high byte of cell flags. */
+static uint16_t lo_attrs[256], hi_attrs[256];
+static uint8_t hi_underline[256];
+void term_snapshot_tables_init(void)
+{
+ for (int b = 0; b < 256; b++) {
+ uint16_t lo = 0, hi = 0;
+ if (b & CELL_BOLD) lo |= TERM_ATTR_BOLD;
+ if (b & CELL_ITALIC) lo |= TERM_ATTR_ITALIC;
+ if (b & CELL_STRIKE) lo |= TERM_ATTR_STRIKE;
+ if (b & CELL_OVERLINE) lo |= TERM_ATTR_OVERLINE;
+ if (b & CELL_EMOJI) lo |= TERM_ATTR_EMOJI;
+ if (b & CELL_TEXT) lo |= TERM_ATTR_TEXT;
+ uint16_t f = (uint16_t)(b << 8);
+ if (f & CELL_FAINT) hi |= TERM_ATTR_FAINT;
+ if (f & CELL_LINK) hi |= TERM_ATTR_LINK;
+ uint8_t ul = 0;
+ if (f & CELL_DOUBLE_UNDERLINE) ul = TERM_UNDERLINE_DOUBLE;
+ else if (f & CELL_CURLY_UNDERLINE) ul = TERM_UNDERLINE_CURLY;
+ else if (f & CELL_DOTTED_UNDERLINE) ul = TERM_UNDERLINE_DOTTED;
+ else if (f & CELL_DASHED_UNDERLINE) ul = TERM_UNDERLINE_DASHED;
+ lo_attrs[b] = lo;
+ hi_attrs[b] = hi;
+ hi_underline[b] = ul;
+ }
+}
+
+static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint32_t handle)
+{
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int n = term_gstore_expand(g, handle, cps, GRAPHEME_MAX_LEN);
+ if (n <= 0)
+ return ' ';
+ if (s->arena_len + (uint32_t)n + 1 > s->arena_cap) {
+ uint32_t cap = s->arena_cap ? s->arena_cap * 2 : 256;
+ while (cap < s->arena_len + (uint32_t)n + 1)
+ cap *= 2;
+ uint32_t *a = realloc(s->arena, cap * sizeof(*a));
+ if (!a)
+ return cps[0];
+ s->arena = a;
+ s->arena_cap = cap;
+ }
+ uint32_t off = s->arena_len;
+ s->arena[off] = (uint32_t)n;
+ memcpy(&s->arena[off + 1], cps, (size_t)n * sizeof(*cps));
+ s->arena_len += (uint32_t)n + 1;
+ return TERM_TEXT_CLUSTER | off;
+}
+
+/* Convert one stored row; returns the number of arena words it added. */
+static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g,
+ struct term_cell *dst,
+ const struct cell *src, int cols)
+{
+ uint32_t before = s->arena_len;
+ for (int x = 0; x < cols; x++) {
+ const struct cell *c = &src[x];
+ uint16_t f = c->flags;
+ uint8_t lo = (uint8_t)f, hi = (uint8_t)(f >> 8);
+ uint16_t ul = hi_underline[hi];
+ if (!ul && (f & CELL_UNDERLINE))
+ ul = TERM_UNDERLINE_SINGLE;
+ uint32_t text = c->c;
+ if (TERM_EXPECT(text < 0x20, 0))
+ text = text ? ' ' : 0;
+ else if (TERM_EXPECT(grapheme_is_handle(text), 0))
+ text = arena_add(s, g, text);
+ dst[x] = (struct term_cell){
+ .text = text,
+ .fg = c->fg,
+ .bg = c->bg,
+ .attrs = (uint16_t)(lo_attrs[lo] | hi_attrs[hi] |
+ ul << TERM_ATTR_UNDERLINE_SHIFT),
+ .width = (f & CELL_TRAIL) ? 0 : (f & CELL_WIDE) ? 2 : 1,
+ };
+ }
+ return s->arena_len - before;
+}
+
+static bool term_snapshot_update_inner(struct term_snapshot *s, struct term *t)
+{
+ if (!s || !t)
+ return false;
+ int cols = t->cols, rows = t->rows;
+ if (!reserve(s, cols, rows)) {
+ s->view.size = (struct term_vector2i){ 0 };
+ s->source = 0;
+ return false;
+ }
+
+ bool resized = s->view.size.x != cols || s->view.size.y != rows;
+ bool all = resized || s->source != t->serial;
+ /* Reclaim cluster records once most of the arena is garbage. */
+ if (s->arena_dead > 4096 && s->arena_dead * 2 > s->arena_len)
+ all = true;
+ if (all) {
+ s->arena_len = 0;
+ s->arena_dead = 0;
+ }
+
+ bool changed = all;
+ term_hist_prepare_rows(&t->hist, cols);
+ if (!term_hist_view_sync(&t->view, &t->hist, cols))
+ return false;
+ term_hist_view_refine(&t->view, &t->hist, cols, rows);
+ uint64_t stack_keys[256];
+ uint64_t *new_keys = rows <= 256 ? stack_keys : malloc((size_t)rows * sizeof(uint64_t));
+ if (!new_keys)
+ return false;
+ for (int y = 0; y < rows; y++)
+ new_keys[y] = term_screen_view_row_key(t, y);
+
+ int scrolled = 0;
+ if (!all && rows > 1) {
+ int cand_k = 0;
+ /* Find candidate scroll offset k by checking where top rows moved. */
+ for (int p = 0; p < 2 && p < rows - 1 && !cand_k; p++) {
+ uint64_t key = new_keys[p];
+ if (!key)
+ continue;
+ for (int k = 1; p + k < rows; k++) {
+ if (s->keys[p + k] == key) {
+ cand_k = k;
+ break;
+ }
+ }
+ }
+
+ if (cand_k > 0) {
+ int total = rows - cand_k;
+ int threshold = total / 2;
+ int matches = 0, mismatches = 0;
+ for (int y = 0; y < total; y++) {
+ if (new_keys[y] == s->keys[y + cand_k]) {
+ if (++matches > threshold) {
+ scrolled = cand_k;
+ break;
+ }
+ } else {
+ if (++mismatches > threshold)
+ break;
+ }
+ }
+ }
+ }
+
+ if (scrolled > 0) {
+ int k = scrolled;
+ memmove(s->cells, s->cells + (size_t)k * cols,
+ (size_t)(rows - k) * cols * sizeof(struct term_cell));
+ memmove(s->row_clusters, s->row_clusters + k,
+ (size_t)(rows - k) * sizeof(uint32_t));
+ memmove(s->keys, s->keys + k,
+ (size_t)(rows - k) * sizeof(uint64_t));
+ for (int y = rows - k; y < rows; y++) {
+ s->keys[y] = 0;
+ s->row_clusters[y] = 0;
+ }
+ }
+
+ for (int y = 0; y < rows; y++) {
+ uint64_t key = new_keys[y];
+ if (!all && s->keys[y] == key)
+ continue;
+ if (!all)
+ s->arena_dead += s->row_clusters[y];
+ const struct cell *src = term_screen_view_row(t, y);
+ s->row_clusters[y] = convert_row(s, t->gstore, s->cells + (size_t)y * cols, src, cols);
+ s->keys[y] = key;
+ changed = true;
+ }
+ if (new_keys != stack_keys)
+ free(new_keys);
+
+ int stack[2][256];
+ int *c0 = rows <= 256 ? stack[0] : malloc((size_t)rows * 2 * sizeof(int));
+ if (!c0)
+ return false;
+ int *c1 = rows <= 256 ? stack[1] : c0 + rows;
+ term_selection_view_spans(t, c0, c1);
+ for (int y = 0; y < rows; y++) {
+ struct term_span span = c1[y] >= c0[y] ? (struct term_span){ c0[y], c1[y] + 1 }
+ : (struct term_span){ 0 };
+ if (all || span.begin != s->selection[y].begin || span.end != s->selection[y].end)
+ changed = true;
+ s->selection[y] = span;
+ }
+ if (c0 != stack[0])
+ free(c0);
+
+ struct term_cursor cursor = term_cursor(t);
+ int view_offset = term_view_offset(t);
+ cursor.pos.y += view_offset;
+ if (cursor.pos.y >= rows)
+ cursor.visible = false;
+ const struct term_cursor *old = &s->view.cursor;
+ if (cursor.pos.x != old->pos.x || cursor.pos.y != old->pos.y ||
+ cursor.style != old->style || cursor.visible != old->visible ||
+ cursor.blink != old->blink ||
+ s->view.scroll_offset != view_offset ||
+ s->view.reverse_video != t->reverse_video)
+ changed = true;
+ if (all || s->view.palette_version != t->palette.version) {
+ memcpy(s->palette, t->palette.colors, sizeof(s->palette));
+ s->view.palette_version = t->palette.version;
+ changed = true;
+ }
+
+ s->source = t->serial;
+ s->view.size = (struct term_vector2i){ .x = cols, .y = rows };
+ s->view.cursor = cursor;
+ s->view.scroll_offset = view_offset;
+ s->view.scrolled = scrolled;
+ s->view.reverse_video = t->reverse_video;
+ if (changed || scrolled)
+ s->view.version++;
+ return true;
+}
+
+bool term_snapshot_update(struct term_snapshot *s, struct term *t)
+{
+ if (!s || !t)
+ return false;
+ term_write_begin(t);
+ bool ok = term_snapshot_update_inner(s, t);
+ term_write_end(t);
+ return ok;
+}
+
+const struct term_view *term_snapshot_view(const struct term_snapshot *s)
+{
+ return &s->view;
+}
+
+const struct term_cell *term_snapshot_row(const struct term_snapshot *s, int y)
+{
+ if (y < 0 || y >= s->view.size.y)
+ return NULL;
+ return s->cells + (size_t)y * s->view.size.x;
+}
+
+uint64_t term_snapshot_row_key(const struct term_snapshot *s, int y)
+{
+ if (y < 0 || y >= s->view.size.y)
+ return 0;
+ return s->keys[y];
+}
+
+struct term_span term_snapshot_row_selection(const struct term_snapshot *s, int y)
+{
+ if (y < 0 || y >= s->view.size.y)
+ return (struct term_span){ 0 };
+ return s->selection[y];
+}
+
+int term_snapshot_cluster(const struct term_snapshot *s, uint32_t text, uint32_t *cps, int cap)
+{
+ if (!(text & TERM_TEXT_CLUSTER)) {
+ if (cap > 0)
+ cps[0] = text;
+ return 1;
+ }
+ uint32_t off = text & ~TERM_TEXT_CLUSTER;
+ if (off >= s->arena_len)
+ return 0;
+ int n = (int)s->arena[off];
+ for (int i = 0; i < n && i < cap; i++)
+ cps[i] = s->arena[off + 1 + i];
+ return n;
+}