From 4384435941b8c7658856b381d071aee2e0035e89 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 17:49:34 +0300 Subject: 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:. - 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 --- src/snapshot.c | 346 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 src/snapshot.c (limited to 'src/snapshot.c') 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 +#include + +/* + * 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; +} -- cgit v1.2.3