#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; }