summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cell.h46
-rw-r--r--src/compat.h171
-rw-r--r--src/core.c1311
-rw-r--r--src/core.h385
-rw-r--r--src/csi.h109
-rw-r--r--src/escape.c1116
-rw-r--r--src/escape.h10
-rw-r--r--src/fastcsi.c378
-rw-r--r--src/gstore.c380
-rw-r--r--src/gstore.h36
-rw-r--r--src/hist.c3256
-rw-r--r--src/hist.h321
-rw-r--r--src/hist_view.c366
-rw-r--r--src/hist_view.h60
-rw-r--r--src/input.c771
-rw-r--r--src/internal.h411
-rw-r--r--src/isa.c78
-rw-r--r--src/kernels.h790
-rw-r--r--src/meson.build27
-rw-r--r--src/osc/clipboard.c76
-rw-r--r--src/osc/dispatch.c227
-rw-r--r--src/osc/link.c260
-rw-r--r--src/osc/meson.build6
-rw-r--r--src/osc/misc.c69
-rw-r--r--src/osc/osc.h30
-rw-r--r--src/palette.c492
-rw-r--r--src/palette.h34
-rw-r--r--src/pen.h165
-rw-r--r--src/scan.c11
-rw-r--r--src/screen.c1513
-rw-r--r--src/screen.h453
-rw-r--r--src/selection.c824
-rw-r--r--src/simd.h117
-rw-r--r--src/snapshot.c346
-rw-r--r--src/term.c125
-rw-r--r--src/text.c350
-rw-r--r--src/text.h10
-rw-r--r--src/text_run.c682
-rw-r--r--src/text_run.h90
39 files changed, 15902 insertions, 0 deletions
diff --git a/src/cell.h b/src/cell.h
new file mode 100644
index 0000000..41b16eb
--- /dev/null
+++ b/src/cell.h
@@ -0,0 +1,46 @@
+#ifndef TERM_CELL_H
+#define TERM_CELL_H
+
+/*
+ * The cell layout that the terminal stores. Only src/term uses it. A
+ * snapshot converts these cells to the public struct term_cell.
+ */
+
+#include "term.h"
+
+enum cell_flags {
+ CELL_BOLD = 1 << 0,
+ CELL_ITALIC = 1 << 1,
+ CELL_UNDERLINE = 1 << 2,
+ CELL_STRIKE = 1 << 3,
+ CELL_OVERLINE = 1 << 4,
+ CELL_EMOJI = 1 << 5,
+ CELL_TEXT = 1 << 6,
+ CELL_WIDE = 1 << 7,
+ CELL_TRAIL = 1 << 8,
+ CELL_WRAP_PAD = 1 << 9,
+ CELL_FAINT = 1 << 10,
+ CELL_DOUBLE_UNDERLINE = 1 << 11,
+ CELL_CURLY_UNDERLINE = 1 << 12,
+ CELL_DOTTED_UNDERLINE = 1 << 13,
+ CELL_DASHED_UNDERLINE = 1 << 14,
+ CELL_LINK = 1 << 15,
+};
+
+#define CELL_ANY_UNDERLINE \
+ (CELL_UNDERLINE | CELL_DOUBLE_UNDERLINE | CELL_CURLY_UNDERLINE | \
+ CELL_DOTTED_UNDERLINE | CELL_DASHED_UNDERLINE)
+
+/* c is a code point or a grapheme handle. pad holds the OSC 8 link id. */
+struct cell {
+ uint32_t c;
+ uint32_t fg;
+ uint32_t bg;
+ uint16_t flags;
+ uint16_t pad;
+};
+
+_Static_assert(sizeof(struct cell) == 16,
+ "struct cell must stay 16 bytes (4 cells per AVX-512 register)");
+
+#endif
diff --git a/src/compat.h b/src/compat.h
new file mode 100644
index 0000000..d1264fe
--- /dev/null
+++ b/src/compat.h
@@ -0,0 +1,171 @@
+#ifndef TERM_COMPAT_H
+#define TERM_COMPAT_H
+
+/*
+ * Compiler and libc differences, kept in one place. GCC and Clang use their
+ * builtins. MSVC uses its intrinsics. Any other compiler, and any build with
+ * TERM_NO_COMPILER_BUILTINS, uses plain C. Plain C gives the same results,
+ * only slower.
+ */
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(__GNUC__) || defined(__clang__)
+#define TERM_COMPILER_GNU 1
+#elif defined(_MSC_VER)
+#define TERM_COMPILER_MSVC 1
+#include <intrin.h>
+#endif
+
+#if !defined(TERM_NO_COMPILER_BUILTINS) && defined(TERM_COMPILER_GNU)
+#define TERM_HAVE_BUILTINS 1
+#elif !defined(TERM_NO_COMPILER_BUILTINS) && defined(TERM_COMPILER_MSVC)
+#define TERM_HAVE_MSVC_BITOPS 1
+#endif
+
+#if defined(TERM_COMPILER_GNU)
+#define TERM_HOT __attribute__((hot))
+#define TERM_NOINLINE __attribute__((noinline))
+#define TERM_ALWAYS_INLINE __attribute__((always_inline))
+#define TERM_MAYBE_UNUSED __attribute__((unused))
+#elif defined(TERM_COMPILER_MSVC)
+#define TERM_HOT
+#define TERM_NOINLINE __declspec(noinline)
+#define TERM_ALWAYS_INLINE __forceinline
+#define TERM_MAYBE_UNUSED
+#else
+#define TERM_HOT
+#define TERM_NOINLINE
+#define TERM_ALWAYS_INLINE
+#define TERM_MAYBE_UNUSED
+#endif
+
+#if defined(TERM_HAVE_BUILTINS)
+#define TERM_EXPECT(x, v) __builtin_expect((x), (v))
+#else
+#define TERM_EXPECT(x, v) (x)
+#endif
+
+/* term_ctz32(0) is 32 and term_ctz64(0) is 64 on every path. */
+#if defined(TERM_HAVE_BUILTINS)
+
+static inline int term_ctz32(uint32_t x) { return x ? __builtin_ctz(x) : 32; }
+static inline int term_popcount32(uint32_t x) { return __builtin_popcount(x); }
+static inline int term_ctz64(uint64_t x) { return x ? __builtin_ctzll(x) : 64; }
+static inline int term_popcount64(uint64_t x) { return __builtin_popcountll(x); }
+
+#else
+
+#if defined(TERM_HAVE_MSVC_BITOPS)
+static inline int term_ctz32(uint32_t x)
+{
+ unsigned long i;
+ return _BitScanForward(&i, x) ? (int)i : 32;
+}
+static inline int term_popcount32(uint32_t x) { return (int)__popcnt(x); }
+#else
+static inline int term_ctz32(uint32_t x)
+{
+ if (!x)
+ return 32;
+ int n = 0;
+ while (!(x & 1u)) {
+ x >>= 1;
+ n++;
+ }
+ return n;
+}
+static inline int term_popcount32(uint32_t x)
+{
+ int n = 0;
+ for (; x; x &= x - 1)
+ n++;
+ return n;
+}
+#endif
+
+/* Without builtins, the 64-bit forms are built from the 32-bit ones. */
+static inline int term_ctz64(uint64_t x)
+{
+ uint32_t lo = (uint32_t)x;
+ return lo ? term_ctz32(lo) : 32 + term_ctz32((uint32_t)(x >> 32));
+}
+
+static inline int term_popcount64(uint64_t x)
+{
+ return term_popcount32((uint32_t)x) + term_popcount32((uint32_t)(x >> 32));
+}
+
+#endif
+
+/* A spin-wait hint. Compilers without one get a no-op. */
+static inline void term_cpu_relax(void)
+{
+#if defined(TERM_COMPILER_GNU)
+#if defined(TERM_HAVE_BUILTINS) && defined(__x86_64__)
+ __builtin_ia32_pause();
+#elif defined(__aarch64__)
+ __asm__ __volatile__("yield");
+#endif
+#elif defined(TERM_COMPILER_MSVC)
+#if defined(_M_X64) || defined(_M_IX86)
+ _mm_pause();
+#elif defined(_M_ARM64)
+ __yield();
+#endif
+#endif
+}
+
+/* strdup is POSIX, so the core copies strings itself. */
+static inline char *term_strdup(const char *s)
+{
+ size_t n = strlen(s) + 1;
+ char *copy = malloc(n);
+ if (copy)
+ memcpy(copy, s, n);
+ return copy;
+}
+
+/* ASCII-only prefix test, as strncasecmp() does in the C locale. */
+static inline bool term_has_prefix_ci(const char *s, const char *prefix)
+{
+ for (; *prefix; prefix++, s++) {
+ unsigned char a = (unsigned char)*s;
+ unsigned char b = (unsigned char)*prefix;
+ if (a >= 'A' && a <= 'Z')
+ a += 'a' - 'A';
+ if (b >= 'A' && b <= 'Z')
+ b += 'a' - 'A';
+ if (a != b)
+ return false;
+ }
+ return true;
+}
+
+/* Allocates size bytes that start on an align-byte boundary. align must be a
+ * power of two. The raw pointer is stored just before the block, so
+ * term_aligned_free() can release it. C11 aligned_alloc() and POSIX
+ * posix_memalign() are not on every toolchain. */
+static inline void *term_aligned_alloc(size_t align, size_t size)
+{
+ if (size > SIZE_MAX - align - sizeof(void *))
+ return NULL;
+ void *raw = malloc(size + align + sizeof(void *));
+ if (!raw)
+ return NULL;
+ uintptr_t p = ((uintptr_t)raw + sizeof(void *) + align - 1) & ~((uintptr_t)align - 1);
+ ((void **)p)[-1] = raw;
+ return (void *)p;
+}
+
+static inline void term_aligned_free(void *p)
+{
+ if (p)
+ free(((void **)p)[-1]);
+}
+
+#endif
diff --git a/src/core.c b/src/core.c
new file mode 100644
index 0000000..5d50db9
--- /dev/null
+++ b/src/core.c
@@ -0,0 +1,1311 @@
+#include "compat.h"
+#include "core.h"
+#include "kernels.h"
+#include "pen.h"
+#include "screen.h"
+#include "text_run.h"
+#include "unicode/grapheme.h"
+#include "unicode/unicode.h"
+#include "unicode/utf8.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static uint16_t cluster_presentation(const uint32_t *cps, int len)
+{
+ bool pictographic = unicode_extended_pictographic(cps[0]);
+ bool emoji = false;
+ for (int i = 1; i < len; i++) {
+ if (cps[i] == 0xFE0F)
+ return CELL_EMOJI;
+ if (cps[i] == 0xFE0E)
+ return CELL_TEXT;
+ if (cps[i] == 0x20E3 || grapheme_regional_indicator(cps[i]) ||
+ (pictographic && (cps[i] == 0x200D || grapheme_emoji_modifier(cps[i]) ||
+ (cps[i] >= 0xE0020 && cps[i] <= 0xE007F))))
+ emoji = true;
+ }
+ return emoji ? CELL_EMOJI : 0;
+}
+
+void term_sc_print_glyph(struct term *t, uint32_t cp, int width, uint16_t flags)
+{
+ uint32_t base = cp;
+ if (TERM_EXPECT(grapheme_is_handle(cp), 0)) {
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int len = term_gstore_expand(t->gstore, cp, cps, GRAPHEME_MAX_LEN);
+ base = cps[0];
+ flags = (flags & ~(CELL_EMOJI | CELL_TEXT)) | cluster_presentation(cps, len);
+ }
+ if (TERM_EXPECT(width <= 0, 0))
+ width = term_text_class(base);
+ if (width == 2 || (flags & CELL_EMOJI))
+ width = 2;
+ else
+ width = 1;
+ if (TERM_EXPECT(flags & CELL_TEXT, 0))
+ width = 1;
+
+ if (TERM_EXPECT(width == 1 && !t->pending_wrap && !t->insert_mode &&
+ !t->lr_margin_mode && t->cursor_x < t->cols, 1)) {
+ struct srow *srow = term_row(t, t->cursor_y);
+ int cx = t->cursor_x;
+ int len = srow->len;
+ if (TERM_EXPECT(srow->kind == ROW_CELLS && cx <= len, 1)) {
+ struct cell *row = srow->cells;
+ if (TERM_EXPECT(!((cx < len && (row[cx].flags & CELL_TRAIL)) ||
+ (cx + 1 < len && (row[cx + 1].flags & CELL_TRAIL))), 1)) {
+ row[cx] = (struct cell){cp,
+ t->inverse ? t->cur_bg : t->cur_fg,
+ t->inverse ? t->cur_fg : t->cur_bg,
+ flags, t->cur_link};
+ t->last_char = cp;
+ cx++;
+ if (cx > len) srow->len = (uint16_t)cx;
+ srow->version++;
+ if (cx < t->cols) {
+ t->cursor_x = cx;
+ t->pending_wrap = false;
+ } else {
+ t->cursor_x = t->cols - 1;
+ t->pending_wrap = t->autowrap;
+ }
+ return;
+ }
+ }
+ }
+
+ if (TERM_EXPECT(t->pending_wrap, 0)) {
+ row_set_wrapped(term_row(t, t->cursor_y), true);
+ t->cursor_x = t->lr_margin_mode ? t->left_margin : 0;
+ sc_linefeed(t, 1);
+ t->pending_wrap = false;
+ }
+
+ int p = phys_row(t, t->cursor_y);
+ struct cell *row = row_cells(t, p);
+ struct srow *srow = &t->scr.slot[p];
+ int cx = t->cursor_x;
+ int cols = t->cols;
+ int right = t->lr_margin_mode ? t->right_margin : cols - 1;
+
+ if (TERM_EXPECT(width == 2 && cols > 1 && cx == right && t->autowrap, 0)) {
+ row_fill_gap(t, p, row, cx);
+ clear_wide_edges(row, cx, right + 1, srow->len);
+ row[cx] = (struct cell){ ' ', TERM_COLOR_FG, TERM_COLOR_BG, CELL_WRAP_PAD, 0 };
+ srow->len = (uint16_t)cols;
+ srow->version++;
+ row_set_wrapped(srow, true);
+ t->cursor_x = t->lr_margin_mode ? t->left_margin : 0;
+ sc_linefeed(t, 1);
+ t->pending_wrap = false;
+ p = phys_row(t, t->cursor_y);
+ row = row_cells(t, p);
+ srow = &t->scr.slot[p];
+ cx = t->cursor_x;
+ }
+
+ int span = width > right + 1 - cx ? 1 : width;
+ if (span == 2) {
+ flags |= CELL_WIDE;
+ srow->flags |= RF_WIDE;
+ } else {
+ flags &= ~CELL_WIDE;
+ }
+ row_fill_gap(t, p, row, cx);
+ if (TERM_EXPECT(t->insert_mode, 0)) {
+ if (cx == srow->len)
+ row[cx] = (struct cell){ ' ', TERM_COLOR_FG, TERM_COLOR_BG, 0, 0 };
+ int move = right + 1 - cx - span;
+ if (move > 0)
+ memmove(&row[cx + span], &row[cx], (size_t)move * sizeof(struct cell));
+ int new_len = srow->len + span;
+ srow->len = (uint16_t)(new_len > cols ? cols : new_len);
+ repair_wide_row(row, right, right + 1, cols, srow->len);
+ }
+ clear_wide_edges(row, cx, cx + span, srow->len);
+
+ uint32_t fg = t->inverse ? t->cur_bg : t->cur_fg;
+ uint32_t bg = t->inverse ? t->cur_fg : t->cur_bg;
+ write_cell_span(row, cx, (struct cell){ cp, fg, bg, flags, t->cur_link }, span);
+ if (cx + span > srow->len)
+ srow->len = (uint16_t)(cx + span);
+ srow->version++;
+ t->last_char = cp;
+
+ if (cx + span <= right) {
+ t->cursor_x = cx + span;
+ t->pending_wrap = false;
+ } else {
+ t->cursor_x = right;
+ t->pending_wrap = t->autowrap;
+ }
+}
+
+static inline struct cell *last_written_cell(struct term *t)
+{
+ int right = (t->lr_margin_mode && t->cursor_x <= t->right_margin)
+ ? t->right_margin : t->cols - 1;
+ int x = t->pending_wrap ? right : t->cursor_x - 1;
+ if (t->lr_margin_mode && x < t->left_margin)
+ return NULL;
+
+ struct srow *srow = term_row(t, t->cursor_y);
+ if (x < 0 || x >= srow->len)
+ return NULL;
+ int p = phys_row(t, t->cursor_y);
+ struct cell *row = row_cells(t, p);
+ if (x > 0 && (row[x].flags & CELL_TRAIL))
+ x--;
+ return &row[x];
+}
+
+bool term_sc_extend_cluster(struct term *t, uint32_t cp, int klass)
+{
+ if (!can_extend_cluster(t->last_char, cp, klass))
+ return false;
+
+ struct cell *prev = last_written_cell(t);
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int len = prev && prev->c > ' ' ? term_gstore_expand(t->gstore, prev->c, cps, GRAPHEME_MAX_LEN) : 0;
+ if (!len)
+ return joins_cluster(cp, klass);
+ if (!cluster_joins(cps, len, cp, klass))
+ return false;
+ if (len >= GRAPHEME_MAX_LEN)
+ return true;
+ cps[len++] = cp;
+
+ struct cell cell = *prev;
+ int p = phys_row(t, t->cursor_y);
+ struct cell *row = row_cells(t, p);
+ int x = (int)(prev - row);
+ int span = (cell.flags & CELL_WIDE) && x + 1 < t->cols ? 2 : 1;
+ struct srow *srow = &t->scr.slot[p];
+ if (srow->len == x + span) {
+ fill_cells(prev, span, TERM_COLOR_FG, TERM_COLOR_BG);
+ srow->len = (uint16_t)x;
+ } else {
+ fill_cells(prev, span, cell.fg, cell.bg);
+ }
+ t->cursor_x = x;
+ t->pending_wrap = false;
+ t->last_char = term_gstore_intern(t->gstore, cps, len);
+ term_sc_print_glyph(t, t->last_char, -1,
+ cell.flags & ~(CELL_WIDE | CELL_EMOJI | CELL_TEXT));
+
+ prev = last_written_cell(t);
+ if (prev) {
+ row = row_cells(t, phys_row(t, t->cursor_y));
+ prev->fg = cell.fg;
+ prev->bg = cell.bg;
+ if ((prev->flags & CELL_WIDE) && prev < row + t->cols - 1) {
+ prev[1].fg = cell.fg;
+ prev[1].bg = cell.bg;
+ }
+ }
+ return true;
+}
+
+/* n blank default rows scroll off the top, after the screen rows. */
+static void push_blank_lines(struct term *t, size_t n)
+{
+ if (screen_has_history(t)) {
+ term_hist_push_blank(&t->hist, n);
+ } else {
+ t->scr.top_id += n;
+ t->scr.top_seg = 0;
+ }
+}
+
+static void scroll_up_bce(struct term *t, size_t count)
+{
+ size_t rows = (size_t)t->rows;
+ if (t->alternate || count <= rows) {
+ for (size_t i = count < rows ? count : rows; i > 0; i--)
+ term_screen_scroll_up(t);
+ return;
+ }
+ size_t rem = count - rows;
+ if (!using_default_colors(t)) {
+ /* Rows past the history limit would leave again at once. */
+ size_t keep = screen_has_history(t) ? t->hist.line_limit : 0;
+ for (size_t i = rows + (rem < keep ? rem : keep); i > 0; i--)
+ term_screen_scroll_up(t);
+ return;
+ }
+ bool skip = screen_has_history(t) && rem >= t->hist.line_limit;
+ if (!skip && !hist_is_open(&t->hist)) {
+ skip = true;
+ for (size_t y = 0; y < rows && skip; y++)
+ skip = t->scr.slot[y].len == 0 && !(t->scr.slot[y].flags & RF_WRAPPED);
+ }
+ if (skip) {
+ /* The screen is blank, or all of it leaves history again. */
+ for (size_t y = 0; y < rows; y++)
+ screen_new_row(t, &t->scr.slot[y]);
+ push_blank_lines(t, count);
+ } else {
+ for (size_t i = 0; i < rows; i++)
+ term_screen_scroll_up(t);
+ push_blank_lines(t, rem);
+ }
+ if (t->prompt_y > 0)
+ t->prompt_y = 0;
+ clamp_selection_after_scroll(t);
+ t->full_gen++;
+}
+
+void term_sc_linefeed_slow(struct term *t, size_t n)
+{
+ if (n == 0)
+ return;
+ t->pending_wrap = false;
+ term_row(t, t->cursor_y)->version++;
+
+ if (t->cursor_y > t->scroll_bottom) {
+ if (t->cursor_y + (int)n < t->rows)
+ t->cursor_y += (int)n;
+ else
+ t->cursor_y = t->rows - 1;
+ return;
+ }
+
+ if (t->cursor_y + (int)n <= t->scroll_bottom) {
+ t->cursor_y += (int)n;
+ return;
+ }
+
+ size_t scroll = (size_t)(t->cursor_y + (int)n - t->scroll_bottom);
+ t->cursor_y = t->scroll_bottom;
+
+ if (TERM_EXPECT(t->scroll_top == 0 && t->scroll_bottom == t->rows - 1, 1)) {
+ if (TERM_EXPECT(scroll == 1, 1))
+ term_screen_scroll_up(t);
+ else
+ scroll_up_bce(t, scroll);
+ } else if (t->scroll_top > 0 || t->alternate) {
+ int height = t->scroll_bottom - t->scroll_top + 1;
+ if (height <= 0) height = 1;
+ int count = scroll > (size_t)height ? height : (int)scroll;
+ term_screen_scroll_region(t, t->scroll_top, t->scroll_bottom, count, false, false);
+ } else {
+ int height = t->scroll_bottom - t->scroll_top + 1;
+ if (height <= 0) height = 1;
+ if (scroll <= (size_t)height) {
+ term_screen_scroll_region(t, t->scroll_top, t->scroll_bottom, (int)scroll, false, true);
+ } else {
+ term_screen_scroll_region(t, t->scroll_top, t->scroll_bottom, height, false, true);
+ size_t rem = scroll - (size_t)height;
+ if (using_default_colors(t)) {
+ push_blank_lines(t, rem);
+ clamp_selection_after_scroll(t);
+ } else {
+ size_t cap = t->hist.line_limit > 0 ? t->hist.line_limit : 1;
+ if (rem > cap)
+ rem = cap;
+ while (rem > 0) {
+ int chunk = rem > (size_t)height ? height : (int)rem;
+ term_screen_scroll_region(t, t->scroll_top, t->scroll_bottom, chunk, false, true);
+ rem -= (size_t)chunk;
+ }
+ }
+ }
+ }
+}
+
+void term_sc_tab(struct term *t, int n)
+{
+ int dir = n >= 0 ? 1 : -1;
+ int count = n >= 0 ? n : -n;
+ if (count > t->cols) count = t->cols;
+ while (count-- > 0) {
+ int x = t->cursor_x + dir;
+ while (x > 0 && x < t->cols - 1 && !t->tabs[x])
+ x += dir;
+ t->cursor_x = x < 0 ? 0 : x >= t->cols ? t->cols - 1 : x;
+ }
+ t->pending_wrap = false;
+ struct srow *cur = term_row(t, t->cursor_y);
+ if (cur) cur->version++;
+}
+
+void term_sc_scroll_region(struct term *t, int top, int bottom, int n, bool down)
+{
+ t->pending_wrap = false;
+ term_screen_scroll_region(t, top, bottom, n, down, false);
+}
+
+void term_sc_scroll(struct term *t, int n, bool down)
+{
+ t->pending_wrap = false;
+ term_screen_scroll_region(t, t->scroll_top, t->scroll_bottom, n, down, true);
+}
+
+#define CELL_SGR_FLAGS (CELL_BOLD | CELL_FAINT | CELL_ITALIC | \
+ CELL_STRIKE | CELL_OVERLINE | CELL_ANY_UNDERLINE)
+
+static inline void sgr_cell_reset(struct cell *cell)
+{
+ cell->fg = TERM_COLOR_FG;
+ cell->bg = TERM_COLOR_BG;
+ cell->flags &= (uint16_t)~CELL_SGR_FLAGS;
+}
+
+static inline void sgr_cell_underline(struct cell *cell, int style)
+{
+ if (style < 0 || style > 5) style = 1;
+ cell->flags = (uint16_t)((cell->flags & ~CELL_ANY_UNDERLINE) |
+ term_ul_flags[style]);
+}
+
+static void apply_sgr_to_cell(struct cell *cell, const struct csi *csi, int first)
+{
+ if (first >= csi->nparams) {
+ sgr_cell_reset(cell);
+ return;
+ }
+ for (int i = first; i < csi->nparams; i++) {
+ int v = csi->params[i];
+ uint32_t color;
+ bool ok;
+ switch (v) {
+ case 0: sgr_cell_reset(cell); break;
+ case 1: cell->flags |= CELL_BOLD; break;
+ case 2: cell->flags |= CELL_FAINT; break;
+ case 3: cell->flags |= CELL_ITALIC; break;
+ case 4: {
+ int style = 1;
+ if (i + 1 < csi->nparams && (csi->subparams & (1u << (i + 1))))
+ style = csi->params[++i];
+ sgr_cell_underline(cell, style);
+ break;
+ }
+ case 7: { uint32_t tmp = cell->fg; cell->fg = cell->bg; cell->bg = tmp; break; }
+ case 9: cell->flags |= CELL_STRIKE; break;
+ case 21: sgr_cell_underline(cell, 2); break;
+ case 22: cell->flags &= (uint16_t)~(CELL_BOLD | CELL_FAINT); break;
+ case 23: cell->flags &= (uint16_t)~CELL_ITALIC; break;
+ case 24: sgr_cell_underline(cell, 0); break;
+ case 29: cell->flags &= (uint16_t)~CELL_STRIKE; break;
+ case 38:
+ i += sgr_extended_color(csi->params, csi->nparams, csi->subparams, i, &color, &ok);
+ if (ok) cell->fg = color;
+ break;
+ case 39: cell->fg = TERM_COLOR_FG; break;
+ case 48:
+ i += sgr_extended_color(csi->params, csi->nparams, csi->subparams, i, &color, &ok);
+ if (ok) cell->bg = color;
+ break;
+ case 49: cell->bg = TERM_COLOR_BG; break;
+ case 53: cell->flags |= CELL_OVERLINE; break;
+ case 55: cell->flags &= (uint16_t)~CELL_OVERLINE; break;
+ case 58:
+ i += sgr_extended_color(csi->params, csi->nparams, csi->subparams, i, &color, &ok);
+ break;
+ default:
+ if ((unsigned)(v - 30) < 8) cell->fg = term_color_index(v - 30);
+ else if ((unsigned)(v - 40) < 8) cell->bg = term_color_index(v - 40);
+ else if ((unsigned)(v - 90) < 8) cell->fg = term_color_index(v - 90 + 8);
+ else if ((unsigned)(v - 100) < 8) cell->bg = term_color_index(v - 100 + 8);
+ break;
+ }
+ }
+}
+
+static void deccara_row(struct term *t, int y, int x0, int x1, const struct csi *csi)
+{
+ int p = phys_row(t, y);
+ struct cell *row = row_cells(t, p);
+ struct srow *srow = term_row(t, y);
+ int len = srow->len;
+ if (x0 > 0 && x0 < len && (row[x0].flags & CELL_TRAIL)) x0--;
+ if (x1 + 1 < t->cols && x1 < len && (row[x1].flags & CELL_WIDE)) x1++;
+ term_row_fill_gap(srow, x1 + 1);
+ for (int x = x0; x <= x1; x++)
+ apply_sgr_to_cell(&row[x], csi, 4);
+ srow->version++;
+}
+
+void term_sc_deccara(struct term *t, const struct csi *csi)
+{
+ int top = csi->params[0] > 0 ? csi->params[0] - 1 : 0;
+ int left = csi->nparams > 1 && csi->params[1] > 0 ? csi->params[1] - 1 : 0;
+ int bottom = csi->nparams > 2 && csi->params[2] > 0 ? csi->params[2] - 1 : t->rows - 1;
+ int right = csi->nparams > 3 && csi->params[3] > 0 ? csi->params[3] - 1 : t->cols - 1;
+ if (top > bottom || left > right || top >= t->rows || left >= t->cols) return;
+ if (bottom >= t->rows) bottom = t->rows - 1;
+ if (right >= t->cols) right = t->cols - 1;
+ if (t->decsace == 2) {
+ for (int y = top; y <= bottom; y++)
+ deccara_row(t, y, left, right, csi);
+ } else {
+ deccara_row(t, top, left, top == bottom ? right : t->cols - 1, csi);
+ for (int y = top + 1; y < bottom; y++)
+ deccara_row(t, y, 0, t->cols - 1, csi);
+ if (bottom > top)
+ deccara_row(t, bottom, 0, right, csi);
+ }
+}
+
+void term_sc_decaln(struct term *t)
+{
+ struct cell fill = { .c = 'E', .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG };
+ for (int y = 0; y < t->rows; y++) {
+ int p = phys_row(t, y);
+ struct cell *row = row_cells(t, p);
+ for (int x = 0; x < t->cols; x++) row[x] = fill;
+ struct srow *srow = &t->scr.slot[p];
+ srow->len = (uint16_t)t->cols;
+ srow->flags = RF_TAIL_PADDED;
+ srow->kind = ROW_CELLS;
+ srow->version++;
+ }
+ sc_move(t, 0, 0);
+}
+
+void term_sc_reset(struct term *t)
+{
+ t->cursor_x = t->cursor_y = 0;
+ t->saved = term_saved_cursor_initial();
+ t->prompt_y = -1;
+ t->has_prompt = false;
+ t->graphics = t->pending_wrap = false;
+ t->last_char = 0;
+ t->charsets[0] = t->charsets[1] = 'B';
+ t->charset_active = 0;
+ term_tabs_clear(t);
+ term_tabs_default(t);
+ term_screen_erase_all(t);
+ t->full_gen++;
+}
+
+void term_sc_set_reverse_video(struct term *t, bool enabled)
+{
+ if (t->reverse_video != enabled) {
+ t->reverse_video = enabled;
+ t->full_gen++;
+ }
+}
+
+void term_sc_set_autowrap(struct term *t, bool enabled)
+{
+ t->autowrap = enabled;
+ t->pending_wrap = false;
+}
+
+void term_sc_set_cursor_style(struct term *t, int ps)
+{
+ if (ps == 0) {
+ t->cursor_style = t->default_cursor_style;
+ t->cursor_blink = t->default_cursor_blink;
+ } else if (ps >= 1 && ps <= 6) {
+ t->cursor_style = ps < 3 ? TERM_CURSOR_BLOCK
+ : ps < 5 ? TERM_CURSOR_UNDERLINE : TERM_CURSOR_BAR;
+ t->cursor_blink = (ps & 1);
+ }
+ term_row(t, t->cursor_y)->version++;
+}
+
+static inline int newline_bytes(const char *buf, size_t len)
+{
+ if (len >= 2 && buf[0] == '\r' && buf[1] == '\n') return 2;
+ if (len >= 1 && buf[0] == '\n') return 1;
+ return 0;
+}
+
+static inline bool next_byte_wraps(const char *buf, size_t len, size_t pos, uint32_t last)
+{
+ if (pos >= len) return false;
+ uint32_t cp;
+ if (!utf8_decode_one(buf + pos, len - pos, &cp)) return false;
+ int w = term_text_class(cp);
+ return w > 0 && !can_extend_cluster(last, cp, w);
+}
+
+static void flush_orig_rows(struct term *t, struct screen *s,
+ int *orig_y, int *orig_rem, int count)
+{
+ int y = *orig_y;
+ int end = y + count;
+ *orig_y = end;
+ *orig_rem -= count;
+ struct hist_ascii_line q[64];
+ while (y < end) {
+ int p = s->top + y;
+ struct srow *first = &s->slot[p >= s->rows ? p - s->rows : p];
+ uintptr_t begin = (uintptr_t)t->ascii_tail.data;
+ uintptr_t limit = begin + t->ascii_tail.cap;
+ uintptr_t base = (uintptr_t)first->text.text;
+ if (first->kind == ROW_ASCII && base >= begin && base < limit) {
+ /* The LF stream stores checked lines here, including each LF.
+ * A read or edit changes the row kind before it can change text. */
+ uint16_t ends[64 + 65];
+ ends[0] = 0;
+ int span = 0, lines = 0, closed = y;
+ for (int j = y; j < end && j - y < 64; j++) {
+ int p = s->top + j;
+ struct srow *row = &s->slot[p >= s->rows ? p - s->rows : p];
+ bool wrapped = (row->flags & RF_WRAPPED) != 0;
+ size_t size = (size_t)row->len + !wrapped;
+ if (row->kind != ROW_ASCII || (uintptr_t)row->text.text != base + span ||
+ size > limit - base - span || size + span > UINT16_MAX) break;
+ span += row->len;
+ if (!wrapped) {
+ ends[++lines] = (uint16_t)++span;
+ closed = j + 1;
+ }
+ }
+ if (lines) {
+ term_hist_push_ascii_lf(&t->hist, (const char *)base, ends, lines);
+ if (t->view.anchor.active) term_hist_view_note_push(&t->view, &t->hist, t->cols);
+ y = closed;
+ continue;
+ }
+ }
+ int n = 0;
+ while (y + n < end && n < 64) {
+ int p = s->top + y + n;
+ struct srow *row = &s->slot[p >= s->rows ? p - s->rows : p];
+ struct text_row *tr = &row->text;
+ bool wrapped = (row->flags & RF_WRAPPED) != 0;
+ if (wrapped && row->len < s->cols) break;
+ if (row->kind == ROW_TEXT) {
+ if (!(tr->flags & HLINE_NARROW) || tr->runs.len || tr->fill_to > tr->cells ||
+ !pen_attr_is_default(&tr->start) || !pen_attr_is_default(&tr->runs.pen)) break;
+ } else if (row->kind != ROW_ASCII && row->len) break;
+ q[n++] = (struct hist_ascii_line){
+ .ptr = (const char *)tr->text, .len = row->len, .wrapped = wrapped,
+ };
+ }
+ if (n) {
+ term_hist_push_ascii_lines(&t->hist, q, n, NULL);
+ if (t->view.anchor.active) term_hist_view_note_push(&t->view, &t->hist, t->cols);
+ y += n;
+ } else term_screen_push_row(t, screen_get_row(s, y++));
+ }
+}
+
+static void flush_stream_queue(struct term *t, struct screen *s,
+ struct hist_ascii_line *q, int *q_count, int q_cap,
+ int *orig_y, int *orig_rem, size_t *flushed_scrolled,
+ const struct pen_attr *attr, bool def_pen)
+{
+ if (*orig_rem > 0)
+ flush_orig_rows(t, s, orig_y, orig_rem, *orig_rem);
+ int flush_n = *q_count - q_cap;
+ (void)def_pen;
+ term_hist_push_ascii_lines(&t->hist, q, flush_n, attr);
+ if (q_cap > 0) {
+ if (flush_n >= q_cap)
+ memcpy(q, q + flush_n, (size_t)q_cap * sizeof(q[0]));
+ else
+ memmove(q, q + flush_n, (size_t)q_cap * sizeof(q[0]));
+ }
+ *q_count = q_cap;
+ *flushed_scrolled += (size_t)flush_n;
+}
+
+static inline void emit_stream_bulk_wrapped(struct term *t, struct screen *s,
+ const char *src, int bulk_lines, int cols,
+ const struct pen_attr *attr, bool def_pen,
+ struct hist_ascii_line *q, int q_cap,
+ int *q_count, int *orig_y, int *orig_rem,
+ size_t *flushed_scrolled)
+{
+ if (TERM_EXPECT(t->pending_wrap, 0)) {
+ row_set_wrapped(screen_get_row(s, s->rows - 1), true);
+ t->pending_wrap = false;
+ t->cursor_x = 0;
+ term_screen_scroll_up(t);
+ }
+ if (bulk_lines < q_cap) {
+ const char *p = src;
+ for (int r = 0; r < bulk_lines; r++, p += cols) {
+ if (*q_count == 256) {
+ flush_stream_queue(t, s, q, q_count, q_cap, orig_y, orig_rem,
+ flushed_scrolled, attr, def_pen);
+ }
+ q[(*q_count)++] = (struct hist_ascii_line){ p, (uint16_t)cols, 1 };
+ }
+ return;
+ }
+
+ if (*orig_rem > 0)
+ flush_orig_rows(t, s, orig_y, orig_rem, *orig_rem);
+ if (*q_count > 0) {
+ term_hist_push_ascii_lines(&t->hist, q, *q_count, attr);
+ *flushed_scrolled += (size_t)*q_count;
+ *q_count = 0;
+ }
+ int hist_rows = bulk_lines - q_cap;
+ if (hist_rows > 0) {
+ term_hist_push_ascii(&t->hist, src, hist_rows * cols, attr, true);
+ *flushed_scrolled += (size_t)hist_rows;
+ }
+ const char *sp = src + (size_t)hist_rows * (size_t)cols;
+ for (int r = 0; r < q_cap; r++, sp += cols)
+ q[r] = (struct hist_ascii_line){ sp, (uint16_t)cols, 1 };
+ *q_count = q_cap;
+}
+
+static TERM_NOINLINE size_t stream_ascii_wrapped(struct term *t, const char *buf, size_t len)
+{
+ int limit = len > INT32_MAX ? INT32_MAX : (int)len;
+ int run = scan_ascii_run(buf, limit);
+ if (!run) return 0;
+ int cols = t->cols;
+ int count = (run - 1) / cols;
+ if (!count) return 0;
+ struct screen *s = &t->scr;
+ int rows = s->rows, keep = rows - 1;
+ int scrolls = count + t->pending_wrap;
+ int old_rows = keep + t->pending_wrap;
+ if (t->pending_wrap) {
+ row_set_wrapped(screen_get_row(s, rows - 1), true);
+ t->pending_wrap = false;
+ t->cursor_x = 0;
+ }
+ int old_count = scrolls < old_rows ? scrolls : old_rows;
+ int orig_y = 0, orig_rem = old_rows;
+ flush_orig_rows(t, s, &orig_y, &orig_rem, old_count);
+ int extra = count - (keep - orig_rem);
+ if (extra) term_hist_push_ascii(&t->hist, buf, extra * cols, NULL, true);
+ s->top = (s->top + scrolls % rows) % rows;
+ const char *src = buf + extra * cols;
+ uint8_t *copy_dst = NULL;
+ const char *copy_src = src;
+ size_t copy_len = 0;
+ uint64_t uid = t->next_row_uid;
+ for (int y = orig_rem; y < rows - 1; y++, src += cols) {
+ int p = s->top + y;
+ struct srow *dst = &s->slot[p >= rows ? p - rows : p];
+ dst->uid = ++uid;
+ dst->version++;
+ dst->kind = ROW_ASCII;
+ dst->len = (uint16_t)cols;
+ dst->flags = RF_WRAPPED;
+ struct text_row *tr = &dst->text;
+ tr->text = dst->ascii;
+ if (copy_len && tr->text != copy_dst + copy_len) {
+ memcpy(copy_dst, copy_src, copy_len);
+ copy_len = 0;
+ }
+ if (!copy_len) {
+ copy_dst = tr->text;
+ copy_src = src;
+ }
+ copy_len += (size_t)cols;
+ }
+ if (copy_len) memcpy(copy_dst, copy_src, copy_len);
+ t->next_row_uid = uid;
+ screen_new_row(t, screen_get_row(s, rows - 1));
+ if (t->prompt_y > 0)
+ t->prompt_y = t->prompt_y > scrolls ? t->prompt_y - scrolls : 0;
+ clamp_selection_after_scroll(t);
+ t->last_char = (unsigned char)buf[count * cols - 1];
+ t->cursor_x = 0;
+ t->pending_wrap = false;
+ t->full_gen++;
+ return (size_t)count * cols;
+}
+
+#if defined(TERM_KERNEL_AVX512)
+/* Cut *count at the first line longer than half a history page. Check
+ * whole blocks of 32 lines. Return the line where the check stopped. */
+TERM_TARGET_AVX512
+static int long_lines_avx512(const uint16_t *ends, int *count)
+{
+ int j = 0;
+ for (; j + 32 <= *count; j += 32) {
+ __m512i start = _mm512_loadu_si512(ends + j);
+ __m512i end = _mm512_loadu_si512(ends + j + 1);
+ uint32_t bad = _mm512_cmpgt_epu16_mask(_mm512_sub_epi16(end, start),
+ _mm512_set1_epi16(HIST_PAGE_USABLE / 2));
+ if (bad) {
+ *count = j + term_ctz32(bad);
+ break;
+ }
+ }
+ return j;
+}
+#endif
+
+static TERM_NOINLINE size_t stream_ascii_lf(struct term *t, const char *buf, size_t len)
+{
+ int limit = len < UINT16_MAX ? (int)len : UINT16_MAX;
+ size_t need = ((size_t)limit + 65) * sizeof(uint16_t);
+ if (need > t->ascii_ends.cap) {
+ void *data = realloc(t->ascii_ends.data, need);
+ if (!data) return 0;
+ t->ascii_ends.data = data;
+ t->ascii_ends.cap = need;
+ }
+ uint16_t *ends = t->ascii_ends.data;
+ int count = scan_ascii_lf(buf, limit, ends);
+ int j = 0;
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ j = long_lines_avx512(ends, &count);
+#endif
+ for (; j < count; j++) {
+ if (ends[j + 1] - ends[j] > HIST_PAGE_USABLE / 2) {
+ count = j;
+ break;
+ }
+ }
+ struct screen *s = &t->scr;
+ int cols = t->cols, rows = t->rows;
+ int keep = rows - 1, first = count;
+ int tail_rows = 0;
+ while (first > 0 && tail_rows < keep) {
+ first--;
+ tail_rows += (int)rows_narrow(ends[first + 1] - ends[first] - 1, cols);
+ }
+ if (!first) return 0;
+ int skip = (tail_rows - keep) * cols;
+ int source_start = ends[first] + skip;
+ size_t tail_bytes = (size_t)(ends[count] - source_start);
+ uint8_t *tail_data = t->ascii_tail.data;
+ if (tail_bytes > t->ascii_tail.cap) {
+ tail_data = malloc(tail_bytes);
+ if (!tail_data) return 0;
+ }
+ int orig_y = 0, orig_rem = rows - 1;
+ flush_orig_rows(t, s, &orig_y, &orig_rem, orig_rem);
+ if (tail_data != t->ascii_tail.data) {
+ free(t->ascii_tail.data);
+ t->ascii_tail.data = tail_data;
+ t->ascii_tail.cap = tail_bytes;
+ }
+ memcpy(tail_data, buf + source_start, tail_bytes);
+ term_hist_push_ascii_lf(&t->hist, buf, ends, first);
+ if (skip)
+ term_hist_push_ascii(&t->hist, buf + ends[first], skip, NULL, true);
+ uint32_t last = t->last_char;
+ for (int j = count; j > 0; j--) {
+ if (ends[j] - ends[j - 1] > 1) {
+ last = (unsigned char)buf[ends[j] - 2];
+ break;
+ }
+ }
+ s->top = 0;
+ int y = 0;
+ uint64_t uid = t->next_row_uid;
+ for (int j = first; j < count; j++) {
+ int start = ends[j] + (j == first ? skip : 0);
+ int n = ends[j + 1] - 1 - start;
+ do {
+ int k = n > cols ? cols : n;
+ struct srow *dst = &s->slot[y++];
+ dst->uid = ++uid;
+ dst->version++;
+ dst->flags = n > cols ? RF_WRAPPED : 0;
+ dst->len = (uint16_t)k;
+ dst->kind = ROW_ASCII;
+ dst->text.text = tail_data + start - source_start;
+ n -= k;
+ start += k;
+ } while (n > 0);
+ }
+ t->next_row_uid = uid;
+ screen_new_row(t, &s->slot[rows - 1]);
+ if (t->prompt_y > 0) t->prompt_y = 0;
+ clamp_selection_after_scroll(t);
+ t->last_char = last;
+ t->cursor_x = 0;
+ t->pending_wrap = false;
+ t->full_gen++;
+ return ends[count];
+}
+
+/* Bit i of the result is set when buf[i] is not printable ASCII. *nl gets
+ * the line feeds. Both cover 64 bytes. */
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline uint64_t line_masks_avx512(const char *buf, uint64_t *nl)
+{
+ __m512i v = _mm512_loadu_si512((const void *)buf);
+ *nl = _mm512_cmpeq_epi8_mask(v, _mm512_set1_epi8('\n'));
+ return _mm512_cmpge_epu8_mask(_mm512_sub_epi8(v, _mm512_set1_epi8(0x20)),
+ _mm512_set1_epi8(0x7f - 0x20));
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline uint64_t line_masks_avx2(const char *buf, uint64_t *nl)
+{
+ uint64_t lf = 0, controls = 0;
+ for (int b = 0; b < 64; b += 32) {
+ __m256i v = _mm256_loadu_si256((const __m256i *)(buf + b));
+ __m256i d = _mm256_sub_epi8(v, _mm256_set1_epi8(0x20));
+ __m256i printable = _mm256_cmpeq_epi8(
+ _mm256_min_epu8(d, _mm256_set1_epi8(0x7f - 0x21)), d);
+ lf |= (uint64_t)(uint32_t)_mm256_movemask_epi8(
+ _mm256_cmpeq_epi8(v, _mm256_set1_epi8('\n'))) << b;
+ controls |= (uint64_t)(uint32_t)~_mm256_movemask_epi8(printable) << b;
+ }
+ *nl = lf;
+ return controls;
+}
+#endif
+
+static inline uint64_t line_masks(const char *buf, uint64_t *nl)
+{
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return line_masks_avx2(buf, nl);
+#endif
+ uint64_t lf = 0, controls = 0;
+#if defined(__aarch64__)
+ for (int b = 0; b < 64; b += 16) {
+ uint8x16_t v = vld1q_u8((const uint8_t *)buf + b);
+ lf |= (uint64_t)term_lane_bits(vceqq_u8(v, vdupq_n_u8('\n'))) << b;
+ controls |= (uint64_t)term_lane_bits(vcgeq_u8(
+ vsubq_u8(v, vdupq_n_u8(0x20)), vdupq_n_u8(0x7f - 0x20))) << b;
+ }
+#else
+ for (int b = 0; b < 64; b++) {
+ unsigned char c = (unsigned char)buf[b];
+ if (c == '\n') lf |= UINT64_C(1) << b;
+ if (c < 0x20 || c >= 0x7f) controls |= UINT64_C(1) << b;
+ }
+#endif
+ *nl = lf;
+ return controls;
+}
+
+/* One body for every level. isa is a constant in each copy below, so each
+ * copy keeps only its own kernels. */
+static inline TERM_ALWAYS_INLINE size_t bulk_ascii_lines_body(struct term *t, const char *buf,
+ size_t len, int isa)
+{
+ (void)isa;
+ struct screen *s = &t->scr;
+ int rows = s->rows;
+ if (TERM_EXPECT(rows > 256, 0))
+ return 0;
+
+ size_t i = 0;
+ int cols = t->cols;
+ uint32_t flags = term_flags_pad(t);
+ bool autowrap = t->autowrap;
+ bool lnm = term_lnm(t);
+ int last_char = (int)t->last_char;
+ size_t flushed_scrolled = 0;
+
+ struct pen_attr attr = {
+ .fg = t->cur_fg,
+ .bg = t->cur_bg,
+ .flags = (uint16_t)flags,
+ .link = (uint16_t)(flags >> 16),
+ };
+ bool def_pen = (t->cur_fg == TERM_COLOR_FG && t->cur_bg == TERM_COLOR_BG && flags == 0);
+
+ if (t->pending_wrap &&
+ (len == 0 || (unsigned char)buf[0] < 0x20 || (unsigned char)buf[0] >= 0x7f))
+ return 0;
+
+ struct hist_ascii_line q[256];
+ int q_cap = rows - 1;
+ int q_count = 0;
+ int orig_y = 0;
+ int orig_rem = q_cap;
+
+#define EMIT_STREAM_ROW_FAST(row_ptr, row_len, row_wrapped) do { \
+ if (TERM_EXPECT(q_count == 256, 0)) \
+ flush_stream_queue(t, s, q, &q_count, q_cap, &orig_y, &orig_rem, \
+ &flushed_scrolled, &attr, def_pen); \
+ q[q_count++] = (struct hist_ascii_line){ \
+ (row_ptr), (uint16_t)(row_len), (uint8_t)(row_wrapped) \
+ }; \
+} while (0)
+
+#define EMIT_STREAM_ROW(row_ptr, row_len, row_wrapped) do { \
+ if (TERM_EXPECT(t->pending_wrap, 0)) { \
+ row_set_wrapped(screen_get_row(s, rows - 1), true); \
+ t->pending_wrap = false; \
+ t->cursor_x = 0; \
+ term_screen_scroll_up(t); \
+ } \
+ EMIT_STREAM_ROW_FAST(row_ptr, row_len, row_wrapped); \
+} while (0)
+
+ while (i < len) {
+ /* Scan each block once. Keep the unfinished line in the input buffer. */
+ if (lnm && !t->pending_wrap) {
+ size_t scan = i;
+ while (scan + 64 <= len) {
+ uint64_t nl, controls;
+#if defined(TERM_KERNEL_AVX512)
+ if (isa == TERM_ISA_AVX512)
+ controls = line_masks_avx512(buf + scan, &nl);
+ else
+#endif
+ controls = line_masks(buf + scan, &nl);
+ if (controls != nl)
+ break;
+ while (nl) {
+ size_t end = scan + (size_t)term_ctz64(nl);
+ if (!autowrap && end - i > (size_t)cols)
+ goto narrow_done;
+ if (end > i)
+ last_char = (unsigned char)buf[end - 1];
+ EMIT_STREAM_ROW_FAST(buf + i, (int)(end - i), false);
+ i = end + 1;
+ nl &= nl - 1;
+ }
+ scan += 64;
+ if (!autowrap && scan - i > (size_t)cols)
+ break;
+ if (scan - i > HIST_PAGE_USABLE / 2)
+ break;
+ }
+ }
+narrow_done:
+ if (i >= len)
+ break;
+#if defined(TERM_KERNEL_AVX512)
+ if (isa == TERM_ISA_AVX512 && TERM_EXPECT(!lnm && !t->pending_wrap, 1)) {
+ while (i + 64 <= len) {
+ uint64_t nl;
+ uint64_t mask = line_masks_avx512(buf + i, &nl);
+
+ if (mask == 0) {
+ int total_avail = (int)(len - i);
+ int run = scan_ascii_run(buf + i, total_avail);
+
+ int bulk_lines = run / cols - (run % cols == 0);
+ if (bulk_lines > 0 && autowrap && (i + run == len || ((unsigned char)buf[i + run] >= 0x20 && (unsigned char)buf[i + run] < 0x7f) || (run % cols == 0))) {
+ emit_stream_bulk_wrapped(t, s, buf + i, bulk_lines, cols, &attr, def_pen,
+ q, q_cap, &q_count, &orig_y, &orig_rem,
+ &flushed_scrolled);
+ size_t write_cells = (size_t)bulk_lines * (size_t)cols;
+ last_char = (unsigned char)buf[i + write_cells - 1];
+ i += write_cells;
+ continue;
+ }
+
+ int limit = run > cols ? cols : run;
+ int eol = 0;
+ if (i + limit < len && buf[i + limit] == '\n' && lnm) {
+ eol = 1;
+ } else if (i + limit + 1 < len && buf[i + limit] == '\r' && buf[i + limit + 1] == '\n') {
+ eol = 2;
+ }
+
+ if (eol > 0) {
+ last_char = (unsigned char)buf[i + limit - 1];
+ EMIT_STREAM_ROW_FAST(buf + i, limit, false);
+ i += (size_t)(limit + eol);
+ continue;
+ }
+
+ bool wrapped = limit == cols && autowrap &&
+ next_byte_wraps(buf, len, i + (size_t)limit,
+ (unsigned char)buf[i + limit - 1]);
+ if (!wrapped) break;
+ last_char = (unsigned char)buf[i + limit - 1];
+ EMIT_STREAM_ROW_FAST(buf + i, limit, true);
+ i += (size_t)limit;
+ continue;
+ }
+
+ int p1 = term_ctz64(mask);
+ if (p1 > cols)
+ break;
+
+ int eol1 = 0;
+ if (buf[i + p1] == '\n' && lnm) {
+ eol1 = 1;
+ } else if (buf[i + p1] == '\r' && (p1 + 1 < 64 ? buf[i + p1 + 1] == '\n' : (i + p1 + 1 < len && buf[i + p1 + 1] == '\n'))) {
+ eol1 = 2;
+ } else if (p1 < cols) {
+ goto done;
+ } else {
+ break;
+ }
+
+ if (p1 > 0)
+ last_char = (unsigned char)buf[i + p1 - 1];
+ EMIT_STREAM_ROW_FAST(buf + i, p1, false);
+
+ int next_start = p1 + eol1;
+ int last_consumed = next_start;
+
+ if (next_start < 64) {
+ uint64_t mask2 = mask >> next_start;
+ if (mask2 != 0) {
+ int rel_p2 = term_ctz64(mask2);
+ int p2 = next_start + rel_p2;
+ if (rel_p2 <= cols) {
+ int eol2 = 0;
+ if (buf[i + p2] == '\n' && lnm) {
+ eol2 = 1;
+ } else if (buf[i + p2] == '\r' && (p2 + 1 < 64 ? buf[i + p2 + 1] == '\n' : (i + p2 + 1 < len && buf[i + p2 + 1] == '\n'))) {
+ eol2 = 2;
+ }
+
+ if (eol2 > 0) {
+ if (rel_p2 > 0)
+ last_char = (unsigned char)buf[i + p2 - 1];
+ EMIT_STREAM_ROW_FAST(buf + i + next_start, rel_p2, false);
+
+ int next_start2 = p2 + eol2;
+ last_consumed = next_start2;
+
+ if (next_start2 < 64) {
+ uint64_t mask3 = mask >> next_start2;
+ if (mask3 != 0) {
+ int rel_p3 = term_ctz64(mask3);
+ int p3 = next_start2 + rel_p3;
+ if (rel_p3 <= cols) {
+ int eol3 = 0;
+ if (buf[i + p3] == '\n' && lnm) {
+ eol3 = 1;
+ } else if (buf[i + p3] == '\r' && (p3 + 1 < 64 ? buf[i + p3 + 1] == '\n' : (i + p3 + 1 < len && buf[i + p3 + 1] == '\n'))) {
+ eol3 = 2;
+ }
+
+ if (eol3 > 0) {
+ if (rel_p3 > 0)
+ last_char = (unsigned char)buf[i + p3 - 1];
+ EMIT_STREAM_ROW_FAST(buf + i + next_start2, rel_p3, false);
+
+ last_consumed = p3 + eol3;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ i += (size_t)last_consumed;
+ }
+ if (i >= len) break;
+ }
+#endif
+ int limit = len - i > (size_t)cols ? cols : (int)(len - i);
+ int run = scan_ascii_run(buf + i, limit);
+ int eol = 0;
+ if (TERM_EXPECT(i + run + 1 < len && buf[i + run] == '\r' && buf[i + run + 1] == '\n', 1)) {
+ eol = 2;
+ } else if (i + run < len && buf[i + run] == '\n' && lnm) {
+ eol = 1;
+ }
+
+ if (TERM_EXPECT(eol > 0, 1)) {
+ if (run > 0)
+ last_char = (unsigned char)buf[i + run - 1];
+ EMIT_STREAM_ROW(buf + i, run, false);
+ i += (size_t)(run + eol);
+ continue;
+ }
+
+ if (!run) {
+ int neol = newline_bytes(buf + i, len - i);
+ if (neol == 2 || (neol == 1 && lnm)) {
+ EMIT_STREAM_ROW(buf + i, 0, false);
+ i += (size_t)neol;
+ continue;
+ }
+ break;
+ }
+
+ bool wrapped = run == cols && autowrap &&
+ next_byte_wraps(buf, len, i + (size_t)run,
+ (unsigned char)buf[i + run - 1]);
+ if (!wrapped) break;
+ int full = cols + (int)scan_ascii_run(buf + i + cols,
+ (int)(len - i) - cols);
+ int bulk_lines = full / cols - (full % cols == 0);
+ if (bulk_lines > 0 &&
+ (i + (size_t)full == len ||
+ ((unsigned char)buf[i + full] >= 0x20 &&
+ (unsigned char)buf[i + full] < 0x7f) ||
+ full % cols == 0)) {
+ emit_stream_bulk_wrapped(t, s, buf + i, bulk_lines, cols, &attr, def_pen,
+ q, q_cap, &q_count, &orig_y, &orig_rem,
+ &flushed_scrolled);
+ size_t write_cells = (size_t)bulk_lines * (size_t)cols;
+ last_char = (unsigned char)buf[i + write_cells - 1];
+ i += write_cells;
+ continue;
+ }
+ last_char = (unsigned char)buf[i + run - 1];
+ EMIT_STREAM_ROW(buf + i, run, true);
+ i += (size_t)run;
+ }
+#undef EMIT_STREAM_ROW
+#undef EMIT_STREAM_ROW_FAST
+#if defined(TERM_KERNEL_AVX512)
+done:
+#endif
+ if (i == 0)
+ return 0;
+
+ size_t queued_rows = 0;
+ for (int j = 0; j < q_count; j++)
+ queued_rows += q[j].wrapped ? 1 : rows_narrow(q[j].len, cols);
+ size_t loop_scrolled = flushed_scrolled + queued_rows;
+ if (loop_scrolled > 0) {
+ int push_orig = queued_rows < (size_t)orig_rem ? (int)queued_rows : orig_rem;
+ if (push_orig > 0)
+ flush_orig_rows(t, s, &orig_y, &orig_rem, push_orig);
+
+ int needed = rows - 1 - orig_rem;
+ int tail_rows = 0;
+ int k = q_count;
+ while (k > 0 && tail_rows < needed) {
+ k--;
+ struct hist_ascii_line pr = q[k];
+ int r = (pr.wrapped || pr.len == 0) ? 1 : (int)rows_narrow(pr.len, cols);
+ tail_rows += r;
+ }
+
+ int flush_n = k;
+ if (flush_n > 0) {
+ term_hist_push_ascii_lines(&t->hist, q, flush_n, &attr);
+ }
+
+ int skip_rows = tail_rows - needed;
+ if (skip_rows > 0) {
+ struct hist_ascii_line pr = q[k];
+ int skip_bytes = skip_rows * cols;
+ if (skip_bytes > (int)pr.len) skip_bytes = (int)pr.len;
+ term_hist_push_ascii(&t->hist, pr.ptr, skip_bytes, &attr, true);
+ }
+
+ if (orig_rem == 0) {
+ s->top = 0;
+ } else {
+ s->top += tail_rows;
+ if (s->top >= rows)
+ s->top -= rows;
+ }
+ /* Rows that stay on the screen become text rows. A row that
+ * continues the row above starts from its pen. */
+ int cur_y = orig_rem;
+ bool prev_wrapped = false;
+ for (int j = k; j < q_count && cur_y < rows - 1; j++) {
+ struct hist_ascii_line pr = q[j];
+ int p_len = pr.len;
+ const char *p_ptr = pr.ptr;
+
+ if (p_len == 0) {
+ int p = phys_row(t, cur_y);
+ struct srow *dst = &s->slot[p];
+ dst->uid = ++t->next_row_uid;
+ dst->version++;
+ uint8_t tail_padded = (dst->kind == ROW_CELLS && dst->len == 0)
+ ? (dst->flags & RF_TAIL_PADDED) : 0;
+ dst->kind = ROW_CELLS;
+ dst->len = 0;
+ dst->flags = tail_padded;
+ prev_wrapped = false;
+ cur_y++;
+ continue;
+ }
+
+ int off_in_line = 0;
+ if (j == k && skip_rows > 0) {
+ off_in_line = skip_rows * cols;
+ prev_wrapped = true;
+ }
+
+ while (off_in_line < p_len && cur_y < rows - 1) {
+ int chunk = p_len - off_in_line;
+ bool row_wraps = false;
+ if (chunk > cols) {
+ chunk = cols;
+ row_wraps = true;
+ } else if (pr.wrapped && off_in_line + chunk == p_len) {
+ row_wraps = true;
+ }
+
+ int p = phys_row(t, cur_y);
+ struct srow *dst = &s->slot[p];
+ dst->uid = ++t->next_row_uid;
+ dst->version++;
+
+ struct pen_attr start = prev_wrapped ? attr : pen_attr_default();
+ dst->kind = ROW_TEXT;
+ dst->len = 0;
+ dst->flags = row_wraps ? RF_WRAPPED : 0;
+ text_row_start(&dst->text, &start);
+ dst->text.text = dst->ascii;
+ text_append(dst, (const uint8_t *)(p_ptr + off_in_line),
+ chunk, chunk, chunk, HLINE_NARROW,
+ attr.fg, attr.bg, pen_sl(&attr));
+
+ prev_wrapped = row_wraps;
+ off_in_line += chunk;
+ cur_y++;
+ }
+ }
+ while (cur_y < rows - 1) {
+ int p = phys_row(t, cur_y);
+ struct srow *dst = &s->slot[p];
+ dst->uid = ++t->next_row_uid;
+ dst->version++;
+ dst->kind = ROW_CELLS;
+ dst->len = 0;
+ dst->flags = 0;
+ cur_y++;
+ }
+ int bot_p = phys_row(t, rows - 1);
+ struct srow *bot = &s->slot[bot_p];
+ uint8_t bot_tail = (bot->kind == ROW_CELLS && bot->len == 0)
+ ? (bot->flags & RF_TAIL_PADDED) : 0;
+ bot->uid = ++t->next_row_uid;
+ bot->version++;
+ bot->kind = ROW_CELLS;
+ bot->len = 0;
+ bot->flags = bot_tail;
+ if (t->prompt_y > 0) {
+ t->prompt_y = (size_t)t->prompt_y > loop_scrolled
+ ? t->prompt_y - (int)loop_scrolled : 0;
+ }
+ clamp_selection_after_scroll(t);
+ }
+
+ t->last_char = (uint32_t)last_char;
+ t->cursor_x = 0;
+ t->cursor_y = rows - 1;
+ t->pending_wrap = false;
+ t->full_gen++;
+ return i;
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static TERM_NOINLINE size_t bulk_ascii_lines_avx512(struct term *t, const char *buf, size_t len)
+{
+ return bulk_ascii_lines_body(t, buf, len, TERM_ISA_AVX512);
+}
+#endif
+
+static TERM_NOINLINE size_t bulk_ascii_lines_base(struct term *t, const char *buf, size_t len)
+{
+ return bulk_ascii_lines_body(t, buf, len, TERM_ISA_BASE);
+}
+
+size_t term_sc_bulk_ascii_lines(struct term *t, const char *buf, size_t len)
+{
+ if (TERM_EXPECT(t->scr.rows > 256, 0)) return 0;
+ if (len >= 1024 && t->autowrap && using_default_colors(t) && !term_flags_pad(t)) {
+ size_t done = 0;
+ if (!term_lnm(t)) done = stream_ascii_wrapped(t, buf, len);
+ else if (!t->pending_wrap) done = stream_ascii_lf(t, buf, len);
+ if (done) return done;
+ }
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return bulk_ascii_lines_avx512(t, buf, len);
+#endif
+ return bulk_ascii_lines_base(t, buf, len);
+}
diff --git a/src/core.h b/src/core.h
new file mode 100644
index 0000000..2460938
--- /dev/null
+++ b/src/core.h
@@ -0,0 +1,385 @@
+#ifndef WSAP_TERM_CORE_H
+#define WSAP_TERM_CORE_H
+
+#include "compat.h"
+#include "internal.h"
+#include "screen.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+struct span {
+ struct srow *row;
+ struct cell *cells;
+ int avail;
+ int x;
+ int old_len;
+};
+
+/* Begin writing a span of cells. Settle cursor row, repair left edge. */
+static inline struct span sc_span_begin(struct term *t, int want);
+
+/* Finish writing a span of cells. Repair right edge, raise len, bump gen. */
+static inline void sc_span_end(struct term *t, struct span *s, int cols,
+ uint8_t row_flags, uint32_t last);
+
+/* Print ASCII characters through span and SIMD chunking. */
+static inline void sc_print_ascii(struct term *t, const char *s, int n);
+
+/* Print one glyph (single codepoint or cluster handle). */
+void term_sc_print_glyph(struct term *t, uint32_t cp, int width, uint16_t flags);
+
+/* Extend the last printed glyph into a grapheme cluster. */
+bool term_sc_extend_cluster(struct term *t, uint32_t cp, int klass);
+
+/* Multi-line or region-constrained line feed helper. */
+void term_sc_linefeed_slow(struct term *t, size_t n);
+
+/* Line feed: move down or scroll with BCE. */
+static inline void sc_linefeed(struct term *t, size_t n)
+{
+ if (TERM_EXPECT(n == 1 && t->scroll_top == 0 &&
+ t->scroll_bottom == t->rows - 1, 1)) {
+ t->pending_wrap = false;
+ term_row(t, t->cursor_y)->version++;
+ if (TERM_EXPECT(t->cursor_y == t->rows - 1, 1))
+ term_screen_scroll_up(t);
+ else
+ t->cursor_y++;
+ return;
+ }
+ term_sc_linefeed_slow(t, n);
+}
+
+/* Carriage return: move cursor to left margin. */
+static inline void sc_cr(struct term *t)
+{
+ t->pending_wrap = false;
+ if (t->cursor_x != 0) {
+ t->cursor_x = 0;
+ term_row(t, t->cursor_y)->version++;
+ }
+}
+
+/* Move cursor to (x, y), margin-aware. */
+static inline void sc_move(struct term *t, int x, int y)
+{
+ t->pending_wrap = false;
+ if (x < 0) x = 0;
+ if (x >= t->cols) x = t->cols - 1;
+ if (y < 0) y = 0;
+ if (y >= t->rows) y = t->rows - 1;
+
+ int old_y = t->cursor_y;
+ if (x == t->cursor_x && y == old_y)
+ return;
+ t->cursor_x = x;
+ t->cursor_y = y;
+ term_row(t, old_y)->version++;
+ if (y != old_y)
+ term_row(t, y)->version++;
+}
+
+/* Advance cursor by n tab stops. */
+void term_sc_tab(struct term *t, int n);
+
+/* Move cursor back one column. */
+static inline void sc_bs(struct term *t)
+{
+ t->pending_wrap = false;
+ if (t->cursor_x > 0)
+ t->cursor_x--;
+ term_row(t, t->cursor_y)->version++;
+}
+
+/* Erase cells in span [from, to) on physical row y. */
+static inline void sc_erase_span(struct term *t, int y, int from, int to)
+{
+ t->pending_wrap = false;
+ if (to > t->cols) to = t->cols;
+ int p = phys_row(t, y);
+ erase_span(t, p, from, to);
+ t->scr.slot[p].version++;
+}
+
+/* Erase display mode 0, 1, 2, or 3. */
+static inline void sc_erase_display(struct term *t, int mode)
+{
+ t->pending_wrap = false;
+ term_screen_erase(t, mode);
+}
+
+/* Erase line mode 0, 1, or 2. */
+static inline void sc_erase_line(struct term *t, int mode)
+{
+ t->pending_wrap = false;
+ term_screen_erase_line(t, mode);
+}
+
+/* Insert n blank characters at cursor. */
+static inline void sc_insert_chars(struct term *t, int n)
+{
+ t->pending_wrap = false;
+ term_screen_insert_chars(t, n);
+}
+
+/* Delete n characters at cursor. */
+static inline void sc_delete_chars(struct term *t, int n)
+{
+ t->pending_wrap = false;
+ term_screen_delete_chars(t, n);
+}
+
+/* Scroll margins: top to bottom, n lines up or down. */
+void term_sc_scroll_region(struct term *t, int top, int bottom, int n, bool down);
+
+/* Scroll the active scroll region up or down by n lines (with history). */
+void term_sc_scroll(struct term *t, int n, bool down);
+
+/* Restore saved cursor state and position. */
+static inline void sc_restore_cursor(struct term *t, const struct term_saved_cursor *s)
+{
+ term_cursor_apply(t, s);
+ sc_move(t, s->x, s->y);
+ t->pending_wrap = s->pending_wrap;
+}
+
+/* Change attributes in a rectangular or stream area (DECCARA). */
+void term_sc_deccara(struct term *t, const struct csi *csi);
+
+/* Screen alignment pattern (DECALN). */
+void term_sc_decaln(struct term *t);
+
+/* Reset screen and cursor state for full reset (RIS). */
+void term_sc_reset(struct term *t);
+
+/* Set reverse video mode (DECSCNM). */
+void term_sc_set_reverse_video(struct term *t, bool enabled);
+
+/* Set autowrap mode (DECAWM). */
+void term_sc_set_autowrap(struct term *t, bool enabled);
+
+/* Set cursor style (DECSCUSR). */
+void term_sc_set_cursor_style(struct term *t, int ps);
+
+/* Bulk streaming of ASCII lines with raw rows and skip-ahead. */
+size_t term_sc_bulk_ascii_lines(struct term *t, const char *buf, size_t len);
+
+/* Fast CSI parser. */
+int term_try_fast_csi(struct term *t, const char *buf, size_t rem);
+
+static inline struct span sc_span_begin(struct term *t, int want)
+{
+ (void)want;
+ if (TERM_EXPECT(t->pending_wrap, 0)) {
+ row_set_wrapped(term_row(t, t->cursor_y), true);
+ t->cursor_x = t->lr_margin_mode ? t->left_margin : 0;
+ sc_linefeed(t, 1);
+ t->pending_wrap = false;
+ }
+ struct srow *row = term_row(t, t->cursor_y);
+ if (TERM_EXPECT(row->kind != ROW_CELLS, 0))
+ srow_settle(row, t->cols);
+ int cx = t->cursor_x;
+ term_row_fill_gap(row, cx);
+ int len = row->len;
+ if (TERM_EXPECT(cx < len && (row->cells[cx].flags & CELL_TRAIL), 0))
+ fill_cells(row->cells + cx - 1, 1, row->cells[cx - 1].fg, row->cells[cx - 1].bg);
+
+ int right = TERM_EXPECT(t->lr_margin_mode, 0) ? t->right_margin : t->cols - 1;
+ int avail = right + 1 > cx ? right + 1 - cx : 0;
+ return (struct span){
+ .row = row,
+ .cells = row->cells + cx,
+ .avail = avail,
+ .x = cx,
+ .old_len = len,
+ };
+}
+
+static inline void sc_span_end(struct term *t, struct span *s, int cols,
+ uint8_t row_flags, uint32_t last)
+{
+ struct srow *row = s->row;
+ int end_x = s->x + cols;
+ if (TERM_EXPECT(end_x < s->old_len && (row->cells[end_x].flags & CELL_TRAIL), 0))
+ fill_cells(row->cells + end_x, 1, row->cells[end_x].fg, row->cells[end_x].bg);
+ if (end_x > s->old_len)
+ row->len = (uint16_t)end_x;
+ if (TERM_EXPECT(row_flags != 0, 0))
+ row->flags |= row_flags;
+ row->version++;
+ t->last_char = last;
+
+ if (TERM_EXPECT(cols < s->avail, 1)) {
+ t->cursor_x = end_x;
+ t->pending_wrap = false;
+ } else {
+ t->cursor_x = s->x + s->avail - 1;
+ t->pending_wrap = t->autowrap;
+ }
+}
+
+/* Move the cursor after cols columns written at x, of avail. */
+static inline void sc_text_advance(struct term *t, int x, int cols, int avail)
+{
+ if (cols < avail) {
+ t->cursor_x = x + cols;
+ t->pending_wrap = false;
+ } else {
+ t->cursor_x = x + avail - 1;
+ t->pending_wrap = t->autowrap;
+ }
+}
+
+/* Append ASCII at the cursor, with the pen fg, bg, sl (flags | link << 16),
+ * when the cursor row takes text. Return the bytes taken. */
+static inline int sc_text_ascii(struct term *t, const char *s, int n, uint32_t fg, uint32_t bg,
+ uint32_t sl)
+{
+ if (TERM_EXPECT(t->insert_mode || t->lr_margin_mode, 0))
+ return 0;
+ if (t->pending_wrap) {
+ row_set_wrapped(term_row(t, t->cursor_y), true);
+ t->cursor_x = 0;
+ sc_linefeed(t, 1);
+ t->pending_wrap = false;
+ }
+ struct srow *row = text_append_row(t);
+ if (!row)
+ return 0;
+ int x = t->cursor_x, avail = t->cols - x;
+ int take = n < avail ? n : avail;
+ if (take <= 0)
+ return 0;
+ text_append(row, (const uint8_t *)s, take, take, take, HLINE_NARROW, fg, bg, sl);
+ t->last_char = (unsigned char)s[take - 1];
+ sc_text_advance(t, x, take, avail);
+ return take;
+}
+
+static inline void sc_print_ascii(struct term *t, const char *s, int n)
+{
+ uint32_t fg = TERM_EXPECT(t->inverse, 0) ? t->cur_bg : t->cur_fg;
+ uint32_t bg = TERM_EXPECT(t->inverse, 0) ? t->cur_fg : t->cur_bg;
+ uint32_t flags = term_flags_pad(t);
+
+ for (int k; n > 0 && (k = sc_text_ascii(t, s, n, fg, bg, flags)) > 0; s += k, n -= k) {}
+
+ while (n > 0) {
+ struct span span = sc_span_begin(t, n);
+ int take = n < span.avail ? n : span.avail;
+ if (TERM_EXPECT(take <= 0, 0)) {
+ t->pending_wrap = t->autowrap;
+ if (!t->autowrap)
+ break;
+ span = sc_span_begin(t, n);
+ take = n < span.avail ? n : span.avail;
+ if (take <= 0)
+ break;
+ }
+ put_ascii_chunk(span.cells, s, take, fg, bg, flags);
+ sc_span_end(t, &span, take, 0, (unsigned char)s[take - 1]);
+ if (TERM_EXPECT(n <= take, 1)) break;
+ s += take;
+ n -= take;
+ }
+}
+
+/* Write a short ASCII span without settling an already writable row. */
+static inline bool sc_ascii_fast(struct term *t, const char *s, int n,
+ uint32_t fg, uint32_t bg, uint32_t flags_pad)
+{
+ int cx = t->cursor_x;
+ if (TERM_EXPECT(t->pending_wrap || t->insert_mode ||
+ t->lr_margin_mode || t->inverse ||
+ n <= 0 || n > t->cols - cx, 0))
+ return false;
+ struct srow *row = term_row(t, t->cursor_y);
+ if (row->kind == ROW_TEXT || (row->len == 0 && cx == 0)) {
+ if (sc_text_ascii(t, s, n, fg, bg, flags_pad) == n)
+ return true;
+ if (row->kind == ROW_TEXT)
+ return false;
+ }
+ if (TERM_EXPECT(row->kind != ROW_CELLS || cx > row->len, 0))
+ return false;
+ int len = row->len;
+ struct cell *cells = row->cells;
+ if (TERM_EXPECT((cx < len && (cells[cx].flags & CELL_TRAIL)) ||
+ (cx + n < len && (cells[cx + n].flags & CELL_TRAIL)), 0))
+ return false;
+ put_ascii_chunk(cells + cx, s, n, fg, bg, flags_pad);
+ cx += n;
+ if (cx > len)
+ row->len = (uint16_t)cx;
+ row->version++;
+ t->last_char = (unsigned char)s[n - 1];
+ if (cx < t->cols) {
+ t->cursor_x = cx;
+ t->pending_wrap = false;
+ } else {
+ t->cursor_x = t->cols - 1;
+ t->pending_wrap = t->autowrap;
+ }
+ return true;
+}
+
+/* Fast inline check for 1-character write (fg-char fast path). */
+static inline bool sc_put1_fast(struct term *t, char ch, uint32_t fg, uint32_t bg,
+ uint32_t flags_pad)
+{
+ int cx = t->cursor_x;
+ int cols = t->cols;
+ if (TERM_EXPECT(t->pending_wrap | t->insert_mode | t->lr_margin_mode | t->inverse, 0))
+ return false;
+ if (TERM_EXPECT(cx >= cols, 0))
+ return false;
+ struct srow *row = term_row(t, t->cursor_y);
+ if (row->kind == ROW_TEXT) {
+ struct text_row *tr = &row->text;
+ if (TERM_EXPECT(cx != tr->cells || !tr->cells || cx + 1 == cols, 0))
+ return sc_text_ascii(t, &ch, 1, fg, bg, flags_pad) == 1;
+ /* One character in the middle of a text row. */
+ tr->text[tr->text_len++] = (uint8_t)ch;
+ tr->cells++;
+ hist_runs_add_pen(&tr->runs, fg, bg, flags_pad, 1);
+ if (flags_pad >> 16)
+ tr->flags |= HLINE_HAS_LINK;
+ if (tr->cells > row->len)
+ row->len = tr->cells;
+ row->version++;
+ t->last_char = (uint8_t)ch;
+ t->cursor_x = cx + 1;
+ return true;
+ }
+ if (row->len == 0 && cx == 0) {
+ if (sc_text_ascii(t, &ch, 1, fg, bg, flags_pad) == 1)
+ return true;
+ }
+ int len = row->len;
+ if (TERM_EXPECT(cx > len, 0))
+ return false;
+ struct cell *cells = row->cells;
+ if (TERM_EXPECT((cx < len && (cells[cx].flags & CELL_TRAIL)) ||
+ (cx + 1 < len && (cells[cx + 1].flags & CELL_TRAIL)), 0))
+ return false;
+
+ cells[cx] = (struct cell){ (uint8_t)ch, fg, bg,
+ (uint16_t)flags_pad, (uint16_t)(flags_pad >> 16) };
+ t->last_char = (uint8_t)ch;
+ cx++;
+ if (cx > len)
+ row->len = (uint16_t)cx;
+ row->version++;
+ if (TERM_EXPECT(cx < cols, 1)) {
+ t->cursor_x = cx;
+ } else {
+ t->cursor_x = cols - 1;
+ t->pending_wrap = t->autowrap;
+ }
+ return true;
+}
+
+#endif
diff --git a/src/csi.h b/src/csi.h
new file mode 100644
index 0000000..b4609ea
--- /dev/null
+++ b/src/csi.h
@@ -0,0 +1,109 @@
+#ifndef CSI_H
+#define CSI_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <limits.h>
+
+#define MAX_PARAMS 16
+
+struct csi {
+ int params[MAX_PARAMS];
+ int nparams;
+ uint16_t subparams;
+ char prefix, intermediate, final;
+};
+
+enum csi_phase { CSI_ENTRY, CSI_PARAMETERS, CSI_INTERMEDIATES, CSI_IGNORE };
+enum csi_result { CSI_MORE, CSI_DISPATCH, CSI_DISCARD, CSI_CONTROL };
+
+struct csi_parser {
+ struct csi csi;
+ enum csi_phase phase;
+};
+
+static inline void csi_parser_reset(struct csi_parser *p)
+{
+ p->csi.params[0] = 0;
+ p->csi.nparams = 0;
+ p->csi.subparams = 0;
+ p->csi.prefix = p->csi.intermediate = p->csi.final = 0;
+ p->phase = CSI_ENTRY;
+}
+
+static inline int csi_count(const struct csi *csi)
+{
+ return csi->params[0] > 0 ? csi->params[0] : 1;
+}
+
+static inline enum csi_result csi_read(struct csi_parser *restrict p, const char *restrict s,
+ size_t len, size_t *restrict consumed)
+{
+ size_t i = 0;
+ enum csi_result result = CSI_MORE;
+ while (i < len) {
+ unsigned char ch = (unsigned char)s[i];
+ if (ch < 0x20 || ch >= 0x80) {
+ result = CSI_CONTROL;
+ break;
+ }
+ i++;
+ if (ch == 0x7f) continue;
+ if (ch >= 0x40) {
+ p->csi.final = (char)ch;
+ result = p->phase == CSI_IGNORE ? CSI_DISCARD : CSI_DISPATCH;
+ break;
+ }
+ if (p->phase == CSI_IGNORE) continue;
+
+ if (ch >= '0' && ch <= ';') {
+ if (p->phase == CSI_INTERMEDIATES) {
+ p->phase = CSI_IGNORE;
+ continue;
+ }
+ p->phase = CSI_PARAMETERS;
+ int n = p->csi.nparams ? p->csi.nparams : 1;
+ int value = p->csi.params[n - 1];
+ for (;;) {
+ if (ch <= '9') {
+ for (;;) {
+ int digit = ch - '0';
+ value = value < 214748364 ? value * 10 + digit : INT_MAX;
+ if (i == len) break;
+ ch = (unsigned char)s[i];
+ if (ch < '0' || ch > '9') break;
+ i++;
+ }
+ } else {
+ p->csi.params[n - 1] = value;
+ if (n == MAX_PARAMS) {
+ p->phase = CSI_IGNORE;
+ break;
+ }
+ if (ch == ':') p->csi.subparams |= (uint16_t)(1u << n);
+ n++;
+ value = 0;
+ }
+ if (i == len) break;
+ ch = (unsigned char)s[i];
+ if (ch < '0' || ch > ';') break;
+ i++;
+ }
+ p->csi.params[n - 1] = value;
+ p->csi.nparams = n;
+ } else if (ch >= '<') {
+ if (p->phase != CSI_ENTRY || p->csi.prefix) p->phase = CSI_IGNORE;
+ else p->csi.prefix = (char)ch;
+ } else {
+ if (p->phase == CSI_INTERMEDIATES) p->phase = CSI_IGNORE;
+ else {
+ p->phase = CSI_INTERMEDIATES;
+ p->csi.intermediate = (char)ch;
+ }
+ }
+ }
+ *consumed = i;
+ return result;
+}
+
+#endif
diff --git a/src/escape.c b/src/escape.c
new file mode 100644
index 0000000..119010c
--- /dev/null
+++ b/src/escape.c
@@ -0,0 +1,1116 @@
+#include "escape.h"
+#include "compat.h"
+#include "core.h"
+#include "kernels.h"
+#include "pen.h"
+#include "screen.h"
+#include "text.h"
+#include "osc/osc.h"
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static int sgr_color_param(const struct term *t, char *buf, size_t sz, int base, uint32_t c)
+{
+ if (term_color_is_slot(c) && (c & 0x1FFu) < 256) {
+ int i = (int)(c & 0xFFu);
+ /* The underline color has no short form. */
+ if (i < 16 && base != 50)
+ return snprintf(buf, sz, ";%d", i < 8 ? base + i : base + 60 + i - 8);
+ return snprintf(buf, sz, ";%d:5:%d", base + 8, i);
+ }
+ c = palette_resolve(&t->palette, c);
+ return snprintf(buf, sz, ";%d:2:%d:%d:%d", base + 8,
+ (int)((c >> 16) & 0xff), (int)((c >> 8) & 0xff), (int)(c & 0xff));
+}
+
+static int sgr_serialize(const struct term *t, char *buf, size_t sz)
+{
+ int n = 0;
+ if (!(t->bold || t->faint || t->italic || t->underline || t->strike ||
+ t->overline || t->inverse || t->cur_fg != TERM_COLOR_FG ||
+ t->cur_bg != TERM_COLOR_BG || t->cur_ul))
+ return 0;
+#define SGR_ADD(expr) \
+ do { \
+ n += (expr); \
+ if ((size_t)n >= sz) return sz ? (int)sz - 1 : 0; \
+ } while (0)
+ SGR_ADD(snprintf(buf + n, sz - (size_t)n, "0"));
+ if (t->bold) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";1"));
+ if (t->faint) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";2"));
+ if (t->italic) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";3"));
+ if (t->underline == 1) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";4"));
+ else if (t->underline) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";4:%d", t->underline));
+ if (t->inverse) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";7"));
+ if (t->strike) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";9"));
+ if (t->overline) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";53"));
+ if (t->cur_fg != TERM_COLOR_FG) SGR_ADD(sgr_color_param(t, buf + n, sz - (size_t)n, 30, t->cur_fg));
+ if (t->cur_bg != TERM_COLOR_BG) SGR_ADD(sgr_color_param(t, buf + n, sz - (size_t)n, 40, t->cur_bg));
+ if (t->cur_ul) SGR_ADD(sgr_color_param(t, buf + n, sz - (size_t)n, 50, t->cur_ul));
+#undef SGR_ADD
+ return n;
+}
+
+static int dcs_decode_hex(const char *s, int len, char *out, int cap)
+{
+ if (len & 1) return -1;
+ int n = 0;
+ for (int i = 0; i < len; i += 2) {
+ int hi = term_hex_nib(s[i]), lo = term_hex_nib(s[i + 1]);
+ if (hi < 0 || lo < 0 || n >= cap) return -1;
+ out[n++] = (char)(hi << 4 | lo);
+ }
+ return n;
+}
+
+static const char *term_tcap(const struct term *t, const char *name)
+{
+ static const struct { const char *name, *value; } caps[] = {
+ { "Co", "16777216" },
+ { "RGB", "8/8/8" },
+ { "colors", "16777216" },
+ { "kDC", "\x1b[3;2~" }, { "kDC3", "\x1b[3;3~" },
+ { "kDC4", "\x1b[3;4~" }, { "kDC5", "\x1b[3;5~" },
+ { "kDC6", "\x1b[3;6~" }, { "kDC7", "\x1b[3;7~" },
+ { "kDN", "\x1b[1;2B" }, { "kDN3", "\x1b[1;3B" },
+ { "kDN4", "\x1b[1;4B" }, { "kDN5", "\x1b[1;5B" },
+ { "kDN6", "\x1b[1;6B" }, { "kDN7", "\x1b[1;7B" },
+ { "kEND", "\x1b[1;2F" }, { "kEND3", "\x1b[1;3F" },
+ { "kEND4", "\x1b[1;4F" }, { "kEND5", "\x1b[1;5F" },
+ { "kEND6", "\x1b[1;6F" }, { "kEND7", "\x1b[1;7F" },
+ { "kHOM", "\x1b[1;2H" }, { "kHOM3", "\x1b[1;3H" },
+ { "kHOM4", "\x1b[1;4H" }, { "kHOM5", "\x1b[1;5H" },
+ { "kHOM6", "\x1b[1;6H" }, { "kHOM7", "\x1b[1;7H" },
+ { "kIC", "\x1b[2;2~" }, { "kIC3", "\x1b[2;3~" },
+ { "kIC4", "\x1b[2;4~" }, { "kIC5", "\x1b[2;5~" },
+ { "kIC6", "\x1b[2;6~" }, { "kIC7", "\x1b[2;7~" },
+ { "kLFT", "\x1b[1;2D" }, { "kLFT3", "\x1b[1;3D" },
+ { "kLFT4", "\x1b[1;4D" }, { "kLFT5", "\x1b[1;5D" },
+ { "kLFT6", "\x1b[1;6D" }, { "kLFT7", "\x1b[1;7D" },
+ { "kNXT", "\x1b[6;2~" }, { "kNXT3", "\x1b[6;3~" },
+ { "kNXT4", "\x1b[6;4~" }, { "kNXT5", "\x1b[6;5~" },
+ { "kNXT6", "\x1b[6;6~" }, { "kNXT7", "\x1b[6;7~" },
+ { "kPRV", "\x1b[5;2~" }, { "kPRV3", "\x1b[5;3~" },
+ { "kPRV4", "\x1b[5;4~" }, { "kPRV5", "\x1b[5;5~" },
+ { "kPRV6", "\x1b[5;6~" }, { "kPRV7", "\x1b[5;7~" },
+ { "kRIT", "\x1b[1;2C" }, { "kRIT3", "\x1b[1;3C" },
+ { "kRIT4", "\x1b[1;4C" }, { "kRIT5", "\x1b[1;5C" },
+ { "kRIT6", "\x1b[1;6C" }, { "kRIT7", "\x1b[1;7C" },
+ { "kUP", "\x1b[1;2A" }, { "kUP3", "\x1b[1;3A" },
+ { "kUP4", "\x1b[1;4A" }, { "kUP5", "\x1b[1;5A" },
+ { "kUP6", "\x1b[1;6A" }, { "kUP7", "\x1b[1;7A" },
+ { "kbs", "\x7f" }, { "kcbt", "\x1b[Z" },
+ { "kcub1", "\x1bOD" }, { "kcud1", "\x1bOB" },
+ { "kcuf1", "\x1bOC" }, { "kcuu1", "\x1bOA" },
+ { "kdch1", "\x1b[3~" }, { "kend", "\x1bOF" },
+ { "kent", "\x1bOM" }, { "khome", "\x1bOH" },
+ { "kich1", "\x1b[2~" }, { "kind", "\x1b[1;2B" },
+ { "kmous", "\x1b[<" }, { "knp", "\x1b[6~" },
+ { "kpp", "\x1b[5~" }, { "kri", "\x1b[1;2A" },
+ { "kxIN", "\x1b[I" }, { "kxOUT", "\x1b[O" },
+ { "kf1", "\x1bOP" }, { "kf2", "\x1bOQ" },
+ { "kf3", "\x1bOR" }, { "kf4", "\x1bOS" },
+ { "kf5", "\x1b[15~" }, { "kf6", "\x1b[17~" },
+ { "kf7", "\x1b[18~" }, { "kf8", "\x1b[19~" },
+ { "kf9", "\x1b[20~" }, { "kf10", "\x1b[21~" },
+ { "kf11", "\x1b[23~" }, { "kf12", "\x1b[24~" },
+ { "kf13", "\x1b[1;2P" }, { "kf14", "\x1b[1;2Q" },
+ { "kf15", "\x1b[1;2R" }, { "kf16", "\x1b[1;2S" },
+ { "kf17", "\x1b[15;2~" }, { "kf18", "\x1b[17;2~" },
+ { "kf19", "\x1b[18;2~" }, { "kf20", "\x1b[19;2~" },
+ { "kf21", "\x1b[20;2~" }, { "kf22", "\x1b[21;2~" },
+ { "kf23", "\x1b[23;2~" }, { "kf24", "\x1b[24;2~" },
+ { "kf25", "\x1b[1;5P" }, { "kf26", "\x1b[1;5Q" },
+ { "kf27", "\x1b[1;5R" }, { "kf28", "\x1b[1;5S" },
+ { "kf29", "\x1b[15;5~" }, { "kf30", "\x1b[17;5~" },
+ { "kf31", "\x1b[18;5~" }, { "kf32", "\x1b[19;5~" },
+ { "kf33", "\x1b[20;5~" }, { "kf34", "\x1b[21;5~" },
+ { "kf35", "\x1b[23;5~" }, { "kf36", "\x1b[24;5~" },
+ { "kf37", "\x1b[1;6P" }, { "kf38", "\x1b[1;6Q" },
+ { "kf39", "\x1b[1;6R" }, { "kf40", "\x1b[1;6S" },
+ { "kf41", "\x1b[15;6~" }, { "kf42", "\x1b[17;6~" },
+ { "kf43", "\x1b[18;6~" }, { "kf44", "\x1b[19;6~" },
+ { "kf45", "\x1b[20;6~" }, { "kf46", "\x1b[21;6~" },
+ { "kf47", "\x1b[23;6~" }, { "kf48", "\x1b[24;6~" },
+ };
+ if (!strcmp(name, "TN") || !strcmp(name, "name"))
+ return t->tcap_name;
+ for (size_t i = 0; i < sizeof(caps) / sizeof(caps[0]); i++)
+ if (!strcmp(name, caps[i].name))
+ return caps[i].value;
+ return NULL;
+}
+
+struct resp_buf {
+ char *s;
+ size_t len, cap;
+ bool failed;
+};
+
+static void resp_put(struct resp_buf *r, const char *s, size_t n)
+{
+ if (r->failed)
+ return;
+ if (r->len + n + 1 > r->cap) {
+ size_t cap = r->cap ? r->cap : 256;
+ while (cap < r->len + n + 1)
+ cap *= 2;
+ char *p = realloc(r->s, cap);
+ if (!p) {
+ r->failed = true;
+ return;
+ }
+ r->s = p;
+ r->cap = cap;
+ }
+ memcpy(r->s + r->len, s, n);
+ r->len += n;
+ r->s[r->len] = '\0';
+}
+
+static void dcs_xtgettcap(struct term *t, const char *buf, int len)
+{
+ static const char hex[] = "0123456789abcdef";
+ struct resp_buf r = {0};
+ resp_put(&r, "\033P0+r", 5);
+ char *name = malloc((size_t)len / 2 + 1);
+ if (!name) {
+ free(r.s);
+ return;
+ }
+ int i = 0;
+ bool first = true;
+ while (i < len) {
+ int span = 0;
+ while (i + span < len && buf[i + span] != ';') span++;
+ int n = dcs_decode_hex(buf + i, span, name, len / 2);
+ if (n < 0)
+ break;
+ name[n] = 0;
+ const char *cap = term_tcap(t, name);
+ if (first && r.s) { r.s[2] = cap ? '1' : '0'; first = false; }
+ resp_put(&r, buf + i, (size_t)span);
+ if (!cap) break;
+ resp_put(&r, "=", 1);
+ for (const char *c = cap; *c; c++) {
+ unsigned char b = (unsigned char)*c;
+ char pair[2] = { hex[b >> 4], hex[b & 15] };
+ resp_put(&r, pair, 2);
+ }
+ i += span;
+ if (i < len) resp_put(&r, &buf[i++], 1);
+ }
+ free(name);
+ resp_put(&r, "\033\\", 2);
+ if (!r.failed)
+ send_response(t, r.s);
+ free(r.s);
+}
+
+static int dcs_param_int(const char **p, const char *end)
+{
+ int v = 0, any = 0;
+ while (*p < end && **p >= '0' && **p <= '9') {
+ v = v < 214748364 ? v * 10 + (**p - '0') : INT_MAX;
+ (*p)++;
+ any = 1;
+ }
+ if (*p < end && **p == ';') (*p)++;
+ return any ? v : -1;
+}
+
+static int dcs_param_chr(const char **p, const char *end)
+{
+ if (*p >= end) return -1;
+ int v = (unsigned char)*(*p)++;
+ if (*p < end && **p == ';') (*p)++;
+ return v;
+}
+
+static void restore_deccir(struct term *t, const char *p, const char *end)
+{
+ int row = dcs_param_int(&p, end);
+ if (row <= 0 || row > t->rows) return;
+ int col = dcs_param_int(&p, end);
+ if (col <= 0 || col > t->cols) return;
+ if (dcs_param_int(&p, end) != 1) return;
+ sc_move(t, col - 1, row - 1);
+ int rend = dcs_param_chr(&p, end);
+ if ((rend & 0xf0) != 0x40) return;
+ t->bold = rend & 1;
+ t->underline = (rend & 2) ? 1 : 0;
+ t->inverse = (rend & 8) != 0;
+ int attr = dcs_param_chr(&p, end);
+ if ((attr & 0xf0) != 0x40) return;
+ int flag = dcs_param_chr(&p, end);
+ if ((flag & 0xf0) != 0x40) return;
+ t->autowrap = (flag & 8) != 0;
+ t->origin_mode = (flag & 1) != 0;
+ int curss = (flag & 4) ? 3 : (flag & 2) ? 2 : 0;
+ int gl = dcs_param_chr(&p, end) - '0';
+ if (gl < 0 || gl > 3) return;
+ int gr = dcs_param_chr(&p, end) - '0';
+ if (gr < 0 || gr > 3) return;
+ t->charset_active = gl < 2 ? gl : (curss ? 1 : 0);
+ if (dcs_param_chr(&p, end) < 0) return;
+ for (int i = 0; i < 4 && p < end; i++) {
+ char d = *p++;
+ if ((d == '%' || d == '&' || d == '"') && p < end) d = *p++;
+ if (i < 2) t->charsets[i] = d;
+ }
+ t->graphics = t->charsets[t->charset_active] == '0';
+}
+
+static void restore_dectabsr(struct term *t, const char *p, const char *end)
+{
+ term_tabs_clear(t);
+ int stop = 0, any = 0;
+ for (; p < end; p++) {
+ if (*p >= '0' && *p <= '9') {
+ stop = stop < 214748364 ? stop * 10 + (*p - '0') : INT_MAX;
+ any = 1;
+ } else if (*p == '/') {
+ if (any && --stop >= 0 && stop < t->tabs_cap) t->tabs[stop] = 1;
+ stop = any = 0;
+ } else {
+ return;
+ }
+ }
+ if (any && --stop >= 0 && stop < t->tabs_cap) t->tabs[stop] = 1;
+}
+
+/* Grows the DCS string to hold extra more bytes, up to TERM_OSC_MAX.
+ * False when nothing more fits. */
+static bool dcs_reserve(struct term *t, size_t extra)
+{
+ size_t need = (size_t)t->dcs_len + extra;
+ if (need <= t->dcs_cap)
+ return true;
+ if (t->dcs_cap >= TERM_OSC_MAX)
+ return false;
+ size_t cap = t->dcs_cap ? t->dcs_cap : 256;
+ while (cap < need && cap < TERM_OSC_MAX)
+ cap *= 2;
+ if (cap > TERM_OSC_MAX)
+ cap = TERM_OSC_MAX;
+ char *p = realloc(t->dcs_buf, cap);
+ if (!p)
+ return false;
+ t->dcs_buf = p;
+ t->dcs_cap = cap;
+ return need <= cap;
+}
+
+static void dcs_dispatch(struct term *t)
+{
+ if (term_str_was_truncated(t))
+ return;
+ const char *buf = t->dcs_buf;
+ int len = t->dcs_len;
+ char resp[512];
+ if (len < 2) return;
+ if (buf[0] == '+' && buf[1] == 'q') {
+ dcs_xtgettcap(t, buf + 2, len - 2);
+ } else if (buf[0] == '+' && buf[1] == 'p') {
+ int n = dcs_decode_hex(buf + 2, len - 2, t->tcap_name, sizeof(t->tcap_name) - 1);
+ if (n >= 0) t->tcap_name[n] = 0;
+ } else if (buf[0] == '+' && buf[1] == 'Q') {
+ int span = 0;
+ while (span < len - 2 && buf[2 + span] != ';') span++;
+ char *name = malloc((size_t)span / 2 + 1);
+ if (name && span && dcs_decode_hex(buf + 2, span, name, span / 2) >= 0) {
+ struct resp_buf r = {0};
+ resp_put(&r, "\033P0+R", 5);
+ resp_put(&r, buf + 2, (size_t)span);
+ resp_put(&r, "\033\\", 2);
+ if (!r.failed)
+ send_response(t, r.s);
+ free(r.s);
+ }
+ free(name);
+ } else if (buf[0] == '$' && buf[1] == 'q') {
+ const char *spec = buf + 2;
+ int slen = len - 2, p;
+ if (slen == 2 && spec[0] == ' ' && spec[1] == 'q') {
+ int shape = t->cursor_style == TERM_CURSOR_BLOCK ? (t->cursor_blink ? 1 : 2)
+ : t->cursor_style == TERM_CURSOR_UNDERLINE ? (t->cursor_blink ? 3 : 4)
+ : (t->cursor_blink ? 5 : 6);
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d q\033\\", shape);
+ } else if (slen == 1 && spec[0] == 'm') {
+ char sgr[256] = "";
+ sgr_serialize(t, sgr, sizeof(sgr));
+ p = snprintf(resp, sizeof(resp), "\033P1$r%sm\033\\", sgr);
+ } else if (slen == 1 && spec[0] == 'r') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d;%dr\033\\",
+ t->scroll_top + 1, t->scroll_bottom + 1);
+ } else if (slen == 1 && spec[0] == 's') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d;%ds\033\\",
+ t->left_margin + 1, t->right_margin + 1);
+ } else if (slen == 1 && spec[0] == 't') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%dt\033\\", t->rows);
+ } else if (slen == 2 && spec[0] == '"' && spec[1] == 'p') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r64;1\"p\033\\");
+ } else if (slen == 2 && spec[0] == '"' && spec[1] == 'q') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r0\"q\033\\");
+ } else if (slen == 2 && spec[0] == '$' && spec[1] == '|') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d$|\033\\", t->cols);
+ } else if (slen == 2 && spec[0] == '$' && spec[1] == '}') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r0$}\033\\");
+ } else if (slen == 2 && spec[0] == '$' && spec[1] == '~') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r0$~\033\\");
+ } else if (slen == 2 && spec[0] == '*' && spec[1] == 'x') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d*x\033\\", t->decsace);
+ } else if (slen == 2 && spec[0] == '*' && spec[1] == '|') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d*|\033\\", t->rows);
+ } else {
+ p = snprintf(resp, sizeof(resp), "\033P0$r\033\\");
+ }
+ (void)p;
+ send_response(t, resp);
+ } else if (buf[0] == '=' && len >= 3 && buf[len - 1] == 's') {
+ const char *p = buf + 1, *end = buf + len;
+ int mode = dcs_param_int(&p, end);
+ if (mode == 1) t->synchronized_output = true;
+ else if (mode == 2) t->synchronized_output = false;
+ } else if (buf[0] >= '0' && buf[0] <= '9') {
+ const char *p = buf, *end = buf + len;
+ int ps = dcs_param_int(&p, end);
+ while (p < end && *p == ';') { p++; dcs_param_int(&p, end); }
+ if (end - p >= 2 && p[0] == '$' && p[1] == 't') {
+ if (ps == 1) restore_deccir(t, p + 2, end);
+ else if (ps == 2) restore_dectabsr(t, p + 2, end);
+ }
+ }
+}
+
+static void soft_reset(struct term *t)
+{
+ t->application_cursor = t->application_keypad = t->backarrow_sends_bs = false;
+ t->insert_mode = t->origin_mode = t->reverse_video = t->lnm = false;
+ t->focus_events = t->synchronized_output = t->lr_margin_mode = false;
+ t->mouse_mode = t->mouse_encoding = 0;
+ t->bracketed_paste = false;
+ t->autowrap = t->cursor_visible = true;
+ t->cursor_blink = t->default_cursor_blink;
+ t->cursor_style = t->default_cursor_style;
+ t->scroll_top = 0; t->scroll_bottom = t->rows - 1;
+ t->left_margin = 0; t->right_margin = t->cols - 1;
+ t->decsace = 0;
+ t->cur_link = 0;
+ memset(t->keyboard, 0, sizeof(t->keyboard));
+ reset_sgr(t);
+}
+
+static inline void sgr_set_slot(struct term *t, int code, uint32_t c)
+{
+ if (code == 38) t->cur_fg = c;
+ else if (code == 48) t->cur_bg = c;
+ else t->cur_ul = c;
+}
+
+static inline void handle_sgr(struct term *t, const struct csi *csi)
+{
+ if (csi->nparams == 0) {
+ reset_sgr(t);
+ return;
+ }
+
+ if (csi->nparams == 1 && !csi->subparams) {
+ int p = csi->params[0];
+ if (p == 0) { reset_sgr(t); return; }
+ if ((unsigned)(p - 30) < 8) { t->cur_fg = term_color_index(p - 30); return; }
+ if ((unsigned)(p - 40) < 8) { t->cur_bg = term_color_index(p - 40); return; }
+ if ((unsigned)(p - 90) < 8) { t->cur_fg = term_color_index(p - 90 + 8); return; }
+ if ((unsigned)(p - 100) < 8) { t->cur_bg = term_color_index(p - 100 + 8); return; }
+ if (p == 1) { t->bold = true; return; }
+ if (p == 39) { t->cur_fg = TERM_COLOR_FG; return; }
+ if (p == 49) { t->cur_bg = TERM_COLOR_BG; return; }
+ }
+
+ if (!csi->subparams) {
+ if (csi->nparams == 5 && csi->params[1] == 2 &&
+ (csi->params[0] == 38 || csi->params[0] == 48)) {
+ sgr_set_slot(t, csi->params[0],
+ term_rgb(csi->params[2], csi->params[3], csi->params[4]));
+ return;
+ }
+
+ if (csi->nparams == 10 && csi->params[1] == 2 && csi->params[6] == 2 &&
+ (csi->params[0] == 38 || csi->params[0] == 48) &&
+ csi->params[5] == (csi->params[0] == 38 ? 48 : 38)) {
+ sgr_set_slot(t, csi->params[0],
+ term_rgb(csi->params[2], csi->params[3], csi->params[4]));
+ sgr_set_slot(t, csi->params[5],
+ term_rgb(csi->params[7], csi->params[8], csi->params[9]));
+ return;
+ }
+
+ if (csi->nparams == 3 && csi->params[1] == 5 &&
+ (csi->params[0] == 38 || csi->params[0] == 48) &&
+ (unsigned)csi->params[2] < 256) {
+ sgr_set_slot(t, csi->params[0], term_color_index(csi->params[2]));
+ return;
+ }
+
+ if (csi->nparams == 6 && csi->params[1] == 5 && csi->params[4] == 5 &&
+ (csi->params[0] == 38 || csi->params[0] == 48) &&
+ csi->params[3] == (csi->params[0] == 38 ? 48 : 38) &&
+ (unsigned)csi->params[2] < 256 && (unsigned)csi->params[5] < 256) {
+ sgr_set_slot(t, csi->params[0], term_color_index(csi->params[2]));
+ sgr_set_slot(t, csi->params[3], term_color_index(csi->params[5]));
+ return;
+ }
+ }
+
+ struct term_pen pen = {
+ .fg = t->cur_fg, .bg = t->cur_bg, .ul = t->cur_ul,
+ .underline = t->underline,
+ .bold = t->bold, .faint = t->faint, .italic = t->italic,
+ .strike = t->strike, .overline = t->overline, .inverse = t->inverse,
+ };
+ sgr_apply(&pen, csi->params, csi->nparams, csi->subparams);
+ t->cur_fg = pen.fg;
+ t->cur_bg = pen.bg;
+ t->cur_ul = pen.ul;
+ t->underline = pen.underline;
+ t->bold = pen.bold;
+ t->faint = pen.faint;
+ t->italic = pen.italic;
+ t->strike = pen.strike;
+ t->overline = pen.overline;
+ t->inverse = pen.inverse;
+}
+
+#define TERM_PRIV_MODE_FIELDS(X) \
+ X(1, application_cursor) \
+ X(12, cursor_blink) \
+ X(25, cursor_visible) \
+ X(67, backarrow_sends_bs) \
+ X(1004, focus_events) \
+ X(2004, bracketed_paste) \
+ X(2026, synchronized_output)
+
+#define TERM_ANSI_MODE_FIELDS(X) \
+ X(4, insert_mode) \
+ X(20, lnm)
+
+static int mode_get(const struct term *t, int mode, bool priv)
+{
+ if (priv) {
+ switch (mode) {
+#define MODE_FIELD_GET(n, f) case n: return t->f;
+ TERM_PRIV_MODE_FIELDS(MODE_FIELD_GET)
+#undef MODE_FIELD_GET
+ case 5: return t->reverse_video;
+ case 6: return t->origin_mode;
+ case 7: return t->autowrap;
+ case 47: case 1047: case 1049: return t->alternate;
+ case 69: return t->lr_margin_mode;
+ default: break;
+ }
+ if (mode_is_mouse(mode)) return t->mouse_mode == mode;
+ if (mode_is_mouse_enc(mode)) return t->mouse_encoding == mode;
+ return -1;
+ }
+ switch (mode) {
+#define MODE_FIELD_GET(n, f) case n: return t->f;
+ TERM_ANSI_MODE_FIELDS(MODE_FIELD_GET)
+#undef MODE_FIELD_GET
+ }
+ return -1;
+}
+
+static int mode_status(const struct term *t, int mode, bool priv)
+{
+ int set = mode_get(t, mode, priv);
+ return set < 0 ? 0 : set ? 1 : 2;
+}
+
+static int cursor_top(const struct term *t) { return t->origin_mode ? t->scroll_top : 0; }
+static int cursor_bottom(const struct term *t) { return t->origin_mode ? t->scroll_bottom : t->rows - 1; }
+static int cursor_left(const struct term *t) { return t->origin_mode && t->lr_margin_mode ? t->left_margin : 0; }
+static int cursor_right(const struct term *t) { return t->origin_mode && t->lr_margin_mode ? t->right_margin : t->cols - 1; }
+
+static void mode_set(struct term *t, int mode, bool enabled, bool priv)
+{
+ if (!priv) {
+ switch (mode) {
+#define MODE_FIELD_SET(n, f) case n: t->f = enabled; break;
+ TERM_ANSI_MODE_FIELDS(MODE_FIELD_SET)
+#undef MODE_FIELD_SET
+ }
+ return;
+ }
+ switch (mode) {
+#define MODE_FIELD_SET(n, f) case n: t->f = enabled; return;
+ TERM_PRIV_MODE_FIELDS(MODE_FIELD_SET)
+#undef MODE_FIELD_SET
+ default: break;
+ }
+ if (mode == 5) {
+ term_sc_set_reverse_video(t, enabled);
+ } else if (mode == 6) {
+ t->origin_mode = enabled;
+ sc_move(t, 0, enabled ? t->scroll_top : 0);
+ } else if (mode == 7) {
+ term_sc_set_autowrap(t, enabled);
+ } else if (mode == 1048) {
+ if (enabled) t->saved = term_cursor_save(t);
+ else sc_restore_cursor(t, &t->saved);
+ } else if (mode == 47 || mode == 1047 || mode == 1049) {
+ term_screen_set_alternate(t, enabled);
+ } else if (mode_is_mouse(mode)) {
+ t->mouse_mode = enabled ? mode : 0;
+ } else if (mode == 69) {
+ t->lr_margin_mode = enabled;
+ t->left_margin = 0; t->right_margin = t->cols - 1;
+ sc_move(t, 0, cursor_top(t));
+ } else if (mode_is_mouse_enc(mode)) {
+ t->mouse_encoding = enabled ? mode : 0;
+ }
+}
+
+/* Vertical moves stop at a margin when they start inside the scroll
+ * region (xterm, DEC STD 070). */
+static inline void move_v(struct term *t, int n, bool col0)
+{
+ int top = t->cursor_y >= t->scroll_top ? t->scroll_top : 0;
+ int bottom = t->cursor_y <= t->scroll_bottom ? t->scroll_bottom : t->rows - 1;
+ int y = n < 0 ? (t->cursor_y - top >= -n ? t->cursor_y + n : top)
+ : (n < bottom - t->cursor_y ? t->cursor_y + n : bottom);
+ sc_move(t, col0 ? cursor_left(t) : t->cursor_x, y);
+}
+static inline void move_h(struct term *t, int n, bool margin_aware)
+{
+ int right = margin_aware ? cursor_right(t) : t->cols - 1;
+ int x = n < 0 ? (t->cursor_x >= -n ? t->cursor_x + n : 0)
+ : (n < right - t->cursor_x ? t->cursor_x + n : right);
+ sc_move(t, x, t->cursor_y);
+}
+
+static inline TERM_ALWAYS_INLINE void dispatch_csi(struct term *t, const struct csi *csi)
+{
+ if (TERM_EXPECT(csi->final == 'm', 1)) {
+ if (TERM_EXPECT(!csi->prefix && !csi->intermediate, 1))
+ handle_sgr(t, csi);
+ return;
+ }
+ if (TERM_EXPECT(csi->final == 'H' || csi->final == 'f', 0)) {
+ if (TERM_EXPECT(!csi->prefix && !csi->intermediate && !csi->subparams, 1)) {
+ int64_t r = (int64_t)(csi->params[0] > 0 ? csi->params[0] - 1 : 0) + cursor_top(t);
+ if (r > cursor_bottom(t)) r = cursor_bottom(t);
+ int64_t c = (int64_t)(csi->nparams > 1 && csi->params[1] > 0 ? csi->params[1] - 1 : 0) + cursor_left(t);
+ if (c > cursor_right(t)) c = cursor_right(t);
+ sc_move(t, (int)c, (int)r);
+ }
+ return;
+ }
+
+ if (csi->intermediate) {
+ bool plain = (csi->final == 'q' && csi->intermediate == ' ') ||
+ (csi->final == 'p' && csi->intermediate == '!') ||
+ (csi->final == 'x' && csi->intermediate == '*');
+ if (plain) {
+ if (csi->prefix || csi->subparams) return;
+ } else if (csi->final == 'p' && csi->intermediate == '$') {
+ if (csi->subparams) return;
+ } else if (csi->final == 'r' && csi->intermediate == '$') {
+ if (csi->prefix) return;
+ } else {
+ return;
+ }
+ }
+ if (csi->prefix) {
+ if (csi->final != 'u' &&
+ !(csi->final == 'c' && (csi->prefix == '>' || csi->prefix == '=')) &&
+ !(csi->final == 'q' && csi->prefix == '>' && !csi->intermediate) &&
+ !(csi->final == 'p' && csi->intermediate == '$' && csi->prefix == '?') &&
+ !(csi->final == 'W' && csi->prefix == '?') &&
+ !((csi->final == 'h' || csi->final == 'l') && csi->prefix == '?')) return;
+ }
+ if (csi->subparams && !(csi->intermediate == '$' && csi->final == 'r')) return;
+ switch (csi->final) {
+ case 'r': {
+ if (csi->intermediate == '$') {
+ term_sc_deccara(t, csi);
+ break;
+ }
+ if (csi->prefix || csi->intermediate) break;
+ int top = csi_count(csi) - 1;
+ int bottom = csi->nparams > 1 && csi->params[1] ? csi->params[1] - 1 : t->rows - 1;
+ if (top < bottom && bottom < t->rows) {
+ t->scroll_top = top;
+ t->scroll_bottom = bottom;
+ sc_move(t, 0, t->origin_mode ? top : 0);
+ }
+ break;
+ }
+ case 'L':
+ case 'M':
+ if (csi->prefix || t->cursor_y < t->scroll_top || t->cursor_y > t->scroll_bottom) break;
+ term_sc_scroll_region(t, t->cursor_y, t->scroll_bottom, csi_count(csi), csi->final == 'L');
+ /* IL and DL move the cursor to the left margin (xterm). */
+ sc_move(t, t->lr_margin_mode ? t->left_margin : 0, t->cursor_y);
+ break;
+ case 'S':
+ case 'T':
+ if (csi->prefix || csi->nparams > 1) break;
+ term_sc_scroll(t, csi_count(csi), csi->final == 'T');
+ break;
+ case 'd': {
+ int64_t r = (int64_t)csi_count(csi) - 1 + cursor_top(t);
+ if (r > cursor_bottom(t)) r = cursor_bottom(t);
+ sc_move(t, t->cursor_x, (int)r);
+ break;
+ }
+ case 'b':
+ term_text_repeat(t, csi_count(csi));
+ break;
+ case 'A': move_v(t, -csi_count(csi), false); break;
+ case 'B': move_v(t, csi_count(csi), false); break;
+ case 'E': move_v(t, csi_count(csi), true); break;
+ case 'F': move_v(t, -csi_count(csi), true); break;
+ case 'e': move_v(t, csi_count(csi), false); break;
+ case 'a': move_h(t, csi_count(csi), true); break;
+ case 'C': move_h(t, csi_count(csi), false); break;
+ case 'D': move_h(t, -csi_count(csi), false); break;
+ case 'G':
+ case '`': {
+ int64_t x = (int64_t)csi_count(csi) - 1 + cursor_left(t);
+ sc_move(t, (int)(x > cursor_right(t) ? cursor_right(t) : x), t->cursor_y);
+ break;
+ }
+ case 's':
+ if (!csi->prefix && t->lr_margin_mode && csi->nparams > 1) {
+ int left = csi_count(csi) - 1;
+ int right = csi->params[1] > 0 ? csi->params[1] - 1 : t->cols - 1;
+ if (left < right && right < t->cols) {
+ t->left_margin = left; t->right_margin = right;
+ sc_move(t, t->origin_mode ? left : 0, cursor_top(t));
+ }
+ } else if (!csi->prefix) {
+ t->saved = term_cursor_save(t);
+ }
+ break;
+ case 'I':
+ case 'Z':
+ term_sc_tab(t, csi->final == 'I' ? csi_count(csi) : -csi_count(csi));
+ break;
+ case 'g':
+ if (csi->params[0] == 0) t->tabs[t->cursor_x] = 0;
+ else if (csi->params[0] == 3) term_tabs_clear(t);
+ break;
+ case 'W':
+ if (csi->prefix == '?' && csi->params[0] == 5) {
+ term_tabs_clear(t);
+ term_tabs_default(t);
+ }
+ break;
+ case 'n':
+ if (!csi->prefix && csi->params[0] == 5) send_response(t, "\033[0n");
+ else if (!csi->prefix && csi->params[0] == 6) {
+ /* Report the cursor cell, also with a pending wrap (xterm). */
+ char reply[48];
+ snprintf(reply, sizeof(reply), "\033[%d;%dR", t->cursor_y - cursor_top(t) + 1,
+ t->cursor_x - cursor_left(t) + 1);
+ send_response(t, reply);
+ }
+ break;
+ case 'J':
+ sc_erase_display(t, csi->params[0]);
+ break;
+ case 'X': {
+ int n = csi_count(csi);
+ sc_erase_span(t, t->cursor_y, t->cursor_x,
+ n > t->cols - t->cursor_x ? t->cols : t->cursor_x + n);
+ break;
+ }
+ case 'x':
+ if (csi->intermediate == '*')
+ t->decsace = (uint8_t)csi->params[0];
+ break;
+ case 'K':
+ sc_erase_line(t, csi->params[0]);
+ break;
+ case 'P':
+ sc_delete_chars(t, csi_count(csi));
+ break;
+ case '@':
+ sc_insert_chars(t, csi_count(csi));
+ break;
+ case 'c': {
+ if (csi->prefix == '>')
+ send_response(t, "\x1b[>0;10;0c");
+ else if (!csi->prefix && csi->params[0] == 0)
+ send_response(t, "\x1b[?62;22c");
+ break;
+ }
+ case 't':
+ if (!csi->prefix && csi->params[0] == 14) {
+ char reply[32];
+ snprintf(reply, sizeof(reply), "\033[4;%d;%dt",
+ t->rows * 20, t->cols * 10);
+ send_response(t, reply);
+ }
+ break;
+ case 'u': {
+ if (!csi->prefix) { sc_restore_cursor(t, &t->saved); break; }
+ if (csi->nparams > (csi->prefix == '=' ? 2 : 1)) break;
+ struct keyboard_mode *k = &t->keyboard[t->alternate];
+ unsigned flags = (unsigned)csi->params[0] & 31;
+ if (csi->prefix == '?') {
+ char reply[32];
+ snprintf(reply, sizeof(reply), "\033[?%uu", k->flags);
+ send_response(t, reply);
+ } else if (csi->prefix == '>') {
+ if (k->depth == KEYBOARD_STACK_SIZE) {
+ memmove(k->saved, k->saved + 1, sizeof(k->saved) - sizeof(k->saved[0]));
+ k->depth--;
+ }
+ k->saved[k->depth++] = k->flags;
+ k->flags = flags;
+ } else if (csi->prefix == '<') {
+ unsigned count = (unsigned)csi_count(csi);
+ if (count > k->depth) { k->depth = 0; k->flags = 0; }
+ else { k->depth -= count; k->flags = k->saved[k->depth]; }
+ } else if (csi->prefix == '=') {
+ int mode = csi->nparams > 1 && csi->params[1] ? csi->params[1] : 1;
+ if (mode == 1) k->flags = flags;
+ else if (mode == 2) k->flags |= flags;
+ else if (mode == 3) k->flags &= ~flags;
+ }
+ break;
+ }
+ case 'h':
+ case 'l': {
+ bool enabled = csi->final == 'h';
+ bool priv = csi->prefix == '?';
+ for (int p = 0; p < csi->nparams; p++)
+ mode_set(t, csi->params[p], enabled, priv);
+ break;
+ }
+ case 'q': {
+ if (csi->prefix == '>' && !csi->intermediate &&
+ (!csi->nparams || !csi->params[0])) {
+ send_response(t, "\033P>|wsap_term\033\\");
+ } else if (!csi->prefix && csi->intermediate == ' ') {
+ term_sc_set_cursor_style(t, csi->nparams > 0 ? csi->params[0] : 0);
+ }
+ break;
+ }
+ case 'p':
+ if (csi->intermediate == '!') {
+ soft_reset(t);
+ sc_move(t, 0, 0);
+ } else if (csi->intermediate == '$' && csi->nparams >= 1) {
+ char reply[32];
+ snprintf(reply, sizeof(reply), "\033[%s%d;%d$y",
+ csi->prefix == '?' ? "?" : "", csi->params[0],
+ mode_status(t, csi->params[0], csi->prefix == '?'));
+ send_response(t, reply);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+/* Formats one trace line and hands it to the host's trace callback. */
+static void esc_tracef(const struct term *t, const char *fmt, ...)
+{
+ char line[512];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(line, sizeof line, fmt, ap);
+ va_end(ap);
+ t->cb.trace(t->cb.ud, line);
+}
+
+/* The guard keeps formatting off the parser path when tracing is off. */
+#define ESC_TRACE(t, ...) \
+ do { if ((t)->cb.trace) esc_tracef((t), __VA_ARGS__); } while (0)
+
+static void dispatch_esc(struct term *t, unsigned char ch)
+{
+ t->state = ST_GROUND;
+ switch (ch) {
+ case '=':
+ t->application_keypad = true;
+ break;
+ case '>':
+ t->application_keypad = false;
+ break;
+ case 'H':
+ t->tabs[t->cursor_x] = 1;
+ break;
+ case 'c':
+ if (t->alternate) term_screen_set_alternate(t, false);
+ soft_reset(t);
+ term_sc_reset(t);
+ break;
+ case '[':
+ ESC_TRACE(t, "ESC[ → ST_CSI");
+ t->state = ST_CSI;
+ csi_parser_reset(&t->csi_parser);
+ break;
+ case ']':
+ ESC_TRACE(t, "ESC] → ST_OSC");
+ t->state = ST_OSC;
+ t->osc_len = 0;
+ t->str_truncated = false;
+ break;
+ case 'P':
+ ESC_TRACE(t, "ESCP → ST_DCS");
+ t->state = ST_DCS;
+ t->dcs_len = 0;
+ t->str_truncated = false;
+ break;
+ case '_':
+ case '^':
+ case 'X':
+ t->state = ST_IGNORE;
+ break;
+ case '#':
+ t->state = ST_DECALN;
+ break;
+ case '%':
+ t->state = ST_DOCS;
+ break;
+ case '(':
+ case ')':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '/':
+ t->charset_designation = (char)ch;
+ t->state = ST_CHARSET;
+ break;
+ case 'D':
+ case 'E':
+ if (ch == 'E') sc_cr(t);
+ sc_linefeed(t, 1);
+ break;
+ case 'M':
+ if (t->cursor_y == t->scroll_top)
+ term_sc_scroll_region(t, t->scroll_top, t->scroll_bottom, 1, true);
+ else
+ sc_move(t, t->cursor_x, t->cursor_y > 0 ? t->cursor_y - 1 : 0);
+ break;
+ case '7':
+ t->saved = term_cursor_save(t);
+ break;
+ case '8':
+ sc_restore_cursor(t, &t->saved);
+ break;
+ default:
+ break;
+ }
+}
+
+void term_escape_byte(struct term *t, unsigned char ch)
+{
+ if (ch >= 0x80 && t->state != ST_OSC && t->state != ST_DCS &&
+ t->state != ST_IGNORE) {
+ ESC_TRACE(t, " 0x%02x in state %d → abort → ST_GROUND", ch, t->state);
+ t->osc_len = 0;
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ return;
+ }
+ switch (t->state) {
+ case ST_ESC:
+ ESC_TRACE(t, " ESC+0x%02x('%c')", ch, ch >= 0x20 && ch < 0x7f ? ch : '.');
+ if (ch < 0x20) {
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ } else {
+ dispatch_esc(t, ch);
+ }
+ break;
+ case ST_CSI:
+ if (ch == 0x1b) {
+ ESC_TRACE(t, " CSI interrupted by ESC");
+ t->state = ST_ESC;
+ } else if (ch == 0x18 || ch == 0x1a) {
+ ESC_TRACE(t, " CSI cancelled by 0x%02x", ch);
+ t->state = ST_GROUND;
+ } else if (ch < 0x20) {
+ /* Other C0 controls run inside the sequence (ECMA-48 5.5,
+ * xterm). */
+ term_text_ground(t, ch);
+ }
+ break;
+ case ST_CHARSET:
+ if (t->charset_designation == '(' || t->charset_designation == ')') {
+ t->charsets[t->charset_designation == ')'] = (char)ch;
+ t->graphics = t->charsets[t->charset_active] == '0';
+ }
+ t->state = ST_GROUND;
+ break;
+ case ST_DOCS:
+ if (ch != 'G' && ch != '@' && ch != '/')
+ term_text_ground(t, '%');
+ t->state = ST_GROUND;
+ if (ch != 'G' && ch != '@' && ch != '/')
+ term_text_ground(t, ch);
+ break;
+ case ST_DECALN:
+ t->state = ST_GROUND;
+ if (ch == '8')
+ term_sc_decaln(t);
+ break;
+ case ST_IGNORE:
+ if (ch == 0x07 || ch == 0x18 || ch == 0x1a)
+ t->state = ST_GROUND;
+ else if (ch == 0x1b)
+ t->state = ST_IGNORE_ESC;
+ break;
+ case ST_IGNORE_ESC:
+ if (ch == '\\' || ch == 0x07 || ch == 0x18 || ch == 0x1a)
+ t->state = ST_GROUND;
+ else {
+ t->state = ST_ESC;
+ term_escape_byte(t, ch);
+ }
+ break;
+ case ST_OSC:
+ if (ch == 0x07) {
+ term_osc_finish(t);
+ t->state = ST_GROUND;
+ } else if (ch == 0x1b) {
+ t->state = ST_STR_ESC;
+ } else if (ch < 0x20) {
+ t->osc_len = 0;
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ } else if (term_osc_room(t) || term_osc_grow(t)) {
+ *term_osc_tail(t) = (char)ch;
+ term_osc_commit(t, 1);
+ } else {
+ t->str_truncated = true;
+ }
+ break;
+ case ST_DCS:
+ if (ch == 0x07) {
+ dcs_dispatch(t);
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ } else if (ch == 0x1b) {
+ t->state = ST_DCS_ESC;
+ } else if (ch < 0x20) {
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ } else if (dcs_reserve(t, 1)) {
+ t->dcs_buf[t->dcs_len++] = (char)ch;
+ } else {
+ t->str_truncated = true;
+ }
+ break;
+ case ST_DCS_ESC:
+ if (ch == '\\') {
+ dcs_dispatch(t);
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ } else {
+ t->dcs_len = 0;
+ t->state = ST_ESC;
+ term_escape_byte(t, ch);
+ }
+ break;
+ case ST_STR_ESC:
+ if (ch == '\\') {
+ term_osc_finish(t);
+ t->state = ST_GROUND;
+ } else {
+ t->osc_len = 0;
+ t->dcs_len = 0;
+ t->state = ST_ESC;
+ term_escape_byte(t, ch);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static inline size_t str_appendable_span(const char *buf, size_t len)
+{
+ return scan_printable_span(buf, len, 0x80);
+}
+
+size_t term_escape_str(struct term *t, const char *buf, size_t len)
+{
+ if (t->state == ST_IGNORE) {
+ size_t take = str_appendable_span(buf, len);
+ if (take < len)
+ term_escape_byte(t, (unsigned char)buf[take++]);
+ return take;
+ }
+ int is_osc = t->state == ST_OSC;
+ size_t take = str_appendable_span(buf, len);
+ if (is_osc) {
+ size_t room = term_osc_room(t);
+ if (take > room && term_osc_grow(t))
+ room = term_osc_room(t);
+ if (room) {
+ if (take > room)
+ take = room;
+ memcpy(term_osc_tail(t), buf, take);
+ term_osc_commit(t, take);
+ } else {
+ t->str_truncated = true;
+ }
+ } else {
+ dcs_reserve(t, take);
+ size_t room = t->dcs_cap - (size_t)t->dcs_len;
+ if (room) {
+ if (take > room)
+ take = room;
+ memcpy(t->dcs_buf + t->dcs_len, buf, take);
+ t->dcs_len += (int)take;
+ } else {
+ t->str_truncated = true;
+ }
+ }
+ if (take < len)
+ term_escape_byte(t, (unsigned char)buf[take++]);
+ return take;
+}
+
+size_t term_escape_csi(struct term *t, const char *buf, size_t len)
+{
+ size_t consumed;
+ enum csi_result result = csi_read(&t->csi_parser, buf, len, &consumed);
+ if (result == CSI_CONTROL) {
+ term_escape_byte(t, (unsigned char)buf[consumed++]);
+ } else if (result == CSI_DISPATCH || result == CSI_DISCARD) {
+ if (t->cb.trace) {
+ const struct csi *c = &t->csi_parser.csi;
+ char params[256] = "-1";
+ if (c->nparams) {
+ size_t n = 0;
+ for (int j = 0; j < c->nparams && n < sizeof params; j++)
+ n += (size_t)snprintf(params + n, sizeof params - n,
+ j ? ";%d" : "%d", c->params[j]);
+ }
+ esc_tracef(t, " CSI complete: %s%c p=[%s] prefix='%c' inter='%c'",
+ result == CSI_DISCARD ? "INVALID " : "", c->final, params,
+ c->prefix ? c->prefix : ' ', c->intermediate ? c->intermediate : ' ');
+ }
+ if (result == CSI_DISPATCH)
+ dispatch_csi(t, &t->csi_parser.csi);
+ t->state = ST_GROUND;
+ }
+ return consumed;
+}
+
+void term_escape_begin(struct term *t)
+{
+ ESC_TRACE(t, "0x1b → ST_ESC");
+ t->state = ST_ESC;
+}
diff --git a/src/escape.h b/src/escape.h
new file mode 100644
index 0000000..a66d02c
--- /dev/null
+++ b/src/escape.h
@@ -0,0 +1,10 @@
+#ifndef TERM_ESCAPE_H
+#define TERM_ESCAPE_H
+#include "internal.h"
+
+void term_escape_begin(struct term *t);
+void term_escape_byte(struct term *t, unsigned char ch);
+size_t term_escape_str(struct term *t, const char *buf, size_t len);
+size_t term_escape_csi(struct term *t, const char *buf, size_t len);
+
+#endif
diff --git a/src/fastcsi.c b/src/fastcsi.c
new file mode 100644
index 0000000..d8983dc
--- /dev/null
+++ b/src/fastcsi.c
@@ -0,0 +1,378 @@
+#include "compat.h"
+#include "core.h"
+#include "kernels.h"
+#include "pen.h"
+#include "screen.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+TERM_ALWAYS_INLINE
+static inline const char *parse_color_sequence(const char *p, const char *end, uint32_t *color)
+{
+ const char *s = p + 5;
+ uint32_t r, g, b;
+
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ unsigned char c0 = (unsigned char)*s++;
+ unsigned char d0 = (unsigned char)(c0 - '0');
+ if (TERM_EXPECT(d0 > 9, 0)) return NULL;
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ unsigned char c1 = (unsigned char)*s++;
+ if (c1 == ';') {
+ r = d0;
+ } else {
+ unsigned char d1 = (unsigned char)(c1 - '0');
+ if (TERM_EXPECT(d1 > 9, 0)) return NULL;
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ unsigned char c2 = (unsigned char)*s++;
+ if (c2 == ';') {
+ r = d0 * 10 + d1;
+ } else {
+ unsigned char d2 = (unsigned char)(c2 - '0');
+ if (TERM_EXPECT(d2 > 9 || s >= end || *s++ != ';', 0)) return NULL;
+ r = d0 * 100 + d1 * 10 + d2;
+ if (TERM_EXPECT(r > 255, 0)) return NULL;
+ }
+ }
+
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ c0 = (unsigned char)*s++;
+ d0 = (unsigned char)(c0 - '0');
+ if (TERM_EXPECT(d0 > 9, 0)) return NULL;
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ c1 = (unsigned char)*s++;
+ if (c1 == ';') {
+ g = d0;
+ } else {
+ unsigned char d1 = (unsigned char)(c1 - '0');
+ if (TERM_EXPECT(d1 > 9, 0)) return NULL;
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ unsigned char c2 = (unsigned char)*s++;
+ if (c2 == ';') {
+ g = d0 * 10 + d1;
+ } else {
+ unsigned char d2 = (unsigned char)(c2 - '0');
+ if (TERM_EXPECT(d2 > 9 || s >= end || *s++ != ';', 0)) return NULL;
+ g = d0 * 100 + d1 * 10 + d2;
+ if (TERM_EXPECT(g > 255, 0)) return NULL;
+ }
+ }
+
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ c0 = (unsigned char)*s++;
+ d0 = (unsigned char)(c0 - '0');
+ if (TERM_EXPECT(d0 > 9, 0)) return NULL;
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ c1 = (unsigned char)*s++;
+ if (c1 == 'm') {
+ b = d0;
+ } else {
+ unsigned char d1 = (unsigned char)(c1 - '0');
+ if (TERM_EXPECT(d1 > 9, 0)) return NULL;
+ if (TERM_EXPECT(s >= end, 0)) return NULL;
+ unsigned char c2 = (unsigned char)*s++;
+ if (c2 == 'm') {
+ b = d0 * 10 + d1;
+ } else {
+ unsigned char d2 = (unsigned char)(c2 - '0');
+ if (TERM_EXPECT(d2 > 9 || s >= end || *s++ != 'm', 0)) return NULL;
+ b = d0 * 100 + d1 * 10 + d2;
+ if (TERM_EXPECT(b > 255, 0)) return NULL;
+ }
+ }
+
+ *color = term_rgb((int)r, (int)g, (int)b);
+ return s;
+}
+
+static const char *parse_256color(const char *p, const char *end, uint32_t *color)
+{
+ const char *q = p + 2;
+ unsigned v = 0;
+ int nd = 0;
+ while (q < end && (unsigned char)(*q - '0') <= 9) {
+ v = v * 10 + (unsigned char)(*q++ - '0');
+ if (++nd > 3)
+ return NULL;
+ }
+ if (!nd || q >= end || *q != 'm' || v > 255)
+ return NULL;
+ *color = term_color_index(v);
+ return q + 1;
+}
+
+TERM_NOINLINE
+int term_try_fast_csi(struct term *t, const char *buf, size_t rem)
+{
+ const char *end = buf + rem;
+ const char *p = buf;
+ const char *esc = buf;
+ const int cols = t->cols;
+ const int rows = t->rows;
+ uint32_t fg = t->cur_fg;
+ uint32_t bg = t->cur_bg;
+ uint32_t flags_pad = 0;
+ bool flags_ok = false;
+
+ while (p < end) {
+ if (p + 5 <= end && (p[0] == '3' || p[0] == '4') && p[1] == '8' &&
+ p[2] == ';' && (p[3] == '2' || p[3] == '5') && p[4] == ';') {
+ bool is_fg = p[0] == '3';
+ uint32_t *slot = is_fg ? &fg : &bg;
+ uint32_t *other = is_fg ? &bg : &fg;
+ const char *next = p[3] == '2' ? parse_color_sequence(p, end, slot)
+ : parse_256color(p + 3, end, slot);
+ if (!next) {
+ p = esc;
+ break;
+ }
+ p = next;
+ if (p + 7 <= end && p[0] == '\033' && p[1] == '[' &&
+ p[2] == (is_fg ? '4' : '3') && p[3] == '8' &&
+ p[4] == ';' && (p[5] == '2' || p[5] == '5') && p[6] == ';') {
+ next = p[5] == '2' ? parse_color_sequence(p + 2, end, other)
+ : parse_256color(p + 5, end, other);
+ if (!next) break;
+ p = next;
+ }
+ if (p < end && (unsigned char)*p >= 0x20 && (unsigned char)*p < 0x7f) {
+ char ch = *p++;
+ if (TERM_EXPECT(!flags_ok, 0)) {
+ flags_pad = term_flags_pad(t);
+ flags_ok = true;
+ }
+ if (TERM_EXPECT(!sc_put1_fast(t, ch, fg, bg, flags_pad), 0)) {
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_print_ascii(t, &ch, 1);
+ }
+ if (p + 2 <= end && p[0] == '\033' && p[1] == '[') {
+ esc = p;
+ p += 2;
+ continue;
+ }
+ }
+ } else if (p[0] == 'm' || (p[0] == '0' && p + 1 < end && p[1] == 'm')) {
+ reset_sgr(t);
+ fg = t->cur_fg;
+ bg = t->cur_bg;
+ /* Recompute instead of zeroing. The cached flags carry the link
+ * id, and reset_sgr leaves cur_link set, so zeroing here would
+ * drop the hyperlink from every cell written after the reset. */
+ flags_pad = term_flags_pad(t);
+ flags_ok = true;
+ p += p[0] == 'm' ? 1 : 2;
+ } else if (p[0] == 'J' ||
+ (p[0] >= '0' && p[0] <= '3' && p + 1 < end && p[1] == 'J')) {
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_erase_display(t, p[0] == 'J' ? 0 : p[0] - '0');
+ p += p[0] == 'J' ? 1 : 2;
+ } else if (p[0] == 'K' ||
+ (p[0] >= '0' && p[0] <= '2' && p + 1 < end && p[1] == 'K')) {
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_erase_line(t, p[0] == 'K' ? 0 : p[0] - '0');
+ p += p[0] == 'K' ? 1 : 2;
+ } else if (!t->origin_mode && !t->lr_margin_mode &&
+ ((*p >= '0' && *p <= '9') || *p == ';' ||
+ *p == 'A' || *p == 'B' || *p == 'C' || *p == 'D' ||
+ *p == 'E' || *p == 'F' || *p == 'G' || *p == '`' ||
+ *p == 'a' || *p == 'd' || *p == 'e' ||
+ *p == 'H' || *p == 'f' || *p == 'm' ||
+ *p == 'J' || *p == 'K' || *p == 'X' ||
+ *p == 'P' || *p == '@' || *p == 's' || *p == 'u')) {
+ const char *q = p;
+ int pv[12] = { 0 };
+ int np = 0;
+ while (q < end && ((*q >= '0' && *q <= '9') || *q == ';')) {
+ if (*q == ';') {
+ if (np == 11) { q = NULL; break; }
+ np++;
+ q++;
+ continue;
+ }
+ if (pv[np] < 100000)
+ pv[np] = pv[np] * 10 + (*q - '0');
+ q++;
+ }
+ if (!q || q >= end) {
+ p = esc;
+ break;
+ }
+ int nparams = q > p ? np + 1 : 0;
+ int n = pv[0] > 0 ? pv[0] : 1;
+ int nx = t->cursor_x, ny = t->cursor_y;
+ switch (*q) {
+ case 'A': ny -= n; goto vmove;
+ case 'B': case 'e': ny += n; goto vmove;
+ case 'E': ny += n; nx = 0; goto vmove;
+ case 'F': ny -= n; nx = 0; goto vmove;
+ vmove:
+ if (TERM_EXPECT(t->scroll_top != 0 || t->scroll_bottom != rows - 1, 0)) {
+ p = esc;
+ goto done;
+ }
+ goto move;
+ case 'C': case 'a': nx += n; goto move;
+ case 'D': nx -= n; goto move;
+ case 'G': case '`': nx = n - 1; goto move;
+ case 'd': ny = n - 1; goto move;
+ case 'H': case 'f':
+ ny = n - 1;
+ nx = np && pv[1] > 0 ? pv[1] - 1 : 0;
+ goto move;
+ move:
+ sc_move(t, nx, ny);
+ p = q + 1;
+ break;
+ case 'm': {
+ for (int j = 0; j < nparams; j++) {
+ int v = pv[j];
+ switch (v) {
+ case 0: reset_sgr(t); fg = t->cur_fg; bg = t->cur_bg; break;
+ case 1: t->bold = true; break;
+ case 2: t->faint = true; break;
+ case 3: t->italic = true; break;
+ case 4: t->underline = 1; break;
+ case 7: t->inverse = true; break;
+ case 9: t->strike = true; break;
+ case 21: t->underline = 2; break;
+ case 22: t->bold = false; t->faint = false; break;
+ case 23: t->italic = false; break;
+ case 24: t->underline = 0; break;
+ case 27: t->inverse = false; break;
+ case 29: t->strike = false; break;
+ case 38: case 48: case 58: {
+ uint32_t *slot = v == 38 ? &fg : v == 48 ? &bg : &t->cur_ul;
+ if (j + 2 < nparams && pv[j + 1] == 5) {
+ if (pv[j + 2] >= 0 && pv[j + 2] <= 255)
+ *slot = term_color_index(pv[j + 2]);
+ j += 2;
+ } else if (j + 1 < nparams && pv[j + 1] == 2 &&
+ j + 4 < nparams) {
+ *slot = term_rgb(pv[j + 2], pv[j + 3], pv[j + 4]);
+ j += 4;
+ }
+ break;
+ }
+ case 39: fg = TERM_COLOR_FG; break;
+ case 49: bg = TERM_COLOR_BG; break;
+ case 53: t->overline = true; break;
+ case 55: t->overline = false; break;
+ case 59: t->cur_ul = 0; break;
+ default:
+ if ((unsigned)(v - 30) < 8) fg = term_color_index(v - 30);
+ else if ((unsigned)(v - 40) < 8) bg = term_color_index(v - 40);
+ else if ((unsigned)(v - 90) < 8) fg = term_color_index(v - 90 + 8);
+ else if ((unsigned)(v - 100) < 8) bg = term_color_index(v - 100 + 8);
+ break;
+ }
+ }
+ flags_ok = false;
+ p = q + 1;
+ break;
+ }
+ case 'J':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_erase_display(t, pv[0]);
+ p = q + 1;
+ break;
+ case 'K':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_erase_line(t, pv[0]);
+ p = q + 1;
+ break;
+ case 'X':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_erase_span(t, t->cursor_y, t->cursor_x,
+ n > cols - t->cursor_x ? cols : t->cursor_x + n);
+ p = q + 1;
+ break;
+ case 'P':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_delete_chars(t, n);
+ p = q + 1;
+ break;
+ case '@':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_insert_chars(t, n);
+ p = q + 1;
+ break;
+ case 's':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ t->saved = term_cursor_save(t);
+ p = q + 1;
+ break;
+ case 'u':
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ sc_restore_cursor(t, &t->saved);
+ fg = t->cur_fg;
+ bg = t->cur_bg;
+ flags_ok = false;
+ p = q + 1;
+ if (t->origin_mode || t->graphics)
+ goto done;
+ break;
+ default:
+ p = esc;
+ goto done;
+ }
+ } else {
+ p = esc;
+ break;
+ }
+tail:
+ if (p < end && (unsigned char)(*p - 0x20) < 0x5f && !t->pending_wrap) {
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ int avail = cols - t->cursor_x;
+ int limit = end - p < (ptrdiff_t)avail ? (int)(end - p) : avail;
+ int run = scan_ascii_run(p, limit);
+ if (TERM_EXPECT(!flags_ok, 0)) {
+ flags_pad = term_flags_pad(t);
+ flags_ok = true;
+ }
+ if (!sc_ascii_fast(t, p, run, fg, bg, flags_pad))
+ sc_print_ascii(t, p, run);
+ p += run;
+ }
+ if (p + 2 <= end && p[0] == '\033' && p[1] == '[') {
+ esc = p;
+ p += 2;
+ continue;
+ }
+ if (p < end && *p == '\r') {
+ sc_cr(t);
+ p++;
+ goto tail;
+ }
+ if (p < end && *p == '\n') {
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ if (term_lnm(t)) sc_cr(t);
+ sc_linefeed(t, 1);
+ p++;
+ goto tail;
+ }
+ break;
+ }
+done:
+ if (p == end && esc != buf && esc == p - 2)
+ p = esc;
+ t->cur_fg = fg;
+ t->cur_bg = bg;
+ if (p > buf)
+ return (int)(p - buf);
+ return 0;
+}
diff --git a/src/gstore.c b/src/gstore.c
new file mode 100644
index 0000000..c5109c6
--- /dev/null
+++ b/src/gstore.c
@@ -0,0 +1,380 @@
+#include "gstore.h"
+#include "internal.h"
+#include "screen.h"
+#include "unicode/grapheme.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#define GSTORE_MAX_ENTRIES 0x00FFFFFFu
+
+struct gentry {
+ union {
+ uint32_t offset; /* Offset into arena for code points past index 1 */
+ uint32_t next_free; /* Next entry in free list (1-based, 0 = none) */
+ };
+ uint16_t len; /* Number of code points */
+ bool mark; /* Reachable in current GC sweep */
+ bool allocated; /* True if actively holding a cluster */
+ uint32_t cp0; /* First code point (inlined) */
+ uint32_t cp1; /* Second code point (inlined) */
+};
+
+_Static_assert(sizeof(struct gentry) == 16,
+ "struct gentry must stay 16 bytes (4 entries per cache line)");
+
+struct gslot {
+ uint32_t hash; /* 32-bit hash of cluster code points */
+ uint32_t entry_idx; /* 1-based index into g->entries (0 = empty) */
+};
+
+struct gstore {
+ struct term *term;
+ uint32_t *arena;
+ size_t arena_len;
+ size_t arena_cap;
+
+ struct gentry *entries;
+ uint32_t entries_count;
+ uint32_t entries_cap;
+ uint32_t free_head; /* 1-based index, 0 = none */
+
+ uint32_t live_count;
+ uint32_t last_live;
+ uint32_t sweep_threshold;
+ uint32_t pin_depth;
+
+ struct gslot *slots;
+ size_t slot_count;
+};
+
+static uint64_t hash_cps(const uint32_t *cps, int len)
+{
+ uint64_t h = HASH64_BASIS;
+ for (int i = 0; i < len; i++)
+ h = hash64_step(h, cps[i]);
+ return hash64_finish(h);
+}
+
+static inline bool gentry_matches(const struct gstore *g, const struct gentry *e,
+ const uint32_t *cps, int n)
+{
+ if ((int)e->len != n)
+ return false;
+ if (e->cp0 != cps[0] || e->cp1 != cps[1])
+ return false;
+ if (n == 2)
+ return true;
+ return !memcmp(&g->arena[e->offset], &cps[2], (size_t)(n - 2) * sizeof(*cps));
+}
+
+int term_gstore_expand(const struct gstore *g, uint32_t handle, uint32_t *cps, int cap)
+{
+ if (cap <= 0)
+ return 0;
+ if (!g || !grapheme_is_handle(handle)) {
+ cps[0] = handle;
+ return 1;
+ }
+ uint32_t idx = handle - GRAPHEME_HANDLE_BASE;
+ if (idx >= g->entries_count || !g->entries[idx].allocated) {
+ cps[0] = 0xFFFD;
+ return 1;
+ }
+ const struct gentry *e = &g->entries[idx];
+ int n = (int)e->len;
+ if (n > cap) {
+ cps[0] = 0xFFFD;
+ return 1;
+ }
+ cps[0] = e->cp0;
+ cps[1] = e->cp1;
+ if (n > 2)
+ memcpy(&cps[2], &g->arena[e->offset], (size_t)(n - 2) * sizeof(*cps));
+ return n;
+}
+
+static void gstore_insert_slot(struct gstore *g, uint32_t h32, uint32_t entry_idx)
+{
+ uint32_t mask = (uint32_t)g->slot_count - 1;
+ uint32_t idx = h32 & mask;
+ for (uint32_t probe = 0; probe <= mask; probe++) {
+ struct gslot *slot = &g->slots[idx];
+ if (slot->entry_idx == 0) {
+ slot->hash = h32;
+ slot->entry_idx = entry_idx + 1;
+ return;
+ }
+ idx = (idx + 1) & mask;
+ }
+}
+
+static bool grow_slots(struct gstore *g)
+{
+ size_t want = g->slot_count ? g->slot_count * 2 : 256;
+ struct gslot *next = calloc(want, sizeof(*next));
+ if (!next)
+ return false;
+
+ free(g->slots);
+ g->slots = next;
+ g->slot_count = want;
+
+ for (uint32_t i = 0; i < g->entries_count; i++) {
+ struct gentry *e = &g->entries[i];
+ if (!e->allocated)
+ continue;
+ uint32_t cps_buf[GRAPHEME_MAX_LEN];
+ int n = term_gstore_expand(g, GRAPHEME_HANDLE_BASE + i, cps_buf, GRAPHEME_MAX_LEN);
+ uint64_t h = hash_cps(cps_buf, n);
+ uint32_t h32 = (uint32_t)h ? (uint32_t)h : 1;
+ gstore_insert_slot(g, h32, i);
+ }
+ return true;
+}
+
+static uint32_t gstore_find(const struct gstore *g, const uint32_t *cps, int n, uint32_t h32)
+{
+ if (!g->slots || g->slot_count == 0)
+ return 0;
+ uint32_t mask = (uint32_t)g->slot_count - 1;
+ uint32_t idx = h32 & mask;
+ for (uint32_t probe = 0; probe <= mask; probe++) {
+ const struct gslot *slot = &g->slots[idx];
+ if (slot->entry_idx == 0)
+ return 0;
+ if (slot->hash == h32) {
+ uint32_t e_idx = slot->entry_idx - 1;
+ const struct gentry *e = &g->entries[e_idx];
+ if (e->allocated && gentry_matches(g, e, cps, n))
+ return GRAPHEME_HANDLE_BASE + e_idx;
+ }
+ idx = (idx + 1) & mask;
+ }
+ return 0;
+}
+
+struct gstore *term_gstore_new(struct term *t)
+{
+ struct gstore *g = calloc(1, sizeof(*g));
+ if (!g)
+ return NULL;
+ g->term = t;
+ g->sweep_threshold = 1024;
+ return g;
+}
+
+void term_gstore_free(struct gstore *g)
+{
+ if (!g)
+ return;
+ free(g->arena);
+ free(g->entries);
+ free(g->slots);
+ free(g);
+}
+
+uint32_t term_gstore_count(const struct gstore *g)
+{
+ return g ? g->live_count : 0;
+}
+
+void term_gstore_pin(struct gstore *g)
+{
+ if (g)
+ g->pin_depth++;
+}
+
+void term_gstore_unpin(struct gstore *g)
+{
+ if (g && g->pin_depth > 0)
+ g->pin_depth--;
+}
+
+void term_gstore_mark(struct gstore *g, uint32_t handle)
+{
+ if (!g || !grapheme_is_handle(handle))
+ return;
+ uint32_t idx = handle - GRAPHEME_HANDLE_BASE;
+ if (idx < g->entries_count && g->entries[idx].allocated)
+ g->entries[idx].mark = true;
+}
+
+static void sweep_mark_row(struct gstore *g, const struct cell *cells, int len)
+{
+ for (int x = 0; x < len; x++) {
+ uint32_t c = cells[x].c;
+ if (grapheme_is_handle(c))
+ term_gstore_mark(g, c);
+ }
+}
+
+void term_gstore_sweep(struct gstore *g, struct term *t)
+{
+ if (!g)
+ return;
+
+ if (t) {
+ if (grapheme_is_handle(t->last_char))
+ term_gstore_mark(g, t->last_char);
+
+ struct screen *s = &t->scr;
+ for (int y = 0; y < s->rows; y++) {
+ struct srow *row = screen_get_row(s, y);
+ if (row && row->kind == ROW_CELLS && row->len > 0)
+ sweep_mark_row(g, row->cells, (int)row->len);
+ }
+ if (t->alternate && t->alt_allocated) {
+ struct screen *as = &t->alt_scr;
+ for (int y = 0; y < as->rows; y++) {
+ struct srow *row = screen_get_row(as, y);
+ if (row && row->kind == ROW_CELLS && row->len > 0)
+ sweep_mark_row(g, row->cells, (int)row->len);
+ }
+ }
+ }
+
+ /* Compact the code point arena for clusters with n > 2. */
+ uint32_t new_arena_len = 0;
+ uint32_t *packed = g->arena ? malloc(g->arena_cap * sizeof(*packed)) : NULL;
+ if (packed) {
+ for (uint32_t i = 0; i < g->entries_count; i++) {
+ struct gentry *e = &g->entries[i];
+ if (e->allocated && e->mark && e->len > 2) {
+ size_t extra = (size_t)(e->len - 2);
+ memcpy(&packed[new_arena_len], &g->arena[e->offset],
+ extra * sizeof(*packed));
+ e->offset = new_arena_len;
+ new_arena_len += (uint32_t)extra;
+ }
+ }
+ free(g->arena);
+ g->arena = packed;
+ g->arena_len = new_arena_len;
+ }
+
+ /* Rebuild open-addressing slot table and free list. */
+ g->free_head = 0;
+ uint32_t live = 0;
+ if (g->slots)
+ memset(g->slots, 0, g->slot_count * sizeof(*g->slots));
+
+ for (uint32_t i = 0; i < g->entries_count; i++) {
+ struct gentry *e = &g->entries[i];
+ if (e->allocated) {
+ if (e->mark) {
+ live++;
+ e->mark = false;
+ if (g->slots) {
+ uint32_t cps_buf[GRAPHEME_MAX_LEN];
+ int n = term_gstore_expand(g, GRAPHEME_HANDLE_BASE + i, cps_buf, GRAPHEME_MAX_LEN);
+ uint64_t h = hash_cps(cps_buf, n);
+ uint32_t h32 = (uint32_t)h ? (uint32_t)h : 1;
+ gstore_insert_slot(g, h32, i);
+ }
+ } else {
+ e->allocated = false;
+ e->len = 0;
+ e->offset = 0;
+ e->cp0 = 0;
+ e->cp1 = 0;
+ e->next_free = g->free_head;
+ g->free_head = i + 1;
+ }
+ } else {
+ e->next_free = g->free_head;
+ g->free_head = i + 1;
+ }
+ }
+
+ g->live_count = live;
+ g->last_live = live;
+ g->sweep_threshold = live * 2;
+ if (g->sweep_threshold < 1024)
+ g->sweep_threshold = 1024;
+}
+
+uint32_t term_gstore_intern(struct gstore *g, const uint32_t *cps, int n)
+{
+ if (!g || n <= 0)
+ return 0;
+ if (n == 1)
+ return cps[0];
+ if (n > GRAPHEME_MAX_LEN)
+ n = GRAPHEME_MAX_LEN;
+
+ uint64_t h = hash_cps(cps, n);
+ uint32_t h32 = (uint32_t)h ? (uint32_t)h : 1;
+
+ uint32_t existing = gstore_find(g, cps, n, h32);
+ if (existing)
+ return existing;
+
+ if (g->term && g->pin_depth == 0 &&
+ (g->live_count >= g->sweep_threshold ||
+ g->live_count >= GSTORE_MAX_ENTRIES)) {
+ term_gstore_sweep(g, g->term);
+ existing = gstore_find(g, cps, n, h32);
+ if (existing)
+ return existing;
+ }
+
+ if (!g->slots || (g->live_count + 1) * 2 > g->slot_count) {
+ if (!grow_slots(g) && !g->slots)
+ return cps[0];
+ }
+
+ uint32_t entry_idx;
+ if (g->free_head) {
+ entry_idx = g->free_head - 1;
+ g->free_head = g->entries[entry_idx].next_free;
+ } else {
+ if (g->entries_count >= GSTORE_MAX_ENTRIES)
+ return cps[0];
+ if (g->entries_count == g->entries_cap) {
+ size_t new_cap = g->entries_cap ? (size_t)g->entries_cap * 2 : 128;
+ if (new_cap > GSTORE_MAX_ENTRIES)
+ new_cap = GSTORE_MAX_ENTRIES;
+ struct gentry *next = realloc(g->entries, new_cap * sizeof(*next));
+ if (!next)
+ return cps[0];
+ g->entries = next;
+ g->entries_cap = (uint32_t)new_cap;
+ }
+ entry_idx = g->entries_count++;
+ }
+
+ struct gentry *e = &g->entries[entry_idx];
+ e->cp0 = cps[0];
+ e->cp1 = cps[1];
+ e->len = (uint16_t)n;
+ e->mark = false;
+ e->allocated = true;
+ e->offset = 0;
+
+ if (n > 2) {
+ size_t extra = (size_t)(n - 2);
+ if (g->arena_len + extra > g->arena_cap) {
+ size_t new_cap = g->arena_cap ? g->arena_cap * 2 : 512;
+ while (new_cap < g->arena_len + extra)
+ new_cap *= 2;
+ uint32_t *next = realloc(g->arena, new_cap * sizeof(*next));
+ if (!next) {
+ e->allocated = false;
+ e->next_free = g->free_head;
+ g->free_head = entry_idx + 1;
+ return cps[0];
+ }
+ g->arena = next;
+ g->arena_cap = new_cap;
+ }
+ uint32_t off = (uint32_t)g->arena_len;
+ memcpy(&g->arena[off], &cps[2], extra * sizeof(*cps));
+ g->arena_len += extra;
+ e->offset = off;
+ }
+
+ gstore_insert_slot(g, h32, entry_idx);
+ g->live_count++;
+ return GRAPHEME_HANDLE_BASE + entry_idx;
+}
diff --git a/src/gstore.h b/src/gstore.h
new file mode 100644
index 0000000..0cedd30
--- /dev/null
+++ b/src/gstore.h
@@ -0,0 +1,36 @@
+#ifndef WSAP_TERM_GSTORE_H
+#define WSAP_TERM_GSTORE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+struct gstore;
+struct term;
+
+/* Create a grapheme store for a terminal. */
+struct gstore *term_gstore_new(struct term *t);
+
+/* Free the grapheme store and its memory. */
+void term_gstore_free(struct gstore *g);
+
+/* Store a cluster of code points. Return its handle. */
+uint32_t term_gstore_intern(struct gstore *g, const uint32_t *cps, int n);
+
+/* Expand a handle to its code points. Return the count. */
+int term_gstore_expand(const struct gstore *g, uint32_t handle, uint32_t *cps, int cap);
+
+/* Mark reachable clusters and free unused ones. */
+void term_gstore_sweep(struct gstore *g, struct term *t);
+
+/* Mark one cluster handle as reachable. */
+void term_gstore_mark(struct gstore *g, uint32_t handle);
+
+/* Return the count of active clusters in the store. */
+uint32_t term_gstore_count(const struct gstore *g);
+
+/* Suppress automatic GC sweeps while temporary cell buffers hold handles. */
+void term_gstore_pin(struct gstore *g);
+void term_gstore_unpin(struct gstore *g);
+
+#endif
diff --git a/src/hist.c b/src/hist.c
new file mode 100644
index 0000000..123ed7b
--- /dev/null
+++ b/src/hist.c
@@ -0,0 +1,3256 @@
+#include "hist.h"
+#include "compat.h"
+#include "kernels.h"
+#include "unicode/grapheme.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+/*
+ * Text format. A NARROW part is raw ASCII, one byte per cell. Other parts are
+ * UTF-8 with prefix bytes that UTF-8 never uses:
+ * 0xF5 cp wide
+ * 0xF6 cp wide, emoji presentation
+ * 0xF7 cp narrow, text presentation
+ * 0xF8 k cp... cluster of k & 0x1f code points; k & 0x80 wide,
+ * k & 0x40 emoji, k & 0x20 text presentation
+ * Trail cells and wrap pads are not stored.
+ *
+ * Attribute runs follow the text. A run is a header byte that names the
+ * fields that differ from the previous run, a LEB128 cell count, then those
+ * fields. Cells after the last run use the default pen.
+ */
+
+#define RUN_FG (1u << 0)
+#define RUN_BG (1u << 1)
+#define RUN_ATTR (1u << 2)
+#define RUN_LINK (1u << 3)
+#define RUN_MAX 18
+
+#define PREFIX_WIDE 0xF5
+#define PREFIX_EMOJI 0xF6
+#define PREFIX_TEXT 0xF7
+#define PREFIX_CLUSTER 0xF8
+
+#define STYLE_FLAGS (CELL_BOLD | CELL_ITALIC | CELL_ANY_UNDERLINE | CELL_STRIKE | \
+ CELL_OVERLINE | CELL_FAINT | CELL_LINK)
+#define SHAPE_FLAGS (CELL_WIDE | CELL_TRAIL | CELL_WRAP_PAD | CELL_EMOJI | CELL_TEXT)
+
+#define NO_TEXT UINT32_MAX
+#define ROWS_UNKNOWN 255
+/* Bytes one cell can need, except a cluster. */
+#define CELL_BYTES 5
+#define CLUSTER_BYTES (2 + GRAPHEME_MAX_LEN * 4)
+
+/* ---- Parts and pages ---- */
+
+static inline struct hline *part(const struct hist *h, uint64_t seq)
+{
+ return &h->ring[seq & h->ring_mask];
+}
+
+static inline uint8_t *part_text(const struct hist *h, const struct hline *p)
+{
+ return h->pages[p->off >> 16] + (p->off & 0xffff);
+}
+
+static inline struct hline *open_part(const struct hist *h)
+{
+ return hist_is_open(h) ? part(h, h->head - 1) : NULL;
+}
+
+/* Grow the ring so n more parts fit. The caller checks the fast path first. */
+static TERM_NOINLINE bool ring_grow(struct hist *h, uint64_t n)
+{
+ uint64_t cap = h->ring ? (uint64_t)h->ring_mask + 1 : 65536;
+ if (cap < 65536)
+ cap = 65536;
+ while (cap < h->head - h->tail + n)
+ cap *= 2;
+ struct hline *ring = term_aligned_alloc(64, (size_t)cap * sizeof(*ring));
+ if (!ring)
+ return false;
+ struct hist_block *blocks = calloc((size_t)cap / 16, sizeof(*blocks));
+ if (!blocks) {
+ term_aligned_free(ring);
+ return false;
+ }
+ uint32_t *exact = h->exact_rows ? calloc((size_t)cap, sizeof(*exact)) : NULL;
+ if (h->exact_rows && !exact) {
+ free(blocks);
+ term_aligned_free(ring);
+ return false;
+ }
+ for (uint64_t s = h->tail; s < h->head; s++) {
+ ring[s & (cap - 1)] = *part(h, s);
+ if (exact)
+ exact[s & (cap - 1)] = h->exact_rows[s & h->ring_mask];
+ }
+ free(h->exact_rows);
+ h->exact_rows = exact;
+ term_aligned_free(h->ring);
+ free(h->blocks);
+ h->blocks = blocks;
+ h->ring = ring;
+ h->ring_mask = (uint32_t)(cap - 1);
+ return true;
+}
+
+/* Room for n more parts. The check runs on the write path per batch, so
+ * keep it inline and leave only the rehash to a call. */
+static inline bool ring_fit(struct hist *h, uint64_t n)
+{
+ if (TERM_EXPECT(h->ring && h->head - h->tail + n <= (uint64_t)h->ring_mask + 1, 1))
+ return true;
+ return ring_grow(h, n);
+}
+
+static inline bool ring_reserve(struct hist *h)
+{
+ return ring_fit(h, 1);
+}
+
+static inline uint64_t cont_at(const struct hist *h, uint32_t i)
+{
+ return h->cont[(h->cont_first + i) & h->cont_mask];
+}
+
+static bool cont_push(struct hist *h, uint64_t seq)
+{
+ if (!h->cont || h->cont_count > h->cont_mask) {
+ uint32_t cap = h->cont ? (h->cont_mask + 1) * 2 : 16;
+ uint64_t *cont = malloc((size_t)cap * sizeof(*cont));
+ if (!cont)
+ return false;
+ for (uint32_t i = 0; i < h->cont_count; i++)
+ cont[i] = cont_at(h, i);
+ free(h->cont);
+ h->cont = cont;
+ h->cont_mask = cap - 1;
+ h->cont_first = 0;
+ }
+ h->cont[(h->cont_first + h->cont_count++) & h->cont_mask] = seq;
+ return true;
+}
+
+/* Keep the CONT list to the parts in (tail, head). */
+static void cont_trim(struct hist *h)
+{
+ while (h->cont_count && cont_at(h, 0) <= h->tail) {
+ h->cont_first = (h->cont_first + 1) & h->cont_mask;
+ h->cont_count--;
+ }
+ while (h->cont_count && cont_at(h, h->cont_count - 1) >= h->head)
+ h->cont_count--;
+}
+
+/* The first part of line id. Line starts are the parts without CONT, and the
+ * tail part, which always starts the base line. */
+static uint64_t line_first(const struct hist *h, uint64_t id)
+{
+ uint64_t want = h->tail + (id - h->base_id);
+ if (id == h->base_id || !h->cont_count)
+ return want;
+ uint32_t lo = 0, hi = h->cont_count;
+ while (lo < hi) {
+ uint32_t mid = lo + (hi - lo) / 2;
+ if (cont_at(h, mid) - mid <= want)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ return want + lo;
+}
+
+static inline uint64_t line_end(const struct hist *h, uint64_t first)
+{
+ uint64_t s = first + 1;
+ while (s < h->head && (part(h, s)->flags & HLINE_CONT))
+ s++;
+ return s;
+}
+
+static inline bool line_valid(const struct hist *h, uint64_t id)
+{
+ return id >= h->base_id && id < h->base_id + h->lines;
+}
+
+static TERM_ALWAYS_INLINE inline void page_drop_ref(struct hist *h, uint32_t pg)
+{
+ if (--h->page_parts[pg] == 0) {
+ if (pg == h->cur_page)
+ h->page_used[pg] = 0;
+ else
+ h->free_pages[h->nfree++] = (uint16_t)pg;
+ }
+}
+
+/* Take a counted line out of counted_rows. Call it while the line is whole. */
+static void rows_uncount(struct hist *h, uint64_t id)
+{
+ if (id < h->rows_id)
+ h->counted_rows -= term_hist_line_estimate(h, id, h->wrap_cols);
+}
+
+static void line_gone(struct hist *h)
+{
+ h->base_id++;
+ h->lines--;
+ if (h->rows_id < h->base_id)
+ h->rows_id = h->base_id;
+}
+
+/* Drop the tail part. A line whose later parts remain keeps its id. */
+static bool evict_part(struct hist *h)
+{
+ if (h->tail == h->head)
+ return false;
+ struct hline *p = part(h, h->tail);
+ if (p->flags & HLINE_OPEN)
+ return false;
+ bool cut = h->tail + 1 < h->head && (part(h, h->tail + 1)->flags & HLINE_CONT);
+ if (cut) {
+ /* The line loses its head: count every line again. */
+ h->index_epoch++;
+ if (h->base_cell_id != h->base_id) {
+ h->base_cell_id = h->base_id;
+ h->base_cell = 0;
+ }
+ h->base_cell += p->cells;
+ if (h->lines == 1) {
+ h->last_cells -= p->cells;
+ h->last_wide -= p->wide;
+ }
+ h->rows_id = h->base_id;
+ h->counted_rows = 0;
+ h->walk_on = false;
+ } else {
+ rows_uncount(h, h->base_id);
+ }
+ if (p->off != NO_TEXT)
+ page_drop_ref(h, p->off >> 16);
+ h->tail++;
+ if (cut)
+ cont_trim(h);
+ else
+ line_gone(h);
+ return true;
+}
+
+static TERM_NOINLINE void evict_line_parts(struct hist *h)
+{
+ uint64_t end = line_end(h, h->tail);
+ if (part(h, end - 1)->flags & HLINE_OPEN)
+ return;
+ rows_uncount(h, h->base_id);
+ for (uint64_t s = h->tail; s < end; s++)
+ if (part(h, s)->off != NO_TEXT)
+ page_drop_ref(h, part(h, s)->off >> 16);
+ h->tail = end;
+ cont_trim(h);
+ line_gone(h);
+}
+
+static void evict_line(struct hist *h);
+
+/* Drop n refs to one page at once. Same effect as n calls to
+ * page_drop_ref, because the count cannot pass zero. */
+static inline void page_drop_n(struct hist *h, uint32_t pg, uint32_t n)
+{
+ if ((h->page_parts[pg] -= n) == 0) {
+ if (pg == h->cur_page)
+ h->page_used[pg] = 0;
+ else
+ h->free_pages[h->nfree++] = (uint16_t)pg;
+ }
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline bool same_page_narrow_rows_avx512(const struct hline *p, uint32_t page)
+{
+ const __m512i pg = _mm512_set1_epi32((int)(page << 16));
+ const __m512i mask = _mm512_set1_epi32((int)0xffff0000u);
+ const __m512i tag = _mm512_set1_epi32(0x01010000);
+ for (int i = 0; i < 16; i += 4) {
+ __m512i v = _mm512_loadu_si512((const void *)(p + i));
+ if ((_mm512_cmpeq_epi32_mask(v, tag) & 0x8888) != 0x8888 ||
+ (_mm512_cmpeq_epi32_mask(_mm512_and_si512(v, mask), pg) & 0x1111) != 0x1111)
+ return false;
+ }
+ return true;
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline bool same_page_narrow_rows_avx2(const struct hline *p, uint32_t page)
+{
+ const __m256i pg = _mm256_set1_epi32((int)(page << 16));
+ const __m256i mask = _mm256_set1_epi32((int)0xffff0000u);
+ const __m256i tag = _mm256_set1_epi32(0x01010000);
+ for (int i = 0; i < 16; i += 2) {
+ __m256i v = _mm256_loadu_si256((const __m256i *)(p + i));
+ if ((_mm256_movemask_ps(_mm256_castsi256_ps(_mm256_cmpeq_epi32(v, tag))) & 0x88) != 0x88 ||
+ (_mm256_movemask_ps(_mm256_castsi256_ps(_mm256_cmpeq_epi32(_mm256_and_si256(v, mask), pg))) & 0x11) != 0x11)
+ return false;
+ }
+ return true;
+}
+#endif
+
+/* Check 16 narrow rows. Each descriptor has four words. */
+static inline bool same_page_narrow_rows(const struct hline *p, uint32_t page)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return same_page_narrow_rows_avx512(p, page);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return same_page_narrow_rows_avx2(p, page);
+#endif
+#if defined(__aarch64__)
+ const uint32x4_t expected = {page << 16, 0, 0, 0x01010000};
+ const uint32x4_t mask = {0xffff0000u, 0, 0, 0xffffffffu};
+ for (int i = 0; i < 16; i++) {
+ uint32x4_t v = vreinterpretq_u32_u8(vld1q_u8((const uint8_t *)(p + i)));
+ if (vminvq_u32(vceqq_u32(vandq_u32(v, mask), expected)) != UINT32_MAX)
+ return false;
+ }
+ return true;
+#else
+ for (int i = 0; i < 16; i++) {
+ if (p[i].off == NO_TEXT || (p[i].off >> 16) != page ||
+ p[i].flags != HLINE_NARROW || p[i].attr_len || p[i].wrap_rows != 1)
+ return false;
+ }
+ return true;
+#endif
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline bool same_page_rows_64(const struct hline *p, uint32_t page,
+ uint32_t *text_count, uint32_t *row_count)
+{
+ const __m512i pg = _mm512_set1_epi32((int)(page << 16));
+ const __m512i mask = _mm512_set1_epi32((int)0xffff0000u);
+ const __m512i open = _mm512_set1_epi32(0x00300000);
+ const __m512i unknown = _mm512_set1_epi32((int)0xff000000u);
+ const __m512i no_text = _mm512_set1_epi32(-1);
+ __m512i sums = _mm512_setzero_si512();
+ __mmask16 tag_fail = 0, pg_fail = 0;
+ uint32_t blanks = 0;
+ for (int i = 0; i < 64; i += 4) {
+ __m512i v = _mm512_loadu_si512((const void *)(p + i));
+ __mmask16 empty = _mm512_cmpeq_epi32_mask(v, no_text) & 0x1111;
+ blanks += term_popcount32(empty);
+ tag_fail |= (_mm512_test_epi32_mask(v, open) |
+ _mm512_cmpeq_epi32_mask(_mm512_and_si512(v, unknown), unknown)) & 0x8888;
+ pg_fail |= (uint16_t)(~(_mm512_cmpeq_epi32_mask(_mm512_and_si512(v, mask), pg) | empty) & 0x1111);
+ sums = _mm512_mask_add_epi32(sums, 0x8888, sums, _mm512_srli_epi32(v, 24));
+ }
+ *text_count = 64 - blanks;
+ *row_count = (uint32_t)_mm512_reduce_add_epi32(sums);
+ return (tag_fail | pg_fail) == 0;
+}
+#endif
+
+/* Drop whole lines in one pass. Update page references once per run.
+ * Use the normal path for open lines, continuations, and unknown row counts. */
+static uint64_t evict_simple_run(struct hist *h, uint64_t k)
+{
+ if (k == 0 || h->tail == h->head || !h->ring)
+ return 0;
+ const struct hline *ring = h->ring;
+ uint32_t rmask = h->ring_mask;
+ uint64_t tail = h->tail, head = h->head, base = h->base_id;
+ /* Neither side moves inside the loop, so count the lines once. */
+ uint64_t counted = h->rows_id > base ? h->rows_id - base : 0;
+ uint64_t rows_take = 0;
+ uint64_t n = 0;
+ uint32_t page = 0, run = 0;
+ bool have_page = false;
+
+ while (n < k && tail < head) {
+ if ((tail & 15) == 0 && n + 16 <= k && tail + 16 <= head &&
+ (n >= counted || n + 16 <= counted)) {
+ const struct hist_block *b = &h->blocks[(tail & rmask) / 16];
+ if (b->seq == tail + 1 && b->epoch == h->index_epoch && b->cols == h->wrap_cols) {
+ if (b->refs) {
+ if (have_page && page != b->page) {
+ page_drop_n(h, page, run);
+ run = 0;
+ }
+ page = b->page;
+ have_page = true;
+ run += b->refs;
+ }
+ if (n < counted) rows_take += b->rows;
+ tail += 16;
+ n += 16;
+ continue;
+ }
+ }
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512() && (tail & 63) == 0 && n + 64 <= k && tail + 64 <= head &&
+ ((tail & rmask) + 64 <= rmask + 1)) {
+ const struct hline *f = &ring[tail & rmask];
+
+ uint32_t pg = f->off >> 16;
+ uint32_t text_count, row_count;
+ if (f->off != NO_TEXT && (n >= counted || n + 64 <= counted) &&
+ same_page_rows_64(f, pg, &text_count, &row_count) &&
+ (tail + 64 == head || !(ring[(tail + 64) & rmask].flags & HLINE_CONT))) {
+ if (have_page && page != pg) {
+ page_drop_n(h, page, run);
+ run = 0;
+ }
+ page = pg;
+ have_page = true;
+ run += text_count;
+ if (n < counted) rows_take += row_count;
+ tail += 64;
+ n += 64;
+ continue;
+ }
+ }
+#endif
+ if ((tail & 15) == 0 && n + 16 <= k && tail + 16 <= head && ((tail & rmask) + 16 <= rmask + 1)) {
+ const struct hline *f = &ring[tail & rmask];
+
+ uint32_t pg = f->off >> 16;
+ if (f->off != NO_TEXT && same_page_narrow_rows(f, pg) &&
+ (tail + 16 == head || !(ring[(tail + 16) & rmask].flags & HLINE_CONT))) {
+ if (have_page && page != pg) {
+ page_drop_n(h, page, run);
+ run = 0;
+ }
+ page = pg;
+ have_page = true;
+ run += 16;
+ uint64_t remaining = counted > n ? counted - n : 0;
+ rows_take += remaining < 16 ? remaining : 16;
+ tail += 16;
+ n += 16;
+ continue;
+ }
+
+ uint32_t t0 = *(const uint32_t *)((const char *)&f[0] + 12);
+ uint32_t t1 = *(const uint32_t *)((const char *)&f[1] + 12);
+ uint32_t t2 = *(const uint32_t *)((const char *)&f[2] + 12);
+ uint32_t t3 = *(const uint32_t *)((const char *)&f[3] + 12);
+ uint32_t t4 = *(const uint32_t *)((const char *)&f[4] + 12);
+ uint32_t t5 = *(const uint32_t *)((const char *)&f[5] + 12);
+ uint32_t t6 = *(const uint32_t *)((const char *)&f[6] + 12);
+ uint32_t t7 = *(const uint32_t *)((const char *)&f[7] + 12);
+ uint32_t t8 = *(const uint32_t *)((const char *)&f[8] + 12);
+ uint32_t t9 = *(const uint32_t *)((const char *)&f[9] + 12);
+ uint32_t t10 = *(const uint32_t *)((const char *)&f[10] + 12);
+ uint32_t t11 = *(const uint32_t *)((const char *)&f[11] + 12);
+ uint32_t t12 = *(const uint32_t *)((const char *)&f[12] + 12);
+ uint32_t t13 = *(const uint32_t *)((const char *)&f[13] + 12);
+ uint32_t t14 = *(const uint32_t *)((const char *)&f[14] + 12);
+ uint32_t t15 = *(const uint32_t *)((const char *)&f[15] + 12);
+
+ uint32_t or_all = t0 | t1 | t2 | t3 | t4 | t5 | t6 | t7 |
+ t8 | t9 | t10 | t11 | t12 | t13 | t14 | t15;
+ if (TERM_EXPECT((or_all & 0x0030ffff) == 0 &&
+ (t0 >> 24) != 255 && (t1 >> 24) != 255 && (t2 >> 24) != 255 && (t3 >> 24) != 255 &&
+ (t4 >> 24) != 255 && (t5 >> 24) != 255 && (t6 >> 24) != 255 && (t7 >> 24) != 255 &&
+ (t8 >> 24) != 255 && (t9 >> 24) != 255 && (t10 >> 24) != 255 && (t11 >> 24) != 255 &&
+ (t12 >> 24) != 255 && (t13 >> 24) != 255 && (t14 >> 24) != 255 && (t15 >> 24) != 255, 1)) {
+
+ uint32_t o0 = f[0].off, o15 = f[15].off;
+ if (have_page && (o0 >> 16) == page && (o15 >> 16) == page &&
+ o0 != NO_TEXT && f[1].off != NO_TEXT && f[2].off != NO_TEXT && f[3].off != NO_TEXT &&
+ f[4].off != NO_TEXT && f[5].off != NO_TEXT && f[6].off != NO_TEXT && f[7].off != NO_TEXT &&
+ f[8].off != NO_TEXT && f[9].off != NO_TEXT && f[10].off != NO_TEXT && f[11].off != NO_TEXT &&
+ f[12].off != NO_TEXT && f[13].off != NO_TEXT && f[14].off != NO_TEXT && o15 != NO_TEXT) {
+ run += 16;
+ } else {
+ for (int j = 0; j < 16; j++) {
+ uint32_t o = f[j].off;
+ if (o != NO_TEXT) {
+ uint32_t pg = o >> 16;
+ if (have_page && pg == page) {
+ run++;
+ } else {
+ if (have_page)
+ page_drop_n(h, page, run);
+ page = pg;
+ run = 1;
+ have_page = true;
+ }
+ }
+ }
+ }
+ if (n + 16 <= counted) {
+ rows_take += (t0 >> 24) + (t1 >> 24) + (t2 >> 24) + (t3 >> 24) +
+ (t4 >> 24) + (t5 >> 24) + (t6 >> 24) + (t7 >> 24) +
+ (t8 >> 24) + (t9 >> 24) + (t10 >> 24) + (t11 >> 24) +
+ (t12 >> 24) + (t13 >> 24) + (t14 >> 24) + (t15 >> 24);
+ }
+ tail += 16;
+ n += 16;
+ continue;
+ }
+ }
+
+ const struct hline *f = &ring[tail & rmask];
+ if (f->flags & HLINE_OPEN)
+ break;
+ if (tail + 1 < head && (ring[(tail + 1) & rmask].flags & HLINE_CONT))
+ break;
+ if (f->wrap_rows == ROWS_UNKNOWN)
+ break;
+ if (n < counted)
+ rows_take += f->wrap_rows;
+ if (f->off != NO_TEXT) {
+ uint32_t pg = f->off >> 16;
+ if (have_page && pg == page) {
+ run++;
+ } else {
+ if (have_page)
+ page_drop_n(h, page, run);
+ page = pg;
+ run = 1;
+ have_page = true;
+ }
+ }
+ tail++;
+ n++;
+ }
+ if (n == 0)
+ return 0;
+ if (have_page)
+ page_drop_n(h, page, run);
+ h->tail = tail;
+ h->base_id += n;
+ h->lines -= (uint32_t)n;
+ if (counted) {
+ h->counted_rows -= rows_take;
+ if (n >= counted)
+ h->rows_id = h->base_id;
+ }
+ return n;
+}
+
+/* Make room for n more lines. Takes the simple lines in one pass, then
+ * falls back to one line at a time for the rest. The count is guarded
+ * before the subtraction: an unguarded `lines + n - line_limit` wraps when
+ * there is nothing to evict. */
+static inline void evict_for(struct hist *h, uint64_t n)
+{
+ uint64_t have = h->lines;
+ if (have + n <= (uint64_t)h->line_limit)
+ return;
+ uint64_t need = have + n - h->line_limit;
+ uint64_t done = evict_simple_run(h, need);
+ for (uint64_t i = done; i < need; i++)
+ evict_line(h);
+}
+
+/* Drop the oldest line with all its parts. It is never the open line. */
+static inline TERM_ALWAYS_INLINE void evict_line(struct hist *h)
+{
+ if (h->tail == h->head)
+ return;
+ const struct hline *f = part(h, h->tail);
+ if (TERM_EXPECT(!(f->flags & HLINE_OPEN) &&
+ (h->tail + 1 == h->head || !(part(h, h->tail + 1)->flags & HLINE_CONT)), 1)) {
+ if (h->base_id < h->rows_id)
+ h->counted_rows -= f->wrap_rows != ROWS_UNKNOWN
+ ? f->wrap_rows : term_hist_line_estimate(h, h->base_id, h->wrap_cols);
+ if (f->off != NO_TEXT)
+ page_drop_ref(h, f->off >> 16);
+ h->tail++;
+ line_gone(h);
+ return;
+ }
+ evict_line_parts(h);
+}
+
+static TERM_NOINLINE int page_next(struct hist *h)
+{
+ for (;;) {
+ if (h->nfree) {
+ uint32_t pg = h->free_pages[--h->nfree];
+ h->page_used[pg] = 0;
+ h->cur_page = pg;
+ return (int)pg;
+ }
+ if (h->npages < h->max_pages) {
+ uint8_t *page = term_aligned_alloc(64, HIST_PAGE_SIZE);
+ if (!page)
+ return -1;
+ uint32_t pg = h->npages++;
+ h->pages[pg] = page;
+ h->page_used[pg] = 0;
+ h->page_parts[pg] = 0;
+ h->cur_page = pg;
+ return (int)pg;
+ }
+ if (!evict_part(h))
+ return -1;
+ }
+}
+
+/* A page with need free bytes at its end. It becomes the current page. */
+static inline int page_for(struct hist *h, size_t need)
+{
+ /* No page can hold a request larger than the usable area. Returning a page
+ * here would let the caller copy past the end of the mapping. */
+ if (need > HIST_PAGE_USABLE)
+ return -1;
+ uint32_t pg = h->cur_page;
+ if (TERM_EXPECT(pg < h->npages && h->page_used[pg] + need <= HIST_PAGE_USABLE, 1))
+ return (int)pg;
+ return page_next(h);
+}
+
+/* ---- Attribute runs ---- */
+
+static inline int put_varint(uint8_t *p, uint32_t v)
+{
+ int n = 0;
+ while (v >= 0x80) {
+ p[n++] = (uint8_t)(v | 0x80);
+ v >>= 7;
+ }
+ p[n++] = (uint8_t)v;
+ return n;
+}
+
+static inline int get_varint(const uint8_t *p, uint32_t *v)
+{
+ uint32_t r = 0;
+ int shift = 0, n = 0;
+ uint8_t b;
+ do {
+ b = p[n++];
+ r |= (uint32_t)(b & 0x7f) << shift;
+ shift += 7;
+ } while ((b & 0x80) && shift < 35);
+ *v = r;
+ return n;
+}
+
+static inline void put_u32(uint8_t *p, uint32_t v) { memcpy(p, &v, 4); }
+static inline void put_u16(uint8_t *p, uint16_t v) { memcpy(p, &v, 2); }
+static inline uint32_t get_u32(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v; }
+static inline uint16_t get_u16(const uint8_t *p) { uint16_t v; memcpy(&v, p, 2); return v; }
+
+static inline uint8_t *emit_run(uint8_t *p, const struct pen_attr *base, const struct pen_attr *pen,
+ uint32_t count)
+{
+ /* Most runs change only colors and cover fewer than 128 cells. */
+ if (TERM_EXPECT(count < 0x80 && pen_sl(pen) == pen_sl(base), 1)) {
+ uint32_t dfg = pen->fg != base->fg, dbg = pen->bg != base->bg;
+ p[0] = (uint8_t)(dfg * RUN_FG | dbg * RUN_BG);
+ p[1] = (uint8_t)count;
+ p += 2;
+ put_u32(p, pen->fg);
+ p += dfg * 4;
+ put_u32(p, pen->bg);
+ return p + dbg * 4;
+ }
+ uint8_t header = (pen->fg != base->fg ? RUN_FG : 0) |
+ (pen->bg != base->bg ? RUN_BG : 0) |
+ (pen->flags != base->flags ? RUN_ATTR : 0) |
+ (pen->link != base->link ? RUN_LINK : 0);
+ *p++ = header;
+ p += put_varint(p, count);
+ if (header & RUN_FG) { put_u32(p, pen->fg); p += 4; }
+ if (header & RUN_BG) { put_u32(p, pen->bg); p += 4; }
+ if (header & RUN_ATTR) { put_u16(p, pen->flags); p += 2; }
+ if (header & RUN_LINK) { put_u16(p, pen->link); p += 2; }
+ return p;
+}
+
+static inline const uint8_t *read_run(const uint8_t *p, struct pen_attr *pen, uint32_t *count)
+{
+ uint8_t header = *p++;
+ p += get_varint(p, count);
+ if (header & RUN_FG) { pen->fg = get_u32(p); p += 4; }
+ if (header & RUN_BG) { pen->bg = get_u32(p); p += 4; }
+ if (header & RUN_ATTR) { pen->flags = get_u16(p); p += 2; }
+ if (header & RUN_LINK) { pen->link = get_u16(p); p += 2; }
+ return p;
+}
+
+static bool runs_reserve(struct hist_runs *r, size_t extra)
+{
+ if (TERM_EXPECT(r->len + extra <= r->cap, 1))
+ return true;
+ size_t cap = r->cap ? r->cap : 256;
+ while (cap < r->len + extra)
+ cap *= 2;
+ uint8_t *data = realloc(r->data, cap);
+ if (!data)
+ return false;
+ r->data = data;
+ r->cap = (uint32_t)cap;
+ return true;
+}
+
+static inline void runs_reset(struct hist_runs *r)
+{
+ r->len = 0;
+ r->base = pen_attr_default();
+ r->pen = pen_attr_default();
+ r->count = 0;
+}
+
+void term_hist_runs_emit_slow(struct hist_runs *r)
+{
+ r->len = (uint32_t)(emit_run(r->data + r->len, &r->base, &r->pen, r->count) - r->data);
+}
+
+static inline void runs_repeat(struct hist_runs *r)
+{
+ hist_runs_switch(r, r->pen.fg, r->pen.bg, pen_sl(&r->pen));
+}
+
+/* Emit the pending run unless it is a default tail, which readers imply. */
+static void runs_finish(struct hist_runs *r)
+{
+ if (r->count && !pen_attr_is_default(&r->pen))
+ runs_repeat(r);
+ r->count = 0;
+}
+
+/* Emit the pending run, default or not, so the next run can use it as its
+ * delta base. */
+static void runs_flush(struct hist_runs *r)
+{
+ if (r->count)
+ runs_repeat(r);
+}
+
+/* ---- Encoding ---- */
+
+static inline size_t put_utf8(uint8_t *dst, uint32_t cp)
+{
+ if (cp < 0x80) {
+ dst[0] = (uint8_t)cp;
+ return 1;
+ }
+ if (cp < 0x800) {
+ put_u16(dst, (uint16_t)(0x80C0u | (cp >> 6) | ((cp & 0x3Fu) << 8)));
+ return 2;
+ }
+ if (cp < 0x10000) {
+ put_u32(dst, 0x008080E0u | (cp >> 12) | ((cp & 0x0FC0u) << 2) | ((cp & 0x3Fu) << 16));
+ return 3;
+ }
+ if (cp <= 0x10FFFF) {
+ put_u32(dst, 0x808080F0u | (cp >> 18) | ((cp & 0x3F000u) >> 4) |
+ ((cp & 0x0FC0u) << 10) | ((cp & 0x3Fu) << 24));
+ return 4;
+ }
+ put_u32(dst, 0x00BDBFEFu);
+ return 3;
+}
+
+/* Decode one code point that this file wrote. It does not validate. */
+static inline int get_utf8(const uint8_t *s, const uint8_t *end, uint32_t *cp)
+{
+ uint8_t b = s[0];
+ int n = b < 0xE0 ? (b < 0x80 ? 1 : 2) : b < 0xF0 ? 3 : 4;
+ if (s + n > end) {
+ *cp = 0xFFFD;
+ return (int)(end - s);
+ }
+ switch (n) {
+ case 1: *cp = b; break;
+ case 2: *cp = ((uint32_t)(b & 0x1f) << 6) | (s[1] & 0x3f); break;
+ case 3: *cp = ((uint32_t)(b & 0x0f) << 12) | ((uint32_t)(s[1] & 0x3f) << 6) | (s[2] & 0x3f); break;
+ default:
+ *cp = ((uint32_t)(b & 0x07) << 18) | ((uint32_t)(s[1] & 0x3f) << 12) |
+ ((uint32_t)(s[2] & 0x3f) << 6) | (s[3] & 0x3f);
+ break;
+ }
+ return n;
+}
+
+struct enc {
+ uint32_t bytes;
+ uint32_t cells;
+ uint8_t flags; /* HAS_* flags; NARROW when every cell was one ASCII byte */
+ uint16_t wide;
+};
+
+#if defined(TERM_KERNEL_AVX512)
+/* The prefix of cells where (lane & keep) - target <= span holds in every
+ * lane. Their code points go to dst as bytes. Eight cells per step; the
+ * last step uses masked loads. */
+TERM_TARGET_AVX512
+static inline int narrow_prefix_avx512(const struct cell *cells, int len, uint8_t *dst,
+ __m512i keep, __m512i target, __m512i span)
+{
+ const __m512i idx = _mm512_setr_epi32(0, 4, 8, 12, 16, 20, 24, 28, 0, 0, 0, 0, 0, 0, 0, 0);
+ int k = 0;
+ for (; k < len; k += 8) {
+ int left = len - k;
+ __mmask16 m0 = left >= 4 ? 0xFFFF : (__mmask16)((1u << (left * 4)) - 1);
+ __mmask16 m1 = left >= 8 ? 0xFFFF : left <= 4 ? 0 : (__mmask16)((1u << ((left - 4) * 4)) - 1);
+ __m512i v0 = _mm512_maskz_loadu_epi32(m0, (const void *)(cells + k));
+ __m512i v1 = _mm512_maskz_loadu_epi32(m1, (const void *)(cells + k + 4));
+ __mmask16 ok0 = _mm512_mask_cmple_epu32_mask(m0, _mm512_sub_epi32(_mm512_and_si512(v0, keep), target), span);
+ __mmask16 ok1 = _mm512_mask_cmple_epu32_mask(m1, _mm512_sub_epi32(_mm512_and_si512(v1, keep), target), span);
+ __m512i cp = _mm512_permutex2var_epi32(v0, idx, v1);
+ if (ok0 != m0 || ok1 != m1) {
+ /* Keep the cells before the first failing one. */
+ uint32_t bad = (uint32_t)(~ok0 & m0) | ((uint32_t)(~ok1 & m1) << 16);
+ int good = term_ctz32(bad) / 4;
+ _mm_mask_storeu_epi8(dst + k, (__mmask16)((1u << good) - 1), _mm512_cvtepi32_epi8(cp));
+ return k + good;
+ }
+ if (left >= 8)
+ _mm_storel_epi64((__m128i *)(void *)(dst + k), _mm512_cvtepi32_epi8(cp));
+ else
+ _mm_mask_storeu_epi8(dst + k, (__mmask16)((1u << left) - 1), _mm512_cvtepi32_epi8(cp));
+ }
+ return len;
+}
+
+TERM_TARGET_AVX512
+static inline int narrow_any_pen_prefix_avx512(const struct cell *cells, int len, uint8_t *dst)
+{
+ const __m512i keep = _mm512_setr_epi32(-1, 0, 0, SHAPE_FLAGS, -1, 0, 0, SHAPE_FLAGS,
+ -1, 0, 0, SHAPE_FLAGS, -1, 0, 0, SHAPE_FLAGS);
+ const __m512i target = _mm512_setr_epi32(0x20, 0, 0, 0, 0x20, 0, 0, 0, 0x20, 0, 0, 0, 0x20, 0, 0, 0);
+ const __m512i span = _mm512_setr_epi32(0x5f, 0, 0, 0, 0x5f, 0, 0, 0, 0x5f, 0, 0, 0, 0x5f, 0, 0, 0);
+ return narrow_prefix_avx512(cells, len, dst, keep, target, span);
+}
+
+TERM_TARGET_AVX512
+static inline int narrow_uniform_prefix_avx512(const struct cell *cells, int len, uint8_t *dst)
+{
+ uint32_t sl = (uint32_t)(cells[0].flags & STYLE_FLAGS) | ((uint32_t)cells[0].pad << 16);
+ const __m512i target = _mm512_setr_epi32(
+ 0x20, (int)cells[0].fg, (int)cells[0].bg, (int)sl, 0x20, (int)cells[0].fg, (int)cells[0].bg, (int)sl,
+ 0x20, (int)cells[0].fg, (int)cells[0].bg, (int)sl, 0x20, (int)cells[0].fg, (int)cells[0].bg, (int)sl);
+ const __m512i keep = _mm512_setr_epi32(
+ -1, -1, -1, (int)(STYLE_FLAGS | SHAPE_FLAGS | 0xFFFF0000u), -1, -1, -1, (int)(STYLE_FLAGS | SHAPE_FLAGS | 0xFFFF0000u),
+ -1, -1, -1, (int)(STYLE_FLAGS | SHAPE_FLAGS | 0xFFFF0000u), -1, -1, -1, (int)(STYLE_FLAGS | SHAPE_FLAGS | 0xFFFF0000u));
+ const __m512i span = _mm512_setr_epi32(0x5f, 0, 0, 0, 0x5f, 0, 0, 0, 0x5f, 0, 0, 0, 0x5f, 0, 0, 0);
+ return narrow_prefix_avx512(cells, len, dst, keep, target, span);
+}
+#endif
+
+/* The prefix of cells that are printable ASCII with any pen. Their bytes go
+ * to dst. */
+static inline int narrow_any_pen_prefix(const struct cell *cells, int len, uint8_t *dst)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return narrow_any_pen_prefix_avx512(cells, len, dst);
+#endif
+ int k = 0;
+ for (; k < len; k++) {
+ if (cells[k].c - 0x20u >= 0x60u || (cells[k].flags & SHAPE_FLAGS))
+ break;
+ dst[k] = (uint8_t)cells[k].c;
+ }
+ return k;
+}
+
+/* Add the pens of n cells to r, one run per change. Cells carry no
+ * underline color, so no pen here has one. */
+static TERM_NOINLINE uint32_t runs_scan(struct hist_runs *r, const struct cell *cells, int n)
+{
+ uint32_t links = 0;
+ uint32_t pfg = r->pen.fg, pbg = r->pen.bg;
+ uint32_t psl = (uint32_t)r->pen.flags | ((uint32_t)r->pen.link << 16);
+ uint32_t bfg = r->base.fg, bbg = r->base.bg;
+ uint32_t bsl = (uint32_t)r->base.flags | ((uint32_t)r->base.link << 16);
+ uint32_t count = r->count;
+ uint8_t *rp = r->data + r->len;
+ for (int i = 0; i < n; i++) {
+ const struct cell *c = &cells[i];
+ uint32_t sl = (uint32_t)(c->flags & STYLE_FLAGS) | ((uint32_t)c->pad << 16);
+ links |= c->pad;
+ if (TERM_EXPECT(((c->fg ^ pfg) | (c->bg ^ pbg) | (sl ^ psl)) != 0, 0)) {
+ if (count) {
+ uint32_t header = (pfg != bfg ? RUN_FG : 0) | (pbg != bbg ? RUN_BG : 0) |
+ ((uint16_t)psl != (uint16_t)bsl ? RUN_ATTR : 0) |
+ ((psl >> 16) != (bsl >> 16) ? RUN_LINK : 0);
+ rp[0] = (uint8_t)header;
+ if (TERM_EXPECT(count < 0x80, 1)) {
+ rp[1] = (uint8_t)count;
+ rp += 2;
+ } else {
+ rp += 1 + put_varint(rp + 1, count);
+ }
+ put_u32(rp, pfg);
+ rp += (header & RUN_FG) ? 4 : 0;
+ put_u32(rp, pbg);
+ rp += (header & RUN_BG) ? 4 : 0;
+ if (TERM_EXPECT(header & (RUN_ATTR | RUN_LINK), 0)) {
+ if (header & RUN_ATTR) { put_u16(rp, (uint16_t)psl); rp += 2; }
+ if (header & RUN_LINK) { put_u16(rp, (uint16_t)(psl >> 16)); rp += 2; }
+ }
+ bfg = pfg;
+ bbg = pbg;
+ bsl = psl;
+ }
+ pfg = c->fg;
+ pbg = c->bg;
+ psl = sl;
+ count = 0;
+ }
+ count++;
+ }
+ r->len = (uint32_t)(rp - r->data);
+ pen_set(&r->base, bfg, bbg, bsl);
+ pen_set(&r->pen, pfg, pbg, psl);
+ r->count = count;
+ return links;
+}
+
+/* The prefix of cells that are printable ASCII with the pen of cells[0].
+ * Their bytes go to dst. */
+static inline int narrow_uniform_prefix(const struct cell *cells, int len, uint8_t *dst)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return narrow_uniform_prefix_avx512(cells, len, dst);
+#endif
+ uint32_t sl = (uint32_t)(cells[0].flags & STYLE_FLAGS) | ((uint32_t)cells[0].pad << 16);
+ int k = 0;
+ for (; k < len; k++) {
+ const struct cell *c = &cells[k];
+ uint32_t csl = (uint32_t)(c->flags & (STYLE_FLAGS | SHAPE_FLAGS)) | ((uint32_t)c->pad << 16);
+ if (c->c - 0x20u >= 0x60u || c->fg != cells[0].fg || c->bg != cells[0].bg || csl != sl)
+ break;
+ dst[k] = (uint8_t)c->c;
+ }
+ return k;
+}
+
+#if defined(TERM_KERNEL_AVX512)
+/* Encode a prefix of cells with the pen of cells[0] and no clusters, eight
+ * cells at a time. Return the cells used; runcells counts the cells that
+ * are not trail cells. */
+TERM_TARGET_AVX512
+static int uniform_utf8_prefix(const struct cell *cells, int len, uint8_t *dst, struct enc *o,
+ uint32_t *runcells)
+{
+ uint32_t sl = (uint32_t)(cells[0].flags & STYLE_FLAGS) | ((uint32_t)cells[0].pad << 16);
+ const __m512i target = _mm512_setr_epi32(
+ 0, (int)cells[0].fg, (int)cells[0].bg, (int)sl, 0, (int)cells[0].fg, (int)cells[0].bg, (int)sl,
+ 0, (int)cells[0].fg, (int)cells[0].bg, (int)sl, 0, (int)cells[0].fg, (int)cells[0].bg, (int)sl);
+ /* The code point lane only checks that the value is below 0x10000. The
+ * flags lane allows WIDE and TRAIL. */
+ const __m512i keep = _mm512_setr_epi32(
+ (int)0xFFFF0000u, -1, -1, (int)(STYLE_FLAGS | CELL_WRAP_PAD | CELL_EMOJI | CELL_TEXT | 0xFFFF0000u),
+ (int)0xFFFF0000u, -1, -1, (int)(STYLE_FLAGS | CELL_WRAP_PAD | CELL_EMOJI | CELL_TEXT | 0xFFFF0000u),
+ (int)0xFFFF0000u, -1, -1, (int)(STYLE_FLAGS | CELL_WRAP_PAD | CELL_EMOJI | CELL_TEXT | 0xFFFF0000u),
+ (int)0xFFFF0000u, -1, -1, (int)(STYLE_FLAGS | CELL_WRAP_PAD | CELL_EMOJI | CELL_TEXT | 0xFFFF0000u));
+ const __m512i idx_cp = _mm512_setr_epi32(0, 4, 8, 12, 16, 20, 24, 28, 0, 0, 0, 0, 0, 0, 0, 0);
+ const __m512i idx_fl = _mm512_setr_epi32(3, 7, 11, 15, 19, 23, 27, 31, 0, 0, 0, 0, 0, 0, 0, 0);
+ int i = 0;
+ uint32_t bytes = 0, ncells = 0, wide = 0, ascii = 0xFF, nwide = 0;
+ for (; i + 8 <= len; i += 8) {
+ __m512i v0 = _mm512_loadu_si512((const void *)(cells + i));
+ __m512i v1 = _mm512_loadu_si512((const void *)(cells + i + 4));
+ __m512i d = _mm512_or_si512(_mm512_xor_si512(_mm512_and_si512(v0, keep), target),
+ _mm512_xor_si512(_mm512_and_si512(v1, keep), target));
+ if (_mm512_test_epi32_mask(d, d))
+ break;
+ __m256i cp = _mm512_castsi512_si256(_mm512_permutex2var_epi32(v0, idx_cp, v1));
+ __m256i fl = _mm512_castsi512_si256(_mm512_permutex2var_epi32(v0, idx_fl, v1));
+ __m256i ch = _mm256_max_epu32(cp, _mm256_set1_epi32(0x20));
+ __mmask8 ge80 = _mm256_cmpge_epu32_mask(ch, _mm256_set1_epi32(0x80));
+ __mmask8 ge800 = _mm256_cmpge_epu32_mask(ch, _mm256_set1_epi32(0x800));
+ __mmask8 m_wide = _mm256_test_epi32_mask(fl, _mm256_set1_epi32(CELL_WIDE));
+ __mmask8 m_trail = _mm256_test_epi32_mask(fl, _mm256_set1_epi32(CELL_TRAIL));
+ __mmask8 surrogate = _mm256_cmplt_epu32_mask(_mm256_sub_epi32(ch, _mm256_set1_epi32(0xD800)),
+ _mm256_set1_epi32(0x800));
+ if ((m_wide & ~ge800) || (surrogate & ~m_trail))
+ break;
+ /* Lane bytes, low first: F5 E? 8? 8?. A wide lane keeps all four, a
+ * three-byte lane the last three. A two-byte lane turns byte 2 into
+ * its lead byte (C0 | cp >> 6) and keeps the last two. ASCII keeps
+ * byte 3 only. */
+ __m256i b1 = _mm256_and_si256(_mm256_srli_epi32(ch, 4), _mm256_set1_epi32(0x00000F00));
+ __m256i b2 = _mm256_and_si256(_mm256_slli_epi32(ch, 10), _mm256_set1_epi32(0x003F0000));
+ __m256i b3 = _mm256_and_si256(_mm256_slli_epi32(ch, 24), _mm256_set1_epi32(0x3F000000));
+ __m256i w = _mm256_or_si256(_mm256_or_si256(b1, b2), _mm256_or_si256(b3, _mm256_set1_epi32((int)0x8080E0F5u)));
+ w = _mm256_mask_or_epi32(w, (__mmask8)~ge800, w, _mm256_set1_epi32(0x00400000));
+ w = _mm256_mask_mov_epi32(w, (__mmask8)~ge80, _mm256_slli_epi32(ch, 24));
+ __m256i bm = _mm256_set1_epi32((int)0x80000000u);
+ bm = _mm256_mask_mov_epi32(bm, ge80, _mm256_set1_epi32((int)0x80800000u));
+ bm = _mm256_mask_mov_epi32(bm, ge800, _mm256_set1_epi32((int)0x80808000u));
+ bm = _mm256_mask_mov_epi32(bm, m_wide, _mm256_set1_epi32((int)0x80808080u));
+ bm = _mm256_maskz_mov_epi32((__mmask8)~m_trail, bm);
+ __mmask32 keep_bytes = _mm256_movepi8_mask(bm);
+ _mm256_storeu_si256((__m256i *)(void *)(dst + bytes), _mm256_maskz_compress_epi8(keep_bytes, w));
+ bytes += (uint32_t)term_popcount32(keep_bytes);
+ uint32_t trails = (uint32_t)term_popcount32(m_trail);
+ ncells += 8u - trails + (uint32_t)term_popcount32(m_wide);
+ *runcells += 8u - trails;
+ nwide += (uint32_t)term_popcount32(m_wide);
+ wide |= m_wide;
+ ascii &= (uint32_t)(uint8_t)~(ge80 & ~m_trail);
+ }
+ o->bytes = bytes;
+ o->cells = ncells;
+ o->wide = (uint16_t)nwide;
+ o->flags = (wide ? HLINE_HAS_WIDE : 0) | (ascii == 0xFF ? HLINE_NARROW : 0) |
+ (cells[0].pad ? HLINE_HAS_LINK : 0);
+ return i;
+}
+#endif
+
+static TERM_NOINLINE size_t encode_cluster(uint8_t *dst, const struct gstore *g,
+ uint32_t handle, uint16_t f)
+{
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int n = term_gstore_expand(g, handle, cps, GRAPHEME_MAX_LEN);
+ size_t b = 0;
+ dst[b++] = PREFIX_CLUSTER;
+ dst[b++] = (uint8_t)((n & 0x1f) | ((f & CELL_WIDE) ? 0x80 : 0) |
+ ((f & CELL_EMOJI) ? 0x40 : 0) | ((f & CELL_TEXT) ? 0x20 : 0));
+ for (int k = 0; k < n; k++)
+ b += put_utf8(dst + b, cps[k]);
+ return b;
+}
+
+static size_t encode_shaped(uint8_t *dst, uint32_t cp, uint16_t f, const struct gstore *g,
+ uint8_t *flags)
+{
+ if (grapheme_is_handle(cp)) {
+ *flags |= HLINE_HAS_CLUSTER | ((f & CELL_WIDE) ? HLINE_HAS_WIDE : 0);
+ return encode_cluster(dst, g, cp, f);
+ }
+ if (cp < 0x20)
+ cp = ' ';
+ uint16_t shape = f & (CELL_WIDE | CELL_EMOJI | CELL_TEXT);
+ size_t b = 0;
+ if (shape == 0)
+ return put_utf8(dst, cp);
+ if (shape == CELL_WIDE) {
+ *flags |= HLINE_HAS_WIDE;
+ dst[b++] = PREFIX_WIDE;
+ } else if (shape == (CELL_WIDE | CELL_EMOJI)) {
+ *flags |= HLINE_HAS_WIDE;
+ dst[b++] = PREFIX_EMOJI;
+ } else if (shape == CELL_TEXT) {
+ dst[b++] = PREFIX_TEXT;
+ } else {
+ *flags |= HLINE_HAS_CLUSTER | ((f & CELL_WIDE) ? HLINE_HAS_WIDE : 0);
+ dst[b++] = PREFIX_CLUSTER;
+ dst[b++] = (uint8_t)(1 | ((f & CELL_WIDE) ? 0x80 : 0) |
+ ((f & CELL_EMOJI) ? 0x40 : 0) | ((f & CELL_TEXT) ? 0x20 : 0));
+ }
+ return b + put_utf8(dst + b, cp);
+}
+
+/* Encode cells[0..n) into dst and add their pens to r. Stop before a
+ * cluster that could overflow cap. Return the cells used. The caller
+ * reserved (n + 1) * RUN_MAX bytes in r. */
+static int encode_cells(struct hist_runs *r, const struct cell *cells, int n, uint8_t *dst,
+ size_t cap, const struct gstore *g, struct enc *o)
+{
+ o->bytes = 0;
+ o->cells = 0;
+ o->flags = HLINE_NARROW;
+ o->wide = 0;
+ int i = 0;
+ if (n <= 0)
+ return 0;
+ const struct cell *c0 = &cells[0];
+ struct pen_attr pen0 = { c0->fg, c0->bg, (uint16_t)(c0->flags & STYLE_FLAGS), c0->pad };
+ bool ascii0 = !(c0->flags & SHAPE_FLAGS) && c0->c - 0x20u < 0x60u;
+ if (ascii0)
+ i = narrow_uniform_prefix(cells, n, dst);
+ if (i == n) {
+ hist_runs_add(r, &pen0, (uint32_t)n);
+ *o = (struct enc){ .bytes = (uint32_t)n, .cells = (uint32_t)n,
+ .flags = (uint8_t)(HLINE_NARROW | (c0->pad ? HLINE_HAS_LINK : 0)) };
+ return n;
+ }
+ const struct cell *ci = &cells[i];
+ if (i > 0 && !(ci->flags & SHAPE_FLAGS) && ci->c - 0x20u < 0x60u) {
+ /* The pen changed inside ASCII: a pass for the bytes, one for the pens. */
+ i += narrow_any_pen_prefix(ci, n - i, dst + i);
+ uint32_t links = runs_scan(r, cells, i);
+ *o = (struct enc){ .bytes = (uint32_t)i, .cells = (uint32_t)i,
+ .flags = (uint8_t)(HLINE_NARROW | (links ? HLINE_HAS_LINK : 0)) };
+ if (i == n)
+ return n;
+ } else if (i > 0) {
+ hist_runs_add(r, &pen0, (uint32_t)i);
+ *o = (struct enc){ .bytes = (uint32_t)i, .cells = (uint32_t)i,
+ .flags = (uint8_t)(HLINE_NARROW | (c0->pad ? HLINE_HAS_LINK : 0)) };
+ }
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512() && n - i >= 8 && ((cells[i].flags & SHAPE_FLAGS) || cells[i].c >= 0x80)) {
+ /* Text with one pen, eight cells per step. */
+ struct enc u;
+ uint32_t runcells = 0;
+ const struct cell *cu = &cells[i];
+ int k = uniform_utf8_prefix(cu, n - i, dst + o->bytes, &u, &runcells);
+ if (k > 0) {
+ hist_runs_add_pen(r, cu->fg, cu->bg,
+ (uint32_t)(cu->flags & STYLE_FLAGS) | ((uint32_t)cu->pad << 16), runcells);
+ o->bytes += u.bytes;
+ o->cells += u.cells;
+ o->wide += u.wide;
+ o->flags = (uint8_t)((o->flags & u.flags & HLINE_NARROW) |
+ ((o->flags | u.flags) & ~HLINE_NARROW));
+ i += k;
+ if (i == n)
+ return n;
+ }
+ }
+#endif
+
+ /* The pending run and the delta base stay in registers. */
+ uint32_t pfg = r->pen.fg, pbg = r->pen.bg, psl = pen_sl(&r->pen);
+ uint32_t bfg = r->base.fg, bbg = r->base.bg, bsl = pen_sl(&r->base);
+ uint32_t count = r->count;
+ uint8_t *rp = r->data + r->len;
+ uint32_t bytes = o->bytes, ncells = o->cells, nwide = o->wide;
+ uint8_t flags = o->flags;
+ uint32_t links = 0;
+ for (; i < n; i++) {
+ const struct cell *c = &cells[i];
+ uint16_t f = c->flags;
+ if (f & (CELL_TRAIL | CELL_WRAP_PAD))
+ continue;
+ uint32_t cp = c->c;
+ bool simple = !(f & (CELL_WIDE | CELL_EMOJI | CELL_TEXT)) && cp - 0x20u < 0x60u;
+ if (!simple && grapheme_is_handle(cp) &&
+ bytes + CLUSTER_BYTES + (size_t)(n - i) * CELL_BYTES > cap)
+ break;
+ uint32_t sl = (uint32_t)(f & STYLE_FLAGS) | ((uint32_t)c->pad << 16);
+ links |= c->pad;
+ if (TERM_EXPECT(((c->fg ^ pfg) | (c->bg ^ pbg) | (sl ^ psl)) != 0, 0)) {
+ if (count) {
+ uint32_t header = (pfg != bfg ? RUN_FG : 0) | (pbg != bbg ? RUN_BG : 0) |
+ ((uint16_t)psl != (uint16_t)bsl ? RUN_ATTR : 0) |
+ ((psl >> 16) != (bsl >> 16) ? RUN_LINK : 0);
+ *rp++ = (uint8_t)header;
+ if (TERM_EXPECT(count < 0x80, 1))
+ *rp++ = (uint8_t)count;
+ else
+ rp += put_varint(rp, count);
+ put_u32(rp, pfg);
+ rp += (header & RUN_FG) ? 4 : 0;
+ put_u32(rp, pbg);
+ rp += (header & RUN_BG) ? 4 : 0;
+ if (TERM_EXPECT(header & (RUN_ATTR | RUN_LINK), 0)) {
+ if (header & RUN_ATTR) { put_u16(rp, (uint16_t)psl); rp += 2; }
+ if (header & RUN_LINK) { put_u16(rp, (uint16_t)(psl >> 16)); rp += 2; }
+ }
+ bfg = pfg;
+ bbg = pbg;
+ bsl = psl;
+ }
+ pfg = c->fg;
+ pbg = c->bg;
+ psl = sl;
+ count = 0;
+ }
+ count++;
+ nwide += (f & CELL_WIDE) != 0;
+ if (TERM_EXPECT(simple, 1)) {
+ dst[bytes++] = (uint8_t)cp;
+ ncells++;
+ continue;
+ }
+ flags &= (uint8_t)~HLINE_NARROW;
+ uint16_t shape = f & (CELL_WIDE | CELL_EMOJI | CELL_TEXT);
+ if ((shape == 0 || shape == CELL_WIDE) && cp - 0x80u < 0xD800u - 0x80u) {
+ /* Narrow or wide text below the surrogates: inline. */
+ if (shape) {
+ dst[bytes++] = PREFIX_WIDE;
+ flags |= HLINE_HAS_WIDE;
+ ncells++;
+ }
+ bytes += (uint32_t)put_utf8(dst + bytes, cp);
+ ncells++;
+ continue;
+ }
+ bytes += (uint32_t)encode_shaped(dst + bytes, cp, f, g, &flags);
+ ncells += (f & CELL_WIDE) ? 2 : 1;
+ }
+ r->len = (uint32_t)(rp - r->data);
+ pen_set(&r->base, bfg, bbg, bsl);
+ pen_set(&r->pen, pfg, pbg, psl);
+ r->count = count;
+ o->bytes = bytes;
+ o->cells = ncells;
+ o->wide = (uint16_t)nwide;
+ o->flags = flags | (links ? HLINE_HAS_LINK : 0);
+ return i;
+}
+
+/* ---- Writing ---- */
+
+static inline uint8_t row_count_byte(uint64_t rows)
+{
+ return rows < ROWS_UNKNOWN ? (uint8_t)rows : ROWS_UNKNOWN;
+}
+
+/*
+ * The fixed half of a narrow descriptor: wide 0, attr_len 0,
+ * HLINE_NARROW, and wrap_rows 1. Only `cells` varies inside it, so a
+ * one-row narrow line shares one 64-bit constant. Byte 8 is cells,
+ * byte 14 is flags, and byte 15 is wrap_rows.
+ */
+#define HLINE_NARROW_TAIL ((uint64_t)HLINE_NARROW << 48 | 1ULL << 56)
+
+/* Every blank part is the same 16 bytes: no text, no cells, one row.
+ * Reading the constant beats zeroing 16 bytes and patching fields. */
+static const struct hline HLINE_BLANK = {
+ .off = NO_TEXT, .flags = HLINE_NARROW, .wrap_rows = 1,
+};
+
+static inline void part_put_narrow_rows(struct hline *p, uint32_t off, uint32_t len, uint32_t rows)
+{
+ uint64_t v[2] = {
+ (uint64_t)off | (uint64_t)len << 32,
+ (uint64_t)len | ((uint64_t)HLINE_NARROW << 48) | ((uint64_t)row_count_byte(rows) << 56),
+ };
+ memcpy(p, v, sizeof(v));
+}
+
+/* Store the descriptor of one narrow line that holds a single row and uses
+ * the default pen. This is the common case on the write path. The whole
+ * 16 bytes leave in one store. The compound literal form instead costs a
+ * 16-byte zero store plus four field patches. */
+static inline void part_put_narrow(struct hline *p, uint32_t off, uint32_t len)
+{
+ uint64_t v[2] = {
+ (uint64_t)off | (uint64_t)len << 32,
+ (uint64_t)len | HLINE_NARROW_TAIL,
+ };
+ memcpy(p, v, sizeof(v));
+}
+
+static void walk_advance(struct hist *h);
+
+/* Descriptor estimate. A viewport read accounts for wide wrap pads. */
+static inline uint64_t cheap_rows(uint64_t cells, uint64_t wide, int cols)
+{
+ return rows_narrow(cols == 1 ? cells - wide : cells, cols);
+}
+
+/* Count the last line, which just closed, when it is next in order. */
+static inline void count_closed(struct hist *h, struct hline *first, uint64_t rows)
+{
+ if (rows && h->wrap_cols && h->rows_id == hist_end_id(h) - 1) {
+ first->wrap_rows = row_count_byte(rows);
+ h->counted_rows += rows;
+ h->rows_id++;
+ }
+}
+
+/* A whole line in one push: text, runs, and one descriptor. It returns false
+ * when a cluster does not fit; nothing is committed then. */
+static bool push_line(struct hist *h, const struct cell *cells, int len, const char *ascii,
+ const struct pen_attr *pen, const struct gstore *g)
+{
+ if (h->lines >= h->line_limit)
+ evict_line(h);
+ size_t runs_need = ascii ? RUN_MAX * 2 : (size_t)(len + 2) * RUN_MAX;
+ size_t text_need = ascii ? (size_t)len : (size_t)len * CELL_BYTES;
+ int pg = page_for(h, text_need + runs_need + RUN_MAX);
+ if (pg < 0 || !ring_reserve(h) || !runs_reserve(&h->runs, runs_need + RUN_MAX))
+ return false;
+ uint32_t off = h->page_used[pg];
+ uint8_t *dst = h->pages[pg] + off;
+ runs_reset(&h->runs);
+ struct enc e;
+ if (ascii) {
+ bytes_copy_small(dst, ascii, len);
+ hist_runs_add(&h->runs, pen, (uint32_t)len);
+ e = (struct enc){ .bytes = (uint32_t)len, .cells = (uint32_t)len,
+ .flags = (uint8_t)(HLINE_NARROW | (pen->link ? HLINE_HAS_LINK : 0)) };
+ } else {
+ size_t cap = HIST_PAGE_USABLE - off - (runs_need + RUN_MAX);
+ if (encode_cells(&h->runs, cells, len, dst, cap, g, &e) < len)
+ return false;
+ }
+ runs_finish(&h->runs);
+ if (h->runs.len)
+ memcpy(dst + e.bytes, h->runs.data, h->runs.len);
+ struct hline *p = part(h, h->head++);
+ *p = (struct hline){
+ .off = ((uint32_t)pg << 16) | off,
+ .text_len = e.bytes,
+ .cells = (uint16_t)e.cells,
+ .wide = e.wide,
+ .attr_len = (uint16_t)h->runs.len,
+ .flags = e.flags,
+ .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->page_used[pg] = off + e.bytes + h->runs.len;
+ h->page_parts[pg]++;
+ h->lines++;
+ h->all_blank = false;
+ h->runs.len = 0;
+ if (h->wrap_cols)
+ count_closed(h, p, cheap_rows(e.cells, e.wide, h->wrap_cols));
+ return true;
+}
+
+/* A whole line of printable ASCII cells with one pen: the bytes, at most one
+ * run, one descriptor. Return false, with nothing committed, for any other
+ * line. */
+static inline bool push_uniform_line(struct hist *h, const struct cell *cells, int len)
+{
+ if (h->lines >= h->line_limit)
+ evict_line(h);
+ uint32_t pg = h->cur_page;
+ if (pg >= h->npages || h->page_used[pg] + (uint32_t)len + RUN_MAX > HIST_PAGE_USABLE ||
+ !h->ring || h->head - h->tail > h->ring_mask)
+ return false;
+ uint32_t off = h->page_used[pg];
+ uint8_t *dst = h->pages[pg] + off;
+ if (narrow_uniform_prefix(cells, len, dst) != len)
+ return false;
+ struct pen_attr pen = { cells[0].fg, cells[0].bg, (uint16_t)(cells[0].flags & STYLE_FLAGS),
+ cells[0].pad };
+ uint32_t attr = 0;
+ if (!pen_attr_is_default(&pen)) {
+ struct pen_attr base = pen_attr_default();
+ attr = (uint32_t)(emit_run(dst + len, &base, &pen, (uint32_t)len) - (dst + len));
+ }
+ struct hline *q = part(h, h->head++);
+ *q = (struct hline){
+ .off = (pg << 16) | off, .text_len = (uint32_t)len, .cells = (uint32_t)len,
+ .attr_len = (uint16_t)attr,
+ .flags = (uint8_t)(HLINE_NARROW | (pen.link ? HLINE_HAS_LINK : 0)),
+ .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->page_used[pg] = off + (uint32_t)len + attr;
+ h->page_parts[pg]++;
+ h->lines++;
+ h->all_blank = false;
+ if (h->wrap_cols)
+ count_closed(h, q, cheap_rows((uint64_t)len, 0, h->wrap_cols));
+ return true;
+}
+
+/* Start a line. The caller made sure that no line is open. */
+static struct hline *begin_line(struct hist *h, size_t need)
+{
+ if (h->lines >= h->line_limit)
+ evict_line(h);
+ int pg = page_for(h, need);
+ if (pg < 0 || !ring_reserve(h))
+ return NULL;
+ struct hline *p = part(h, h->head++);
+ *p = (struct hline){
+ .off = ((uint32_t)pg << 16) | h->page_used[pg],
+ .flags = HLINE_NARROW | HLINE_OPEN,
+ .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->page_parts[pg]++;
+ h->lines++;
+ h->last_id = hist_end_id(h) - 1;
+ h->last_cells = h->last_wide = 0;
+ h->all_blank = false;
+ h->walk_on = false;
+ runs_reset(&h->runs);
+ return p;
+}
+
+/* Write the runs of the open part after its text. */
+static void close_part(struct hist *h, struct hline *p)
+{
+ runs_finish(&h->runs);
+ uint32_t pg = p->off >> 16;
+ uint32_t end = (p->off & 0xffff) + p->text_len;
+ if (h->runs.len) {
+ memcpy(h->pages[pg] + end, h->runs.data, h->runs.len);
+ p->attr_len = (uint16_t)h->runs.len;
+ end += h->runs.len;
+ }
+ h->page_used[pg] = end;
+ p->flags &= (uint8_t)~HLINE_OPEN;
+}
+
+void term_hist_close_open_line(struct hist *h)
+{
+ struct hline *p = open_part(h);
+ if (!p)
+ return;
+ close_part(h, p);
+ runs_reset(&h->runs);
+ uint64_t id = hist_end_id(h) - 1;
+ int cols = h->wrap_cols;
+ if (!cols) {
+ h->walk_on = false;
+ return;
+ }
+ uint64_t first = line_first(h, id);
+ uint64_t rows = first == h->head - 1
+ ? cheap_rows(p->cells, p->wide, cols)
+ : cheap_rows(term_hist_line_cells(h, id), term_hist_line_wide(h, id), cols);
+ if (!rows && h->walk_on) {
+ walk_advance(h);
+ rows = h->walk_rows;
+ }
+ h->walk_on = false;
+ count_closed(h, part(h, first), rows);
+}
+
+/* Make room in the open part for text_need bytes and runs_need run bytes.
+ * Relocate a small part or continue the line in a new part. */
+static struct hline *open_room(struct hist *h, struct hline *p, size_t text_need,
+ size_t runs_need, uint32_t add_cells)
+{
+ uint32_t start = p->off & 0xffff;
+ size_t runs = h->runs.len + runs_need + RUN_MAX;
+ if (TERM_EXPECT(start + p->text_len + text_need + runs <= HIST_PAGE_USABLE &&
+ p->cells + add_cells <= HIST_MAX_PART_CELLS &&
+ runs <= UINT16_MAX, 1))
+ return p;
+
+ if (p->text_len + text_need + runs <= HIST_PAGE_USABLE / 4 &&
+ p->cells + add_cells <= HIST_MAX_PART_CELLS && start > 0) {
+ uint32_t old_pg = p->off >> 16;
+ int pg = h->page_parts[old_pg] == 1 ? -1 : page_next(h);
+ if (pg >= 0) {
+ memcpy(h->pages[pg], h->pages[old_pg] + start, p->text_len);
+ if (h->page_used[old_pg] == start + p->text_len)
+ h->page_used[old_pg] = start;
+ page_drop_ref(h, old_pg);
+ p->off = (uint32_t)pg << 16;
+ h->page_parts[pg]++;
+ h->page_used[pg] = p->text_len;
+ return p;
+ }
+ /* The part is alone on its page, or no other page is free and the
+ * evictions left it alone: move it to the page start. */
+ if (h->page_parts[old_pg] == 1) {
+ memmove(h->pages[old_pg], h->pages[old_pg] + start, p->text_len);
+ p->off = old_pg << 16;
+ if (h->cur_page != old_pg && h->cur_page < h->npages && !h->page_parts[h->cur_page])
+ h->free_pages[h->nfree++] = (uint16_t)h->cur_page;
+ h->cur_page = old_pg;
+ h->page_used[old_pg] = p->text_len;
+ return p;
+ }
+ }
+
+ /* Continue the line in a new part. The old part stays OPEN until the new
+ * part exists, so a page eviction does not drop the line. */
+ struct pen_attr pen = h->runs.pen;
+ runs_finish(&h->runs);
+ uint32_t pg0 = p->off >> 16;
+ uint32_t end = (p->off & 0xffff) + p->text_len;
+ if (h->runs.len) {
+ memcpy(h->pages[pg0] + end, h->runs.data, h->runs.len);
+ p->attr_len = (uint16_t)h->runs.len;
+ end += h->runs.len;
+ }
+ h->page_used[pg0] = end;
+ int pg = page_next(h);
+ if (pg < 0) {
+ /* Only the open part is left and no page is free. Drop its content
+ * and reuse its page. The line keeps its id. */
+ if (h->tail + 1 != h->head || h->page_parts[pg0] != 1)
+ return NULL;
+ h->page_used[pg0] = 0;
+ if (h->base_cell_id != h->base_id) {
+ h->base_cell_id = h->base_id;
+ h->base_cell = 0;
+ }
+ h->base_cell += p->cells;
+ h->last_cells = h->last_wide = 0;
+ h->index_epoch++;
+ h->cur_page = pg0;
+ *p = (struct hline){
+ .off = pg0 << 16,
+ .flags = HLINE_OPEN | HLINE_NARROW | (p->flags & HLINE_CONT),
+ .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->walk_on = false;
+ runs_reset(&h->runs);
+ h->runs.pen = pen;
+ return p;
+ }
+ if (!ring_reserve(h))
+ return NULL;
+ p = part(h, h->head - 1);
+ p->flags &= (uint8_t)~HLINE_OPEN;
+ struct hline *q = part(h, h->head++);
+ *q = (struct hline){
+ .off = ((uint32_t)pg << 16) | h->page_used[pg],
+ .flags = HLINE_OPEN | HLINE_CONT | HLINE_NARROW,
+ .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->page_parts[pg]++;
+ if (!cont_push(h, h->head - 1)) {
+ /* Without the index entry, line_first() would map the next logical
+ * line to this part and scrollback would show the wrong text. Put
+ * the line back the way it was: the old part is open again and the
+ * new part never existed. */
+ h->page_parts[pg]--;
+ h->head--;
+ p->flags |= HLINE_OPEN;
+ return NULL;
+ }
+ runs_reset(&h->runs);
+ h->runs.pen = pen;
+ return q;
+}
+
+static inline void part_add(struct hist *h, struct hline *p, const struct enc *e)
+{
+ p->text_len += e->bytes;
+ p->cells += (uint16_t)e->cells;
+ p->wide += e->wide;
+ h->last_cells += e->cells;
+ h->last_wide += e->wide;
+ p->flags = (uint8_t)((p->flags & ~HLINE_NARROW) | (e->flags & ~HLINE_NARROW) |
+ (p->flags & e->flags & HLINE_NARROW));
+ h->page_used[p->off >> 16] = (p->off & 0xffff) + p->text_len;
+}
+
+void term_hist_push_row(struct hist *h, const struct cell *cells, int len, bool wrapped,
+ const struct gstore *g)
+{
+ if (!h->line_limit)
+ return;
+ struct hline *p = open_part(h);
+ if (!p) {
+ if (len <= 0 && !wrapped) {
+ term_hist_push_blank(h, 1);
+ return;
+ }
+ if (!wrapped && push_uniform_line(h, cells, len))
+ return;
+ if (!wrapped && push_line(h, cells, len, NULL, NULL, g))
+ return;
+ p = begin_line(h, (size_t)len * CELL_BYTES + (size_t)(len + 2) * RUN_MAX);
+ if (!p)
+ return;
+ }
+ for (int i = 0; i < len;) {
+ int n = len - i;
+ size_t runs_need = (size_t)(n + 1) * RUN_MAX;
+ p = open_room(h, p, (size_t)n * CELL_BYTES, runs_need, (uint32_t)n * 2);
+ if (!p || !runs_reserve(&h->runs, runs_need + RUN_MAX))
+ return;
+ size_t used_bytes = (p->off & 0xffff) + p->text_len;
+ size_t cap = HIST_PAGE_USABLE - used_bytes - (h->runs.len + runs_need + RUN_MAX);
+ struct enc e;
+ i += encode_cells(&h->runs, cells + i, n, part_text(h, p) + p->text_len, cap, g, &e);
+ part_add(h, p, &e);
+ /* A cluster did not fit: continue the line in a new part. */
+ if (i < len && !(p = open_room(h, p, HIST_PAGE_USABLE, 0, 0)))
+ return;
+ }
+ if (!wrapped)
+ term_hist_close_open_line(h);
+ else if (h->wrap_cols && (h->walk_on || (p->flags & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER))))
+ walk_advance(h);
+}
+
+void term_hist_push_ascii(struct hist *h, const char *bytes, int n, const struct pen_attr *pen,
+ bool wrapped)
+{
+ if (!h->line_limit)
+ return;
+ struct pen_attr def = pen_attr_default();
+ if (!pen)
+ pen = &def;
+ struct hline *p = open_part(h);
+ if (!p) {
+ if (n <= 0 && !wrapped) {
+ term_hist_push_blank(h, 1);
+ return;
+ }
+ /* A default-pen line: one copy and one descriptor. */
+ if (!wrapped && h->lines >= h->line_limit)
+ evict_line(h);
+ uint32_t pg = h->cur_page;
+ if (!wrapped && pen_attr_is_default(pen) && pg < h->npages &&
+ h->page_used[pg] + (uint32_t)n <= HIST_PAGE_USABLE &&
+ h->ring && h->head - h->tail <= h->ring_mask) {
+ uint32_t off = h->page_used[pg];
+ bytes_copy_small(h->pages[pg] + off, bytes, n);
+ h->page_used[pg] = off + (uint32_t)n;
+ h->page_parts[pg]++;
+ struct hline *q = part(h, h->head++);
+ *q = (struct hline){
+ .off = (pg << 16) | off, .text_len = (uint32_t)n, .cells = (uint32_t)n,
+ .flags = HLINE_NARROW, .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->lines++;
+ h->all_blank = false;
+ if (h->wrap_cols)
+ count_closed(h, q, rows_narrow((uint64_t)n, h->wrap_cols));
+ return;
+ }
+ if (!wrapped && n <= HIST_PAGE_USABLE / 2 && push_line(h, NULL, n, bytes, pen, NULL))
+ return;
+ p = begin_line(h, (size_t)(n < HIST_PAGE_USABLE / 2 ? n : 0) + RUN_MAX * 2);
+ if (!p)
+ return;
+ }
+ while (n > 0) {
+ int chunk = n < HIST_PAGE_USABLE / 2 ? n : HIST_PAGE_USABLE / 2;
+ p = open_room(h, p, (size_t)chunk, RUN_MAX, (uint32_t)chunk);
+ if (!p || !runs_reserve(&h->runs, RUN_MAX * 2))
+ return;
+ memcpy(part_text(h, p) + p->text_len, bytes, (size_t)chunk);
+ hist_runs_add(&h->runs, pen, (uint32_t)chunk);
+ struct enc e = { .bytes = (uint32_t)chunk, .cells = (uint32_t)chunk,
+ .flags = (uint8_t)(HLINE_NARROW | (pen->link ? HLINE_HAS_LINK : 0)) };
+ part_add(h, p, &e);
+ bytes += chunk;
+ n -= chunk;
+ }
+ if (!wrapped)
+ term_hist_close_open_line(h);
+ else if (h->wrap_cols && h->walk_on)
+ walk_advance(h);
+}
+
+/* Add the runs of a text row to the open part pen by pen, when its bytes
+ * cannot be copied because the delta bases differ. */
+static void text_row_readd(struct hist *h, const struct text_row *row)
+{
+ const struct hist_runs *rr = &row->runs;
+ struct pen_attr pen = row->start;
+ uint32_t count;
+ for (const uint8_t *q = rr->data, *end = rr->data + rr->len; q < end;) {
+ q = read_run(q, &pen, &count);
+ hist_runs_add(&h->runs, &pen, count);
+ }
+ if (rr->count)
+ hist_runs_add(&h->runs, &rr->pen, rr->count);
+}
+
+void term_hist_push_text(struct hist *h, const struct text_row *row, bool wrapped)
+{
+ if (!h->line_limit)
+ return;
+ const struct hist_runs *rr = &row->runs;
+ struct hline *p = open_part(h);
+ size_t runs_need = (size_t)rr->len * RUN_MAX / 2 + 2 * RUN_MAX;
+ if (!p) {
+ if (row->cells == 0 && !wrapped) {
+ term_hist_push_blank(h, 1);
+ return;
+ }
+ if (!wrapped && pen_attr_is_default(&row->start)) {
+ /* A whole line whose runs start from the default pen: copy the
+ * text and the runs, then one descriptor. */
+ if (h->lines >= h->line_limit)
+ evict_line(h);
+ int pg = page_for(h, (size_t)row->text_len + rr->len + RUN_MAX);
+ if (pg < 0 || !ring_reserve(h))
+ return;
+ uint32_t off = h->page_used[pg];
+ uint8_t *dst = h->pages[pg] + off;
+ bytes_copy_small(dst, (const char *)row->text, row->text_len);
+ uint8_t *ap = dst + row->text_len;
+ bytes_copy_small(ap, (const char *)rr->data, (int)rr->len);
+ uint8_t *end = ap + rr->len;
+ if (rr->count && !pen_attr_is_default(&rr->pen))
+ end = emit_run(end, &rr->base, &rr->pen, rr->count);
+ struct hline *q = part(h, h->head++);
+ *q = (struct hline){
+ .off = ((uint32_t)pg << 16) | off,
+ .text_len = row->text_len,
+ .cells = row->cells,
+ .wide = row->wide,
+ .attr_len = (uint16_t)(end - ap),
+ .flags = row->flags,
+ .wrap_rows = ROWS_UNKNOWN,
+ };
+ h->page_used[pg] = (uint32_t)(end - h->pages[pg]);
+ h->page_parts[pg]++;
+ h->lines++;
+ h->all_blank = false;
+ if (h->wrap_cols)
+ count_closed(h, q, cheap_rows(row->cells, row->wide, h->wrap_cols));
+ return;
+ }
+ p = begin_line(h, (size_t)row->text_len + runs_need);
+ if (!p)
+ return;
+ }
+ /* The runs copy when their delta base is the pen of the open part, else
+ * they are added again pen by pen, which can take more room. */
+ runs_flush(&h->runs);
+ bool copy = pen_attr_equal(&h->runs.base, &row->start);
+ struct hline *q = open_room(h, p, row->text_len, copy ? rr->len + RUN_MAX : runs_need,
+ row->cells);
+ if (!q || !runs_reserve(&h->runs, runs_need + RUN_MAX))
+ return;
+ if (q != p && !(copy = pen_attr_equal(&h->runs.base, &row->start)) &&
+ !(q = open_room(h, q, row->text_len, runs_need, row->cells)))
+ return;
+ p = q;
+ memcpy(part_text(h, p) + p->text_len, row->text, row->text_len);
+ if (copy) {
+ memcpy(h->runs.data + h->runs.len, rr->data, rr->len);
+ h->runs.len += rr->len;
+ h->runs.base = rr->base;
+ h->runs.pen = rr->pen;
+ h->runs.count = rr->count;
+ } else {
+ text_row_readd(h, row);
+ }
+ part_add(h, p, &(struct enc){ row->text_len, row->cells, row->flags, row->wide });
+ if (!wrapped)
+ term_hist_close_open_line(h);
+ else if (h->wrap_cols && (h->walk_on || (p->flags & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER))))
+ walk_advance(h);
+}
+
+/* Write the narrow descriptors of ends[i..stop) in blocks, while a block
+ * fits before the ring wraps. Return the first line left for the caller. */
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static int lf_rows_avx512(struct hist *restrict h, const uint16_t *restrict ends, int i, int stop,
+ uint32_t pg, uint32_t base, uint32_t cols, uint64_t *head_io,
+ uint64_t *rows_io, uint32_t *text_count_io)
+{
+ struct hline *restrict ring = h->ring;
+ struct hist_block *restrict blocks = h->blocks;
+ uint32_t mask = h->ring_mask;
+ uint64_t head = *head_io, rows = *rows_io;
+ uint32_t text_count = *text_count_io;
+ const __m512i recip = _mm512_set1_epi32((int)(UINT64_C(0x100000000) / cols + 1));
+ for (; i + 16 <= stop && (head & mask) + 16 <= mask + 1; i += 16, head += 16) {
+ __m512i start = _mm512_cvtepu16_epi32(_mm256_loadu_si256((const __m256i *)(ends + i)));
+ __m512i end = _mm512_cvtepu16_epi32(_mm256_loadu_si256((const __m256i *)(ends + i + 1)));
+ __m512i lens = _mm512_sub_epi32(_mm512_sub_epi32(end, start), _mm512_set1_epi32(1));
+ __mmask16 empty = _mm512_cmpeq_epi32_mask(lens, _mm512_setzero_si512());
+ uint32_t refs = 16 - term_popcount32(empty);
+ text_count += refs;
+ __m512i offsets = _mm512_mask_mov_epi32(_mm512_add_epi32(start, _mm512_set1_epi32((int)base)),
+ empty, _mm512_set1_epi32(-1));
+ __m512i rs = _mm512_set1_epi32(1);
+ uint32_t nr = 16;
+ if (cols == 1) {
+ rs = _mm512_max_epu32(lens, rs);
+ nr = (uint32_t)_mm512_reduce_add_epi32(rs);
+ } else if (_mm512_cmpgt_epu32_mask(lens, _mm512_set1_epi32((int)cols))) {
+ /* For a 16-bit numerator, this reciprocal gives exact division. */
+ __m512i num = _mm512_max_epi32(_mm512_sub_epi32(lens, _mm512_set1_epi32(1)),
+ _mm512_setzero_si512());
+ __m512i even = _mm512_srli_epi64(_mm512_mul_epu32(num, recip), 32);
+ __m512i odd = _mm512_mul_epu32(_mm512_srli_epi64(num, 32), recip);
+ rs = _mm512_add_epi32(_mm512_mask_blend_epi32(0xaaaa, even, odd), rs);
+ nr = (uint32_t)_mm512_reduce_add_epi32(rs);
+ }
+ rows += nr - 16;
+ blocks[(head & mask) / 16] = (struct hist_block){
+ .seq = head + 1, .epoch = h->index_epoch,
+ .page = pg, .refs = refs, .rows = nr, .cols = h->wrap_cols,
+ };
+ __m512i tags = _mm512_or_si512(_mm512_slli_epi32(_mm512_min_epu32(rs, _mm512_set1_epi32(255)), 24),
+ _mm512_set1_epi32(HLINE_NARROW << 16));
+ __m512i a = _mm512_unpacklo_epi32(offsets, lens);
+ __m512i b = _mm512_unpackhi_epi32(offsets, lens);
+ __m512i c = _mm512_unpacklo_epi32(lens, tags);
+ __m512i d = _mm512_unpackhi_epi32(lens, tags);
+ __m512i v0 = _mm512_unpacklo_epi64(a, c);
+ __m512i v1 = _mm512_unpackhi_epi64(a, c);
+ __m512i v2 = _mm512_unpacklo_epi64(b, d);
+ __m512i v3 = _mm512_unpackhi_epi64(b, d);
+ a = _mm512_shuffle_i32x4(v0, v1, 0x44);
+ b = _mm512_shuffle_i32x4(v2, v3, 0x44);
+ c = _mm512_shuffle_i32x4(v0, v1, 0xee);
+ d = _mm512_shuffle_i32x4(v2, v3, 0xee);
+ struct hline *dst = ring + (head & mask);
+ _mm512_storeu_si512(dst, _mm512_shuffle_i32x4(a, b, 0x88));
+ _mm512_storeu_si512(dst + 4, _mm512_shuffle_i32x4(a, b, 0xdd));
+ _mm512_storeu_si512(dst + 8, _mm512_shuffle_i32x4(c, d, 0x88));
+ _mm512_storeu_si512(dst + 12, _mm512_shuffle_i32x4(c, d, 0xdd));
+ }
+ *head_io = head;
+ *rows_io = rows;
+ *text_count_io = text_count;
+ return i;
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static int lf_rows_avx2(struct hist *restrict h, const uint16_t *restrict ends, int i, int stop,
+ uint32_t pg, uint32_t base, uint32_t cols, uint64_t *head_io,
+ uint64_t *rows_io, uint32_t *text_count_io)
+{
+ struct hline *restrict ring = h->ring;
+ struct hist_block *restrict blocks = h->blocks;
+ uint32_t mask = h->ring_mask;
+ uint64_t head = *head_io, rows = *rows_io;
+ uint32_t text_count = *text_count_io;
+ uint32_t recip = (uint32_t)(UINT64_C(0x100000000) / cols + 1);
+ uint32_t block_rows = 0, block_refs = 0;
+ for (; i + 4 <= stop && (head & mask) + 4 <= mask + 1; i += 4, head += 4) {
+ uint32_t nr = 4, refs;
+ struct hline *dst = ring + (head & mask);
+ __m128i start = _mm_cvtepu16_epi32(_mm_loadl_epi64((const __m128i *)(ends + i)));
+ __m128i end = _mm_cvtepu16_epi32(_mm_loadl_epi64((const __m128i *)(ends + i + 1)));
+ __m128i lens = _mm_sub_epi32(_mm_sub_epi32(end, start), _mm_set1_epi32(1));
+ __m128i empty = _mm_cmpeq_epi32(lens, _mm_setzero_si128());
+ refs = 4 - term_popcount32((unsigned)_mm_movemask_ps(_mm_castsi128_ps(empty)));
+ __m128i offsets = _mm_or_si128(_mm_add_epi32(start, _mm_set1_epi32((int)base)), empty);
+ __m128i rs = _mm_set1_epi32(1);
+ if (cols == 1) rs = _mm_max_epu32(lens, rs);
+ else if (_mm_movemask_ps(_mm_castsi128_ps(_mm_cmpgt_epi32(lens, _mm_set1_epi32((int)cols))))) {
+ __m128i num = _mm_max_epi32(_mm_sub_epi32(lens, _mm_set1_epi32(1)), _mm_setzero_si128());
+ __m128i even = _mm_srli_epi64(_mm_mul_epu32(num, _mm_set1_epi32((int)recip)), 32);
+ __m128i odd = _mm_mul_epu32(_mm_srli_epi64(num, 32), _mm_set1_epi32((int)recip));
+ rs = _mm_add_epi32(_mm_blend_epi16(even, odd, 0xcc), rs);
+ }
+ __m128i sum = _mm_hadd_epi32(rs, rs);
+ nr = (uint32_t)_mm_cvtsi128_si32(_mm_hadd_epi32(sum, sum));
+ __m128i tags = _mm_or_si128(_mm_slli_epi32(_mm_min_epu32(rs, _mm_set1_epi32(255)), 24),
+ _mm_set1_epi32(HLINE_NARROW << 16));
+ __m128i a = _mm_unpacklo_epi32(offsets, lens), b = _mm_unpackhi_epi32(offsets, lens);
+ __m128i c = _mm_unpacklo_epi32(lens, tags), d = _mm_unpackhi_epi32(lens, tags);
+ _mm_storeu_si128((__m128i *)(dst + 0), _mm_unpacklo_epi64(a, c));
+ _mm_storeu_si128((__m128i *)(dst + 1), _mm_unpackhi_epi64(a, c));
+ _mm_storeu_si128((__m128i *)(dst + 2), _mm_unpacklo_epi64(b, d));
+ _mm_storeu_si128((__m128i *)(dst + 3), _mm_unpackhi_epi64(b, d));
+ text_count += refs;
+ rows += nr - 4;
+ block_rows += nr;
+ block_refs += refs;
+ if ((head & 15) == 12) {
+ blocks[(head & mask) / 16] = (struct hist_block){
+ .seq = head - 12 + 1, .epoch = h->index_epoch,
+ .page = pg, .refs = block_refs, .rows = block_rows, .cols = h->wrap_cols,
+ };
+ block_rows = block_refs = 0;
+ }
+ }
+ *head_io = head;
+ *rows_io = rows;
+ *text_count_io = text_count;
+ return i;
+}
+#endif
+
+static inline int lf_rows(struct hist *restrict h, const uint16_t *restrict ends, int i, int stop,
+ uint32_t pg, uint32_t base, uint32_t cols, uint64_t *head_io,
+ uint64_t *rows_io, uint32_t *text_count_io)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return lf_rows_avx512(h, ends, i, stop, pg, base, cols, head_io, rows_io, text_count_io);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return lf_rows_avx2(h, ends, i, stop, pg, base, cols, head_io, rows_io, text_count_io);
+#endif
+#if defined(__aarch64__)
+ struct hline *restrict ring = h->ring;
+ struct hist_block *restrict blocks = h->blocks;
+ uint32_t mask = h->ring_mask;
+ uint64_t head = *head_io, rows = *rows_io;
+ uint32_t text_count = *text_count_io;
+ uint32_t recip = (uint32_t)(UINT64_C(0x100000000) / cols + 1);
+ uint32_t block_rows = 0, block_refs = 0;
+ for (; i + 4 <= stop && (head & mask) + 4 <= mask + 1; i += 4, head += 4) {
+ uint32_t nr = 4, refs;
+ struct hline *dst = ring + (head & mask);
+ uint32x4_t start = vmovl_u16(vld1_u16(ends + i));
+ uint32x4_t end = vmovl_u16(vld1_u16(ends + i + 1));
+ uint32x4_t lens = vsubq_u32(vsubq_u32(end, start), vdupq_n_u32(1));
+ uint32x4_t empty = vceqq_u32(lens, vdupq_n_u32(0));
+ refs = 4 - vaddvq_u32(vshrq_n_u32(empty, 31));
+ uint32x4_t offsets = vorrq_u32(vaddq_u32(start, vdupq_n_u32(base)), empty);
+ uint32x4_t rs = vdupq_n_u32(1);
+ if (cols == 1) rs = vmaxq_u32(lens, rs);
+ else if (vmaxvq_u32(lens) > cols) {
+ uint32x4_t num = vqsubq_u32(lens, vdupq_n_u32(1));
+ uint32x2_t lo = vshrn_n_u64(vmull_n_u32(vget_low_u32(num), recip), 32);
+ uint32x2_t hi = vshrn_n_u64(vmull_n_u32(vget_high_u32(num), recip), 32);
+ rs = vaddq_u32(vcombine_u32(lo, hi), rs);
+ }
+ nr = vaddvq_u32(rs);
+ uint32x4_t tags = vorrq_u32(vshlq_n_u32(vminq_u32(rs, vdupq_n_u32(255)), 24),
+ vdupq_n_u32(HLINE_NARROW << 16));
+ uint64x2_t a = vreinterpretq_u64_u32(vzip1q_u32(offsets, lens));
+ uint64x2_t b = vreinterpretq_u64_u32(vzip2q_u32(offsets, lens));
+ uint64x2_t c = vreinterpretq_u64_u32(vzip1q_u32(lens, tags));
+ uint64x2_t d = vreinterpretq_u64_u32(vzip2q_u32(lens, tags));
+ vst1q_u64((uint64_t *)(void *)(dst + 0), vzip1q_u64(a, c));
+ vst1q_u64((uint64_t *)(void *)(dst + 1), vzip2q_u64(a, c));
+ vst1q_u64((uint64_t *)(void *)(dst + 2), vzip1q_u64(b, d));
+ vst1q_u64((uint64_t *)(void *)(dst + 3), vzip2q_u64(b, d));
+ text_count += refs;
+ rows += nr - 4;
+ block_rows += nr;
+ block_refs += refs;
+ if ((head & 15) == 12) {
+ blocks[(head & mask) / 16] = (struct hist_block){
+ .seq = head - 12 + 1, .epoch = h->index_epoch,
+ .page = pg, .refs = block_refs, .rows = block_rows, .cols = h->wrap_cols,
+ };
+ block_rows = block_refs = 0;
+ }
+ }
+ *head_io = head;
+ *rows_io = rows;
+ *text_count_io = text_count;
+#else
+ (void)h, (void)ends, (void)stop, (void)pg, (void)base, (void)cols;
+ (void)head_io, (void)rows_io, (void)text_count_io;
+#endif
+ return i;
+}
+
+TERM_NOINLINE
+void term_hist_push_ascii_lf(struct hist *restrict h, const char *restrict bytes,
+ const uint16_t *restrict ends, int count)
+{
+ if (!h->line_limit) return;
+ int i = 0;
+ if (count && hist_is_open(h)) {
+ term_hist_push_ascii(h, bytes + ends[0], ends[1] - ends[0] - 1, NULL, false);
+ i++;
+ }
+ uint32_t cols = h->wrap_cols ? h->wrap_cols : 1;
+ while (i < count) {
+ int stop = count - i > (int)h->line_limit ? i + (int)h->line_limit : count;
+ if (page_for(h, ends[i + 1] - ends[i]) < 0) return;
+ uint32_t pg = h->cur_page;
+ uint32_t room = HIST_PAGE_USABLE - h->page_used[pg];
+ if ((uint32_t)(ends[stop] - ends[i]) > room) {
+ int lo = i + 1, hi = stop;
+ while (lo < hi) {
+ int mid = lo + (hi - lo + 1) / 2;
+ if ((uint32_t)(ends[mid] - ends[i]) <= room) lo = mid;
+ else hi = mid - 1;
+ }
+ stop = lo;
+ }
+ uint32_t n = (uint32_t)(stop - i);
+ evict_for(h, n);
+ if (!ring_fit(h, n)) return;
+ bool count_rows = h->wrap_cols && h->rows_id == hist_end_id(h);
+ uint32_t off = h->page_used[pg];
+ uint32_t base = (pg << 16) + off - ends[i];
+ uint32_t span = ends[stop] - ends[i];
+ memcpy(h->pages[pg] + off, bytes + ends[i], span);
+ uint64_t head = h->head, rows = n;
+ struct hline *restrict ring = h->ring;
+ uint32_t mask = h->ring_mask;
+ uint32_t text_count = 0;
+ while (i < stop && (head & 15)) {
+ uint32_t len = ends[i + 1] - ends[i] - 1;
+ struct hline *dst = ring + (head & mask);
+ if (!len) *dst = HLINE_BLANK;
+ else {
+ uint32_t r = rows_narrow(len, (int)cols);
+ part_put_narrow_rows(dst, base + ends[i], len, r);
+ rows += r - 1;
+ text_count++;
+ }
+ i++;
+ head++;
+ }
+ i = lf_rows(h, ends, i, stop, pg, base, cols, &head, &rows, &text_count);
+ for (; i < stop; i++, head++) {
+ uint32_t len = ends[i + 1] - ends[i] - 1;
+ struct hline *dst = ring + (head & mask);
+ if (!len) *dst = HLINE_BLANK;
+ else {
+ uint32_t r = rows_narrow(len, (int)cols);
+ part_put_narrow_rows(dst, base + ends[i], len, r);
+ rows += r - 1;
+ text_count++;
+ }
+ }
+ h->head = head;
+ h->page_used[pg] = off + (text_count ? span : 0);
+ h->page_parts[pg] += text_count;
+ h->lines += n;
+ if (text_count) h->all_blank = false;
+ if (count_rows) {
+ h->counted_rows += rows;
+ h->rows_id += n;
+ }
+ }
+}
+
+static_assert(sizeof(struct hist_ascii_line) == 16, "a line is two 64-bit lanes");
+static_assert(offsetof(struct hist_ascii_line, len) == 8, "len is in the second 64-bit lane");
+
+/* Write the narrow one-row descriptors of lines[i..stream_end) in blocks,
+ * while a block fits before the ring wraps. first is the first byte of
+ * the page copy at base. Return the first line left for the caller. */
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static int stream_lines_avx512(struct hist *restrict h, const struct hist_ascii_line *lines,
+ int i, int stream_end, const char *first, uint32_t base,
+ uint32_t pg, uint64_t *head_io, uint32_t *text_count_io)
+{
+ uint64_t head = *head_io;
+ uint32_t text_count = *text_count_io;
+ const __m512i even = _mm512_setr_epi64(0, 2, 4, 6, 8, 10, 12, 14);
+ const __m512i odd = _mm512_setr_epi64(1, 3, 5, 7, 9, 11, 13, 15);
+ for (; i + 16 <= stream_end &&
+ (head & h->ring_mask) + 16 <= h->ring_mask + 1; i += 16, head += 16) {
+ __m512i q0 = _mm512_loadu_si512(lines + i);
+ __m512i q1 = _mm512_loadu_si512(lines + i + 4);
+ __m512i q2 = _mm512_loadu_si512(lines + i + 8);
+ __m512i q3 = _mm512_loadu_si512(lines + i + 12);
+ __m512i p0 = _mm512_permutex2var_epi64(q0, even, q1);
+ __m512i p1 = _mm512_permutex2var_epi64(q2, even, q3);
+ __m512i origin = _mm512_set1_epi64((uintptr_t)first);
+ __m256i lo = _mm512_cvtepi64_epi32(_mm512_sub_epi64(p0, origin));
+ __m256i hi = _mm512_cvtepi64_epi32(_mm512_sub_epi64(p1, origin));
+ __m512i offsets = _mm512_add_epi32(
+ _mm512_inserti64x4(_mm512_castsi256_si512(lo), hi, 1),
+ _mm512_set1_epi32((int)base));
+ lo = _mm512_cvtepi64_epi32(_mm512_permutex2var_epi64(q0, odd, q1));
+ hi = _mm512_cvtepi64_epi32(_mm512_permutex2var_epi64(q2, odd, q3));
+ __m512i lens = _mm512_and_si512(
+ _mm512_inserti64x4(_mm512_castsi256_si512(lo), hi, 1),
+ _mm512_set1_epi32(UINT16_MAX));
+ __mmask16 empty = _mm512_cmpeq_epi32_mask(lens, _mm512_setzero_si512());
+ uint32_t refs = 16 - term_popcount32(empty);
+ text_count += refs;
+ offsets = _mm512_mask_mov_epi32(offsets, empty, _mm512_set1_epi32(-1));
+ __m512i tags = _mm512_set1_epi32((1u << 24) | (HLINE_NARROW << 16));
+ __m512i a = _mm512_unpacklo_epi32(offsets, lens);
+ __m512i b = _mm512_unpackhi_epi32(offsets, lens);
+ __m512i c = _mm512_unpacklo_epi32(lens, tags);
+ __m512i d = _mm512_unpackhi_epi32(lens, tags);
+ __m512i v0 = _mm512_unpacklo_epi64(a, c);
+ __m512i v1 = _mm512_unpackhi_epi64(a, c);
+ __m512i v2 = _mm512_unpacklo_epi64(b, d);
+ __m512i v3 = _mm512_unpackhi_epi64(b, d);
+ a = _mm512_shuffle_i32x4(v0, v1, 0x44);
+ b = _mm512_shuffle_i32x4(v2, v3, 0x44);
+ c = _mm512_shuffle_i32x4(v0, v1, 0xee);
+ d = _mm512_shuffle_i32x4(v2, v3, 0xee);
+ struct hline *dst = h->ring + (head & h->ring_mask);
+ _mm512_storeu_si512(dst, _mm512_shuffle_i32x4(a, b, 0x88));
+ _mm512_storeu_si512(dst + 4, _mm512_shuffle_i32x4(a, b, 0xdd));
+ _mm512_storeu_si512(dst + 8, _mm512_shuffle_i32x4(c, d, 0x88));
+ _mm512_storeu_si512(dst + 12, _mm512_shuffle_i32x4(c, d, 0xdd));
+ if (!(head & 15)) {
+ h->blocks[(head & h->ring_mask) / 16] = (struct hist_block){
+ .seq = head + 1, .epoch = h->index_epoch,
+ .page = pg, .refs = refs, .rows = 16, .cols = h->wrap_cols,
+ };
+ }
+ }
+ *head_io = head;
+ *text_count_io = text_count;
+ return i;
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static int stream_lines_avx2(struct hist *restrict h, const struct hist_ascii_line *lines,
+ int i, int stream_end, const char *first, uint32_t base,
+ uint32_t pg, uint64_t *head_io, uint32_t *text_count_io)
+{
+ (void)pg;
+ uint64_t head = *head_io;
+ uint32_t text_count = *text_count_io;
+ for (; i + 4 <= stream_end &&
+ (head & h->ring_mask) + 4 <= h->ring_mask + 1; i += 4, head += 4) {
+ struct hline *dst = h->ring + (head & h->ring_mask);
+ uint32_t refs;
+ __m128i index = _mm_setr_epi32(0, 16, 32, 48);
+ __m128i ptrs = _mm_i32gather_epi32((const int *)(lines + i), index, 1);
+ __m128i lens = _mm_and_si128(
+ _mm_i32gather_epi32((const int *)((const char *)(lines + i) + 8), index, 1),
+ _mm_set1_epi32(UINT16_MAX));
+ __m128i empty = _mm_cmpeq_epi32(lens, _mm_setzero_si128());
+ refs = 4 - term_popcount32((unsigned)_mm_movemask_ps(_mm_castsi128_ps(empty)));
+ __m128i offsets = _mm_add_epi32(ptrs,
+ _mm_set1_epi32((int)(base - (uint32_t)(uintptr_t)first)));
+ offsets = _mm_or_si128(offsets, empty);
+ __m128i tags = _mm_set1_epi32((1u << 24) | (HLINE_NARROW << 16));
+ __m128i a = _mm_unpacklo_epi32(offsets, lens);
+ __m128i b = _mm_unpackhi_epi32(offsets, lens);
+ __m128i c = _mm_unpacklo_epi32(lens, tags);
+ __m128i d = _mm_unpackhi_epi32(lens, tags);
+ _mm256_storeu_si256((__m256i *)dst, _mm256_inserti128_si256(
+ _mm256_castsi128_si256(_mm_unpacklo_epi64(a, c)), _mm_unpackhi_epi64(a, c), 1));
+ _mm256_storeu_si256((__m256i *)(dst + 2), _mm256_inserti128_si256(
+ _mm256_castsi128_si256(_mm_unpacklo_epi64(b, d)), _mm_unpackhi_epi64(b, d), 1));
+ text_count += refs;
+ }
+ *head_io = head;
+ *text_count_io = text_count;
+ return i;
+}
+#endif
+
+static inline int stream_lines(struct hist *restrict h, const struct hist_ascii_line *lines,
+ int i, int stream_end, const char *first, uint32_t base,
+ uint32_t pg, uint64_t *head_io, uint32_t *text_count_io)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return stream_lines_avx512(h, lines, i, stream_end, first, base, pg, head_io, text_count_io);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return stream_lines_avx2(h, lines, i, stream_end, first, base, pg, head_io, text_count_io);
+#endif
+#if defined(__aarch64__)
+ (void)pg;
+ uint64_t head = *head_io;
+ uint32_t text_count = *text_count_io;
+ for (; i + 4 <= stream_end &&
+ (head & h->ring_mask) + 4 <= h->ring_mask + 1; i += 4, head += 4) {
+ struct hline *dst = h->ring + (head & h->ring_mask);
+ uint32_t refs;
+ uint32x4x4_t q = vld4q_u32((const uint32_t *)(lines + i));
+ uint32x4_t lens = vandq_u32(q.val[2], vdupq_n_u32(UINT16_MAX));
+ uint32x4_t empty = vceqq_u32(lens, vdupq_n_u32(0));
+ refs = 4 - vaddvq_u32(vshrq_n_u32(empty, 31));
+ uint32x4_t offsets = vorrq_u32(empty, vaddq_u32(q.val[0],
+ vdupq_n_u32(base - (uint32_t)(uintptr_t)first)));
+ uint32x4x4_t parts = { .val = {
+ offsets, lens, lens, vdupq_n_u32((1u << 24) | (HLINE_NARROW << 16)),
+ } };
+ vst4q_u32((uint32_t *)dst, parts);
+ text_count += refs;
+ }
+ *head_io = head;
+ *text_count_io = text_count;
+#else
+ (void)h, (void)lines, (void)stream_end, (void)first, (void)base, (void)pg;
+ (void)head_io, (void)text_count_io;
+#endif
+ return i;
+}
+
+void term_hist_push_ascii_lines(struct hist *h, const struct hist_ascii_line *lines, int count,
+ const struct pen_attr *pen)
+{
+ if (!h->line_limit)
+ return;
+ bool def = !pen || pen_attr_is_default(pen);
+ uint16_t cols = h->wrap_cols ? h->wrap_cols : 1;
+ for (int i = 0; i < count;) {
+ const struct hist_ascii_line *l = &lines[i];
+ if (l->wrapped) {
+ int end = i + 1;
+ int bytes = l->len;
+ while (end < count && lines[end].wrapped &&
+ lines[end].ptr == l->ptr + bytes &&
+ lines[end].len <= INT32_MAX - bytes) {
+ bytes += lines[end++].len;
+ }
+ term_hist_push_ascii(h, l->ptr, bytes, pen, true);
+ i = end;
+ continue;
+ }
+ if (!def || hist_is_open(h)) {
+ if (l->len == 0 && !l->wrapped)
+ term_hist_push_blank(h, 1);
+ else
+ term_hist_push_ascii(h, l->ptr, l->len, pen, l->wrapped);
+ i++;
+ continue;
+ }
+ /* A batch of unwrapped default-pen lines that fit the current page:
+ * one copy and one descriptor each. A blank line joins the batch
+ * instead of ending it. */
+ if (page_for(h, l->len) < 0)
+ return;
+ uint32_t pg = h->cur_page;
+ uint32_t room = HIST_PAGE_USABLE - h->page_used[pg];
+ int end = i;
+ uint32_t bytes = 0;
+ /* Keep separators in the page to copy a contiguous input batch once. */
+ int stream_end = i;
+ uint32_t extra_rows = 0;
+ while (stream_end < count && stream_end - i < (int)h->line_limit) {
+ const struct hist_ascii_line *next = &lines[stream_end];
+ if (next->wrapped || next->len > HIST_PAGE_USABLE / 2 ||
+ (stream_end > i &&
+ (next->ptr != lines[stream_end - 1].ptr + lines[stream_end - 1].len + 1 ||
+ next->ptr[-1] != '\n')))
+ break;
+ size_t span = (size_t)(next->ptr - l->ptr) + next->len;
+ if (span > room)
+ break;
+ if (TERM_EXPECT(next->len > (uint32_t)cols, 0))
+ extra_rows += rows_narrow(next->len, cols) - 1;
+ stream_end++;
+ }
+ if (stream_end - i >= 4) {
+ uint32_t n = (uint32_t)(stream_end - i);
+ uint64_t stream_rows = (uint64_t)n + extra_rows;
+ const struct hist_ascii_line *last = &lines[stream_end - 1];
+ uint32_t span = (uint32_t)(last->ptr - l->ptr) + last->len;
+ evict_for(h, n);
+ if (!ring_fit(h, n))
+ return;
+ uint32_t off = h->page_used[pg];
+ memcpy(h->pages[pg] + off, l->ptr, span);
+ uint32_t base = (pg << 16) | off;
+ uint32_t text_count = 0;
+ uint64_t head = h->head;
+ if (!extra_rows)
+ i = stream_lines(h, lines, i, stream_end, l->ptr, base, pg, &head, &text_count);
+ for (; i < stream_end; i++) {
+ uint32_t len = lines[i].len;
+ struct hline *dst = &h->ring[head++ & h->ring_mask];
+ if (len) {
+ uint32_t line_off = base + (uint32_t)(lines[i].ptr - l->ptr);
+ if (TERM_EXPECT(len <= cols, 1)) {
+ part_put_narrow(dst, line_off, len);
+ } else {
+ part_put_narrow_rows(dst, line_off, len, rows_narrow(len, cols));
+ }
+ text_count++;
+ } else {
+ *dst = HLINE_BLANK;
+ }
+ }
+ if (h->wrap_cols && h->rows_id == hist_end_id(h)) {
+ h->counted_rows += stream_rows;
+ h->rows_id += n;
+ }
+ h->head = head;
+ h->page_used[pg] = off + span;
+ h->page_parts[pg] += text_count;
+ h->lines += n;
+ if (text_count)
+ h->all_blank = false;
+ continue;
+ }
+ /* line_limit does not move inside this scan. */
+ int batch_max = (int)h->line_limit;
+ while (end < count && !lines[end].wrapped &&
+ bytes + lines[end].len <= room && end - i < batch_max) {
+ bytes += lines[end].len;
+ end++;
+ }
+ uint32_t n = (uint32_t)(end - i);
+ evict_for(h, n);
+ if (!ring_fit(h, n))
+ return;
+ bool count_rows = h->wrap_cols && h->rows_id == hist_end_id(h);
+ uint64_t rows_add = 0;
+ /* Lines that took a page slot. A blank line takes none. */
+ uint32_t n_text = 0;
+ bool any_text = false;
+ uint32_t off = h->page_used[pg];
+ uint8_t *page = h->pages[pg];
+ /* The ring and head cannot move inside this loop: eviction and the
+ * ring growth both ran above. Keep them in registers. */
+ struct hline *ring = h->ring;
+ uint32_t rmask = h->ring_mask;
+ uint64_t head = h->head;
+ uint32_t page_off = pg << 16;
+ for (; i < end; i++) {
+ uint32_t len = lines[i].len;
+ /* A blank line: no text, no page slot, one row. The scan does
+ * not stop on it, so the batch keeps its room and its setup. */
+ if (TERM_EXPECT(len == 0, 0)) {
+ ring[head++ & rmask] = HLINE_BLANK;
+ rows_add++;
+ continue;
+ }
+ bytes_copy_small(page + off, lines[i].ptr, (int)len);
+ n_text++;
+ any_text = true;
+ if (TERM_EXPECT(len <= cols, 1)) {
+ part_put_narrow(&ring[head++ & rmask], page_off | off, len);
+ rows_add++;
+ } else {
+ uint32_t rows = rows_narrow(len, cols);
+ part_put_narrow_rows(&ring[head++ & rmask], page_off | off, len, rows);
+ rows_add += rows;
+ }
+ off += len;
+ }
+ h->head = head;
+ h->page_used[pg] = off;
+ h->page_parts[pg] += n_text;
+ h->lines += n;
+ if (any_text)
+ h->all_blank = false;
+ if (count_rows) {
+ h->counted_rows += rows_add;
+ h->rows_id += n;
+ }
+ }
+}
+
+void term_hist_push_blank(struct hist *h, size_t n)
+{
+ if (!h->line_limit || n == 0)
+ return;
+ if (hist_is_open(h)) {
+ term_hist_close_open_line(h);
+ n--;
+ }
+ if (n == 0)
+ return;
+ if (h->all_blank && h->lines == h->line_limit) {
+ /* Every line is blank and has one row: only the ids move. */
+ bool all = h->rows_id == hist_end_id(h);
+ h->base_id += n;
+ if (all || h->rows_id < h->base_id)
+ h->rows_id = all ? hist_end_id(h) : h->base_id;
+ h->counted_rows = h->rows_id - h->base_id;
+ return;
+ }
+ if (n >= h->line_limit) {
+ uint64_t end = hist_end_id(h) + n;
+ term_hist_clear(h);
+ h->base_id = h->rows_id = end - h->line_limit;
+ n = h->line_limit;
+ h->all_blank = true;
+ }
+ evict_for(h, n);
+ /* Ring room for the whole run at once. Growing it later would
+ * invalidate the pointers the loop below keeps in registers. */
+ if (!ring_fit(h, n))
+ return;
+ bool count = h->wrap_cols && h->rows_id == hist_end_id(h);
+ /* The ring and head cannot move here: eviction ran above and the
+ * reserve cannot grow the ring again. Keep both in registers. */
+ struct hline *ring = h->ring;
+ uint32_t rmask = h->ring_mask;
+ uint64_t head = h->head;
+ for (size_t i = 0; i < n; i++)
+ ring[head++ & rmask] = HLINE_BLANK;
+ h->head = head;
+ h->lines += n;
+ if (count) {
+ h->rows_id += n;
+ h->counted_rows += n;
+ }
+}
+
+/* ---- Reading ---- */
+
+/* Walks the attribute runs of one part, cell by cell. */
+struct runs_iter {
+ const uint8_t *p, *end;
+ bool pending; /* the open part: the pending run follows */
+ struct pen_attr pending_pen;
+ uint32_t pending_count;
+ struct pen_attr pen;
+ uint32_t left; /* cells left in the run; UINT32_MAX: default tail */
+};
+
+/* Runs held in a hist_runs (the open part, or a text row) from pen start. */
+static void runs_iter_runs(struct runs_iter *it, const struct hist_runs *r, const struct pen_attr *start)
+{
+ *it = (struct runs_iter){
+ .p = r->data, .end = r->data ? r->data + r->len : NULL,
+ .pending = r->count > 0, .pending_pen = r->pen, .pending_count = r->count,
+ .pen = *start,
+ };
+}
+
+static void runs_iter_init(struct runs_iter *it, const struct hist *h, const struct hline *p)
+{
+ *it = (struct runs_iter){ .pen = pen_attr_default() };
+ if (p->flags & HLINE_OPEN) {
+ if (h->runs.len) {
+ it->p = h->runs.data;
+ it->end = h->runs.data + h->runs.len;
+ }
+ it->pending = h->runs.count > 0;
+ it->pending_pen = h->runs.pen;
+ it->pending_count = h->runs.count;
+ } else if (p->attr_len) {
+ it->p = part_text(h, p) + p->text_len;
+ it->end = it->p + p->attr_len;
+ }
+}
+
+/* Take up to n cells of the current run. it->pen is their pen. */
+static inline uint32_t runs_take(struct runs_iter *it, uint32_t n)
+{
+ while (it->left == 0) {
+ if (it->p < it->end) {
+ it->p = read_run(it->p, &it->pen, &it->left);
+ } else if (it->pending) {
+ it->pending = false;
+ it->pen = it->pending_pen;
+ it->left = it->pending_count;
+ } else {
+ it->pen = pen_attr_default();
+ it->left = UINT32_MAX;
+ }
+ }
+ if (it->left == UINT32_MAX)
+ return n;
+ uint32_t k = it->left < n ? it->left : n;
+ it->left -= k;
+ return k;
+}
+
+static inline void runs_iter_save(const struct runs_iter *it, struct hist_dec_cache *d)
+{
+ d->p = it->p;
+ d->end = it->end;
+ d->pending = it->pending;
+ d->pending_pen = it->pending_pen;
+ d->pending_count = it->pending_count;
+ d->pen = it->pen;
+ d->left = it->left;
+}
+
+static inline void runs_iter_restore(struct runs_iter *it, const struct hist_dec_cache *d)
+{
+ it->p = d->p;
+ it->end = d->end;
+ it->pending = d->pending;
+ it->pending_pen = d->pending_pen;
+ it->pending_count = d->pending_count;
+ it->pen = d->pen;
+ it->left = d->left;
+}
+
+static inline int text_width(const uint8_t *s)
+{
+ uint8_t b = s[0];
+ if (b < PREFIX_WIDE || b == PREFIX_TEXT)
+ return 1;
+ if (b != PREFIX_CLUSTER)
+ return 2;
+ return (s[1] & 0x80) ? 2 : 1;
+}
+
+static inline const uint8_t *text_skip(const uint8_t *s, const uint8_t *end)
+{
+ uint8_t b = s[0];
+ uint32_t cp;
+ if (b < 0x80)
+ return s + 1;
+ if (b < PREFIX_WIDE)
+ return s + get_utf8(s, end, &cp);
+ if (b != PREFIX_CLUSTER)
+ return s + 1 + get_utf8(s + 1, end, &cp);
+ uint8_t k = s[1];
+ s += 2;
+ for (int i = 0; i < (k & 0x1f) && s < end; i++)
+ s += get_utf8(s, end, &cp);
+ return s;
+}
+
+/* Read one cell: its code point (a handle for a cluster when g is set) and
+ * its shape flags. */
+static const uint8_t *text_cell(const uint8_t *s, const uint8_t *end, struct gstore *g,
+ uint32_t *cp, uint16_t *shape)
+{
+ uint8_t b = s[0];
+ *shape = 0;
+ if (b < 0x80) {
+ *cp = b;
+ return s + 1;
+ }
+ if (b < PREFIX_WIDE)
+ return s + get_utf8(s, end, cp);
+ if (b != PREFIX_CLUSTER) {
+ *shape = b == PREFIX_WIDE ? CELL_WIDE : b == PREFIX_EMOJI ? (CELL_WIDE | CELL_EMOJI) : CELL_TEXT;
+ return s + 1 + get_utf8(s + 1, end, cp);
+ }
+ uint8_t k = s[1];
+ s += 2;
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int n = 0;
+ for (int i = 0; i < (k & 0x1f) && s < end; i++) {
+ uint32_t c;
+ s += get_utf8(s, end, &c);
+ if (n < GRAPHEME_MAX_LEN)
+ cps[n++] = c;
+ }
+ *cp = n == 0 ? ' ' : (g && n > 1) ? term_gstore_intern(g, cps, n) : cps[0];
+ *shape = (uint16_t)(((k & 0x80) ? CELL_WIDE : 0) | ((k & 0x40) ? CELL_EMOJI : 0) |
+ ((k & 0x20) ? CELL_TEXT : 0));
+ return s;
+}
+
+uint64_t term_hist_line_cells(const struct hist *h, uint64_t id)
+{
+ if (!line_valid(h, id))
+ return 0;
+ if (id == h->last_id && h->last_cells)
+ return h->last_cells;
+ uint64_t first = line_first(h, id), end = line_end(h, first), cells = 0;
+ for (uint64_t s = first; s < end; s++)
+ cells += part(h, s)->cells;
+ return cells;
+}
+
+uint64_t term_hist_line_wide(const struct hist *h, uint64_t id)
+{
+ if (!line_valid(h, id))
+ return 0;
+ if (id == h->last_id && h->last_cells)
+ return h->last_wide;
+ uint64_t first = line_first(h, id), end = line_end(h, first), wide = 0;
+ for (uint64_t seq = first; seq < end; seq++)
+ wide += part(h, seq)->wide;
+ return wide;
+}
+
+uint8_t term_hist_line_flags(const struct hist *h, uint64_t id)
+{
+ if (!line_valid(h, id))
+ return 0;
+ uint64_t first = line_first(h, id), end = line_end(h, first);
+ uint8_t flags = part(h, first)->flags;
+ for (uint64_t s = first + 1; s < end; s++) {
+ uint8_t f = part(h, s)->flags;
+ flags = (uint8_t)((flags | f) & ~HLINE_NARROW) | (flags & f & HLINE_NARROW);
+ }
+ return flags;
+}
+
+static int line_rows_walk(const struct hist *h, uint64_t first, uint64_t end, int cols)
+{
+ int rows = 1, col = 0;
+ for (uint64_t s = first; s < end; s++) {
+ const struct hline *p = part(h, s);
+ if (!p->text_len)
+ continue;
+ const uint8_t *t = part_text(h, p), *end = t + p->text_len;
+ while (t < end) {
+ int w = text_width(t) == 2 && cols > 1 ? 2 : 1;
+ t = text_skip(t, end);
+ if (col == cols || (w == 2 && col + 2 > cols)) {
+ rows++;
+ col = 0;
+ }
+ col += w;
+ }
+ }
+ return rows;
+}
+
+int term_hist_line_rows(const struct hist *h, uint64_t id, int cols)
+{
+ if (cols <= 0 || !line_valid(h, id))
+ return 0;
+ uint64_t first = line_first(h, id);
+ const struct hline *f = part(h, first);
+ if (cols == h->wrap_cols) {
+ if (id < h->rows_id &&
+ ((f->flags & HLINE_ROWS_EXACT) || !(term_hist_line_flags(h, id) & HLINE_HAS_WIDE))) {
+ if (f->wrap_rows != ROWS_UNKNOWN)
+ return f->wrap_rows;
+ if ((f->flags & HLINE_ROWS_EXACT) && h->exact_rows)
+ return (int)h->exact_rows[first & h->ring_mask];
+ }
+ if (h->walk_on && id + 1 == hist_end_id(h) && h->walk_seq + 1 == h->head &&
+ h->walk_off == part(h, h->walk_seq)->text_len)
+ return (int)h->walk_rows;
+ }
+ uint64_t cells = term_hist_line_cells(h, id);
+ if (cells <= (uint64_t)cols ||
+ !(term_hist_line_flags(h, id) & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER)))
+ return (int)rows_narrow(cells, cols);
+ return line_rows_walk(h, first, line_end(h, first), cols);
+}
+
+/* Walk the open line from where the last walk stopped. */
+static void walk_advance(struct hist *h)
+{
+ int cols = h->wrap_cols;
+ if (!h->walk_on) {
+ h->walk_seq = line_first(h, hist_end_id(h) - 1);
+ h->walk_off = 0;
+ h->walk_col = 0;
+ h->walk_rows = 1;
+ h->walk_on = true;
+ }
+ for (;;) {
+ const struct hline *p = part(h, h->walk_seq);
+ if (p->text_len) {
+ const uint8_t *t0 = part_text(h, p), *t = t0 + h->walk_off, *end = t0 + p->text_len;
+ uint32_t col = h->walk_col, rows = h->walk_rows;
+ while (t < end) {
+ uint32_t w = text_width(t) == 2 && cols > 1 ? 2 : 1;
+ t = text_skip(t, end);
+ if (col == (uint32_t)cols || (w == 2 && col + 2 > (uint32_t)cols)) {
+ rows++;
+ col = 0;
+ }
+ col += w;
+ }
+ h->walk_col = col;
+ h->walk_rows = rows;
+ h->walk_off = (uint32_t)(t - t0);
+ }
+ if (h->walk_seq + 1 >= h->head)
+ break;
+ h->walk_seq++;
+ h->walk_off = 0;
+ }
+}
+
+static inline void put_cell(struct cell *out, uint32_t cp, uint16_t shape, const struct pen_attr *pen)
+{
+ *out = (struct cell){ cp, pen->fg, pen->bg, (uint16_t)(shape | pen->flags), pen->link };
+}
+
+/* A line is narrow when every part stores one ASCII byte per cell. Then a
+ * cell offset is also a byte offset, in every part. */
+static bool line_narrow(const struct hist *h, uint64_t first, uint64_t end)
+{
+ for (uint64_t s = first; s < end; s++)
+ if (!(part(h, s)->flags & HLINE_NARROW))
+ return false;
+ return true;
+}
+
+int term_hist_decode_segments(const struct hist *h, uint64_t id, int start_seg, int count, int cols,
+ struct cell *out, int *lens, struct gstore *g)
+{
+ if (count <= 0 || start_seg < 0 || cols <= 0 || !line_valid(h, id))
+ return 0;
+ uint64_t first = line_first(h, id), end = line_end(h, first);
+
+ /* A narrow line has one byte per cell, so a segment starts at a known
+ * cell offset. Walk the parts to reach it, not the text before it. */
+ if (line_narrow(h, first, end)) {
+ uint64_t skip = (uint64_t)start_seg * (uint64_t)cols;
+ int seg_idx = 0;
+ int n = 0;
+ for (uint64_t s = first; s < end; s++) {
+ const struct hline *p = part(h, s);
+ if (skip >= p->cells) {
+ skip -= p->cells;
+ continue;
+ }
+ uint64_t avail = p->cells - skip;
+ struct runs_iter it;
+ runs_iter_init(&it, h, p);
+ for (uint64_t k = skip; k > 0;)
+ k -= runs_take(&it, k > UINT32_MAX ? UINT32_MAX : (uint32_t)k);
+ const char *src = (const char *)part_text(h, p) + skip;
+ while (avail > 0 && seg_idx < count) {
+ int need = cols - n;
+ int take = avail > (uint64_t)need ? need : (int)avail;
+ struct cell *row_out = out + (size_t)seg_idx * (size_t)cols;
+ for (int x = 0; x < take;) {
+ int k = (int)runs_take(&it, (uint32_t)(take - x));
+ put_ascii_chunk(row_out + n + x, src + x, k, it.pen.fg, it.pen.bg,
+ (uint32_t)it.pen.flags | ((uint32_t)it.pen.link << 16));
+ x += k;
+ }
+ n += take;
+ src += take;
+ avail -= (uint64_t)take;
+ if (n == cols) {
+ if (lens)
+ lens[seg_idx] = cols;
+ seg_idx++;
+ n = 0;
+ }
+ }
+ skip = 0;
+ if (seg_idx == count)
+ return count;
+ }
+ if (seg_idx < count) {
+ if (lens)
+ lens[seg_idx] = n;
+ seg_idx++;
+ }
+ return seg_idx;
+ }
+
+ if (g)
+ term_gstore_pin(g);
+ int row = 0, col = 0;
+ int end_seg = start_seg + count;
+ uint64_t s = first;
+ struct runs_iter it;
+ struct hist_dec_cache *dec = &((struct hist *)(uintptr_t)h)->dec;
+ bool resumed = false;
+
+ if (dec->valid && dec->epoch == h->index_epoch && dec->id == id &&
+ dec->cols == cols && dec->seg == (uint32_t)start_seg &&
+ dec->seq >= first && dec->seq < end &&
+ dec->off <= part(h, dec->seq)->text_len) {
+ s = dec->seq;
+ row = start_seg;
+ col = (int)dec->col;
+ runs_iter_restore(&it, dec);
+ resumed = true;
+ }
+
+ for (; s < end; s++) {
+ const struct hline *p = part(h, s);
+ if (!p->text_len)
+ continue;
+ const uint8_t *t = part_text(h, p), *tend = t + p->text_len;
+ if (resumed && s == dec->seq) {
+ t += dec->off;
+ resumed = false;
+ } else {
+ uint32_t skipped_cells = 0;
+ if (row < start_seg) {
+ while (t < tend && row < start_seg) {
+ int w = text_width(t) == 2 && cols > 1 ? 2 : 1;
+ if (col == cols || (w == 2 && col + 2 > cols)) {
+ row++;
+ col = 0;
+ if (row == start_seg)
+ break;
+ }
+ skipped_cells++;
+ col += w;
+ t = text_skip(t, tend);
+ }
+ if (row < start_seg)
+ continue;
+ }
+
+ runs_iter_init(&it, h, p);
+ for (uint32_t k = skipped_cells; k > 0;)
+ k -= runs_take(&it, k > UINT32_MAX ? UINT32_MAX : k);
+ }
+
+ while (t < tend && row < end_seg) {
+ int w = text_width(t) == 2 && cols > 1 ? 2 : 1;
+ if (col == cols || (w == 2 && col + 2 > cols)) {
+ int seg_idx = row - start_seg;
+ struct cell *row_out = out + (size_t)seg_idx * (size_t)cols;
+ if (col < cols)
+ row_out[col] = (struct cell){ ' ', TERM_COLOR_FG, TERM_COLOR_BG, CELL_WRAP_PAD, 0 };
+ if (lens)
+ lens[seg_idx] = col < cols ? cols : col;
+ row++;
+ col = 0;
+ if (row == end_seg) {
+ if (g)
+ term_gstore_unpin(g);
+ dec->epoch = h->index_epoch;
+ dec->id = id;
+ dec->cols = (uint16_t)cols;
+ dec->seq = s;
+ dec->seg = (uint32_t)end_seg;
+ dec->off = (uint32_t)(t - part_text(h, p));
+ dec->col = 0;
+ runs_iter_save(&it, dec);
+ dec->valid = true;
+ return count;
+ }
+ }
+ int seg_idx = row - start_seg;
+ struct cell *row_out = out + (size_t)seg_idx * (size_t)cols;
+ runs_take(&it, 1);
+ uint32_t cp;
+ uint16_t shape;
+ t = text_cell(t, tend, g, &cp, &shape);
+ put_cell(&row_out[col], cp, shape, &it.pen);
+ if (w == 2)
+ put_cell(&row_out[col + 1], 0, (uint16_t)((shape & ~CELL_WIDE) | CELL_TRAIL), &it.pen);
+ col += w;
+ }
+ if (row == end_seg) {
+ if (g)
+ term_gstore_unpin(g);
+ dec->epoch = h->index_epoch;
+ dec->id = id;
+ dec->cols = (uint16_t)cols;
+ dec->seq = s;
+ dec->seg = (uint32_t)end_seg;
+ dec->off = (uint32_t)(t - part_text(h, p));
+ dec->col = (uint32_t)col;
+ runs_iter_save(&it, dec);
+ dec->valid = true;
+ return count;
+ }
+ }
+ int decoded = row - start_seg;
+ if (row >= start_seg && row < end_seg) {
+ if (lens)
+ lens[decoded] = col;
+ decoded++;
+ }
+ dec->valid = false;
+ if (g)
+ term_gstore_unpin(g);
+ return decoded;
+}
+
+int term_hist_decode_segment(const struct hist *h, uint64_t id, int seg, int cols,
+ struct cell *out, struct gstore *g)
+{
+ int len = 0;
+ int got = term_hist_decode_segments(h, id, seg, 1, cols, out, &len, g);
+ return got > 0 ? len : 0;
+}
+
+/* Write the cells of parts [first, end) in one row, without wrapping. */
+static int decode_line(const struct hist *h, uint64_t first, uint64_t end,
+ struct cell *out, int cap, struct gstore *g)
+{
+ int n = 0;
+ if (g)
+ term_gstore_pin(g);
+ for (uint64_t s = first; s < end; s++) {
+ const struct hline *p = part(h, s);
+ if (!p->text_len)
+ continue;
+ struct runs_iter it;
+ runs_iter_init(&it, h, p);
+ const uint8_t *t = part_text(h, p), *tend = t + p->text_len;
+ while (t < tend) {
+ uint32_t cp;
+ uint16_t shape;
+ runs_take(&it, 1);
+ t = text_cell(t, tend, g, &cp, &shape);
+ int w = (shape & CELL_WIDE) ? 2 : 1;
+ if (n + w <= cap) {
+ put_cell(&out[n], cp, shape, &it.pen);
+ if (w == 2)
+ put_cell(&out[n + 1], 0, (uint16_t)((shape & ~CELL_WIDE) | CELL_TRAIL), &it.pen);
+ }
+ n += w;
+ }
+ }
+ if (g)
+ term_gstore_unpin(g);
+ return n;
+}
+
+int term_hist_text_decode(const struct text_row *row, struct cell *out, int cap, struct gstore *g)
+{
+ struct runs_iter it;
+ runs_iter_runs(&it, &row->runs, &row->start);
+ if (row->flags & HLINE_NARROW) {
+ /* One byte per cell: one block store per run. */
+ int take = row->text_len < cap ? row->text_len : cap;
+ for (int x = 0; x < take;) {
+ int k = (int)runs_take(&it, (uint32_t)(take - x));
+ put_ascii_chunk(out + x, (const char *)row->text + x, k, it.pen.fg, it.pen.bg,
+ (uint32_t)it.pen.flags | ((uint32_t)it.pen.link << 16));
+ x += k;
+ }
+ return take;
+ }
+ const uint8_t *t = row->text, *tend = row->text + row->text_len;
+ int n = 0;
+ while (t < tend) {
+ uint32_t cp;
+ uint16_t shape;
+ runs_take(&it, 1);
+ t = text_cell(t, tend, g, &cp, &shape);
+ int w = (shape & CELL_WIDE) ? 2 : 1;
+ if (n + w <= cap) {
+ put_cell(&out[n], cp, shape, &it.pen);
+ if (w == 2)
+ put_cell(&out[n + 1], 0, (uint16_t)((shape & ~CELL_WIDE) | CELL_TRAIL), &it.pen);
+ } else if (n < cap) {
+ put_cell(&out[n], cp, shape, &it.pen);
+ }
+ n += w;
+ }
+ return n < cap ? n : cap;
+}
+
+void term_hist_text_mark_links(const struct text_row *row, void (*mark)(void *ud, uint16_t link), void *ud)
+{
+ if (!(row->flags & HLINE_HAS_LINK) && !row->start.link)
+ return;
+ struct runs_iter it;
+ runs_iter_runs(&it, &row->runs, &row->start);
+ struct pen_attr pen = row->start;
+ uint32_t count;
+ if (pen.link)
+ mark(ud, pen.link);
+ while (it.p < it.end) {
+ it.p = read_run(it.p, &pen, &count);
+ if (pen.link)
+ mark(ud, pen.link);
+ }
+ if (it.pending && it.pending_pen.link)
+ mark(ud, it.pending_pen.link);
+}
+
+void term_hist_mark_links(const struct hist *h, void (*mark)(void *ud, uint16_t link), void *ud)
+{
+ for (uint64_t s = h->tail; s < h->head; s++) {
+ const struct hline *p = part(h, s);
+ if (!(p->flags & HLINE_HAS_LINK))
+ continue;
+ struct runs_iter it;
+ runs_iter_init(&it, h, p);
+ struct pen_attr pen = pen_attr_default();
+ uint32_t count;
+ while (it.p < it.end) {
+ it.p = read_run(it.p, &pen, &count);
+ if (pen.link)
+ mark(ud, pen.link);
+ }
+ if (it.pending && it.pending_pen.link)
+ mark(ud, it.pending_pen.link);
+ }
+}
+
+/* ---- Removing from the end ---- */
+
+static void drop_head_part(struct hist *h)
+{
+ struct hline *p = part(h, --h->head);
+ if (p->off != NO_TEXT) {
+ uint32_t pg = p->off >> 16;
+ uint32_t start = p->off & 0xffff;
+ uint32_t stop = start + p->text_len + ((p->flags & HLINE_OPEN) ? 0 : p->attr_len);
+ if (pg == h->cur_page && h->page_used[pg] == stop)
+ h->page_used[pg] = start;
+ page_drop_ref(h, pg);
+ }
+ cont_trim(h);
+}
+
+int term_hist_peek_last_line(const struct hist *h, struct cell *out, int cap, struct gstore *g)
+{
+ if (h->lines == 0)
+ return 0;
+ uint64_t id = hist_end_id(h) - 1;
+ return decode_line(h, line_first(h, id), h->head, out, cap, g);
+}
+
+int term_hist_pop_line(struct hist *h, struct cell *out, int cap, struct gstore *g)
+{
+ if (h->lines == 0)
+ return 0;
+ uint64_t id = hist_end_id(h) - 1;
+ uint64_t first = line_first(h, id);
+ int n = out ? decode_line(h, first, h->head, out, cap, g) : (int)term_hist_line_cells(h, id);
+ if (id < h->rows_id) {
+ rows_uncount(h, id);
+ h->rows_id = id;
+ }
+ while (h->head > first)
+ drop_head_part(h);
+ h->lines--;
+ h->index_epoch++;
+ if (h->lines) {
+ uint64_t last = hist_end_id(h) - 1;
+ h->last_cells = term_hist_line_cells(h, last);
+ h->last_wide = term_hist_line_wide(h, last);
+ h->last_id = last;
+ }
+ runs_reset(&h->runs);
+ h->walk_on = false;
+ if (h->lines == 0 && h->head == h->tail) {
+ h->all_blank = true;
+ h->base_cell = 0;
+ h->last_cells = h->last_wide = 0;
+ }
+ return n;
+}
+
+int term_hist_pop_tail_cells(struct hist *h, int n, struct cell *out, struct gstore *g)
+{
+ if (h->lines == 0 || n <= 0)
+ return 0;
+ uint64_t total = term_hist_line_cells(h, hist_end_id(h) - 1);
+ if (total == 0) {
+ term_hist_pop_line(h, NULL, 0, g);
+ return 0;
+ }
+ struct cell *all = malloc((size_t)total * sizeof(*all));
+ if (!all)
+ return 0;
+ if (g)
+ term_gstore_pin(g);
+ int got = term_hist_pop_line(h, all, (int)total, g);
+ int take = n < got ? n : got;
+ int keep = got - take;
+ if (keep > 0 && (all[keep].flags & CELL_TRAIL)) {
+ keep++;
+ take--;
+ }
+ if (out && take > 0)
+ memcpy(out, all + keep, (size_t)take * sizeof(*out));
+ if (keep > 0) {
+ uint16_t cols = h->wrap_cols ? h->wrap_cols : 1;
+ for (int i = 0; i < keep; i += cols)
+ term_hist_push_row(h, all + i, keep - i < cols ? keep - i : cols, true, g);
+ }
+ if (g)
+ term_gstore_unpin(g);
+ free(all);
+ return take;
+}
+
+/* ---- Rows ---- */
+
+void term_hist_set_width(struct hist *h, int cols)
+{
+ if (h->wrap_cols == cols)
+ return;
+ h->wrap_cols = (uint16_t)cols;
+ h->rows_id = h->base_id;
+ h->counted_rows = 0;
+ h->walk_on = false;
+}
+
+uint64_t term_hist_line_estimate(const struct hist *h, uint64_t id, int cols)
+{
+ if (!line_valid(h, id))
+ return 0;
+ if (cols == h->wrap_cols && id < h->rows_id) {
+ const struct hline *first = part(h, line_first(h, id));
+ if (first->flags & HLINE_ROWS_EXACT)
+ return first->wrap_rows != ROWS_UNKNOWN ? first->wrap_rows :
+ h->exact_rows[line_first(h, id) & h->ring_mask];
+ }
+ uint64_t cells = term_hist_line_cells(h, id);
+ if (cols == 1)
+ cells -= term_hist_line_wide(h, id);
+ return rows_narrow(cells, cols);
+}
+
+void term_hist_refine_rows(struct hist *h, uint64_t id, int cols, int rows)
+{
+ if (cols != h->wrap_cols || id >= h->rows_id || id < h->base_id)
+ return;
+ uint64_t seq = line_first(h, id);
+ uint64_t old = term_hist_line_estimate(h, id, cols);
+ if (rows >= ROWS_UNKNOWN) {
+ if (!h->exact_rows) {
+ h->exact_rows = calloc((size_t)h->ring_mask + 1, sizeof(*h->exact_rows));
+ if (!h->exact_rows)
+ return;
+ }
+ h->exact_rows[seq & h->ring_mask] = (uint32_t)rows;
+ }
+ struct hline *first = part(h, seq);
+ first->flags |= HLINE_ROWS_EXACT;
+ first->wrap_rows = row_count_byte((uint64_t)rows);
+ h->counted_rows = h->counted_rows - old + (uint64_t)rows;
+}
+
+/* Fill descriptor counts only. This never reads the encoded text. */
+void term_hist_prepare_rows(struct hist *h, int cols)
+{
+ if (cols != h->wrap_cols)
+ term_hist_set_width(h, cols);
+ uint64_t stop = hist_end_id(h) - (hist_is_open(h) ? 1 : 0);
+ for (; h->rows_id < stop; h->rows_id++) {
+ uint64_t rows = term_hist_line_estimate(h, h->rows_id, cols);
+ struct hline *first = part(h, line_first(h, h->rows_id));
+ first->wrap_rows = row_count_byte(rows);
+ first->flags &= (uint8_t)~HLINE_ROWS_EXACT;
+ if (rows < ROWS_UNKNOWN &&
+ (!(term_hist_line_flags(h, h->rows_id) & HLINE_HAS_WIDE) || cols == 1))
+ first->flags |= HLINE_ROWS_EXACT;
+ h->counted_rows += rows;
+ }
+}
+
+uint64_t term_hist_estimated_rows(const struct hist *h, int cols)
+{
+ if (!h || cols <= 0 || !h->lines)
+ return 0;
+ uint64_t total = cols == h->wrap_cols ? h->counted_rows : 0;
+ uint64_t first = cols == h->wrap_cols ? h->rows_id : h->base_id;
+ for (uint64_t id = first; id < hist_end_id(h); id++)
+ total += term_hist_line_estimate(h, id, cols);
+ return total;
+}
+
+uint64_t term_hist_rows(const struct hist *h, int cols)
+{
+ if (!h || cols <= 0)
+ return 0;
+ uint64_t total = 0;
+ for (uint64_t id = hist_first_id(h); id < hist_end_id(h); id++)
+ total += (uint64_t)term_hist_line_rows(h, id, cols);
+ return total;
+}
+
+bool term_hist_find_row(const struct hist *h, int target_row, int cols,
+ uint64_t *out_id, int *out_seg)
+{
+ if (!h || target_row < 0 || cols <= 0)
+ return false;
+ uint64_t total = term_hist_rows(h, cols);
+ if ((uint64_t)target_row >= total)
+ return false;
+ uint64_t id, start;
+ if ((uint64_t)target_row >= total / 2) {
+ start = total;
+ id = hist_end_id(h);
+ do {
+ id--;
+ start -= (uint64_t)term_hist_line_rows(h, id, cols);
+ } while (start > (uint64_t)target_row && id > h->base_id);
+ } else {
+ start = 0;
+ id = h->base_id;
+ for (;;) {
+ uint64_t rows = (uint64_t)term_hist_line_rows(h, id, cols);
+ if ((uint64_t)target_row < start + rows)
+ break;
+ start += rows;
+ id++;
+ }
+ }
+ *out_id = id;
+ *out_seg = target_row - (int)start;
+ return true;
+}
+
+/* Locate a row boundary by its cell offset. Wide wrap pads do not count. */
+uint64_t term_hist_segment_cell(const struct hist *h, uint64_t id, int seg, int cols)
+{
+ if (!line_valid(h, id))
+ return 0;
+ uint64_t base = id == h->base_cell_id ? h->base_cell : 0;
+ if (seg <= 0)
+ return base;
+ if (!(term_hist_line_flags(h, id) & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER)))
+ return base + (uint64_t)seg * cols;
+ uint64_t first = line_first(h, id), end = line_end(h, first), cells = 0;
+ int row = 0, col = 0;
+ for (uint64_t seq = first; seq < end; seq++) {
+ const struct hline *p = part(h, seq);
+ if (!p->text_len)
+ continue;
+ const uint8_t *t = part_text(h, p), *stop = t + p->text_len;
+ while (t < stop) {
+ int width = text_width(t) == 2 && cols > 1 ? 2 : 1;
+ if (col == cols || col + width > cols) {
+ if (++row == seg)
+ return base + cells;
+ col = 0;
+ }
+ cells += width;
+ col += width;
+ t = text_skip(t, stop);
+ }
+ }
+ return base + cells;
+}
+
+int term_hist_cell_segment(const struct hist *h, uint64_t id, uint64_t cell, int cols)
+{
+ if (!line_valid(h, id) || cols <= 0)
+ return 0;
+ uint64_t base = id == h->base_cell_id ? h->base_cell : 0;
+ cell = cell > base ? cell - base : 0;
+ if (!(term_hist_line_flags(h, id) & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER))) {
+ uint64_t cells = term_hist_line_cells(h, id);
+ if (cells && cell >= cells)
+ cell = cells - 1;
+ return (int)(cell / cols);
+ }
+ uint64_t first = line_first(h, id), end = line_end(h, first), cells = 0;
+ int row = 0, col = 0;
+ for (uint64_t seq = first; seq < end; seq++) {
+ const struct hline *p = part(h, seq);
+ if (!p->text_len)
+ continue;
+ const uint8_t *t = part_text(h, p), *stop = t + p->text_len;
+ while (t < stop) {
+ int width = text_width(t) == 2 && cols > 1 ? 2 : 1;
+ if (col == cols || col + width > cols) {
+ row++;
+ col = 0;
+ }
+ if (cells + (uint64_t)width > cell)
+ return row;
+ cells += width;
+ col += width;
+ t = text_skip(t, stop);
+ }
+ }
+ return row;
+}
+
+/* ---- Life cycle ---- */
+
+bool term_hist_init(struct hist *h, int line_limit, size_t byte_cap)
+{
+ memset(h, 0, sizeof(*h));
+ h->all_blank = true;
+ runs_reset(&h->runs);
+ if (line_limit <= 0)
+ return true;
+ if (byte_cap == 0) {
+ size_t scaled = (size_t)line_limit * 800;
+ byte_cap = scaled > 8u * 1024 * 1024 ? scaled : 8u * 1024 * 1024;
+ }
+ h->line_limit = (uint32_t)line_limit;
+ h->max_pages = (uint32_t)(byte_cap / HIST_PAGE_SIZE);
+ if (h->max_pages == 0)
+ h->max_pages = 1;
+ if (h->max_pages > 65535)
+ h->max_pages = 65535;
+ h->pages = calloc(h->max_pages, sizeof(*h->pages));
+ h->page_used = calloc(h->max_pages, sizeof(*h->page_used));
+ h->page_parts = calloc(h->max_pages, sizeof(*h->page_parts));
+ h->page_rows = calloc(h->max_pages, sizeof(*h->page_rows));
+ h->page_simple = calloc(h->max_pages, sizeof(*h->page_simple));
+ h->free_pages = malloc(h->max_pages * sizeof(*h->free_pages));
+ if (!h->pages || !h->page_used || !h->page_parts || !h->page_rows || !h->page_simple || !h->free_pages || !ring_reserve(h)) {
+ term_hist_free(h);
+ return false;
+ }
+ return true;
+}
+
+void term_hist_free(struct hist *h)
+{
+ if (h->pages)
+ for (uint32_t i = 0; i < h->npages; i++)
+ term_aligned_free(h->pages[i]);
+ free(h->pages);
+ free(h->page_used);
+ free(h->page_parts);
+ free(h->page_rows);
+ free(h->page_simple);
+ free(h->free_pages);
+ term_aligned_free(h->ring);
+ free(h->blocks);
+ free(h->exact_rows);
+ free(h->cont);
+ free(h->runs.data);
+ memset(h, 0, sizeof(*h));
+}
+
+void term_hist_clear(struct hist *h)
+{
+ h->index_epoch++;
+ uint64_t end = hist_end_id(h);
+ h->tail = h->head = 0;
+ h->base_id = end;
+ h->base_cell = 0;
+ h->last_cells = h->last_wide = 0;
+ h->lines = 0;
+ h->cont_count = 0;
+ h->nfree = 0;
+ for (uint32_t pg = 0; pg < h->npages; pg++) {
+ h->page_used[pg] = 0;
+ h->page_parts[pg] = 0;
+ if (pg != h->cur_page)
+ h->free_pages[h->nfree++] = (uint16_t)pg;
+ }
+ h->all_blank = true;
+ h->rows_id = end;
+ h->counted_rows = 0;
+ h->walk_on = false;
+ runs_reset(&h->runs);
+}
diff --git a/src/hist.h b/src/hist.h
new file mode 100644
index 0000000..1aceae7
--- /dev/null
+++ b/src/hist.h
@@ -0,0 +1,321 @@
+#ifndef WSAP_TERM_HIST_H
+#define WSAP_TERM_HIST_H
+
+#include "cell.h"
+#include "gstore.h"
+#include "compat.h"
+#include "pen.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+/*
+ * History of logical lines. A line is stored once, independent of the
+ * terminal width. A line id counts logical lines: a line that needs more than
+ * one storage part keeps one id. Ids never change while the line is retained.
+ */
+
+#define HIST_PAGE_SIZE 65536
+#define HIST_PAGE_SLACK 64
+#define HIST_PAGE_USABLE (HIST_PAGE_SIZE - HIST_PAGE_SLACK)
+#define HIST_MAX_PART_CELLS 65535
+
+enum hline_flags {
+ HLINE_NARROW = 1 << 0, /* one ASCII byte per cell */
+ HLINE_HAS_WIDE = 1 << 1,
+ HLINE_HAS_CLUSTER = 1 << 2,
+ HLINE_HAS_LINK = 1 << 3,
+ HLINE_CONT = 1 << 4, /* continues the line of the previous part */
+ HLINE_ROWS_EXACT = 1 << 6, /* first part: cached row count is exact */
+ HLINE_OPEN = 1 << 5, /* the part that grows; only the last part */
+};
+
+/* One storage part. The first part of a line holds the line's row count. */
+struct hline {
+ uint32_t off; /* page << 16 | offset in the page */
+ uint32_t text_len; /* bytes */
+ uint16_t cells; /* display width */
+ uint16_t wide; /* wide glyphs; a one-column view clips them */
+ uint16_t attr_len; /* 0: every cell has the default pen */
+ uint8_t flags;
+ uint8_t wrap_rows; /* first part: rows of the line at wrap_cols; 255: count them */
+};
+
+_Static_assert(sizeof(struct hline) == 16, "struct hline must stay 16 bytes");
+
+/* Attribute runs of the open part. They move to the page when it closes.
+ * count comes before the pens, so a wide store of the pens never covers
+ * it. */
+struct hist_runs {
+ uint8_t *data;
+ uint32_t len, cap;
+ uint32_t count; /* cells of the pending run */
+ struct pen_attr base; /* pen of the last emitted run */
+ struct pen_attr pen; /* pen of the pending run */
+};
+
+/* Totals for 16 closed lines in one page. A sequence and epoch validate it. */
+struct hist_block {
+ uint64_t seq, epoch;
+ uint32_t page, refs, rows;
+ uint16_t cols;
+};
+
+struct hist_ascii_line {
+ const char *ptr;
+ uint16_t len;
+ uint8_t wrapped;
+};
+
+/*
+ * One row of text in the history format, built by appending. A screen text
+ * row holds one, so a row that scrolls into history moves by copy. The runs
+ * start from the pen start; their buffer never grows, because every run
+ * covers at least one cell and the caller sizes it for a full row.
+ */
+struct text_row {
+ uint8_t *text;
+ uint8_t *storage; /* owned buffer, when text uses compact ASCII storage */
+ uint16_t text_len;
+ uint16_t cells;
+ uint16_t wide;
+ uint16_t fill_to; /* blanks with pen fill follow the text up to here */
+ uint8_t flags; /* HLINE_NARROW, HLINE_HAS_WIDE, HLINE_HAS_LINK */
+ struct pen_attr start;
+ struct pen_attr fill;
+ struct hist_runs runs;
+};
+
+/* Bytes a text row of cols cells may need: text, and runs. */
+#define TEXT_ROW_BYTES(cols) ((size_t)(cols) * 4 + 16)
+#define TEXT_ROW_RUNS(cols) (((size_t)(cols) + 2) * 22)
+
+/* A pen as three words: fg, bg, and flags | link << 16 (the layout of
+ * pen_attr.flags and .link). The hot path reads and writes whole words, so
+ * a store is never read back through a wider load. */
+static inline uint32_t pen_sl(const struct pen_attr *p)
+{
+ uint32_t sl;
+ memcpy(&sl, &p->flags, 4);
+ return sl;
+}
+
+static inline void pen_set(struct pen_attr *p, uint32_t fg, uint32_t bg, uint32_t sl)
+{
+ p->fg = fg;
+ p->bg = bg;
+ memcpy(&p->flags, &sl, 4);
+}
+
+/* Emit the pending run in the general form. */
+void term_hist_runs_emit_slow(struct hist_runs *r);
+
+/* Close the pending run and start one with the pen fg, bg, sl. The caller
+ * sized the buffer. A run that changes only colors and covers fewer than
+ * 128 cells is a header byte (bit 0 fg, bit 1 bg), a count byte, and the
+ * changed colors. */
+static inline void hist_runs_switch(struct hist_runs *r, uint32_t fg, uint32_t bg, uint32_t sl)
+{
+ if (r->count) {
+ uint32_t psl = pen_sl(&r->pen);
+ if (TERM_EXPECT(r->count < 0x80 && psl == pen_sl(&r->base), 1)) {
+ uint8_t *p = r->data + r->len;
+ uint32_t dfg = r->pen.fg != r->base.fg, dbg = r->pen.bg != r->base.bg;
+ p[0] = (uint8_t)(dfg | dbg << 1);
+ p[1] = (uint8_t)r->count;
+ memcpy(p + 2, &r->pen.fg, 4);
+ p += 2 + dfg * 4;
+ memcpy(p, &r->pen.bg, 4);
+ r->len = (uint32_t)(p + dbg * 4 - r->data);
+ } else {
+ term_hist_runs_emit_slow(r);
+ }
+ pen_set(&r->base, r->pen.fg, r->pen.bg, psl);
+ }
+ pen_set(&r->pen, fg, bg, sl);
+ r->count = 0;
+}
+
+static inline void hist_runs_add_pen(struct hist_runs *r, uint32_t fg, uint32_t bg, uint32_t sl,
+ uint32_t n)
+{
+ if (fg != r->pen.fg || bg != r->pen.bg || sl != pen_sl(&r->pen))
+ hist_runs_switch(r, fg, bg, sl);
+ r->count += n;
+}
+
+static inline void hist_runs_add(struct hist_runs *r, const struct pen_attr *pen, uint32_t n)
+{
+ hist_runs_add_pen(r, pen->fg, pen->bg, pen_sl(pen), n);
+}
+
+/* Start a text row. Each field gets its own store of its own width, because
+ * the first append reads them back at once. */
+static inline void text_row_start(struct text_row *row, const struct pen_attr *start)
+{
+ if (row->storage) row->text = row->storage;
+ uint32_t fg = start->fg, bg = start->bg, sl = pen_sl(start);
+ row->text_len = 0;
+ row->cells = 0;
+ row->wide = 0;
+ row->fill_to = 0;
+ row->flags = HLINE_NARROW;
+ pen_set(&row->start, fg, bg, sl);
+ row->runs.len = 0;
+ row->runs.count = 0;
+ pen_set(&row->runs.base, fg, bg, sl);
+ pen_set(&row->runs.pen, fg, bg, sl);
+}
+
+static inline void text_row_unshare(struct text_row *row)
+{
+ if (row->storage && row->text != row->storage) {
+ memcpy(row->storage, row->text, row->text_len);
+ row->text = row->storage;
+ }
+}
+
+/* The pen of the last cell of a text row. */
+static inline struct pen_attr text_row_last_pen(const struct text_row *row)
+{
+ return row->runs.pen;
+}
+
+/* Write the cells of a text row to out; return the cell count. */
+int term_hist_text_decode(const struct text_row *row, struct cell *out, int cap, struct gstore *g);
+void term_hist_text_mark_links(const struct text_row *row, void (*mark)(void *ud, uint16_t link), void *ud);
+
+struct hist_dec_cache {
+ const uint8_t *p, *end;
+ struct pen_attr pending_pen;
+ struct pen_attr pen;
+ uint64_t epoch;
+ uint64_t id;
+ uint64_t seq;
+ uint32_t seg;
+ uint32_t off;
+ uint32_t col;
+ uint32_t pending_count;
+ uint32_t left;
+ uint16_t cols;
+ bool pending;
+ bool valid;
+};
+
+struct hist {
+ struct hline *ring;
+ struct hist_block *blocks;
+ uint64_t tail, head; /* part sequence numbers [tail, head) */
+ uint64_t base_id; /* line id of the part at tail */
+ uint64_t *cont; /* CONT parts after tail, oldest first */
+ uint8_t **pages;
+ uint32_t *page_used;
+ uint32_t *page_parts;
+ uint32_t *page_rows;
+ uint8_t *page_simple;
+ uint16_t *free_pages;
+ uint32_t *exact_rows; /* counts that exceed the descriptor byte */
+ struct hist_runs runs;
+ struct hist_dec_cache dec;
+
+ /* Rows at wrap_cols. counted_rows covers [base_id, rows_id).
+ * Snapshot capture counts later closed lines from descriptors.
+ * A wide open line keeps a walk that resumes where it stopped. */
+ uint64_t rows_id;
+ uint64_t counted_rows;
+ uint64_t walk_seq;
+
+ /* Changes when retained line ids can name different content. */
+ uint64_t index_epoch;
+ uint64_t base_cell_id, base_cell; /* removed prefix and its line */
+ uint64_t last_id, last_cells, last_wide;
+
+ uint32_t ring_mask;
+ uint32_t lines;
+ uint32_t line_limit;
+ uint32_t cont_first, cont_count, cont_mask;
+ uint32_t npages, max_pages, nfree, cur_page;
+ uint32_t walk_off, walk_col, walk_rows;
+ uint16_t wrap_cols;
+ bool all_blank;
+ bool walk_on;
+};
+
+static inline uint32_t rows_narrow(uint64_t cells, int cols)
+{
+ return cells <= (uint64_t)cols ? 1 :
+ (uint32_t)((cells + (uint64_t)cols - 1) / (uint64_t)cols);
+}
+
+bool term_hist_init(struct hist *h, int line_limit, size_t byte_cap);
+void term_hist_free(struct hist *h);
+void term_hist_clear(struct hist *h);
+
+/* Push one screen row. It joins the open line, or starts a new line. A
+ * wrapped row leaves the line open for the next row. */
+void term_hist_push_row(struct hist *h, const struct cell *cells, int len, bool wrapped,
+ const struct gstore *g);
+void term_hist_push_ascii(struct hist *h, const char *bytes, int n,
+ const struct pen_attr *pen, bool wrapped);
+void term_hist_push_ascii_lines(struct hist *h, const struct hist_ascii_line *lines,
+ int count, const struct pen_attr *pen);
+/* Ends include LF. ends[0] is the start of the first line. */
+void term_hist_push_ascii_lf(struct hist *h, const char *bytes, const uint16_t *ends, int count);
+/* Push a text row as one screen row, like term_hist_push_row(). */
+void term_hist_push_text(struct hist *h, const struct text_row *row, bool wrapped);
+/* Push n empty unwrapped rows. The first one closes an open line. */
+void term_hist_push_blank(struct hist *h, size_t n);
+void term_hist_close_open_line(struct hist *h);
+
+/* Remove the last line. Write its cells to out (at most cap) and return the
+ * number of cells in the line. */
+int term_hist_pop_line(struct hist *h, struct cell *out, int cap, struct gstore *g);
+/* Copy the last line's cells to out, as term_hist_pop_line() does, and keep the line.
+ * Return the number of cells in the line. */
+int term_hist_peek_last_line(const struct hist *h, struct cell *out, int cap, struct gstore *g);
+/* Take up to n cells back off the end of the last line. Return the count. */
+int term_hist_pop_tail_cells(struct hist *h, int n, struct cell *out, struct gstore *g);
+
+static inline uint64_t hist_first_id(const struct hist *h)
+{
+ return h->base_id;
+}
+
+static inline uint64_t hist_end_id(const struct hist *h)
+{
+ return h->base_id + h->lines;
+}
+
+static inline bool hist_is_open(const struct hist *h)
+{
+ return h->head != h->tail && (h->ring[(h->head - 1) & h->ring_mask].flags & HLINE_OPEN);
+}
+
+uint64_t term_hist_line_cells(const struct hist *h, uint64_t id);
+uint64_t term_hist_line_wide(const struct hist *h, uint64_t id);
+uint8_t term_hist_line_flags(const struct hist *h, uint64_t id);
+int term_hist_line_rows(const struct hist *h, uint64_t id, int cols);
+int term_hist_decode_segment(const struct hist *h, uint64_t id, int seg, int cols,
+ struct cell *out, struct gstore *g);
+int term_hist_decode_segments(const struct hist *h, uint64_t id, int start_seg, int count, int cols,
+ struct cell *out, int *lens, struct gstore *g);
+
+uint64_t term_hist_segment_cell(const struct hist *h, uint64_t id, int seg, int cols);
+int term_hist_cell_segment(const struct hist *h, uint64_t id, uint64_t cell, int cols);
+
+void term_hist_mark_links(const struct hist *h, void (*mark)(void *ud, uint16_t link), void *ud);
+
+/* Set the width that pushed rows have. It costs O(1): the stored row counts
+ * become stale. Reads count them without changing the history. */
+void term_hist_set_width(struct hist *h, int cols);
+
+/* Rows of all retained lines at cols. */
+uint64_t term_hist_rows(const struct hist *h, int cols);
+uint64_t term_hist_line_estimate(const struct hist *h, uint64_t id, int cols);
+uint64_t term_hist_estimated_rows(const struct hist *h, int cols);
+void term_hist_prepare_rows(struct hist *h, int cols);
+void term_hist_refine_rows(struct hist *h, uint64_t id, int cols, int rows);
+bool term_hist_find_row(const struct hist *h, int target_row, int cols, uint64_t *out_id, int *out_seg);
+
+#endif
diff --git a/src/hist_view.c b/src/hist_view.c
new file mode 100644
index 0000000..d413c43
--- /dev/null
+++ b/src/hist_view.c
@@ -0,0 +1,366 @@
+#include "hist_view.h"
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+void term_hist_view_init(struct hist_view *v)
+{
+ *v = (struct hist_view){0};
+}
+
+void term_hist_view_free(struct hist_view *v)
+{
+ if (!v)
+ return;
+ free(v->fenwick);
+ free(v->counts);
+ *v = (struct hist_view){0};
+}
+
+void term_hist_view_clear(struct hist_view *v)
+{
+ v->anchor = (struct view_anchor){0};
+ v->index_cols = 0;
+}
+
+void term_hist_view_reset_bottom(struct hist_view *v)
+{
+ v->anchor = (struct view_anchor){0};
+}
+
+static uint64_t prefix(const struct hist_view *v, uint32_t n)
+{
+ uint64_t sum = 0;
+ for (; n; n &= n - 1)
+ sum += v->fenwick[n];
+ return sum;
+}
+
+static void set_count(struct hist_view *v, uint64_t id, uint32_t count)
+{
+ uint32_t slot = (uint32_t)id & (v->cap - 1);
+ int64_t delta = (int64_t)count - v->counts[slot];
+ v->counts[slot] = count;
+ v->total_rows = (uint64_t)((int64_t)v->total_rows + delta);
+ for (uint32_t i = slot + 1; i <= v->cap; i += i & -i)
+ v->fenwick[i] = (uint64_t)((int64_t)v->fenwick[i] + delta);
+}
+
+static bool current(const struct hist_view *v, const struct hist *h, int cols)
+{
+ return v->index_cols == cols && v->epoch == h->index_epoch &&
+ v->first_id == hist_first_id(h) && v->end_id == hist_end_id(h) &&
+ v->head == h->head && v->tail == h->tail &&
+ v->last_cells == (h->lines ? term_hist_line_cells(h, hist_end_id(h) - 1) : 0);
+}
+
+bool term_hist_view_sync(struct hist_view *v, const struct hist *h, int cols)
+{
+ if (current(v, h, cols))
+ return true;
+ uint64_t first = hist_first_id(h), end = hist_end_id(h);
+ bool rebuild = v->index_cols != cols || v->epoch != h->index_epoch ||
+ first < v->first_id || first >= v->end_id || end < v->end_id;
+ if (!rebuild) {
+ uint64_t changed = first - v->first_id + end - v->end_id;
+ rebuild = changed > v->cap / 4;
+ }
+ if (h->lines > v->cap || !v->cap) {
+ uint32_t cap = 16;
+ while (cap < h->lines) {
+ if (cap >= UINT32_MAX / 2)
+ return false;
+ cap *= 2;
+ }
+ uint64_t *tree = calloc((size_t)cap + 1, sizeof(*tree));
+ uint32_t *counts = calloc(cap, sizeof(*counts));
+ if (!tree || !counts) {
+ free(tree);
+ free(counts);
+ return false;
+ }
+ free(v->fenwick);
+ free(v->counts);
+ v->fenwick = tree;
+ v->counts = counts;
+ v->cap = cap;
+ rebuild = true;
+ }
+ if (rebuild) {
+ memset(v->counts, 0, (size_t)v->cap * sizeof(*v->counts));
+ memset(v->fenwick, 0, ((size_t)v->cap + 1) * sizeof(*v->fenwick));
+ for (uint64_t id = first; id < end; id++)
+ v->counts[(uint32_t)id & (v->cap - 1)] = (uint32_t)term_hist_line_estimate(h, id, cols);
+ v->total_rows = 0;
+ /* Build all tree nodes in one pass. */
+ for (uint32_t i = 1; i <= v->cap; i++) {
+ v->fenwick[i] += v->counts[i - 1];
+ v->total_rows += v->counts[i - 1];
+ uint32_t parent = i + (i & -i);
+ if (parent <= v->cap)
+ v->fenwick[parent] += v->fenwick[i];
+ }
+ } else {
+ for (uint64_t id = v->first_id; id < first; id++)
+ set_count(v, id, 0);
+ /* Only the old final line can grow or close. */
+ if (v->end_id > first && term_hist_line_cells(h, v->end_id - 1) != v->last_cells)
+ set_count(v, v->end_id - 1, (uint32_t)term_hist_line_estimate(h, v->end_id - 1, cols));
+ for (uint64_t id = v->end_id; id < end; id++)
+ set_count(v, id, (uint32_t)term_hist_line_estimate(h, id, cols));
+ }
+ v->first_id = first;
+ v->end_id = end;
+ v->index_cols = cols;
+ v->epoch = h->index_epoch;
+ v->head = h->head;
+ v->tail = h->tail;
+ v->last_cells = h->lines ? term_hist_line_cells(h, end - 1) : 0;
+ return true;
+}
+
+/* The anchor line is a visited line, even before the next capture. */
+static uint64_t anchor_extra(const struct hist_view *v, const struct hist *h, int cols)
+{
+ uint64_t id = v->anchor.line_id;
+ if (!v->anchor.active || id < hist_first_id(h) || id >= hist_end_id(h))
+ return 0;
+ uint64_t estimated = current(v, h, cols) ? v->counts[(uint32_t)id & (v->cap - 1)] :
+ term_hist_line_estimate(h, id, cols);
+ uint64_t exact = (uint64_t)term_hist_line_rows(h, id, cols);
+ return exact > estimated ? exact - estimated : 0;
+}
+
+uint64_t term_hist_view_rows(const struct hist_view *v, const struct hist *h, int cols)
+{
+ uint64_t rows = current(v, h, cols) ? v->total_rows : term_hist_estimated_rows(h, cols);
+ return rows + anchor_extra(v, h, cols);
+}
+
+/* Rows of the retained lines before id, in logical order. */
+static uint64_t before(const struct hist_view *v, uint64_t id)
+{
+ uint32_t start = (uint32_t)v->first_id & (v->cap - 1);
+ uint64_t n = id - v->first_id;
+ uint32_t stop = start + (uint32_t)n;
+ if (stop <= v->cap)
+ return prefix(v, stop) - prefix(v, start);
+ return prefix(v, v->cap) - prefix(v, start) + prefix(v, stop - v->cap);
+}
+
+/* Physical slot of a row in tree order. */
+static uint32_t select_slot(const struct hist_view *v, uint64_t row)
+{
+ uint32_t slot = 0;
+ for (uint32_t bit = v->cap; bit; bit >>= 1) {
+ uint32_t next = slot + bit;
+ if (next <= v->cap && v->fenwick[next] <= row) {
+ row -= v->fenwick[next];
+ slot = next;
+ }
+ }
+ return slot;
+}
+
+bool term_hist_view_find_row(const struct hist_view *v, const struct hist *h, int row,
+ int cols, uint64_t *id, int *seg)
+{
+ if (!current(v, h, cols)) {
+ if (row < 0)
+ return false;
+ uint64_t target = (uint64_t)row;
+ for (uint64_t line = hist_first_id(h); line < hist_end_id(h); line++) {
+ uint64_t count = term_hist_line_estimate(h, line, cols);
+ if (target < count) {
+ *id = line;
+ *seg = (int)target;
+ return true;
+ }
+ target -= count;
+ }
+ return false;
+ }
+ uint64_t total = v->total_rows;
+ if (row < 0 || (uint64_t)row >= total)
+ return false;
+ uint32_t start = (uint32_t)v->first_id & (v->cap - 1);
+ uint64_t low = prefix(v, start), high = total - low;
+ uint64_t physical = (uint64_t)row < high ? low + row : (uint64_t)row - high;
+ uint32_t slot = select_slot(v, physical);
+ *id = v->first_id + ((slot - start) & (v->cap - 1));
+ *seg = (int)(physical - prefix(v, slot));
+ return true;
+}
+
+static int rows_as_int(uint64_t rows)
+{
+ return rows > INT_MAX ? INT_MAX : (int)rows;
+}
+
+int term_hist_view_scroll_offset(const struct hist_view *v, const struct hist *h, int cols)
+{
+ if (!v->anchor.active || !h->lines)
+ return 0;
+ uint64_t id = v->anchor.line_id;
+ if (id < hist_first_id(h))
+ return rows_as_int(term_hist_view_rows(v, h, cols));
+ if (id >= hist_end_id(h))
+ return 0;
+ int seg = v->cols == cols ? (int)v->anchor.seg :
+ term_hist_cell_segment(h, id, v->anchor.cell, cols);
+ uint64_t rows = 0;
+ if (current(v, h, cols))
+ rows = v->total_rows - before(v, id);
+ else
+ for (; id < hist_end_id(h); id++)
+ rows += term_hist_line_estimate(h, id, cols);
+ rows += anchor_extra(v, h, cols);
+ return rows > (uint64_t)seg ? rows_as_int(rows - seg) : 0;
+}
+
+void term_hist_view_note_push(struct hist_view *v, const struct hist *h, int cols)
+{
+ if (!v->anchor.active)
+ return;
+ if (v->anchor.line_id < hist_first_id(h))
+ v->anchor = (struct view_anchor){ .line_id = hist_first_id(h), .active = true };
+ uint64_t base = term_hist_segment_cell(h, v->anchor.line_id, 0, cols);
+ if (v->anchor.base_cell != base) {
+ if (v->anchor.cell < base)
+ v->anchor.cell = base;
+ v->anchor.seg = (uint32_t)term_hist_cell_segment(h, v->anchor.line_id, v->anchor.cell, cols);
+ v->anchor.base_cell = base;
+ }
+}
+
+void term_hist_view_rewrap(struct hist_view *v, const struct hist *h, int cols)
+{
+ term_hist_view_note_push(v, h, cols);
+ if (v->anchor.active) {
+ if (v->anchor.line_id >= hist_end_id(h))
+ term_hist_view_reset_bottom(v);
+ else
+ v->anchor.seg = (uint32_t)term_hist_cell_segment(h, v->anchor.line_id, v->anchor.cell, cols);
+ }
+ v->cols = cols;
+ v->index_cols = 0;
+}
+
+void term_hist_view_scroll_to(struct hist_view *v, const struct hist *h, int cols,
+ struct line_pos screen_top, int offset)
+{
+ (void)screen_top;
+ if (offset <= 0 || cols <= 0 || !h->lines) {
+ term_hist_view_reset_bottom(v);
+ return;
+ }
+ /* Allocation failure uses the read-only history walk. */
+ uint64_t total = term_hist_view_rows(v, h, cols);
+ if ((uint64_t)offset > total)
+ offset = rows_as_int(total);
+ uint64_t id;
+ int seg;
+ uint64_t row = total - offset;
+ if (row > INT_MAX || !term_hist_view_find_row(v, h, (int)row, cols, &id, &seg))
+ return;
+ v->anchor = (struct view_anchor){ .line_id = id, .seg = (uint32_t)seg,
+ .cell = term_hist_segment_cell(h, id, seg, cols),
+ .base_cell = term_hist_segment_cell(h, id, 0, cols), .active = true };
+ v->cols = cols;
+}
+
+/* Relative movement visits only the logical lines it crosses. */
+void term_hist_view_scroll(struct hist_view *v, const struct hist *h, int cols,
+ struct line_pos screen_top, int lines)
+{
+ (void)screen_top;
+ if (!lines || cols <= 0 || !h->lines)
+ return;
+ uint64_t id;
+ int64_t seg;
+ if (v->anchor.active) {
+ id = v->anchor.line_id;
+ if (id < hist_first_id(h))
+ id = hist_first_id(h);
+ if (id >= hist_end_id(h))
+ id = hist_end_id(h) - 1;
+ seg = v->cols == cols ? (int64_t)v->anchor.seg : term_hist_cell_segment(h, id, v->anchor.cell, cols);
+ } else {
+ if (lines < 0)
+ return;
+ id = hist_end_id(h) - 1;
+ seg = term_hist_line_rows(h, id, cols);
+ }
+ seg -= (int64_t)lines;
+ while (seg < 0 && id > hist_first_id(h))
+ seg += term_hist_line_rows(h, --id, cols);
+ if (seg < 0)
+ seg = 0;
+ for (;;) {
+ int count = term_hist_line_rows(h, id, cols);
+ if (seg < count)
+ break;
+ seg -= count;
+ if (++id == hist_end_id(h)) {
+ term_hist_view_reset_bottom(v);
+ return;
+ }
+ }
+ v->anchor = (struct view_anchor){ .line_id = id, .seg = (uint32_t)seg,
+ .cell = term_hist_segment_cell(h, id, (int)seg, cols),
+ .base_cell = term_hist_segment_cell(h, id, 0, cols), .active = true };
+ v->cols = cols;
+}
+
+/* A viewport walks exact segments from its anchor, never an estimated row. */
+bool term_hist_view_row_pos(const struct hist_view *v, const struct hist *h, int cols,
+ int screen_rows, struct line_pos screen_top, int y,
+ uint64_t *out_id, int *out_seg, int *out_screen_y)
+{
+ (void)screen_top;
+ if (v->anchor.active && h->lines) {
+ uint64_t id = v->anchor.line_id;
+ int seg = v->cols == cols ? (int)v->anchor.seg :
+ term_hist_cell_segment(h, id, v->anchor.cell, cols);
+ if (id < hist_first_id(h)) {
+ id = hist_first_id(h);
+ seg = 0;
+ }
+ int64_t target = (int64_t)seg + y;
+ while (id < hist_end_id(h)) {
+ int count = term_hist_line_rows(h, id, cols);
+ if (target < count) {
+ *out_id = id;
+ *out_seg = (int)target;
+ return true;
+ }
+ target -= count;
+ id++;
+ }
+ *out_screen_y = target < screen_rows ? (int)target : screen_rows - 1;
+ return false;
+ }
+ *out_screen_y = y;
+ return false;
+}
+
+/* Wide line counts become exact when the viewport visits them. */
+void term_hist_view_refine(struct hist_view *v, struct hist *h, int cols, int rows)
+{
+ if (!v->anchor.active || !current(v, h, cols))
+ return;
+ uint64_t id = v->anchor.line_id;
+ int remaining = rows;
+ int seg = (int)v->anchor.seg;
+ if (id < hist_first_id(h)) {
+ id = hist_first_id(h);
+ seg = 0;
+ }
+ for (; id < hist_end_id(h) && remaining > 0; id++) {
+ int count = term_hist_line_rows(h, id, cols);
+ set_count(v, id, (uint32_t)count);
+ term_hist_refine_rows(h, id, cols, count);
+ remaining -= count - seg;
+ seg = 0;
+ }
+}
diff --git a/src/hist_view.h b/src/hist_view.h
new file mode 100644
index 0000000..0214c0b
--- /dev/null
+++ b/src/hist_view.h
@@ -0,0 +1,60 @@
+#ifndef TERM_HIST_VIEW_H
+#define TERM_HIST_VIEW_H
+
+#include "hist.h"
+
+struct line_pos {
+ uint64_t id;
+ uint32_t seg;
+};
+
+struct view_anchor {
+ uint64_t line_id;
+ uint64_t cell;
+ uint64_t base_cell;
+ uint32_t seg;
+ bool active;
+};
+
+struct hist_view {
+ struct view_anchor anchor;
+
+ /* Line ids map to circular slots. Tree sums use physical slot order. */
+ uint64_t *fenwick;
+ uint32_t *counts;
+ uint64_t total_rows;
+ uint64_t first_id, end_id;
+ uint64_t epoch, head, tail, last_cells;
+ int cols;
+ uint32_t cap;
+ int index_cols;
+};
+
+void term_hist_view_init(struct hist_view *v);
+void term_hist_view_free(struct hist_view *v);
+void term_hist_view_clear(struct hist_view *v);
+
+/* Update the index during snapshot capture. Reads do not change it. */
+bool term_hist_view_sync(struct hist_view *v, const struct hist *h, int cols);
+void term_hist_view_refine(struct hist_view *v, struct hist *h, int cols, int rows);
+uint64_t term_hist_view_rows(const struct hist_view *v, const struct hist *h, int cols);
+bool term_hist_view_find_row(const struct hist_view *v, const struct hist *h, int row,
+ int cols, uint64_t *id, int *seg);
+
+/* Read the offset from the anchor at the current width. */
+int term_hist_view_scroll_offset(const struct hist_view *v, const struct hist *h, int cols);
+void term_hist_view_scroll(struct hist_view *v, const struct hist *h, int cols,
+ struct line_pos screen_top, int lines);
+void term_hist_view_scroll_to(struct hist_view *v, const struct hist *h, int cols,
+ struct line_pos screen_top, int offset);
+void term_hist_view_reset_bottom(struct hist_view *v);
+
+/* Clamp an evicted anchor. This does not update the index on the write path. */
+void term_hist_view_note_push(struct hist_view *v, const struct hist *h, int cols);
+void term_hist_view_rewrap(struct hist_view *v, const struct hist *h, int cols);
+
+bool term_hist_view_row_pos(const struct hist_view *v, const struct hist *h, int cols,
+ int screen_rows, struct line_pos screen_top, int y,
+ uint64_t *out_id, int *out_seg, int *out_screen_y);
+
+#endif
diff --git a/src/input.c b/src/input.c
new file mode 100644
index 0000000..49e41b2
--- /dev/null
+++ b/src/input.c
@@ -0,0 +1,771 @@
+#include "term.h"
+#include "unicode/utf8.h"
+
+#include <string.h>
+
+struct seqbuf {
+ char *p;
+ size_t left;
+ bool overflow;
+};
+
+static inline void sb_init(struct seqbuf *sb, char *buf, size_t buf_sz)
+{
+ sb->p = buf;
+ sb->left = buf_sz;
+ sb->overflow = false;
+}
+
+static inline void sb_ch(struct seqbuf *sb, char c)
+{
+ if (!sb->left) { sb->overflow = true; return; }
+ *sb->p++ = c;
+ sb->left--;
+}
+
+static inline void sb_u32(struct seqbuf *sb, uint32_t val)
+{
+ char tmp[10];
+ size_t n = 0;
+ do {
+ tmp[n++] = (char)('0' + (val % 10));
+ val /= 10;
+ } while (val);
+ while (n) sb_ch(sb, tmp[--n]);
+}
+
+static inline void sb_str(struct seqbuf *sb, const char *s, size_t len)
+{
+ for (size_t i = 0; i < len; i++) sb_ch(sb, s[i]);
+}
+
+static inline size_t sb_len(const struct seqbuf *sb, const char *buf)
+{
+ return (size_t)(sb->p - buf);
+}
+
+static inline void sb_bytes(struct seqbuf *sb, const char *s, size_t len)
+{
+ if (len > sb->left) { sb->overflow = true; return; }
+ memcpy(sb->p, s, len);
+ sb->p += len;
+ sb->left -= len;
+}
+
+struct kitty_desc {
+ uint32_t code;
+ char final;
+};
+
+static const struct kitty_desc kitty_desc[TERM_KEY_COUNT] = {
+ [TERM_KEY_RETURN] = { 13, 'u' },
+ [TERM_KEY_BACKSPACE] = { 127, 'u' },
+ [TERM_KEY_TAB] = { 9, 'u' },
+ [TERM_KEY_ESCAPE] = { 27, 'u' },
+
+ [TERM_KEY_UP] = { 1, 'A' },
+ [TERM_KEY_DOWN] = { 1, 'B' },
+ [TERM_KEY_RIGHT] = { 1, 'C' },
+ [TERM_KEY_LEFT] = { 1, 'D' },
+ [TERM_KEY_HOME] = { 1, 'H' },
+ [TERM_KEY_END] = { 1, 'F' },
+ [TERM_KEY_PAGE_UP] = { 5, '~' },
+ [TERM_KEY_PAGE_DOWN] = { 6, '~' },
+ [TERM_KEY_INSERT] = { 2, '~' },
+ [TERM_KEY_DELETE] = { 3, '~' },
+
+ [TERM_KEY_F1] = { 1, 'P' },
+ [TERM_KEY_F2] = { 1, 'Q' },
+ [TERM_KEY_F3] = { 13, '~' },
+ [TERM_KEY_F4] = { 1, 'S' },
+ [TERM_KEY_F5] = { 15, '~' },
+ [TERM_KEY_F6] = { 17, '~' },
+ [TERM_KEY_F7] = { 18, '~' },
+ [TERM_KEY_F8] = { 19, '~' },
+ [TERM_KEY_F9] = { 20, '~' },
+ [TERM_KEY_F10] = { 21, '~' },
+ [TERM_KEY_F11] = { 23, '~' },
+ [TERM_KEY_F12] = { 24, '~' },
+ [TERM_KEY_F13] = { 57376, 'u' },
+ [TERM_KEY_F14] = { 57377, 'u' },
+ [TERM_KEY_F15] = { 57378, 'u' },
+ [TERM_KEY_F16] = { 57379, 'u' },
+ [TERM_KEY_F17] = { 57380, 'u' },
+ [TERM_KEY_F18] = { 57381, 'u' },
+ [TERM_KEY_F19] = { 57382, 'u' },
+ [TERM_KEY_F20] = { 57383, 'u' },
+ [TERM_KEY_F21] = { 57384, 'u' },
+ [TERM_KEY_F22] = { 57385, 'u' },
+ [TERM_KEY_F23] = { 57386, 'u' },
+ [TERM_KEY_F24] = { 57387, 'u' },
+ [TERM_KEY_F25] = { 57388, 'u' },
+ [TERM_KEY_F26] = { 57389, 'u' },
+ [TERM_KEY_F27] = { 57390, 'u' },
+ [TERM_KEY_F28] = { 57391, 'u' },
+ [TERM_KEY_F29] = { 57392, 'u' },
+ [TERM_KEY_F30] = { 57393, 'u' },
+ [TERM_KEY_F31] = { 57394, 'u' },
+ [TERM_KEY_F32] = { 57395, 'u' },
+ [TERM_KEY_F33] = { 57396, 'u' },
+ [TERM_KEY_F34] = { 57397, 'u' },
+ [TERM_KEY_F35] = { 57398, 'u' },
+
+ [TERM_KEY_KP_0] = { 57399, 'u' },
+ [TERM_KEY_KP_1] = { 57400, 'u' },
+ [TERM_KEY_KP_2] = { 57401, 'u' },
+ [TERM_KEY_KP_3] = { 57402, 'u' },
+ [TERM_KEY_KP_4] = { 57403, 'u' },
+ [TERM_KEY_KP_5] = { 57404, 'u' },
+ [TERM_KEY_KP_6] = { 57405, 'u' },
+ [TERM_KEY_KP_7] = { 57406, 'u' },
+ [TERM_KEY_KP_8] = { 57407, 'u' },
+ [TERM_KEY_KP_9] = { 57408, 'u' },
+ [TERM_KEY_KP_DECIMAL] = { 57409, 'u' },
+ [TERM_KEY_KP_DIVIDE] = { 57410, 'u' },
+ [TERM_KEY_KP_MULTIPLY] = { 57411, 'u' },
+ [TERM_KEY_KP_SUBTRACT] = { 57412, 'u' },
+ [TERM_KEY_KP_ADD] = { 57413, 'u' },
+ [TERM_KEY_KP_ENTER] = { 57414, 'u' },
+ [TERM_KEY_KP_EQUAL] = { 57415, 'u' },
+ [TERM_KEY_KP_SEPARATOR] = { 57416, 'u' },
+ [TERM_KEY_KP_LEFT] = { 57417, 'u' },
+ [TERM_KEY_KP_RIGHT] = { 57418, 'u' },
+ [TERM_KEY_KP_UP] = { 57419, 'u' },
+ [TERM_KEY_KP_DOWN] = { 57420, 'u' },
+ [TERM_KEY_KP_PAGE_UP] = { 57421, 'u' },
+ [TERM_KEY_KP_PAGE_DOWN] = { 57422, 'u' },
+ [TERM_KEY_KP_HOME] = { 57423, 'u' },
+ [TERM_KEY_KP_END] = { 57424, 'u' },
+ [TERM_KEY_KP_INSERT] = { 57425, 'u' },
+ [TERM_KEY_KP_DELETE] = { 57426, 'u' },
+ [TERM_KEY_KP_BEGIN] = { 1, 'E' },
+
+ [TERM_KEY_CAPS_LOCK] = { 57358, 'u' },
+ [TERM_KEY_SCROLL_LOCK] = { 57359, 'u' },
+ [TERM_KEY_NUM_LOCK] = { 57360, 'u' },
+ [TERM_KEY_PRINT_SCREEN] = { 57361, 'u' },
+ [TERM_KEY_PAUSE] = { 57362, 'u' },
+ [TERM_KEY_MENU] = { 57363, 'u' },
+
+ [TERM_KEY_MEDIA_PLAY] = { 57428, 'u' },
+ [TERM_KEY_MEDIA_PAUSE] = { 57429, 'u' },
+ [TERM_KEY_MEDIA_STOP] = { 57432, 'u' },
+ [TERM_KEY_MEDIA_FORWARD] = { 57433, 'u' },
+ [TERM_KEY_MEDIA_REWIND] = { 57434, 'u' },
+ [TERM_KEY_MEDIA_NEXT] = { 57435, 'u' },
+ [TERM_KEY_MEDIA_PREV] = { 57436, 'u' },
+ [TERM_KEY_MEDIA_RECORD] = { 57437, 'u' },
+ [TERM_KEY_VOLUME_DOWN] = { 57438, 'u' },
+ [TERM_KEY_VOLUME_UP] = { 57439, 'u' },
+ [TERM_KEY_MUTE] = { 57440, 'u' },
+
+ [TERM_KEY_LSHIFT] = { 57441, 'u' },
+ [TERM_KEY_LCTRL] = { 57442, 'u' },
+ [TERM_KEY_LALT] = { 57443, 'u' },
+ [TERM_KEY_LSUPER] = { 57444, 'u' },
+ [TERM_KEY_LHYPER] = { 57445, 'u' },
+ [TERM_KEY_LMETA] = { 57446, 'u' },
+ [TERM_KEY_RSHIFT] = { 57447, 'u' },
+ [TERM_KEY_RCTRL] = { 57448, 'u' },
+ [TERM_KEY_RALT] = { 57449, 'u' },
+ [TERM_KEY_RSUPER] = { 57450, 'u' },
+ [TERM_KEY_RHYPER] = { 57451, 'u' },
+ [TERM_KEY_RMETA] = { 57452, 'u' },
+ [TERM_KEY_ISO_LEVEL3_SHIFT] = { 57453, 'u' },
+ [TERM_KEY_ISO_LEVEL5_SHIFT] = { 57454, 'u' },
+};
+
+struct legacy_desc {
+ uint8_t tilde;
+ char final;
+ bool app_cursor;
+ char keypad_ss3;
+ enum term_key equiv;
+};
+
+static const struct legacy_desc legacy_desc[TERM_KEY_COUNT] = {
+ [TERM_KEY_UP] = { .tilde = 0, .final = 'A', .app_cursor = true },
+ [TERM_KEY_DOWN] = { .tilde = 0, .final = 'B', .app_cursor = true },
+ [TERM_KEY_RIGHT] = { .tilde = 0, .final = 'C', .app_cursor = true },
+ [TERM_KEY_LEFT] = { .tilde = 0, .final = 'D', .app_cursor = true },
+ [TERM_KEY_HOME] = { .tilde = 0, .final = 'H', .app_cursor = true },
+ [TERM_KEY_END] = { .tilde = 0, .final = 'F', .app_cursor = true },
+ [TERM_KEY_PAGE_UP] = { .tilde = 5, .final = '~' },
+ [TERM_KEY_PAGE_DOWN] = { .tilde = 6, .final = '~' },
+ [TERM_KEY_INSERT] = { .tilde = 2, .final = '~' },
+ [TERM_KEY_DELETE] = { .tilde = 3, .final = '~' },
+
+ [TERM_KEY_F1] = { .tilde = 0, .final = 'P' },
+ [TERM_KEY_F2] = { .tilde = 0, .final = 'Q' },
+ [TERM_KEY_F3] = { .tilde = 0, .final = 'R' },
+ [TERM_KEY_F4] = { .tilde = 0, .final = 'S' },
+ [TERM_KEY_F5] = { .tilde = 15, .final = '~' },
+ [TERM_KEY_F6] = { .tilde = 17, .final = '~' },
+ [TERM_KEY_F7] = { .tilde = 18, .final = '~' },
+ [TERM_KEY_F8] = { .tilde = 19, .final = '~' },
+ [TERM_KEY_F9] = { .tilde = 20, .final = '~' },
+ [TERM_KEY_F10] = { .tilde = 21, .final = '~' },
+ [TERM_KEY_F11] = { .tilde = 23, .final = '~' },
+ [TERM_KEY_F12] = { .tilde = 24, .final = '~' },
+
+ [TERM_KEY_KP_0] = { .keypad_ss3 = 'p' },
+ [TERM_KEY_KP_1] = { .keypad_ss3 = 'q' },
+ [TERM_KEY_KP_2] = { .keypad_ss3 = 'r' },
+ [TERM_KEY_KP_3] = { .keypad_ss3 = 's' },
+ [TERM_KEY_KP_4] = { .keypad_ss3 = 't' },
+ [TERM_KEY_KP_5] = { .keypad_ss3 = 'u' },
+ [TERM_KEY_KP_6] = { .keypad_ss3 = 'v' },
+ [TERM_KEY_KP_7] = { .keypad_ss3 = 'w' },
+ [TERM_KEY_KP_8] = { .keypad_ss3 = 'x' },
+ [TERM_KEY_KP_9] = { .keypad_ss3 = 'y' },
+ [TERM_KEY_KP_DECIMAL] = { .keypad_ss3 = 'n' },
+ [TERM_KEY_KP_DIVIDE] = { .keypad_ss3 = 'o' },
+ [TERM_KEY_KP_MULTIPLY] = { .keypad_ss3 = 'j' },
+ [TERM_KEY_KP_SUBTRACT] = { .keypad_ss3 = 'm' },
+ [TERM_KEY_KP_ADD] = { .keypad_ss3 = 'k' },
+ [TERM_KEY_KP_ENTER] = { .keypad_ss3 = 'M', .equiv = TERM_KEY_RETURN },
+ [TERM_KEY_KP_LEFT] = { .equiv = TERM_KEY_LEFT },
+ [TERM_KEY_KP_RIGHT] = { .equiv = TERM_KEY_RIGHT },
+ [TERM_KEY_KP_UP] = { .equiv = TERM_KEY_UP },
+ [TERM_KEY_KP_DOWN] = { .equiv = TERM_KEY_DOWN },
+ [TERM_KEY_KP_PAGE_UP] = { .equiv = TERM_KEY_PAGE_UP },
+ [TERM_KEY_KP_PAGE_DOWN] = { .equiv = TERM_KEY_PAGE_DOWN },
+ [TERM_KEY_KP_HOME] = { .equiv = TERM_KEY_HOME },
+ [TERM_KEY_KP_END] = { .equiv = TERM_KEY_END },
+ [TERM_KEY_KP_INSERT] = { .equiv = TERM_KEY_INSERT },
+ [TERM_KEY_KP_DELETE] = { .equiv = TERM_KEY_DELETE },
+ [TERM_KEY_KP_BEGIN] = { .tilde = 0, .final = 'E' },
+};
+
+enum input_mod_key {
+ MOD_KEY_SHIFT = 0,
+ MOD_KEY_CTRL,
+ MOD_KEY_ALT,
+ MOD_KEY_SUPER,
+ MOD_KEY_HYPER,
+ MOD_KEY_META,
+ MOD_KEY_ISO3,
+ MOD_KEY_ISO5,
+ MOD_KEY_COUNT,
+};
+
+struct mod_desc {
+ enum input_mod_key group;
+ enum term_key partner;
+};
+
+static const struct mod_desc mod_desc[TERM_KEY_COUNT] = {
+ [TERM_KEY_LSHIFT] = { MOD_KEY_SHIFT, TERM_KEY_RSHIFT },
+ [TERM_KEY_RSHIFT] = { MOD_KEY_SHIFT, TERM_KEY_LSHIFT },
+ [TERM_KEY_LCTRL] = { MOD_KEY_CTRL, TERM_KEY_RCTRL },
+ [TERM_KEY_RCTRL] = { MOD_KEY_CTRL, TERM_KEY_LCTRL },
+ [TERM_KEY_LALT] = { MOD_KEY_ALT, TERM_KEY_RALT },
+ [TERM_KEY_RALT] = { MOD_KEY_ALT, TERM_KEY_LALT },
+ [TERM_KEY_LSUPER] = { MOD_KEY_SUPER, TERM_KEY_RSUPER },
+ [TERM_KEY_RSUPER] = { MOD_KEY_SUPER, TERM_KEY_LSUPER },
+ [TERM_KEY_LHYPER] = { MOD_KEY_HYPER, TERM_KEY_RHYPER },
+ [TERM_KEY_RHYPER] = { MOD_KEY_HYPER, TERM_KEY_LHYPER },
+ [TERM_KEY_LMETA] = { MOD_KEY_META, TERM_KEY_RMETA },
+ [TERM_KEY_RMETA] = { MOD_KEY_META, TERM_KEY_LMETA },
+ [TERM_KEY_ISO_LEVEL3_SHIFT] = { MOD_KEY_ISO3, TERM_KEY_NONE },
+ [TERM_KEY_ISO_LEVEL5_SHIFT] = { MOD_KEY_ISO5, TERM_KEY_NONE },
+};
+
+static const unsigned mod_group_input_mod[MOD_KEY_COUNT] = {
+ TERM_MOD_SHIFT, TERM_MOD_CTRL, TERM_MOD_ALT,
+ TERM_MOD_SUPER, TERM_MOD_HYPER, TERM_MOD_META,
+ 0, 0,
+};
+
+static inline bool key_is_modifier(enum term_key k)
+{
+ return (unsigned)k < TERM_KEY_COUNT &&
+ (mod_desc[k].partner != TERM_KEY_NONE ||
+ mod_desc[k].group == MOD_KEY_ISO3 ||
+ mod_desc[k].group == MOD_KEY_ISO5);
+}
+
+static inline bool key_is_lock(enum term_key k)
+{
+ return k == TERM_KEY_CAPS_LOCK || k == TERM_KEY_SCROLL_LOCK ||
+ k == TERM_KEY_NUM_LOCK;
+}
+
+static unsigned effective_mods(const struct term_keyboard_state *s,
+ const struct term_key_event *ev)
+{
+ unsigned mods = ev->mods;
+ if (!key_is_modifier(ev->key)) return mods;
+
+ const struct mod_desc *d = &mod_desc[ev->key];
+ if (d->group >= MOD_KEY_ISO3) return mods;
+
+ unsigned bit = mod_group_input_mod[d->group];
+ bool other_held = term_keyboard_key_down(s, d->partner);
+ if (ev->action != TERM_KEY_RELEASE || other_held) mods |= bit;
+ else mods &= ~bit;
+ return mods;
+}
+
+static unsigned lock_mod_bit(enum term_key key)
+{
+ if (key == TERM_KEY_CAPS_LOCK) return TERM_MOD_CAPS;
+ if (key == TERM_KEY_NUM_LOCK) return TERM_MOD_NUM;
+ return 0;
+}
+
+static unsigned toggle_locks(enum term_key key, enum term_key_action action,
+ unsigned mods)
+{
+ unsigned bit = lock_mod_bit(key);
+ if (bit && action == TERM_KEY_PRESS) mods ^= bit;
+ return mods;
+}
+
+/* Convert logical input modifiers to terminal protocol bits. */
+static unsigned protocol_modifiers(unsigned mods)
+{
+ return ((mods & TERM_MOD_SHIFT) ? 1u : 0u) |
+ ((mods & TERM_MOD_ALT) ? 2u : 0u) |
+ ((mods & TERM_MOD_CTRL) ? 4u : 0u) |
+ ((mods & TERM_MOD_SUPER) ? 8u : 0u) |
+ ((mods & TERM_MOD_HYPER) ? 16u : 0u) |
+ ((mods & TERM_MOD_META) ? 32u : 0u) |
+ ((mods & TERM_MOD_CAPS) ? 64u : 0u) |
+ ((mods & TERM_MOD_NUM) ? 128u : 0u);
+}
+
+static void format_kitty(struct seqbuf *sb, uint32_t code, char final,
+ unsigned flags, bool functional, uint32_t shifted,
+ uint32_t base, unsigned mods, unsigned event,
+ bool report_text, uint32_t text)
+{
+ char tmp[64];
+ struct seqbuf body;
+ sb_init(&body, tmp, sizeof(tmp));
+
+ if (final != 'u' && final != '~' && code <= 1 && !shifted && !base &&
+ !mods && !(flags & TERM_KEYBOARD_EVENT_TYPES) && !report_text) {
+
+ sb_ch(&body, '\033');
+ sb_ch(&body, '[');
+ sb_ch(&body, final);
+ } else {
+ sb_ch(&body, '\033');
+ sb_ch(&body, '[');
+ sb_u32(&body, code);
+ if ((flags & TERM_KEYBOARD_ALTERNATE_KEYS) && !functional) {
+ if (shifted) {
+ sb_ch(&body, ':');
+ sb_u32(&body, shifted);
+ }
+ if (base) {
+ sb_ch(&body, ':');
+ if (!shifted) sb_ch(&body, ':');
+ sb_u32(&body, base);
+ }
+ }
+ if (mods || (flags & TERM_KEYBOARD_EVENT_TYPES) || report_text) {
+ sb_ch(&body, ';');
+ sb_u32(&body, mods + 1);
+ }
+ if ((flags & TERM_KEYBOARD_EVENT_TYPES) && event != 1) {
+ sb_ch(&body, ':');
+ sb_u32(&body, event);
+ }
+ if (report_text) {
+ sb_ch(&body, ';');
+ sb_u32(&body, text);
+ }
+ sb_ch(&body, final);
+ }
+
+ if (body.overflow) {
+ sb->overflow = true;
+ return;
+ }
+ sb_str(sb, tmp, sb_len(&body, tmp));
+}
+
+static bool encode_kitty(const struct term_keyboard_state *s,
+ const struct term_key_event *ev,
+ const struct term_modes *m,
+ struct seqbuf *sb)
+{
+ unsigned flags = m->keyboard;
+ unsigned event = ev->action == TERM_KEY_PRESS ? 1u :
+ ev->action == TERM_KEY_REPEAT ? 2u : 3u;
+ if (!flags) return ev->action == TERM_KEY_RELEASE;
+
+ const struct kitty_desc *d = &kitty_desc[ev->key];
+ bool functional = d->code != 0;
+ char final = functional ? d->final : 'u';
+ bool all = (flags & TERM_KEYBOARD_ALL_KEYS) != 0;
+
+ if ((key_is_modifier(ev->key) || key_is_lock(ev->key)) && !all)
+ return true;
+
+ unsigned mods = effective_mods(s, ev);
+ mods = toggle_locks(ev->key, ev->action, mods);
+
+ bool plain_control = ev->key == TERM_KEY_RETURN || ev->key == TERM_KEY_TAB ||
+ ev->key == TERM_KEY_BACKSPACE;
+ if (ev->action == TERM_KEY_RELEASE &&
+ (!(flags & TERM_KEYBOARD_EVENT_TYPES) || (!all && plain_control)))
+ return true;
+
+ const unsigned command_mods = TERM_MOD_ALT | TERM_MOD_CTRL |
+ TERM_MOD_SUPER | TERM_MOD_HYPER |
+ TERM_MOD_META;
+ uint32_t text = ev->text;
+ bool has_text = text >= 32 && !(text >= 127 && text < 160) &&
+ !(mods & command_mods);
+ if (!all && has_text) return ev->action == TERM_KEY_RELEASE;
+ if (!all && plain_control && !(protocol_modifiers(mods) & 63))
+ return ev->action == TERM_KEY_RELEASE;
+ if (!(flags & (TERM_KEYBOARD_DISAMBIGUATE | TERM_KEYBOARD_EVENT_TYPES | TERM_KEYBOARD_ALL_KEYS)))
+ return ev->action == TERM_KEY_RELEASE;
+
+ uint32_t code = d->code, shifted = 0, base = 0;
+ if (!functional) {
+ code = ev->codepoint;
+ if (flags & TERM_KEYBOARD_ALTERNATE_KEYS) {
+ shifted = (mods & TERM_MOD_SHIFT) ? ev->shifted : 0;
+ base = ev->base;
+ if (shifted == code) shifted = 0;
+ if (base == code) base = 0;
+ }
+ }
+ if (!code) return true;
+
+ bool report_text = all && (flags & TERM_KEYBOARD_ASSOCIATED_TEXT) && has_text &&
+ ev->action != TERM_KEY_RELEASE;
+ format_kitty(sb, code, final, flags, functional, shifted, base, protocol_modifiers(mods),
+ event, report_text, text);
+ return true;
+}
+
+static uint32_t control_base_char(const struct term_key_event *ev)
+{
+ uint32_t ch = ((ev->mods & TERM_MOD_SHIFT) && ev->shifted)
+ ? ev->shifted : ev->codepoint;
+ return ch < 0x80 ? ch : 0;
+}
+
+static bool encode_legacy(const struct term_key_event *ev,
+ const struct term_modes *m,
+ struct seqbuf *sb)
+{
+ if (m->app_keypad && (unsigned)ev->keypad < TERM_KEY_COUNT &&
+ legacy_desc[ev->keypad].keypad_ss3) {
+ sb_ch(sb, '\033');
+ sb_ch(sb, 'O');
+ sb_ch(sb, legacy_desc[ev->keypad].keypad_ss3);
+ return true;
+ }
+
+ uint32_t ch = control_base_char(ev);
+ if ((ev->mods & TERM_MOD_CTRL) && ch) {
+ if ((ch >= 'a' && ch <= 'z') || (ch >= '@' && ch <= '_')) {
+ sb_ch(sb, (char)(ch & 0x1f));
+ return true;
+ }
+ if (ch == ' ') { sb_ch(sb, 0); return true; }
+ if (ch == '-') { sb_ch(sb, 0x1f); return true; }
+ }
+
+ unsigned csi = protocol_modifiers(ev->mods) & 63;
+ unsigned p = csi + 1;
+ bool shift = (ev->mods & TERM_MOD_SHIFT) != 0;
+
+ enum term_key k = ev->key;
+ if ((unsigned)k < TERM_KEY_COUNT && legacy_desc[k].equiv != TERM_KEY_NONE)
+ k = legacy_desc[k].equiv;
+
+ if ((unsigned)k < TERM_KEY_COUNT) {
+ const struct legacy_desc *d = &legacy_desc[k];
+ if (d->final) {
+ if (d->tilde) {
+ sb_ch(sb, '\033');
+ sb_ch(sb, '[');
+ sb_u32(sb, d->tilde);
+ if (csi) {
+ sb_ch(sb, ';');
+ sb_u32(sb, p);
+ }
+ sb_ch(sb, '~');
+ return true;
+ }
+ if (csi) {
+ sb_ch(sb, '\033');
+ sb_ch(sb, '[');
+ sb_ch(sb, '1');
+ sb_ch(sb, ';');
+ sb_u32(sb, p);
+ sb_ch(sb, d->final);
+ return true;
+ }
+ sb_ch(sb, '\033');
+ sb_ch(sb, d->app_cursor && !m->app_cursor ? '[' : 'O');
+ sb_ch(sb, d->final);
+ return true;
+ }
+ }
+
+ switch (k) {
+ case TERM_KEY_RETURN: sb_ch(sb, '\r'); return true;
+ case TERM_KEY_BACKSPACE:
+ sb_ch(sb, m->backarrow_bs ? '\x08' : '\x7f');
+ return true;
+ case TERM_KEY_TAB:
+ if (shift) { sb_str(sb, "\033[Z", 3); }
+ else { sb_ch(sb, '\t'); }
+ return true;
+ case TERM_KEY_ESCAPE: sb_ch(sb, '\033'); return true;
+ default: break;
+ }
+
+ if (ev->text && !ev->text_deferred) {
+ char tmp[8];
+ size_t n = (size_t)utf8_encode(ev->text, tmp);
+ sb_str(sb, tmp, n);
+ return true;
+ }
+ return false;
+}
+
+void term_keyboard_update(struct term_keyboard_state *s, const struct term_key_event *ev)
+{
+ if (!s || !ev || ev->key <= TERM_KEY_NONE || ev->key >= TERM_KEY_COUNT)
+ return;
+ if (ev->action == TERM_KEY_RELEASE)
+ s->down[ev->key >> 6] &= ~(1ull << (ev->key & 63));
+ else if (ev->action == TERM_KEY_PRESS || ev->action == TERM_KEY_REPEAT)
+ s->down[ev->key >> 6] |= 1ull << (ev->key & 63);
+}
+
+bool term_keyboard_key_down(const struct term_keyboard_state *s, enum term_key key)
+{
+ return s && key > TERM_KEY_NONE && (unsigned)key < TERM_KEY_COUNT &&
+ ((s->down[key >> 6] >> (key & 63)) & 1);
+}
+
+struct term_input_result term_input_encode_key(const struct term_keyboard_state *s,
+ const struct term_key_event *ev,
+ const struct term_modes *m,
+ char *buf, size_t buf_sz)
+{
+ struct term_input_result out = {0};
+ if (!s || !ev || !m || !buf ||
+ (ev->action != TERM_KEY_PRESS && ev->action != TERM_KEY_REPEAT &&
+ ev->action != TERM_KEY_RELEASE) ||
+ (unsigned)ev->key >= TERM_KEY_COUNT)
+ return out;
+
+ struct seqbuf sb;
+ sb_init(&sb, buf, buf_sz);
+
+ bool handled = encode_kitty(s, ev, m, &sb);
+ if (!handled) {
+ if (ev->action == TERM_KEY_RELEASE)
+ return out;
+
+ bool alt = (ev->mods & TERM_MOD_ALT) != 0;
+ struct seqbuf body;
+ char tmp[64];
+ if (alt) {
+ sb_init(&body, tmp, sizeof(tmp));
+ } else {
+ body = sb;
+ }
+
+ handled = encode_legacy(ev, m, &body);
+ if (handled) {
+ if (body.overflow) {
+ sb.overflow = true;
+ } else {
+ size_t n = sb_len(&body, alt ? tmp : buf);
+ if (alt) {
+ bool already_esc = n && tmp[0] == '\033';
+ size_t prefix = !already_esc;
+ if (n + prefix <= buf_sz) {
+ if (prefix) buf[0] = '\033';
+ memcpy(buf + prefix, tmp, n);
+ n += prefix;
+ } else {
+ sb.overflow = true;
+ n = 0;
+ }
+ }
+ out.len = n;
+ }
+ }
+ } else {
+ out.len = sb_len(&sb, buf);
+ }
+
+ if (!handled)
+ return out;
+ if (sb.overflow) {
+ out.status = TERM_INPUT_BUFFER_FULL;
+ out.len = 0;
+ } else {
+ out.status = out.len ? TERM_INPUT_BYTES : TERM_INPUT_CONSUMED;
+ }
+ return out;
+}
+
+struct term_input_result term_input_encode_mouse(const struct term_mouse_event *ev,
+ const struct term_modes *m,
+ char *buf, size_t buf_sz)
+{
+ struct term_input_result out = {0};
+ if (!ev || !m || !buf) return out;
+
+ if (!m->mouse || (ev->mods & TERM_MOD_SHIFT)) return out;
+
+ out.status = TERM_INPUT_CONSUMED;
+ if (ev->kind == TERM_MOUSE_LEAVE) return out;
+ struct term_vector2i cell = ev->pos;
+ if (cell.x < 0 || cell.y < 0 || cell.x >= m->grid.x || cell.y >= m->grid.y)
+ return out;
+ if (m->mouse == TERM_MOUSE_X10 && (ev->kind == TERM_MOUSE_RELEASE || ev->kind == TERM_MOUSE_MOTION)) {
+ return out;
+ }
+ if (ev->kind == TERM_MOUSE_MOTION && (m->mouse == TERM_MOUSE_NORMAL ||
+ (m->mouse == TERM_MOUSE_BUTTON && ev->button == 3))) {
+ return out;
+ }
+ int button;
+ if (ev->button >= TERM_MOUSE_BUTTON_PRIMARY && ev->button <= TERM_MOUSE_BUTTON_NONE)
+ button = ev->button;
+ else if (ev->button >= TERM_MOUSE_BUTTON_EXTRA_1 && ev->button <= TERM_MOUSE_BUTTON_EXTRA_5)
+ button = 8 + ev->button - TERM_MOUSE_BUTTON_EXTRA_1;
+ else if (ev->button >= TERM_MOUSE_WHEEL_UP && ev->button <= TERM_MOUSE_WHEEL_RIGHT)
+ button = 64 + ev->button - TERM_MOUSE_WHEEL_UP;
+ else {
+ return out;
+ }
+
+ int mods = 0;
+ if (ev->mods & TERM_MOD_ALT) mods |= 8;
+ if (ev->mods & TERM_MOD_CTRL) mods |= 16;
+
+ int code = button | mods | (ev->kind == TERM_MOUSE_MOTION ? 32 : 0);
+ int legacy_code = ev->kind == TERM_MOUSE_RELEASE ? 3 | mods : code;
+ struct term_vector2i report = m->mouse_encoding == TERM_MOUSE_ENC_SGR_PIXELS
+ ? ev->pixel : cell;
+ if (report.x < 0 || report.y < 0)
+ return out;
+ uint32_t report_x = (uint32_t)report.x + 1;
+ uint32_t report_y = (uint32_t)report.y + 1;
+
+ struct seqbuf sb;
+ sb_init(&sb, buf, buf_sz);
+
+ if (m->mouse_encoding == TERM_MOUSE_ENC_SGR ||
+ m->mouse_encoding == TERM_MOUSE_ENC_SGR_PIXELS) {
+ sb_ch(&sb, '\033');
+ sb_ch(&sb, '[');
+ sb_ch(&sb, '<');
+ sb_u32(&sb, (uint32_t)code);
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)report_x);
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)report_y);
+ sb_ch(&sb, ev->kind == TERM_MOUSE_RELEASE ? 'm' : 'M');
+ } else if (m->mouse_encoding == TERM_MOUSE_ENC_URXVT) {
+ sb_ch(&sb, '\033');
+ sb_ch(&sb, '[');
+ sb_u32(&sb, (uint32_t)(legacy_code + 32));
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)(cell.x + 1));
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)(cell.y + 1));
+ sb_ch(&sb, 'M');
+ } else if (m->mouse_encoding == TERM_MOUSE_ENC_UTF8) {
+ const unsigned values[3] = {
+ (unsigned)legacy_code + 32, (unsigned)cell.x + 33,
+ (unsigned)cell.y + 33,
+ };
+ sb_str(&sb, "\033[M", 3);
+ for (int i = 0; i < 3; i++) {
+ char tmp[8];
+ size_t n = (size_t)utf8_encode(values[i], tmp);
+ sb_str(&sb, tmp, n);
+ }
+ } else {
+ if (legacy_code > 223 || cell.x > 222 || cell.y > 222)
+ return out;
+ sb_ch(&sb, '\033');
+ sb_ch(&sb, '[');
+ sb_ch(&sb, 'M');
+ sb_ch(&sb, (char)(legacy_code + 32));
+ sb_ch(&sb, (char)(cell.x + 33));
+ sb_ch(&sb, (char)(cell.y + 33));
+ }
+
+ if (sb.overflow) {
+ out.status = TERM_INPUT_BUFFER_FULL;
+ out.len = 0;
+ return out;
+ }
+ out.len = sb_len(&sb, buf);
+ out.status = TERM_INPUT_BYTES;
+ return out;
+}
+
+/* Bidi controls can make pasted text look different from what runs. */
+static bool is_direction_format(uint32_t cp)
+{
+ return cp == 0x061c ||
+ (cp >= 0x200e && cp <= 0x200f) ||
+ (cp >= 0x202a && cp <= 0x202e) ||
+ (cp >= 0x2066 && cp <= 0x2069) ||
+ cp == 0xfeff;
+}
+
+struct term_input_result term_input_encode_paste(const struct term_modes *m,
+ const char *text, size_t len,
+ char *buf, size_t capacity)
+{
+ struct term_input_result out = {0};
+ if (!m || !buf || (!text && len)) return out;
+
+ struct seqbuf sb;
+ sb_init(&sb, buf, capacity);
+ if (m->bracketed_paste) sb_bytes(&sb, "\033[200~", 6);
+ size_t body = sb_len(&sb, buf);
+ for (size_t i = 0; i < len;) {
+ size_t start = i;
+ while (i < len && (unsigned char)text[i] >= 0x20 && (unsigned char)text[i] < 0x7f)
+ i++;
+ if (i > start) {
+ sb_bytes(&sb, text + start, i - start);
+ continue;
+ }
+ uint32_t cp;
+ int n = utf8_decode_one(text + i, len - i, &cp);
+ if (!n) { i++; continue; }
+ if (cp == '\r' || cp == '\n') {
+ sb_ch(&sb, '\r');
+ if (cp == '\r' && i + 1 < len && text[i + 1] == '\n') i++;
+ } else if (cp == '\t') {
+ sb_ch(&sb, '\t');
+ } else if (cp >= 0xa0 && !is_direction_format(cp)) {
+ sb_bytes(&sb, text + i, (size_t)n);
+ }
+ i += (size_t)n;
+ }
+
+ out.status = TERM_INPUT_CONSUMED;
+ if (!sb.overflow && sb_len(&sb, buf) == body) return out;
+ if (m->bracketed_paste) sb_bytes(&sb, "\033[201~", 6);
+ if (sb.overflow) {
+ out.status = TERM_INPUT_BUFFER_FULL;
+ return out;
+ }
+ out.len = sb_len(&sb, buf);
+ out.status = TERM_INPUT_BYTES;
+ return out;
+}
diff --git a/src/internal.h b/src/internal.h
new file mode 100644
index 0000000..dd4649c
--- /dev/null
+++ b/src/internal.h
@@ -0,0 +1,411 @@
+#ifndef TERM_INTERNAL_H
+#define TERM_INTERNAL_H
+
+#include "term.h"
+#include "cell.h"
+#include "csi.h"
+#include "gstore.h"
+#include "hist.h"
+#include "hist_view.h"
+#include "palette.h"
+#include "pen.h"
+#include "text_run.h"
+#include "compat.h"
+#include <stdatomic.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define HASH64_BASIS UINT64_C(0x9E3779B97F4A7C15)
+#define KNUTH_HASH_MULT UINT32_C(2654435761)
+
+static inline uint64_t hash64_step(uint64_t hash, uint32_t value)
+{
+ hash ^= value;
+ hash *= HASH64_BASIS;
+ return hash ^ (hash >> 29);
+}
+
+static inline uint64_t hash64_finish(uint64_t hash)
+{
+ hash ^= hash >> 32;
+ hash *= UINT64_C(0xD6E8FEB86659FD93);
+ hash ^= hash >> 32;
+ return hash;
+}
+
+#define KEYBOARD_STACK_SIZE 32
+
+struct keyboard_mode {
+ unsigned flags, saved[KEYBOARD_STACK_SIZE], depth;
+};
+
+/* What DECSC saves, as in xterm: the cursor, the pen, origin mode, the
+ * pending wrap, and the character sets. */
+struct term_saved_cursor {
+ int x, y;
+ uint32_t fg, bg, ul;
+ char charsets[2];
+ uint8_t underline;
+ uint8_t charset_active;
+ bool bold : 1;
+ bool faint : 1;
+ bool italic : 1;
+ bool strike : 1;
+ bool overline : 1;
+ bool inverse : 1;
+ bool origin : 1;
+ bool pending_wrap : 1;
+};
+
+/*
+ * A row stores cells, text, or default-pen ASCII. A text row uses the history
+ * format. An ASCII row stores only its pointer and length. A screen read or
+ * edit sets its text fields first. Other changes settle text into cells.
+ */
+enum { ROW_CELLS, ROW_TEXT, ROW_ASCII };
+enum { RF_WRAPPED = 1, RF_WIDE = 4, RF_CLUSTER = 8, RF_TAIL_PADDED = 16 };
+
+struct srow {
+ struct cell *cells; /* stride >= cols, from a per-screen slab */
+ uint8_t *ascii;
+ struct text_row text; /* TEXT: the content; len == text.cells, or cols with a wrap pad */
+ uint64_t uid; /* new id when the row scrolls in or clears */
+ uint32_t version; /* bumped on every change */
+ uint16_t len;
+ uint8_t kind, flags;
+};
+
+struct screen {
+ struct srow *slot;
+ struct cell *cells_slab;
+ uint8_t *text_slab;
+ size_t cells_cap, text_cap;
+ /* Without history: the line of row 0, counted as rows leave the top. */
+ uint64_t top_id;
+ int rows, cols, top;
+ int stride;
+ /* Capacity of the buffers above. A resize reuses them. */
+ int slot_cap;
+ uint32_t top_seg;
+};
+
+struct term_sel_point {
+ uint64_t line_id;
+ uint32_t cell_off;
+};
+
+/* A buffer that only grows, so work that repeats does not allocate. */
+struct grow_buf {
+ void *data;
+ size_t cap;
+};
+
+/* What a resize needs besides the screens. It keeps its buffers, so a
+ * resize allocates nothing once the terminal has seen its sizes. */
+struct resize_scratch {
+ struct grow_buf row, rows, cells, line, pull, rest, lens;
+};
+
+
+struct term {
+ _Atomic uint64_t seq;
+ _Atomic uint64_t writer_tid;
+ uint32_t write_depth;
+ int cols;
+ int rows;
+ int cursor_x;
+ int cursor_y;
+ int scroll_top, scroll_bottom;
+
+ uint32_t cur_fg;
+ uint32_t cur_bg;
+ uint32_t cur_ul;
+ uint32_t last_char;
+
+ uint32_t utf8_state;
+ uint32_t utf8_codep;
+ struct width_cache text_width;
+
+ enum { ST_GROUND, ST_ESC, ST_CSI, ST_OSC, ST_DCS, ST_STR_ESC, ST_CHARSET,
+ ST_DCS_ESC, ST_IGNORE, ST_IGNORE_ESC, ST_DOCS, ST_DECALN } state;
+ struct csi_parser csi_parser;
+
+ uint16_t cur_link;
+ char charsets[2];
+ uint8_t underline;
+ uint8_t decsace;
+ char charset_designation;
+ bool graphics;
+
+ bool bold : 1;
+ bool faint : 1;
+ bool italic : 1;
+ bool strike : 1;
+ bool overline : 1;
+ bool inverse : 1;
+ bool pending_wrap : 1;
+ bool autowrap : 1;
+ bool insert_mode : 1;
+ bool origin_mode : 1;
+ bool reverse_video : 1;
+ bool synchronized_output : 1;
+ bool alternate : 1;
+ bool application_cursor : 1;
+ bool application_keypad : 1;
+ bool lr_margin_mode : 1;
+ bool backarrow_sends_bs : 1;
+ bool focus_events : 1;
+ bool cursor_visible : 1;
+ bool cursor_blink : 1;
+ bool default_cursor_blink : 1;
+ bool selection : 1;
+ bool bracketed_paste : 1;
+ bool lnm : 1;
+ bool transport_newline : 1;
+ bool str_truncated : 1;
+ bool has_prompt : 1;
+
+ struct screen scr;
+ struct screen alt_scr;
+ struct screen spare; /* the buffers a resize builds into */
+ struct resize_scratch resize;
+ struct grow_buf ascii_ends;
+ struct grow_buf ascii_tail;
+ struct term_saved_cursor normal_saved_cursor;
+ struct term_saved_cursor normal_saved_decsc;
+ int normal_cols, normal_rows;
+ bool alt_allocated;
+ bool normal_cursor_visible;
+ uint8_t normal_cursor_style;
+ bool normal_cursor_blink;
+
+ struct hist hist;
+ struct hist_view view;
+ uint64_t next_row_uid;
+ uint64_t full_gen;
+ int prompt_y;
+ int left_margin, right_margin;
+ struct term_sel_point sel_start, sel_end;
+ struct term_sel_point sel_anchor_first, sel_anchor_last;
+ enum term_select_unit select_unit;
+
+ int mouse_mode, mouse_encoding;
+ enum term_cursor_style cursor_style;
+ enum term_cursor_style default_cursor_style;
+ int charset_active;
+ int osc_len;
+ int dcs_len;
+ int sb_capacity;
+
+ struct keyboard_mode keyboard[2];
+ /* The DECSC slot. Cold: it stays out of the hot fields above. */
+ struct term_saved_cursor saved;
+
+ uint8_t *tabs;
+ struct cell *view_row_scratch;
+ int tabs_cap;
+ int view_scratch_cap;
+
+ char osc_buf[256];
+ char tcap_name[64];
+
+ char *osc_heap;
+ size_t osc_heap_cap;
+ char *dcs_buf;
+ size_t dcs_cap;
+ void *links;
+ char *cwd;
+ char *pointer_shape;
+
+ struct term_callbacks cb;
+ struct palette palette;
+ struct gstore *gstore;
+ /* Unique for the life of the process. A snapshot compares it, not the
+ * address, because a new terminal can reuse a freed one's memory. */
+ uint64_t serial;
+};
+
+static inline void reset_sgr(struct term *t)
+{
+ t->cur_fg = TERM_COLOR_FG;
+ t->cur_bg = TERM_COLOR_BG;
+ t->bold = false;
+ t->faint = false;
+ t->italic = false;
+ t->underline = 0;
+ t->strike = false;
+ t->overline = false;
+ t->inverse = false;
+ t->cur_ul = 0;
+}
+
+/* The DECSC slot before any save: home, the default pen, and ASCII. */
+static inline struct term_saved_cursor term_saved_cursor_initial(void)
+{
+ return (struct term_saved_cursor){
+ .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG, .charsets = { 'B', 'B' },
+ };
+}
+
+static inline struct term_saved_cursor term_cursor_save(const struct term *t)
+{
+ return (struct term_saved_cursor){
+ .x = t->cursor_x, .y = t->cursor_y,
+ .fg = t->cur_fg, .bg = t->cur_bg, .ul = t->cur_ul,
+ .charsets = { t->charsets[0], t->charsets[1] },
+ .underline = t->underline,
+ .charset_active = (uint8_t)t->charset_active,
+ .bold = t->bold, .faint = t->faint, .italic = t->italic,
+ .strike = t->strike, .overline = t->overline, .inverse = t->inverse,
+ .origin = t->origin_mode, .pending_wrap = t->pending_wrap,
+ };
+}
+
+/* Take the pen, origin mode, and character sets from s. The caller places
+ * the cursor. */
+static inline void term_cursor_apply(struct term *t, const struct term_saved_cursor *s)
+{
+ t->cur_fg = s->fg;
+ t->cur_bg = s->bg;
+ t->cur_ul = s->ul;
+ t->underline = s->underline;
+ t->bold = s->bold;
+ t->faint = s->faint;
+ t->italic = s->italic;
+ t->strike = s->strike;
+ t->overline = s->overline;
+ t->inverse = s->inverse;
+ t->origin_mode = s->origin;
+ t->charsets[0] = s->charsets[0];
+ t->charsets[1] = s->charsets[1];
+ t->charset_active = s->charset_active;
+ t->graphics = t->charsets[t->charset_active] == '0';
+}
+
+static inline void send_response(struct term *t, const char *str)
+{
+ if (t && t->cb.reply)
+ t->cb.reply(t->cb.ud, str, strlen(str));
+}
+static inline int term_hex_nib(char c)
+{
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+}
+
+static inline bool term_lnm(const struct term *t)
+{
+ return t->lnm || t->transport_newline;
+}
+
+static inline bool mode_is_mouse(int m)
+{
+ return m == 9 || (m >= 1000 && m <= 1003);
+}
+
+static inline bool mode_is_mouse_enc(int m)
+{
+ return m == 1005 || m == 1006 || m == 1015 || m == 1016;
+}
+
+static const uint16_t term_ul_flags[] = {
+ 0, CELL_UNDERLINE, CELL_DOUBLE_UNDERLINE,
+ CELL_CURLY_UNDERLINE, CELL_DOTTED_UNDERLINE,
+ CELL_DASHED_UNDERLINE,
+};
+
+static inline uint16_t term_current_flags(const struct term *t)
+{
+ uint16_t f = 0;
+ if (t->bold) f |= CELL_BOLD;
+ if (t->faint) f |= CELL_FAINT;
+ if (t->italic) f |= CELL_ITALIC;
+ if (t->underline < sizeof(term_ul_flags) / sizeof(term_ul_flags[0]))
+ f |= term_ul_flags[t->underline];
+ if (t->strike) f |= CELL_STRIKE;
+ if (t->overline) f |= CELL_OVERLINE;
+ if (t->cur_link) f |= CELL_LINK;
+ return f;
+}
+
+static inline uint32_t term_flags_pad(const struct term *t)
+{
+ return term_current_flags(t) | ((uint32_t)t->cur_link << 16);
+}
+
+static inline void term_tabs_clear(struct term *t)
+{
+ memset(t->tabs, 0, (size_t)t->tabs_cap);
+}
+
+static inline void term_tabs_default(struct term *t)
+{
+ for (int c = 8; c < t->tabs_cap; c += 8) t->tabs[c] = 1;
+}
+
+static inline bool term_str_was_truncated(struct term *t)
+{
+ bool truncated = t->str_truncated;
+ t->str_truncated = false;
+ return truncated;
+}
+
+/* Copy text into buf with the rules of term_selection_text(). NULL text
+ * copies as empty. Return the full length. */
+static inline size_t term_copy_out(const char *text, char *buf, size_t cap)
+{
+ if (!text)
+ text = "";
+ size_t n = strlen(text);
+ if (buf && cap) {
+ size_t k = n < cap ? n : cap - 1;
+ if (k) memcpy(buf, text, k);
+ buf[k] = '\0';
+ }
+ return n;
+}
+
+/* Every thread has its own anchor, so its address names the thread. */
+extern _Thread_local char term_thread_anchor;
+
+static inline uint64_t term_thread_id(void)
+{
+ return (uint64_t)(uintptr_t)&term_thread_anchor;
+}
+
+static inline void term_write_begin(struct term *t)
+{
+ if (!t) return;
+ uint64_t self = term_thread_id();
+ if (atomic_load_explicit(&t->writer_tid, memory_order_relaxed) == self) {
+ t->write_depth++;
+ return;
+ }
+ uint64_t s;
+ for (;;) {
+ s = atomic_load_explicit(&t->seq, memory_order_relaxed);
+ if (s & 1) {
+ term_cpu_relax();
+ continue;
+ }
+ if (atomic_compare_exchange_weak_explicit(&t->seq, &s, s + 1,
+ memory_order_acquire,
+ memory_order_relaxed))
+ break;
+ }
+ atomic_store_explicit(&t->writer_tid, self, memory_order_relaxed);
+ t->write_depth = 1;
+}
+
+static inline void term_write_end(struct term *t)
+{
+ if (!t) return;
+ if (--t->write_depth == 0) {
+ atomic_store_explicit(&t->writer_tid, 0, memory_order_relaxed);
+ atomic_fetch_add_explicit(&t->seq, 1, memory_order_release);
+ }
+}
+
+#endif
diff --git a/src/isa.c b/src/isa.c
new file mode 100644
index 0000000..1fe1517
--- /dev/null
+++ b/src/isa.c
@@ -0,0 +1,78 @@
+/*
+ * The instruction set level. See simd.h. The level is picked once, before
+ * the first terminal exists, so every call in a process uses the same
+ * kernels.
+ */
+#include "term.h"
+#include "simd.h"
+
+#include <stdatomic.h>
+#include <stdlib.h>
+#include <string.h>
+
+int term_isa_level;
+static atomic_int isa_state;
+
+static const char *const level_names[] = { "base", "avx2", "avx512" };
+
+#if defined(TERM_ISA_RUNTIME)
+/* The AVX-512 kernels also need VBMI and VBMI2. Every CPU with VBMI except
+ * Cannon Lake has VBMI2. */
+static int cpu_level(void)
+{
+ __builtin_cpu_init();
+ if (__builtin_cpu_supports("x86-64-v4") && __builtin_cpu_supports("avx512vbmi") &&
+ __builtin_cpu_supports("avx512vbmi2"))
+ return TERM_ISA_AVX512;
+ if (__builtin_cpu_supports("x86-64-v3"))
+ return TERM_ISA_AVX2;
+ return TERM_ISA_BASE;
+}
+#else
+static int cpu_level(void)
+{
+ return TERM_ISA_FLOOR;
+}
+#endif
+
+static int pick_level(void)
+{
+ int level = cpu_level();
+ /* TERM_HEADLESS_ISA can only lower the level, never below the floor the
+ * compiler flags fix. */
+ const char *want = getenv("TERM_HEADLESS_ISA");
+ for (int i = TERM_ISA_FLOOR; want && i < level; i++)
+ if (!strcmp(want, level_names[i]))
+ level = i;
+ return level;
+}
+
+/* The first caller picks the level. A second caller waits until it is set. */
+void term_isa_init(void)
+{
+ if (atomic_load_explicit(&isa_state, memory_order_acquire) == 2)
+ return;
+ int expected = 0;
+ if (atomic_compare_exchange_strong(&isa_state, &expected, 1)) {
+ term_isa_level = pick_level();
+ atomic_store_explicit(&isa_state, 2, memory_order_release);
+ return;
+ }
+ while (atomic_load_explicit(&isa_state, memory_order_acquire) != 2)
+ term_cpu_relax();
+}
+
+const char *term_isa(void)
+{
+ term_isa_init();
+#if defined(__aarch64__)
+ return "neon";
+#else
+ int level = TERM_ISA_FLOOR;
+ if (term_use_avx2())
+ level = TERM_ISA_AVX2;
+ if (term_use_avx512())
+ level = TERM_ISA_AVX512;
+ return level_names[level];
+#endif
+}
diff --git a/src/kernels.h b/src/kernels.h
new file mode 100644
index 0000000..48088b5
--- /dev/null
+++ b/src/kernels.h
@@ -0,0 +1,790 @@
+#ifndef WSAP_TERM_KERNELS_H
+#define WSAP_TERM_KERNELS_H
+
+#include "cell.h"
+#include "compat.h"
+#include "simd.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline size_t scan_printable_span_avx512(const char *buf, size_t len, unsigned char hi)
+{
+ size_t i = 0;
+ __m512i v20_512 = _mm512_set1_epi8(0x20);
+ __m512i vlim_512 = _mm512_set1_epi8((char)(hi - 0x20));
+ for (; i + 64 <= len; i += 64) {
+ __m512i v = _mm512_loadu_si512((const void *)(buf + i));
+ __mmask64 mask = _mm512_cmpge_epu8_mask(_mm512_sub_epi8(v, v20_512), vlim_512);
+ if (mask != 0)
+ return i + term_ctz64(mask);
+ }
+ if (i < len) {
+ __mmask64 load_mask = ((uint64_t)1 << (len - i)) - 1;
+ __m512i v = _mm512_maskz_loadu_epi8(load_mask, buf + i);
+ __mmask64 mask = _mm512_mask_cmpge_epu8_mask(load_mask, _mm512_sub_epi8(v, v20_512), vlim_512);
+ if (mask != 0)
+ return i + term_ctz64(mask);
+ }
+ return len;
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline size_t scan_printable_span_avx2(const char *buf, size_t len, unsigned char hi)
+{
+ size_t i = 0;
+ const __m256i v_offset = _mm256_set1_epi8((char)(0x20 + 0x80));
+ const __m256i v_limit = _mm256_set1_epi8((char)(hi - 0xa1));
+ for (; i + 64 <= len; i += 64) {
+ __m256i v0 = _mm256_loadu_si256((const __m256i *)(buf + i));
+ __m256i v1 = _mm256_loadu_si256((const __m256i *)(buf + i + 32));
+ __m256i sub0 = _mm256_sub_epi8(v0, v_offset);
+ __m256i sub1 = _mm256_sub_epi8(v1, v_offset);
+ int m0 = _mm256_movemask_epi8(_mm256_cmpgt_epi8(sub0, v_limit));
+ int m1 = _mm256_movemask_epi8(_mm256_cmpgt_epi8(sub1, v_limit));
+ if (TERM_EXPECT((m0 | m1) != 0, 0)) {
+ if (m0 != 0)
+ return i + term_ctz32(m0);
+ return i + 32 + term_ctz32(m1);
+ }
+ }
+ if (i + 32 <= len) {
+ __m256i v = _mm256_loadu_si256((const __m256i *)(buf + i));
+ __m256i sub = _mm256_sub_epi8(v, v_offset);
+ int mask = _mm256_movemask_epi8(_mm256_cmpgt_epi8(sub, v_limit));
+ if (mask != 0)
+ return i + term_ctz32(mask);
+ i += 32;
+ }
+ if (i + 16 <= len) {
+ __m128i v = _mm_loadu_si128((const __m128i *)(buf + i));
+ __m128i sub = _mm_sub_epi8(v, _mm_set1_epi8((char)(0x20 + 0x80)));
+ int mask = _mm_movemask_epi8(_mm_cmpgt_epi8(sub, _mm_set1_epi8((char)(hi - 0xa1)))) & 0xFFFF;
+ if (mask != 0)
+ return i + term_ctz32(mask);
+ i += 16;
+ }
+ for (; i < len; i++) {
+ unsigned char c = (unsigned char)buf[i];
+ if (c < 0x20 || c >= hi)
+ return i;
+ }
+ return len;
+}
+#endif
+
+static inline size_t scan_printable_span(const char *buf, size_t len,
+ unsigned char hi)
+{
+ /* A short span costs less inline than as a call. */
+#if defined(TERM_KERNEL_AVX512)
+ if (len >= 64 && term_use_avx512())
+ return scan_printable_span_avx512(buf, len, hi);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (len >= 64 && term_use_avx2())
+ return scan_printable_span_avx2(buf, len, hi);
+#endif
+ size_t i = 0;
+#if defined(__aarch64__)
+ uint8x16_t v20 = vdupq_n_u8(0x20);
+ uint8x16_t vlim = vdupq_n_u8((uint8_t)(hi - 0x20));
+ for (; i + 64 <= len; i += 64) {
+ uint8x16_t v0 = vld1q_u8((const uint8_t *)buf + i);
+ uint8x16_t v1 = vld1q_u8((const uint8_t *)buf + i + 16);
+ uint8x16_t v2 = vld1q_u8((const uint8_t *)buf + i + 32);
+ uint8x16_t v3 = vld1q_u8((const uint8_t *)buf + i + 48);
+
+ uint8x16_t m0 = vcgeq_u8(vsubq_u8(v0, v20), vlim);
+ uint8x16_t m1 = vcgeq_u8(vsubq_u8(v1, v20), vlim);
+ uint8x16_t m2 = vcgeq_u8(vsubq_u8(v2, v20), vlim);
+ uint8x16_t m3 = vcgeq_u8(vsubq_u8(v3, v20), vlim);
+
+ uint8x16_t mor = vorrq_u8(vorrq_u8(m0, m1), vorrq_u8(m2, m3));
+ uint64x2_t mor64 = vreinterpretq_u64_u8(mor);
+ if (TERM_EXPECT((vgetq_lane_u64(mor64, 0) | vgetq_lane_u64(mor64, 1)) != 0, 0)) {
+ uint64x2_t m0_64 = vreinterpretq_u64_u8(m0);
+ uint64_t lo0 = vgetq_lane_u64(m0_64, 0), hi0 = vgetq_lane_u64(m0_64, 1);
+ if (lo0) return i + (term_ctz64(lo0) >> 3);
+ if (hi0) return i + 8 + (term_ctz64(hi0) >> 3);
+
+ uint64x2_t m1_64 = vreinterpretq_u64_u8(m1);
+ uint64_t lo1 = vgetq_lane_u64(m1_64, 0), hi1 = vgetq_lane_u64(m1_64, 1);
+ if (lo1) return i + 16 + (term_ctz64(lo1) >> 3);
+ if (hi1) return i + 24 + (term_ctz64(hi1) >> 3);
+
+ uint64x2_t m2_64 = vreinterpretq_u64_u8(m2);
+ uint64_t lo2 = vgetq_lane_u64(m2_64, 0), hi2 = vgetq_lane_u64(m2_64, 1);
+ if (lo2) return i + 32 + (term_ctz64(lo2) >> 3);
+ if (hi2) return i + 40 + (term_ctz64(hi2) >> 3);
+
+ uint64x2_t m3_64 = vreinterpretq_u64_u8(m3);
+ uint64_t lo3 = vgetq_lane_u64(m3_64, 0), hi3 = vgetq_lane_u64(m3_64, 1);
+ if (lo3) return i + 48 + (term_ctz64(lo3) >> 3);
+ if (hi3) return i + 56 + (term_ctz64(hi3) >> 3);
+ }
+ }
+ for (; i + 16 <= len; i += 16) {
+ uint8x16_t v = vld1q_u8((const uint8_t *)buf + i);
+ uint8x16_t mask = vcgeq_u8(vsubq_u8(v, v20), vlim);
+ uint64x2_t m64 = vreinterpretq_u64_u8(mask);
+ uint64_t lo = vgetq_lane_u64(m64, 0);
+ uint64_t hi64 = vgetq_lane_u64(m64, 1);
+ if (lo) return i + (term_ctz64(lo) >> 3);
+ if (hi64) return i + 8 + (term_ctz64(hi64) >> 3);
+ }
+ if (i + 8 <= len) {
+ uint8x8_t v8 = vld1_u8((const uint8_t *)buf + i);
+ uint8x8_t mask8 = vcge_u8(vsub_u8(v8, vget_low_u8(v20)), vget_low_u8(vlim));
+ uint64_t m64 = vget_lane_u64(vreinterpret_u64_u8(mask8), 0);
+ if (m64) return i + (term_ctz64(m64) >> 3);
+ i += 8;
+ }
+#elif defined(__x86_64__)
+ const __m128i v_offset = _mm_set1_epi8((char)(0x20 + 0x80));
+ const __m128i v_limit = _mm_set1_epi8((char)(hi - 0xa1));
+ for (; i + 64 <= len; i += 64) {
+ __m128i m0 = _mm_cmpgt_epi8(_mm_sub_epi8(_mm_loadu_si128((const __m128i *)(buf + i)), v_offset), v_limit);
+ __m128i m1 = _mm_cmpgt_epi8(_mm_sub_epi8(_mm_loadu_si128((const __m128i *)(buf + i + 16)), v_offset), v_limit);
+ __m128i m2 = _mm_cmpgt_epi8(_mm_sub_epi8(_mm_loadu_si128((const __m128i *)(buf + i + 32)), v_offset), v_limit);
+ __m128i m3 = _mm_cmpgt_epi8(_mm_sub_epi8(_mm_loadu_si128((const __m128i *)(buf + i + 48)), v_offset), v_limit);
+ if (TERM_EXPECT(_mm_movemask_epi8(_mm_or_si128(_mm_or_si128(m0, m1), _mm_or_si128(m2, m3))) != 0, 0)) {
+ uint64_t mask = (uint64_t)(unsigned)_mm_movemask_epi8(m0) |
+ (uint64_t)(unsigned)_mm_movemask_epi8(m1) << 16 |
+ (uint64_t)(unsigned)_mm_movemask_epi8(m2) << 32 |
+ (uint64_t)(unsigned)_mm_movemask_epi8(m3) << 48;
+ return i + (size_t)term_ctz64(mask);
+ }
+ }
+ for (; i + 16 <= len; i += 16) {
+ int mask = _mm_movemask_epi8(_mm_cmpgt_epi8(
+ _mm_sub_epi8(_mm_loadu_si128((const __m128i *)(buf + i)), v_offset), v_limit));
+ if (mask != 0)
+ return i + (size_t)term_ctz32((unsigned)mask);
+ }
+#endif
+ for (; i < len; i++) {
+ unsigned char c = (unsigned char)buf[i];
+ if (c < 0x20 || c >= hi)
+ return i;
+ }
+ return len;
+}
+
+/* Copy n bytes with loads that stay inside [src, src + n). Short text runs
+ * are common; a libc call costs more than the copy. */
+static inline TERM_ALWAYS_INLINE void bytes_copy_small(uint8_t *restrict dst,
+ const char *restrict src, int n)
+{
+ if (TERM_EXPECT((unsigned)(n - 16) <= 16u, 1)) {
+ /* 16 to 32 bytes: two overlapping 16-byte copies. Both loads stay
+ * inside [src, src + n). This is the usual history line length, and
+ * two copies cost less here than the loop below. */
+ memcpy(dst, src, 16);
+ memcpy(dst + n - 16, src + n - 16, 16);
+ } else if (n > 256) {
+ memcpy(dst, src, (size_t)n);
+ } else if (n >= 16) {
+ int k = 0;
+ for (; k + 16 <= n; k += 16)
+ memcpy(dst + k, src + k, 16);
+ if (k < n)
+ memcpy(dst + n - 16, src + n - 16, 16);
+ } else if (n >= 8) {
+ uint64_t a, b;
+ memcpy(&a, src, 8);
+ memcpy(&b, src + n - 8, 8);
+ memcpy(dst, &a, 8);
+ memcpy(dst + n - 8, &b, 8);
+ } else if (n >= 4) {
+ uint32_t a, b;
+ memcpy(&a, src, 4);
+ memcpy(&b, src + n - 4, 4);
+ memcpy(dst, &a, 4);
+ memcpy(dst + n - 4, &b, 4);
+ } else {
+ for (int k = 0; k < n; k++)
+ dst[k] = (uint8_t)src[k];
+ }
+}
+
+static inline size_t scan_crlf_tail(const char *buf, size_t i, size_t len)
+{
+ while (i + 1 < len && buf[i] == '\r' && buf[i + 1] == '\n')
+ i += 2;
+ return i;
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline size_t scan_crlf_pairs_avx512(const char *buf, size_t len)
+{
+ size_t i = 0;
+ const __m512i pair = _mm512_set1_epi16(0x0A0D);
+ for (; i + 64 <= len; i += 64) {
+ uint64_t eq = _mm512_cmpeq_epi8_mask(_mm512_loadu_si512((const void *)(buf + i)), pair);
+ if (eq != ~0ull)
+ return i + ((size_t)term_ctz64(~eq) & ~(size_t)1);
+ }
+ return scan_crlf_tail(buf, i, len);
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline size_t scan_crlf_pairs_avx2(const char *buf, size_t len)
+{
+ size_t i = 0;
+ const __m256i pair = _mm256_set1_epi16(0x0A0D);
+ for (; i + 32 <= len; i += 32) {
+ uint32_t eq = (uint32_t)_mm256_movemask_epi8(
+ _mm256_cmpeq_epi8(_mm256_loadu_si256((const __m256i *)(const void *)(buf + i)), pair));
+ if (eq != ~0u)
+ return i + ((size_t)term_ctz32(~eq) & ~(size_t)1);
+ }
+ return scan_crlf_tail(buf, i, len);
+}
+#endif
+
+/* The length of the prefix of buf made of "\r\n" pairs; always even. */
+static inline size_t scan_crlf_pairs(const char *buf, size_t len)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return scan_crlf_pairs_avx512(buf, len);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return scan_crlf_pairs_avx2(buf, len);
+#endif
+ size_t i = 0;
+#if defined(__aarch64__)
+ const uint8x16_t pair = vreinterpretq_u8_u16(vdupq_n_u16(0x0A0D));
+ for (; i + 16 <= len; i += 16) {
+ uint8x16_t eq = vceqq_u8(vld1q_u8((const uint8_t *)buf + i), pair);
+ uint16_t bits = term_lane_bits(eq);
+ if (bits != 0xFFFF)
+ return i + ((size_t)term_ctz32((unsigned)~bits & 0xFFFFu) & ~(size_t)1);
+ }
+#endif
+ return scan_crlf_tail(buf, i, len);
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline size_t scan_byte_run_avx512(const char *buf, size_t len, char byte)
+{
+ size_t i = 0;
+ const __m512i want = _mm512_set1_epi8(byte);
+ for (; i + 64 <= len; i += 64) {
+ uint64_t eq = _mm512_cmpeq_epi8_mask(_mm512_loadu_si512((const void *)(buf + i)), want);
+ if (eq != ~0ull)
+ return i + (size_t)term_ctz64(~eq);
+ }
+ while (i < len && buf[i] == byte)
+ i++;
+ return i;
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline size_t scan_byte_run_avx2(const char *buf, size_t len, char byte)
+{
+ size_t i = 0;
+ const __m256i want = _mm256_set1_epi8(byte);
+ for (; i + 32 <= len; i += 32) {
+ uint32_t eq = (uint32_t)_mm256_movemask_epi8(
+ _mm256_cmpeq_epi8(_mm256_loadu_si256((const __m256i *)(const void *)(buf + i)), want));
+ if (eq != ~0u)
+ return i + (size_t)term_ctz32(~eq);
+ }
+ while (i < len && buf[i] == byte)
+ i++;
+ return i;
+}
+#endif
+
+/* The length of the prefix of buf made of byte. */
+static inline size_t scan_byte_run(const char *buf, size_t len, char byte)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return scan_byte_run_avx512(buf, len, byte);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return scan_byte_run_avx2(buf, len, byte);
+#endif
+ size_t i = 0;
+#if defined(__aarch64__)
+ const uint8x16_t want = vdupq_n_u8((uint8_t)byte);
+ for (; i + 16 <= len; i += 16) {
+ uint16_t bits = term_lane_bits(vceqq_u8(vld1q_u8((const uint8_t *)buf + i), want));
+ if (bits != 0xFFFF)
+ return i + (size_t)term_ctz32((unsigned)~bits & 0xFFFFu);
+ }
+#elif defined(__x86_64__)
+ const __m128i want = _mm_set1_epi8(byte);
+ for (; i + 16 <= len; i += 16) {
+ unsigned eq = (unsigned)_mm_movemask_epi8(
+ _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(const void *)(buf + i)), want));
+ if (eq != 0xFFFFu)
+ return i + (size_t)term_ctz32(~eq & 0xFFFFu);
+ }
+#endif
+ while (i < len && buf[i] == byte)
+ i++;
+ return i;
+}
+
+static inline int scan_ascii_run(const char *buf, int len)
+{
+ return (int)scan_printable_span(buf, (size_t)len, 0x7f);
+}
+
+static inline int scan_ascii_lf_tail(const char *buf, int i, int len, uint16_t *ends, int count)
+{
+ for (; i < len; i++) {
+ unsigned char ch = (unsigned char)buf[i];
+ if (ch == '\n') ends[++count] = (uint16_t)(i + 1);
+ else if (ch - 0x20u >= 0x5fu) break;
+ }
+ return count;
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512 TERM_MAYBE_UNUSED
+static int scan_ascii_lf_avx512(const char *buf, int len, uint16_t *ends)
+{
+ int i = 0, count = 0;
+ ends[0] = 0;
+ const __m512i lanes = _mm512_setr_epi32(
+ 0x03020100,0x07060504,0x0b0a0908,0x0f0e0d0c,
+ 0x13121110,0x17161514,0x1b1a1918,0x1f1e1d1c,
+ 0x23222120,0x27262524,0x2b2a2928,0x2f2e2d2c,
+ 0x33323130,0x37363534,0x3b3a3938,0x3f3e3d3c);
+ __m128i block = _mm_set1_epi16(1);
+ for (; i + 256 <= len; i += 256) {
+ __m512i v[4];
+ uint64_t nl[4];
+ uint64_t bad = 0;
+ for (int q = 0; q < 4; q++) {
+ v[q] = _mm512_loadu_si512(buf + i + q * 64);
+ nl[q] = _mm512_cmpeq_epi8_mask(v[q], _mm512_set1_epi8('\n'));
+ bad |= _mm512_cmpge_epu8_mask(_mm512_sub_epi8(v[q], _mm512_set1_epi8(0x20)),
+ _mm512_set1_epi8(0x5f)) & ~nl[q];
+ }
+ if (bad) break;
+ for (int q = 0; q < 4; q++) {
+ unsigned n = (unsigned)term_popcount64(nl[q]);
+ __m512i packed = _mm512_maskz_compress_epi8(nl[q], lanes);
+ __m128i offsets = _mm_add_epi16(_mm_cvtepu8_epi16(_mm512_castsi512_si128(packed)), block);
+ _mm_storeu_si128((__m128i *)(ends + count + 1), offsets);
+ if (TERM_EXPECT(n > 8, 0)) {
+ __m512i base = _mm512_broadcast_i32x4(block);
+ __m512i wide = _mm512_add_epi16(_mm512_cvtepu8_epi16(_mm512_castsi512_si256(packed)), base);
+ _mm512_storeu_si512(ends + count + 1, wide);
+ if (n > 32) {
+ wide = _mm512_add_epi16(_mm512_cvtepu8_epi16(_mm512_extracti64x4_epi64(packed, 1)), base);
+ _mm512_storeu_si512(ends + count + 33, wide);
+ }
+ }
+ count += (int)n;
+ block = _mm_add_epi16(block, _mm_set1_epi16(64));
+ }
+ }
+ for (; i + 64 <= len; i += 64) {
+ __m512i v = _mm512_loadu_si512(buf + i);
+ uint64_t nl = _mm512_cmpeq_epi8_mask(v, _mm512_set1_epi8('\n'));
+ uint64_t bad = _mm512_cmpge_epu8_mask(
+ _mm512_sub_epi8(v, _mm512_set1_epi8(0x20)), _mm512_set1_epi8(0x5f)) & ~nl;
+ if (bad) break;
+ unsigned n = (unsigned)term_popcount64(nl);
+ __m512i packed = _mm512_maskz_compress_epi8(nl, lanes);
+ __m128i offsets = _mm_add_epi16(_mm_cvtepu8_epi16(_mm512_castsi512_si128(packed)), block);
+ _mm_storeu_si128((__m128i *)(ends + count + 1), offsets);
+ if (TERM_EXPECT(n > 8, 0)) {
+ __m512i base = _mm512_broadcast_i32x4(block);
+ __m512i wide = _mm512_add_epi16(_mm512_cvtepu8_epi16(_mm512_castsi512_si256(packed)), base);
+ _mm512_storeu_si512(ends + count + 1, wide);
+ if (n > 32) {
+ wide = _mm512_add_epi16(_mm512_cvtepu8_epi16(_mm512_extracti64x4_epi64(packed, 1)), base);
+ _mm512_storeu_si512(ends + count + 33, wide);
+ }
+ }
+ count += (int)n;
+ block = _mm_add_epi16(block, _mm_set1_epi16(64));
+ }
+ return scan_ascii_lf_tail(buf, i, len, ends, count);
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2 TERM_MAYBE_UNUSED
+static int scan_ascii_lf_avx2(const char *buf, int len, uint16_t *ends)
+{
+ int i = 0, count = 0;
+ ends[0] = 0;
+ for (; i + 32 <= len; i += 32) {
+ __m256i v = _mm256_loadu_si256((const __m256i *)(buf + i));
+ uint32_t nl = (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, _mm256_set1_epi8('\n')));
+ __m256i d = _mm256_sub_epi8(v, _mm256_set1_epi8((char)0xa0));
+ uint32_t bad = (uint32_t)_mm256_movemask_epi8(
+ _mm256_cmpgt_epi8(d, _mm256_set1_epi8((char)0xde))) & ~nl;
+ if (bad) break;
+ while (nl) {
+ ends[++count] = (uint16_t)(i + term_ctz32(nl) + 1);
+ nl &= nl - 1;
+ }
+ }
+ return scan_ascii_lf_tail(buf, i, len, ends, count);
+}
+#endif
+
+/* Find complete ASCII lines. Stop at the first byte other than text or LF.
+ * The caller supplies len + 65 entries. len must fit in uint16_t. */
+static TERM_NOINLINE TERM_MAYBE_UNUSED int scan_ascii_lf(const char *buf, int len, uint16_t *ends)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return scan_ascii_lf_avx512(buf, len, ends);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return scan_ascii_lf_avx2(buf, len, ends);
+#endif
+ int i = 0, count = 0;
+ ends[0] = 0;
+#if defined(__aarch64__)
+ for (; i + 32 <= len; i += 32) {
+ uint32_t nl = 0, bad = 0;
+ for (int q = 0; q < 2; q++) {
+ uint8x16_t v = vld1q_u8((const uint8_t *)buf + i + q * 16);
+ nl |= (uint32_t)term_lane_bits(vceqq_u8(v, vdupq_n_u8('\n'))) << (q * 16);
+ bad |= (uint32_t)term_lane_bits(vcgeq_u8(vsubq_u8(v, vdupq_n_u8(0x20)),
+ vdupq_n_u8(0x5f))) << (q * 16);
+ }
+ bad &= ~nl;
+ if (bad) break;
+ while (nl) {
+ ends[++count] = (uint16_t)(i + term_ctz32(nl) + 1);
+ nl &= nl - 1;
+ }
+ }
+#endif
+ return scan_ascii_lf_tail(buf, i, len, ends, count);
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline void fill_cells_avx512(struct cell * restrict row, int cols, uint32_t fg, uint32_t bg)
+{
+ __m128i cell = _mm_setr_epi32(' ', (int)fg, (int)bg, 0);
+ __m512i blank = _mm512_broadcast_i32x4(cell);
+ int c = 0;
+ for (; c + 8 <= cols; c += 8) {
+ _mm512_storeu_si512((void *)(row + c), blank);
+ _mm512_storeu_si512((void *)(row + c + 4), blank);
+ }
+ int rem = cols - c;
+ if (rem >= 4) {
+ _mm512_storeu_si512((void *)(row + c), blank);
+ c += 4;
+ rem -= 4;
+ }
+ if (rem > 0) {
+ uint16_t store_mask = (uint16_t)((1u << (4 * rem)) - 1);
+ _mm512_mask_storeu_epi32((void *)(row + c), store_mask, blank);
+ }
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline void fill_cells_avx2(struct cell * restrict row, int cols, uint32_t fg, uint32_t bg)
+{
+ struct cell fill = { .c = ' ', .fg = fg, .bg = bg, .flags = 0, .pad = 0 };
+ __m256i blank = _mm256_setr_epi32(' ', (int)fg, (int)bg, 0, ' ', (int)fg, (int)bg, 0);
+ int c = 0;
+ for (; c + 8 <= cols; c += 8) {
+ char *d = (char *)(row + c);
+ _mm256_storeu_si256((__m256i *)(d), blank);
+ _mm256_storeu_si256((__m256i *)(d + 32), blank);
+ _mm256_storeu_si256((__m256i *)(d + 64), blank);
+ _mm256_storeu_si256((__m256i *)(d + 96), blank);
+ }
+ for (; c < cols; c++) {
+ row[c] = fill;
+ }
+}
+#endif
+
+static inline void fill_cells(struct cell * restrict row, int cols, uint32_t fg, uint32_t bg)
+{
+ struct cell fill = { .c = ' ', .fg = fg, .bg = bg, .flags = 0, .pad = 0 };
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512()) {
+ if (TERM_EXPECT(cols > 0, 1))
+ fill_cells_avx512(row, cols, fg, bg);
+ return;
+ }
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2() && TERM_EXPECT(cols >= 8, 1)) {
+ fill_cells_avx2(row, cols, fg, bg);
+ return;
+ }
+#endif
+#if defined(__aarch64__)
+ if (TERM_EXPECT(cols > 0, 1)) {
+ uint32x4_t blank = { ' ', fg, bg, 0 };
+ uint32x4x4_t b4 = {{ blank, blank, blank, blank }};
+ int c = 0;
+ for (; c + 8 <= cols; c += 8) {
+ vst1q_u32_x4((uint32_t *)(void *)(row + c), b4);
+ vst1q_u32_x4((uint32_t *)(void *)(row + c + 4), b4);
+ }
+ for (; c + 4 <= cols; c += 4) {
+ vst1q_u32_x4((uint32_t *)(void *)(row + c), b4);
+ }
+ for (; c < cols; c++) {
+ vst1q_u32((uint32_t *)(void *)(row + c), blank);
+ }
+ return;
+ }
+#elif defined(__x86_64__)
+ {
+ __m128i blank = _mm_setr_epi32(' ', (int)fg, (int)bg, 0);
+ int c = 0;
+ for (; c + 4 <= cols; c += 4) {
+ _mm_storeu_si128((__m128i *)(void *)(row + c), blank);
+ _mm_storeu_si128((__m128i *)(void *)(row + c + 1), blank);
+ _mm_storeu_si128((__m128i *)(void *)(row + c + 2), blank);
+ _mm_storeu_si128((__m128i *)(void *)(row + c + 3), blank);
+ }
+ for (; c < cols; c++)
+ _mm_storeu_si128((__m128i *)(void *)(row + c), blank);
+ return;
+ }
+#endif
+ for (int c = 0; c < cols; c++) {
+ row[c] = fill;
+ }
+}
+
+static inline void clear_cells_default(struct cell *row, int cols)
+{
+ fill_cells(row, cols, TERM_COLOR_FG, TERM_COLOR_BG);
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline void put_ascii_chunk_avx512(struct cell * restrict dst, const char * restrict src, int n,
+ uint32_t fg, uint32_t bg, uint32_t flags)
+{
+ int k = 0;
+ __m128i cell = _mm_setr_epi32(0, (int)fg, (int)bg, (int)flags);
+ __m512i base = _mm512_broadcast_i32x4(cell);
+ const __m512i idx0 = _mm512_setr_epi32(0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0);
+ const __m512i idx1 = _mm512_setr_epi32(4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0);
+ const __m512i idx2 = _mm512_setr_epi32(8, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0);
+ const __m512i idx3 = _mm512_setr_epi32(12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 15, 0, 0, 0);
+
+ for (; k + 16 <= n; k += 16) {
+ __m512i cps = _mm512_cvtepu8_epi32(_mm_loadu_si128((const __m128i *)(src + k)));
+ _mm512_storeu_si512(dst + k + 0, _mm512_mask_permutexvar_epi32(base, 0x1111, idx0, cps));
+ _mm512_storeu_si512(dst + k + 4, _mm512_mask_permutexvar_epi32(base, 0x1111, idx1, cps));
+ _mm512_storeu_si512(dst + k + 8, _mm512_mask_permutexvar_epi32(base, 0x1111, idx2, cps));
+ _mm512_storeu_si512(dst + k + 12, _mm512_mask_permutexvar_epi32(base, 0x1111, idx3, cps));
+ }
+ int rem = n - k;
+ if (rem > 0) {
+ __m128i v = _mm_maskz_loadu_epi8((1 << rem) - 1, src + k);
+ __m512i cps = _mm512_cvtepu8_epi32(v);
+ if (rem <= 4) {
+ uint16_t m = (uint16_t)((1u << (4 * rem)) - 1);
+ _mm512_mask_storeu_epi32(dst + k, m, _mm512_mask_permutexvar_epi32(base, 0x1111, idx0, cps));
+ return;
+ }
+ _mm512_storeu_si512(dst + k, _mm512_mask_permutexvar_epi32(base, 0x1111, idx0, cps));
+ if (rem <= 8) {
+ uint16_t m = (uint16_t)((1u << (4 * (rem - 4))) - 1);
+ _mm512_mask_storeu_epi32(dst + k + 4, m, _mm512_mask_permutexvar_epi32(base, 0x1111, idx1, cps));
+ return;
+ }
+ _mm512_storeu_si512(dst + k + 4, _mm512_mask_permutexvar_epi32(base, 0x1111, idx1, cps));
+ if (rem <= 12) {
+ uint16_t m = (uint16_t)((1u << (4 * (rem - 8))) - 1);
+ _mm512_mask_storeu_epi32(dst + k + 8, m, _mm512_mask_permutexvar_epi32(base, 0x1111, idx2, cps));
+ return;
+ }
+ _mm512_storeu_si512(dst + k + 8, _mm512_mask_permutexvar_epi32(base, 0x1111, idx2, cps));
+ uint16_t m = (uint16_t)((1u << (4 * (rem - 12))) - 1);
+ _mm512_mask_storeu_epi32(dst + k + 12, m, _mm512_mask_permutexvar_epi32(base, 0x1111, idx3, cps));
+ }
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static inline void put_ascii_chunk_avx2(struct cell * restrict dst, const char * restrict src, int n,
+ uint32_t fg, uint32_t bg, uint32_t flags)
+{
+ int k = 0;
+ __m128i cell = _mm_setr_epi32(0, (int)fg, (int)bg, (int)flags);
+ __m256i base = _mm256_broadcastsi128_si256(cell);
+#define ASCII_PAIR(o, q) _mm256_storeu_si256((__m256i *)(void *)(dst + (o) + 2 * (q)), \
+ _mm256_blend_epi32(_mm256_permutevar8x32_epi32(cps, \
+ _mm256_setr_epi32(2*(q), 0, 0, 0, 2*(q)+1, 0, 0, 0)), base, 0xee))
+#define ASCII_8(o) do { \
+ __m256i cps = _mm256_cvtepu8_epi32(_mm_loadl_epi64((const __m128i *)(void *)(src + (o)))); \
+ ASCII_PAIR(o, 0); ASCII_PAIR(o, 1); ASCII_PAIR(o, 2); ASCII_PAIR(o, 3); \
+ } while (0)
+ for (; k + 16 <= n; k += 16) {
+ ASCII_8(k);
+ ASCII_8(k + 8);
+ }
+ if (k + 8 <= n) {
+ ASCII_8(k);
+ k += 8;
+ }
+ if (k < n && n >= 8) {
+ ASCII_8(n - 8);
+ k = n;
+ }
+#undef ASCII_8
+#undef ASCII_PAIR
+ for (; k < n; k++)
+ _mm_storeu_si128((__m128i *)(void *)(dst + k),
+ _mm_or_si128(cell, _mm_cvtsi32_si128((unsigned char)src[k])));
+}
+#endif
+
+TERM_ALWAYS_INLINE
+static inline void put_ascii_chunk(struct cell * restrict dst, const char * restrict src, int n,
+ uint32_t fg, uint32_t bg, uint32_t flags)
+{
+ /* A short chunk costs less inline than as a call. */
+#if defined(TERM_KERNEL_AVX512)
+ if (n >= 16 && term_use_avx512()) {
+ put_ascii_chunk_avx512(dst, src, n, fg, bg, flags);
+ return;
+ }
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (n >= 16 && term_use_avx2()) {
+ put_ascii_chunk_avx2(dst, src, n, fg, bg, flags);
+ return;
+ }
+#endif
+ int k = 0;
+#if defined(__aarch64__)
+ uint32x4_t fg4 = vdupq_n_u32(fg);
+ uint32x4_t bg_flags = vcombine_u32(vcreate_u32(((uint64_t)flags << 32) | bg),
+ vcreate_u32(((uint64_t)flags << 32) | bg));
+ for (; k + 8 <= n; k += 8) {
+ uint16x8_t cp16 = vmovl_u8(vld1_u8((const uint8_t *)src + k));
+ uint32x4_t cp_lo = vmovl_u16(vget_low_u16(cp16));
+ uint32x4_t cp_hi = vmovl_u16(vget_high_u16(cp16));
+
+ uint32x4_t c_fg_01 = vzip1q_u32(cp_lo, fg4);
+ uint32x4_t c_fg_23 = vzip2q_u32(cp_lo, fg4);
+ uint32x4_t c_fg_45 = vzip1q_u32(cp_hi, fg4);
+ uint32x4_t c_fg_67 = vzip2q_u32(cp_hi, fg4);
+
+ uint32x4_t cell0 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_01, (uint64x2_t)bg_flags);
+ uint32x4_t cell1 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_01, (uint64x2_t)bg_flags);
+ uint32x4_t cell2 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_23, (uint64x2_t)bg_flags);
+ uint32x4_t cell3 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_23, (uint64x2_t)bg_flags);
+ uint32x4_t cell4 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_45, (uint64x2_t)bg_flags);
+ uint32x4_t cell5 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_45, (uint64x2_t)bg_flags);
+ uint32x4_t cell6 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_67, (uint64x2_t)bg_flags);
+ uint32x4_t cell7 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_67, (uint64x2_t)bg_flags);
+
+ uint32x4x4_t q0 = {{ cell0, cell1, cell2, cell3 }};
+ uint32x4x4_t q1 = {{ cell4, cell5, cell6, cell7 }};
+ vst1q_u32_x4((uint32_t *)(void *)(dst + k + 0), q0);
+ vst1q_u32_x4((uint32_t *)(void *)(dst + k + 4), q1);
+ }
+ for (; k + 4 <= n; k += 4) {
+ uint32_t bytes;
+ memcpy(&bytes, src + k, sizeof(bytes));
+ uint16x4_t cp16 = vget_low_u16(vmovl_u8(vreinterpret_u8_u32(vdup_n_u32(bytes))));
+ uint32x4_t cp_lo = vmovl_u16(cp16);
+ uint32x4_t c_fg_01 = vzip1q_u32(cp_lo, fg4);
+ uint32x4_t c_fg_23 = vzip2q_u32(cp_lo, fg4);
+ uint32x4_t cell0 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_01, (uint64x2_t)bg_flags);
+ uint32x4_t cell1 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_01, (uint64x2_t)bg_flags);
+ uint32x4_t cell2 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_23, (uint64x2_t)bg_flags);
+ uint32x4_t cell3 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_23, (uint64x2_t)bg_flags);
+ uint32x4x4_t q0 = {{ cell0, cell1, cell2, cell3 }};
+ vst1q_u32_x4((uint32_t *)(void *)(dst + k + 0), q0);
+ }
+ if (k < n && n >= 4) {
+ uint32_t bytes;
+ memcpy(&bytes, src + n - 4, sizeof(bytes));
+ uint16x4_t cp16 = vget_low_u16(vmovl_u8(vreinterpret_u8_u32(vdup_n_u32(bytes))));
+ uint32x4_t cp_lo = vmovl_u16(cp16);
+ uint32x4_t c_fg_01 = vzip1q_u32(cp_lo, fg4);
+ uint32x4_t c_fg_23 = vzip2q_u32(cp_lo, fg4);
+ uint32x4_t cell0 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_01, (uint64x2_t)bg_flags);
+ uint32x4_t cell1 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_01, (uint64x2_t)bg_flags);
+ uint32x4_t cell2 = (uint32x4_t)vzip1q_u64((uint64x2_t)c_fg_23, (uint64x2_t)bg_flags);
+ uint32x4_t cell3 = (uint32x4_t)vzip2q_u64((uint64x2_t)c_fg_23, (uint64x2_t)bg_flags);
+ uint32x4x4_t q0 = {{ cell0, cell1, cell2, cell3 }};
+ vst1q_u32_x4((uint32_t *)(void *)(dst + n - 4), q0);
+ k = n;
+ }
+#elif defined(__x86_64__)
+ const __m128i zero = _mm_setzero_si128();
+ const __m128i fg4 = _mm_set1_epi32((int)fg);
+ const __m128i bg_flags = _mm_setr_epi32((int)bg, (int)flags, (int)bg, (int)flags);
+#define ASCII_4(o, cp32) do { \
+ __m128i c_fg_01 = _mm_unpacklo_epi32((cp32), fg4); \
+ __m128i c_fg_23 = _mm_unpackhi_epi32((cp32), fg4); \
+ _mm_storeu_si128((__m128i *)(void *)(dst + (o) + 0), _mm_unpacklo_epi64(c_fg_01, bg_flags)); \
+ _mm_storeu_si128((__m128i *)(void *)(dst + (o) + 1), _mm_unpackhi_epi64(c_fg_01, bg_flags)); \
+ _mm_storeu_si128((__m128i *)(void *)(dst + (o) + 2), _mm_unpacklo_epi64(c_fg_23, bg_flags)); \
+ _mm_storeu_si128((__m128i *)(void *)(dst + (o) + 3), _mm_unpackhi_epi64(c_fg_23, bg_flags)); \
+ } while (0)
+ for (; k + 8 <= n; k += 8) {
+ __m128i cp16 = _mm_unpacklo_epi8(_mm_loadl_epi64((const __m128i *)(const void *)(src + k)), zero);
+ ASCII_4(k, _mm_unpacklo_epi16(cp16, zero));
+ ASCII_4(k + 4, _mm_unpackhi_epi16(cp16, zero));
+ }
+#define ASCII_4_AT(o) do { \
+ uint32_t bytes; \
+ memcpy(&bytes, src + (o), sizeof(bytes)); \
+ __m128i cp16 = _mm_unpacklo_epi8(_mm_cvtsi32_si128((int)bytes), zero); \
+ ASCII_4(o, _mm_unpacklo_epi16(cp16, zero)); \
+ } while (0)
+ if (k + 4 <= n) {
+ ASCII_4_AT(k);
+ k += 4;
+ }
+ if (k < n && n >= 4) {
+ ASCII_4_AT(n - 4);
+ k = n;
+ }
+#undef ASCII_4_AT
+#undef ASCII_4
+#endif
+ for (; k < n; k++) {
+ dst[k] = (struct cell){
+ .c = (unsigned char)src[k],
+ .fg = fg,
+ .bg = bg,
+ .flags = (uint16_t)flags,
+ .pad = (uint16_t)(flags >> 16),
+ };
+ }
+}
+
+#endif
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..6eaf0d4
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,27 @@
+subdir('osc')
+
+# The x86-64 SIMD kernels pick their level at run time (see simd.h), so the
+# library needs no ISA flags. -march in c_args sets the lowest level.
+term_headless_lib = both_libraries('term_headless',
+ 'core.c',
+ 'escape.c',
+ 'fastcsi.c',
+ 'gstore.c',
+ 'hist.c',
+ 'hist_view.c',
+ 'input.c',
+ 'isa.c',
+ 'palette.c',
+ 'scan.c',
+ 'screen.c',
+ 'selection.c',
+ 'snapshot.c',
+ 'term.c',
+ 'text.c',
+ 'text_run.c',
+ term_osc_src,
+ include_directories: term_private_inc,
+ dependencies: [m_dep],
+ gnu_symbol_visibility: 'hidden',
+ install: false
+)
diff --git a/src/osc/clipboard.c b/src/osc/clipboard.c
new file mode 100644
index 0000000..a5ac59d
--- /dev/null
+++ b/src/osc/clipboard.c
@@ -0,0 +1,76 @@
+#include "osc.h"
+#include "../internal.h"
+#include <stdlib.h>
+#include <string.h>
+
+static int b64_val(char c)
+{
+ if (c >= 'A' && c <= 'Z') return c - 'A';
+ if (c >= 'a' && c <= 'z') return c - 'a' + 26;
+ if (c >= '0' && c <= '9') return c - '0' + 52;
+ if (c == '+') return 62;
+ if (c == '/') return 63;
+ return -1;
+}
+
+static bool b64_decode(const char *s, size_t n, char *out, size_t *out_len)
+{
+ size_t pad = 0;
+ while (pad < n && s[n - pad - 1] == '=')
+ pad++;
+ n -= pad;
+ if (pad > 2 || (pad && n % 4 + pad != 4))
+ return false;
+ size_t rem = n % 4;
+ if (rem == 1)
+ return false;
+ size_t i = 0, o = 0;
+ for (; i + 4 <= n; i += 4) {
+ int a = b64_val(s[i]), b = b64_val(s[i + 1]);
+ int c = b64_val(s[i + 2]), d = b64_val(s[i + 3]);
+ if ((a | b | c | d) < 0)
+ return false;
+ out[o++] = (char)(a << 2 | b >> 4);
+ out[o++] = (char)(b << 4 | c >> 2);
+ out[o++] = (char)(c << 6 | d);
+ }
+ if (rem >= 2) {
+ int a = b64_val(s[i]), b = b64_val(s[i + 1]);
+ if ((a | b) < 0)
+ return false;
+ out[o++] = (char)(a << 2 | b >> 4);
+ if (rem == 3) {
+ int c = b64_val(s[i + 2]);
+ if (c < 0)
+ return false;
+ out[o++] = (char)(b << 4 | c >> 2);
+ }
+ }
+ *out_len = o;
+ return true;
+}
+
+void term_osc52(struct term *t, const char *params)
+{
+ const char *sep = strchr(params, ';');
+ if (!sep || sep == params)
+ return;
+ const char *pd = sep + 1;
+ if (!strcmp(pd, "?") || !t->cb.clipboard)
+ return;
+ size_t sel_len = (size_t)(sep - params);
+ char sel[64];
+ if (sel_len >= sizeof(sel))
+ return;
+ memcpy(sel, params, sel_len);
+ sel[sel_len] = '\0';
+ if (!*pd) {
+ t->cb.clipboard(t->cb.ud, sel, "", 0);
+ return;
+ }
+ size_t enc_len = strlen(pd);
+ char *dec = (char *)pd;
+ size_t dec_len;
+ if (b64_decode(pd, enc_len, dec, &dec_len))
+ t->cb.clipboard(t->cb.ud, sel, dec, dec_len);
+}
diff --git a/src/osc/dispatch.c b/src/osc/dispatch.c
new file mode 100644
index 0000000..4cf986e
--- /dev/null
+++ b/src/osc/dispatch.c
@@ -0,0 +1,227 @@
+#include "osc.h"
+#include "../internal.h"
+#include "../palette.h"
+#include "../screen.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#define OSC_HEAP_INIT 4096
+#define OSC_HEAP_KEEP (1u << 20)
+
+size_t term_osc_room(const struct term *t)
+{
+ size_t cap = t->osc_heap ? t->osc_heap_cap : sizeof(t->osc_buf);
+ return cap - 1 - (size_t)t->osc_len;
+}
+
+char *term_osc_tail(struct term *t)
+{
+ return (t->osc_heap ? t->osc_heap : t->osc_buf) + t->osc_len;
+}
+
+void term_osc_commit(struct term *t, size_t n)
+{
+ t->osc_len += (int)n;
+}
+
+char *term_osc_data(struct term *t)
+{
+ return t->osc_heap ? t->osc_heap : t->osc_buf;
+}
+
+bool term_osc_grow(struct term *t)
+{
+ if (!t->osc_heap) {
+ char *p = malloc(OSC_HEAP_INIT);
+ if (!p) return false;
+ memcpy(p, t->osc_buf, (size_t)t->osc_len);
+ t->osc_heap = p;
+ t->osc_heap_cap = OSC_HEAP_INIT;
+ return true;
+ }
+ if (t->osc_heap_cap >= TERM_OSC_MAX) return false;
+ size_t cap = t->osc_heap_cap * 2;
+ if (cap > TERM_OSC_MAX) cap = TERM_OSC_MAX;
+ char *p = realloc(t->osc_heap, cap);
+ if (!p) return false;
+ t->osc_heap = p;
+ t->osc_heap_cap = cap;
+ return true;
+}
+
+void term_osc_cleanup(struct term *t)
+{
+ free(t->osc_heap);
+ t->osc_heap = NULL;
+ t->osc_heap_cap = 0;
+ free(t->dcs_buf);
+ t->dcs_buf = NULL;
+ t->dcs_cap = 0;
+ term_osc8_cleanup(t);
+ free(t->cwd);
+ t->cwd = NULL;
+ free(t->pointer_shape);
+ t->pointer_shape = NULL;
+}
+
+/* Reply to a color query. An OSC 4 reply names the index it answers for;
+ * index is negative for OSC 10, 11, and 12. */
+static void send_color_query_reply(struct term *t, int code, int index, uint32_t argb)
+{
+ char resp[64], prefix[16] = "";
+ uint8_t r = (argb >> 16) & 0xff, g = (argb >> 8) & 0xff, b = argb & 0xff;
+ if (index >= 0)
+ snprintf(prefix, sizeof(prefix), "%d;", index);
+ snprintf(resp, sizeof(resp),
+ "\x1b]%d;%srgb:%02x%02x/%02x%02x/%02x%02x\x1b\\",
+ code, prefix, r, r, g, g, b, b);
+ send_response(t, resp);
+}
+
+static void osc_color_set_or_query(struct term *t, int code, const char *params, int slot)
+{
+ if (!strcmp(params, "?")) {
+ send_color_query_reply(t, code, -1, t->palette.colors[slot]);
+ return;
+ }
+ uint32_t argb;
+ if (term_color_parse(params, &argb))
+ term_palette_set(&t->palette, slot, argb);
+}
+
+static long osc_next_index(const char **pp)
+{
+ char *end;
+ long idx = strtol(*pp, &end, 10);
+ if (end == *pp || (*end != ';' && *end != '\0'))
+ return -1;
+ *pp = end;
+ if (**pp == ';') (*pp)++;
+ return idx;
+}
+
+static void osc4_handle(struct term *t, const char *params)
+{
+ const char *p = params;
+ while (*p) {
+ long idx = osc_next_index(&p);
+ if (idx < 0)
+ return;
+
+ const char *spec = p;
+ const char *spec_end = spec;
+ while (*spec_end && *spec_end != ';')
+ spec_end++;
+ size_t spec_len = (size_t)(spec_end - spec);
+ if (spec_len == 1 && spec[0] == '?') {
+ if (idx <= 255)
+ send_color_query_reply(t, 4, (int)idx, t->palette.colors[idx]);
+ } else {
+ char buf[128];
+ if (idx <= 255 && spec_len < sizeof(buf)) {
+ memcpy(buf, spec, spec_len);
+ buf[spec_len] = '\0';
+ uint32_t argb;
+ if (term_color_parse(buf, &argb))
+ term_palette_set(&t->palette, (int)idx, argb);
+ }
+ }
+ p = spec_end;
+ if (*p == ';') p++;
+ }
+}
+
+static void osc104_handle(struct term *t, const char *params)
+{
+ if (!*params) {
+ for (int i = 0; i < 256; i++)
+ term_palette_reset(&t->palette, i);
+ return;
+ }
+ const char *p = params;
+ while (*p) {
+ long idx = osc_next_index(&p);
+ if (idx < 0)
+ return;
+ if (idx <= 255)
+ term_palette_reset(&t->palette, (int)idx);
+ }
+}
+
+void term_osc_finish(struct term *t)
+{
+ if (!term_str_was_truncated(t)) {
+ char *buf = term_osc_data(t);
+ buf[t->osc_len] = '\0';
+
+ if (!t->alternate && t->osc_len >= 5 && !strncmp(buf, "133;", 4) &&
+ (buf[5] == ';' || buf[5] == '\0')) {
+ if (buf[4] == 'A') {
+ t->prompt_y = t->cursor_y;
+ t->has_prompt = true;
+ }
+ if (buf[4] == 'C' || buf[4] == 'D') {
+ t->prompt_y = -1;
+ t->has_prompt = false;
+ }
+ }
+
+ char *rest;
+ long code = strtol(buf, &rest, 10);
+ bool no_code = rest == buf;
+ bool legacy_title = no_code && !strchr(buf, ';');
+ if (no_code) {
+ code = legacy_title || *buf == ';' ? 0 : -1;
+ rest = buf;
+ }
+ if (code >= 0 && (legacy_title || *rest == ';' || *rest == '\0' || code <= 2)) {
+ const char *params = *rest == ';' ? rest + 1 : rest;
+ switch (code) {
+ case 0: case 1: case 2:
+ if (t->cb.title)
+ t->cb.title(t->cb.ud, params);
+ break;
+ case 4:
+ osc4_handle(t, params);
+ break;
+ case 8:
+ term_osc8(t, params);
+ break;
+ case 10:
+ osc_color_set_or_query(t, 10, params, TERM_PALETTE_FG);
+ break;
+ case 11:
+ osc_color_set_or_query(t, 11, params, TERM_PALETTE_BG);
+ break;
+ case 12:
+ osc_color_set_or_query(t, 12, params, TERM_PALETTE_CURSOR);
+ break;
+ case 52:
+ term_osc52(t, params);
+ break;
+ case 104:
+ osc104_handle(t, params);
+ break;
+ case 110:
+ term_palette_reset(&t->palette, TERM_PALETTE_FG);
+ break;
+ case 111:
+ term_palette_reset(&t->palette, TERM_PALETTE_BG);
+ break;
+ case 112:
+ term_palette_reset(&t->palette, TERM_PALETTE_CURSOR);
+ break;
+ default:
+ term_osc_misc_dispatch(t, code, params);
+ break;
+ }
+ }
+ }
+ t->osc_len = 0;
+ if (t->osc_heap && t->osc_heap_cap > OSC_HEAP_KEEP) {
+ free(t->osc_heap);
+ t->osc_heap = NULL;
+ t->osc_heap_cap = 0;
+ }
+}
diff --git a/src/osc/link.c b/src/osc/link.c
new file mode 100644
index 0000000..f31a750
--- /dev/null
+++ b/src/osc/link.c
@@ -0,0 +1,260 @@
+#include "osc.h"
+#include "../internal.h"
+#include "../screen.h"
+#include <stdlib.h>
+#include <string.h>
+
+#define TERM_LINK_MAX 8192
+#define TERM_LINK_URI_MAX 4096
+
+struct link_hash_slot {
+ uint64_t hash;
+ uint16_t id;
+};
+
+struct term_links {
+ char **uri;
+ uint16_t *free_ids;
+ struct link_hash_slot *hash_table;
+ uint32_t hash_cap;
+ uint16_t capacity;
+ uint16_t free_count;
+ uint16_t next;
+};
+
+static inline uint64_t hash_uri(const char *s)
+{
+ uint64_t h = HASH64_BASIS;
+ while (*s)
+ h = hash64_step(h, (uint8_t)*s++);
+ h = hash64_finish(h);
+ return h ? h : 1;
+}
+
+static void links_insert_hash(struct term_links *l, uint64_t h, uint16_t id)
+{
+ if (!l->hash_table || !l->hash_cap)
+ return;
+ uint32_t mask = l->hash_cap - 1;
+ uint32_t idx = (uint32_t)h & mask;
+ for (uint32_t probe = 0; probe <= mask; probe++) {
+ struct link_hash_slot *slot = &l->hash_table[idx];
+ if (slot->hash == 0) {
+ slot->hash = h;
+ slot->id = id;
+ return;
+ }
+ idx = (idx + 1) & mask;
+ }
+}
+
+static bool links_grow(struct term_links *l)
+{
+ uint16_t new_cap = l->capacity == 0 ? 64 : l->capacity < 4096 ? l->capacity * 2 : TERM_LINK_MAX;
+ if (new_cap <= l->capacity)
+ return false;
+ char **new_uri = realloc(l->uri, sizeof(char *) * new_cap);
+ if (!new_uri) return false;
+ uint16_t *new_free = malloc(sizeof(uint16_t) * new_cap);
+ if (!new_free) {
+ l->uri = new_uri;
+ return false;
+ }
+ uint32_t new_hash_cap = (uint32_t)new_cap * 2;
+ struct link_hash_slot *new_hash = calloc(new_hash_cap, sizeof(*new_hash));
+ if (!new_hash) {
+ free(new_free);
+ l->uri = new_uri;
+ return false;
+ }
+ uint16_t *old_free = l->free_ids;
+ l->free_ids = new_free;
+ if (l->capacity)
+ memcpy(new_free, old_free, sizeof(uint16_t) * l->capacity);
+ free(old_free);
+ memset(new_uri + l->capacity, 0, sizeof(char *) * (new_cap - l->capacity));
+ l->uri = new_uri;
+ l->capacity = new_cap;
+
+ free(l->hash_table);
+ l->hash_table = new_hash;
+ l->hash_cap = new_hash_cap;
+
+ for (int i = 0; i + 1 < l->next; i++) {
+ if (l->uri[i])
+ links_insert_hash(l, hash_uri(l->uri[i]), (uint16_t)(i + 1));
+ }
+ return true;
+}
+
+static void links_mark_row(const struct cell *row, int len, uint64_t *mark, uint16_t cap)
+{
+ if (len > TERM_MAX_COLS) len = TERM_MAX_COLS;
+ for (int i = 0; i < len; i++) {
+ uint16_t id = row[i].pad;
+ if (id && id <= cap)
+ mark[(id - 1) >> 6] |= 1ull << ((id - 1) & 63);
+ }
+}
+
+static void links_mark_id(uint16_t id, uint64_t *mark, uint16_t cap)
+{
+ if (id && id <= cap)
+ mark[(id - 1) >> 6] |= 1ull << ((id - 1) & 63);
+}
+
+struct links_mark {
+ uint64_t *mark;
+ uint16_t cap;
+};
+
+static void links_mark_hist(void *ud, uint16_t id)
+{
+ struct links_mark *m = ud;
+ links_mark_id(id, m->mark, m->cap);
+}
+
+static void links_mark_screen(const struct screen *s, struct links_mark *m)
+{
+ for (int y = 0; y < s->rows; y++) {
+ const struct srow *row = screen_get_row(s, y);
+ if (row->len == 0)
+ continue;
+ if (row->kind == ROW_CELLS)
+ links_mark_row(row->cells, row->len, m->mark, m->cap);
+ else
+ term_hist_text_mark_links(&row->text, links_mark_hist, m);
+ }
+}
+
+static void links_gc(struct term *t, struct term_links *l)
+{
+ uint64_t mark[TERM_LINK_MAX / 64] = { 0 };
+ struct links_mark m = { mark, l->capacity };
+ links_mark_id(t->cur_link, mark, l->capacity);
+ links_mark_screen(&t->scr, &m);
+ if (t->alternate && t->alt_allocated)
+ links_mark_screen(&t->alt_scr, &m);
+ term_hist_mark_links(&t->hist, links_mark_hist, &m);
+ l->free_count = 0;
+ for (int i = 0; i + 1 < l->next; i++) {
+ if (l->uri[i] && !(mark[i >> 6] & (1ull << (i & 63)))) {
+ free(l->uri[i]);
+ l->uri[i] = NULL;
+ }
+ if (!l->uri[i])
+ l->free_ids[l->free_count++] = (uint16_t)(i + 1);
+ }
+ if (l->hash_table && l->hash_cap) {
+ memset(l->hash_table, 0, sizeof(*l->hash_table) * l->hash_cap);
+ for (int i = 0; i + 1 < l->next; i++) {
+ if (l->uri[i])
+ links_insert_hash(l, hash_uri(l->uri[i]), (uint16_t)(i + 1));
+ }
+ }
+}
+
+static uint16_t links_find(const struct term_links *l, const char *uri, uint64_t h)
+{
+ if (!l->hash_table || !l->hash_cap)
+ return 0;
+ uint32_t mask = l->hash_cap - 1;
+ uint32_t idx = (uint32_t)h & mask;
+ for (uint32_t probe = 0; probe <= mask; probe++) {
+ const struct link_hash_slot *slot = &l->hash_table[idx];
+ if (slot->hash == 0)
+ return 0;
+ if (slot->hash == h) {
+ uint16_t id = slot->id;
+ if (id && id <= l->capacity && l->uri[id - 1] && !strcmp(l->uri[id - 1], uri))
+ return id;
+ }
+ idx = (idx + 1) & mask;
+ }
+ return 0;
+}
+
+static uint16_t links_alloc(struct term *t, struct term_links *l)
+{
+ if (l->free_count)
+ return l->free_ids[--l->free_count];
+ if (l->next <= l->capacity || links_grow(l))
+ return l->next++;
+ links_gc(t, l);
+ if (l->free_count)
+ return l->free_ids[--l->free_count];
+ return 0;
+}
+
+void term_osc8(struct term *t, const char *params)
+{
+ const char *sep = strchr(params, ';');
+ if (!sep)
+ return;
+ const char *uri = sep + 1;
+ if (!*uri) {
+ t->cur_link = 0;
+ return;
+ }
+ if (strlen(uri) > TERM_LINK_URI_MAX) {
+ t->cur_link = 0;
+ return;
+ }
+ struct term_links *l = t->links;
+ if (!l) {
+ l = calloc(1, sizeof(*l));
+ if (!l) {
+ t->cur_link = 0;
+ return;
+ }
+ l->next = 1;
+ t->links = l;
+ }
+ uint64_t h = hash_uri(uri);
+ uint16_t id = links_find(l, uri, h);
+ if (!id) {
+ id = links_alloc(t, l);
+ if (!id) {
+ t->cur_link = 0;
+ return;
+ }
+ l->uri[id - 1] = term_strdup(uri);
+ if (!l->uri[id - 1]) {
+ l->free_ids[l->free_count++] = id;
+ t->cur_link = 0;
+ return;
+ }
+ links_insert_hash(l, h, id);
+ }
+ t->cur_link = id;
+}
+
+const char *term_osc8_url_at(const struct term *t, int col, int row)
+{
+ const struct term_links *l = t->links;
+ if (!l || col < 0 || col >= t->cols)
+ return NULL;
+ const struct cell *cells = term_screen_view_row(t, row);
+ if (!cells)
+ return NULL;
+ uint16_t id = cells[col].pad;
+ if (!id || id > l->capacity ||
+ !(cells[col].flags & CELL_LINK))
+ return NULL;
+ return l->uri[id - 1];
+}
+
+void term_osc8_cleanup(struct term *t)
+{
+ struct term_links *l = t->links;
+ if (!l)
+ return;
+ for (uint16_t i = 0; i < l->capacity; i++)
+ free(l->uri[i]);
+ free(l->uri);
+ free(l->free_ids);
+ free(l->hash_table);
+ free(l);
+ t->links = NULL;
+ t->cur_link = 0;
+}
diff --git a/src/osc/meson.build b/src/osc/meson.build
new file mode 100644
index 0000000..a3b9efa
--- /dev/null
+++ b/src/osc/meson.build
@@ -0,0 +1,6 @@
+term_osc_src = files(
+ 'clipboard.c',
+ 'dispatch.c',
+ 'link.c',
+ 'misc.c',
+)
diff --git a/src/osc/misc.c b/src/osc/misc.c
new file mode 100644
index 0000000..5d6b506
--- /dev/null
+++ b/src/osc/misc.c
@@ -0,0 +1,69 @@
+#include "osc.h"
+#include "../internal.h"
+#include <stdlib.h>
+#include <string.h>
+
+static void osc7_cwd(struct term *t, const char *uri)
+{
+ if (!term_has_prefix_ci(uri, "file://"))
+ return;
+ const char *p = uri + 7;
+ const char *slash = strchr(p, '/');
+ const char *path = slash ? slash : p;
+ char *cwd = malloc(strlen(path) + 1);
+ if (!cwd)
+ return;
+ char *d = cwd;
+ while (*path) {
+ int hi, lo;
+ if (*path == '%' && (hi = term_hex_nib(path[1])) >= 0 &&
+ (lo = term_hex_nib(path[2])) >= 0) {
+ *d++ = (char)(hi * 16 + lo);
+ path += 3;
+ } else {
+ *d++ = *path++;
+ }
+ }
+ *d = '\0';
+ free(t->cwd);
+ t->cwd = cwd;
+}
+
+static void osc777_notify(struct term *t, const char *params)
+{
+ if (strncmp(params, "notify;", 7))
+ return;
+ const char *title = params + 7;
+ const char *sep = strchr(title, ';');
+ if (!sep)
+ return;
+ char *wsep = (char *)sep;
+ *wsep = '\0';
+ t->cb.notify(t->cb.ud, title, sep + 1);
+ *wsep = ';';
+}
+
+void term_osc_misc_dispatch(struct term *t, long code, const char *params)
+{
+ if (!params)
+ return;
+ switch (code) {
+ case 7:
+ osc7_cwd(t, params);
+ break;
+ case 9:
+ if (t->cb.notify && strncmp(params, "4;", 2))
+ t->cb.notify(t->cb.ud, "wsap_term", params);
+ break;
+ case 22:
+ free(t->pointer_shape);
+ t->pointer_shape = *params ? term_strdup(params) : NULL;
+ break;
+ case 777:
+ if (t->cb.notify)
+ osc777_notify(t, params);
+ break;
+ default:
+ break;
+ }
+}
diff --git a/src/osc/osc.h b/src/osc/osc.h
new file mode 100644
index 0000000..1398534
--- /dev/null
+++ b/src/osc/osc.h
@@ -0,0 +1,30 @@
+#ifndef TERM_OSC_H
+#define TERM_OSC_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+struct term;
+
+#define TERM_OSC_MAX (16u << 20)
+
+size_t term_osc_room(const struct term *t);
+char *term_osc_tail(struct term *t);
+void term_osc_commit(struct term *t, size_t n);
+bool term_osc_grow(struct term *t);
+
+char *term_osc_data(struct term *t);
+
+void term_osc_finish(struct term *t);
+
+void term_osc_cleanup(struct term *t);
+
+void term_osc8(struct term *t, const char *params);
+void term_osc52(struct term *t, const char *params);
+void term_osc_misc_dispatch(struct term *t, long code, const char *params);
+
+const char *term_osc8_url_at(const struct term *t, int col, int row);
+void term_osc8_cleanup(struct term *t);
+
+#endif
diff --git a/src/palette.c b/src/palette.c
new file mode 100644
index 0000000..1b27e3a
--- /dev/null
+++ b/src/palette.c
@@ -0,0 +1,492 @@
+#include "palette.h"
+#include "internal.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+/* Linear light for each byte value of an sRGB channel. The entries are the
+ * transfer curve, evaluated once and stored, so every host reads the same ones. */
+static const float srgb_linear[256] = {
+ 0x0p+0f, 0x1.3e4568p-12f, 0x1.3e4568p-11f, 0x1.dd681cp-11f,
+ 0x1.3e4568p-10f, 0x1.8dd6c2p-10f, 0x1.dd681cp-10f, 0x1.167cbap-9f,
+ 0x1.3e4568p-9f, 0x1.660e16p-9f, 0x1.8dd6c2p-9f, 0x1.b6a31ap-9f,
+ 0x1.e1e31ap-9f, 0x1.07c38cp-8f, 0x1.1fcc2cp-8f, 0x1.390ffap-8f,
+ 0x1.53936ep-8f, 0x1.6f5adep-8f, 0x1.8c6a92p-8f, 0x1.aac6c2p-8f,
+ 0x1.ca7382p-8f, 0x1.eb74ep-8f, 0x1.06e76ap-7f, 0x1.18c2a4p-7f,
+ 0x1.2b4e06p-7f, 0x1.3e8b7cp-7f, 0x1.527cd6p-7f, 0x1.6723eep-7f,
+ 0x1.7c8292p-7f, 0x1.929a86p-7f, 0x1.a96d8ep-7f, 0x1.c0fd62p-7f,
+ 0x1.d94bbep-7f, 0x1.f25a44p-7f, 0x1.06155p-6f, 0x1.135f3ep-6f,
+ 0x1.210bb6p-6f, 0x1.2f1b8ap-6f, 0x1.3d8f84p-6f, 0x1.4c6866p-6f,
+ 0x1.5ba6fap-6f, 0x1.6b4c02p-6f, 0x1.7b584p-6f, 0x1.8bcc72p-6f,
+ 0x1.9ca956p-6f, 0x1.adefaap-6f, 0x1.bfa02p-6f, 0x1.d1bb72p-6f,
+ 0x1.e44258p-6f, 0x1.f73582p-6f, 0x1.054ad2p-5f, 0x1.0f31b8p-5f,
+ 0x1.194fccp-5f, 0x1.23a55ep-5f, 0x1.2e32c6p-5f, 0x1.38f85cp-5f,
+ 0x1.43f678p-5f, 0x1.4f2d64p-5f, 0x1.5a9d76p-5f, 0x1.6647p-5f,
+ 0x1.722a56p-5f, 0x1.7e47c6p-5f, 0x1.8a9fa2p-5f, 0x1.973238p-5f,
+ 0x1.a3ffdep-5f, 0x1.b108d4p-5f, 0x1.be4d6ep-5f, 0x1.cbcdfcp-5f,
+ 0x1.d98ac8p-5f, 0x1.e7841ep-5f, 0x1.f5ba52p-5f, 0x1.0216cep-4f,
+ 0x1.096f2ap-4f, 0x1.10e66p-4f, 0x1.187c94p-4f, 0x1.2031ecp-4f,
+ 0x1.28068ap-4f, 0x1.2ffa94p-4f, 0x1.380e2cp-4f, 0x1.404176p-4f,
+ 0x1.489496p-4f, 0x1.5107aep-4f, 0x1.599aep-4f, 0x1.624e5p-4f,
+ 0x1.6b2224p-4f, 0x1.741676p-4f, 0x1.7d2b6ap-4f, 0x1.866124p-4f,
+ 0x1.8fb7c4p-4f, 0x1.992f6cp-4f, 0x1.a2c83ep-4f, 0x1.ac8258p-4f,
+ 0x1.b65ddep-4f, 0x1.c05aeep-4f, 0x1.ca79aap-4f, 0x1.d4ba32p-4f,
+ 0x1.df1ca4p-4f, 0x1.e9a122p-4f, 0x1.f447dp-4f, 0x1.ff10c2p-4f,
+ 0x1.04fe0ep-3f, 0x1.0a85p-3f, 0x1.101d46p-3f, 0x1.15c6fp-3f,
+ 0x1.1b820ap-3f, 0x1.214ea8p-3f, 0x1.272cd6p-3f, 0x1.2d1ca4p-3f,
+ 0x1.331e2p-3f, 0x1.39315ap-3f, 0x1.3f566p-3f, 0x1.458d46p-3f,
+ 0x1.4bd612p-3f, 0x1.5230d8p-3f, 0x1.589dap-3f, 0x1.5f1c82p-3f,
+ 0x1.65ad88p-3f, 0x1.6c50c2p-3f, 0x1.73063cp-3f, 0x1.79ce06p-3f,
+ 0x1.80a82cp-3f, 0x1.8794bep-3f, 0x1.8e93c8p-3f, 0x1.95a55cp-3f,
+ 0x1.9cc984p-3f, 0x1.a4004ep-3f, 0x1.ab49ccp-3f, 0x1.b2a606p-3f,
+ 0x1.ba1516p-3f, 0x1.c196f8p-3f, 0x1.c92bcp-3f, 0x1.d0d37ep-3f,
+ 0x1.d88e4p-3f, 0x1.e05c18p-3f, 0x1.e83d08p-3f, 0x1.f0312p-3f,
+ 0x1.f8387p-3f, 0x1.002984p-2f, 0x1.044078p-2f, 0x1.08611ap-2f,
+ 0x1.0c8b74p-2f, 0x1.10bf8ap-2f, 0x1.14fd64p-2f, 0x1.194506p-2f,
+ 0x1.1d967ap-2f, 0x1.21f1c2p-2f, 0x1.2656e8p-2f, 0x1.2ac5fp-2f,
+ 0x1.2f3ee2p-2f, 0x1.33c1c4p-2f, 0x1.384e9cp-2f, 0x1.3ce56ep-2f,
+ 0x1.418644p-2f, 0x1.463124p-2f, 0x1.4ae61p-2f, 0x1.4fa512p-2f,
+ 0x1.546e3p-2f, 0x1.59416ep-2f, 0x1.5e1ed2p-2f, 0x1.630666p-2f,
+ 0x1.67f83p-2f, 0x1.6cf43p-2f, 0x1.71fa6ep-2f, 0x1.770af2p-2f,
+ 0x1.7c25c2p-2f, 0x1.814ae2p-2f, 0x1.867a5ap-2f, 0x1.8bb42ep-2f,
+ 0x1.90f866p-2f, 0x1.964706p-2f, 0x1.9ba016p-2f, 0x1.a1039ap-2f,
+ 0x1.a67198p-2f, 0x1.abea16p-2f, 0x1.b16d1ap-2f, 0x1.b6faa8p-2f,
+ 0x1.bc92cap-2f, 0x1.c23582p-2f, 0x1.c7e2d6p-2f, 0x1.cd9acep-2f,
+ 0x1.d35d6cp-2f, 0x1.d92abap-2f, 0x1.df02bap-2f, 0x1.e4e574p-2f,
+ 0x1.ead2ecp-2f, 0x1.f0cb28p-2f, 0x1.f6ce2ep-2f, 0x1.fcdc04p-2f,
+ 0x1.017a5ap-1f, 0x1.048c1cp-1f, 0x1.07a34ep-1f, 0x1.0abff2p-1f,
+ 0x1.0de20cp-1f, 0x1.11099ep-1f, 0x1.1436acp-1f, 0x1.176936p-1f,
+ 0x1.1aa14p-1f, 0x1.1ddecep-1f, 0x1.2121e2p-1f, 0x1.246a7cp-1f,
+ 0x1.27b8a2p-1f, 0x1.2b0c56p-1f, 0x1.2e659ap-1f, 0x1.31c472p-1f,
+ 0x1.3528dep-1f, 0x1.3892e2p-1f, 0x1.3c0282p-1f, 0x1.3f77bep-1f,
+ 0x1.42f29cp-1f, 0x1.46731cp-1f, 0x1.49f94p-1f, 0x1.4d850cp-1f,
+ 0x1.511682p-1f, 0x1.54ada6p-1f, 0x1.584a7ap-1f, 0x1.5bedp-1f,
+ 0x1.5f953cp-1f, 0x1.63432ep-1f, 0x1.66f6d8p-1f, 0x1.6ab04p-1f,
+ 0x1.6e6f66p-1f, 0x1.72344cp-1f, 0x1.75fef8p-1f, 0x1.79cf6ap-1f,
+ 0x1.7da5a4p-1f, 0x1.8181a8p-1f, 0x1.85637cp-1f, 0x1.894b2p-1f,
+ 0x1.8d3896p-1f, 0x1.912be2p-1f, 0x1.952504p-1f, 0x1.992402p-1f,
+ 0x1.9d28dcp-1f, 0x1.a13396p-1f, 0x1.a5443p-1f, 0x1.a95aaep-1f,
+ 0x1.ad7714p-1f, 0x1.b1996p-1f, 0x1.b5c19ap-1f, 0x1.b9efcp-1f,
+ 0x1.be23d8p-1f, 0x1.c25ddcp-1f, 0x1.c69ddep-1f, 0x1.cae3d2p-1f,
+ 0x1.cf2fc6p-1f, 0x1.d381acp-1f, 0x1.d7d99ap-1f, 0x1.dc377ep-1f,
+ 0x1.e09b7p-1f, 0x1.e5055ep-1f, 0x1.e9755cp-1f, 0x1.edeb5cp-1f,
+ 0x1.f26772p-1f, 0x1.f6e98cp-1f, 0x1.fb71cp-1f, 0x1p+0f,
+};
+
+static inline float linf_to_srgbf(float c)
+{
+ return c > 0.0031308f ? 1.055f * powf(c, 1.0f / 2.4f) - 0.055f
+ : 12.92f * c;
+}
+
+static const struct term_theme builtin_theme = {
+ .fg = 0xFFFFFFFFu,
+ .bg = 0xFF1E1E2Eu,
+ .cursor = 0xFF89B4FAu,
+ .ansi = {
+ 0xFF585B70, 0xFFCC6666, 0xFFB5BD68, 0xFFF0C674,
+ 0xFF81A2BE, 0xFFB294BB, 0xFF8ABEB7, 0xFFC5C8C6,
+ 0xFF666666, 0xFFD54E53, 0xFFB9CA4A, 0xFFE7C547,
+ 0xFF7AA6DA, 0xFFC397D8, 0xFF70C0B1, 0xFFEAEAEA,
+ },
+};
+
+void term_theme_default(struct term_theme *theme)
+{
+ *theme = builtin_theme;
+}
+
+static const struct term_theme high_contrast_theme = {
+ .fg = 0xFFFFFFFFu,
+ .bg = 0xFF000000u,
+ .cursor = 0xFF00FFFFu,
+ .ansi = {
+ 0xFF000000, 0xFFFF0000, 0xFF00FF00, 0xFFFFFF00,
+ 0xFF0000FF, 0xFFFF00FF, 0xFF00FFFF, 0xFFFFFFFF,
+ 0xFF808080, 0xFFFF5555, 0xFF55FF55, 0xFFFFFF55,
+ 0xFF5555FF, 0xFFFF55FF, 0xFF55FFFF, 0xFFFFFFFF,
+ },
+};
+
+void term_theme_high_contrast(struct term_theme *theme)
+{
+ *theme = high_contrast_theme;
+}
+
+/* Finds the slot for key and the default it resets to. Keys are
+ * foreground, background, and color0 to color15. */
+static bool palette_slot(struct term_theme *p, const struct term_theme *def,
+ const char *key, uint32_t **slot,
+ const uint32_t **fallback)
+{
+ if (!strcmp(key, "foreground")) {
+ *slot = &p->fg;
+ *fallback = &def->fg;
+ return true;
+ }
+ if (!strcmp(key, "background")) {
+ *slot = &p->bg;
+ *fallback = &def->bg;
+ return true;
+ }
+ if (strncmp(key, "color", 5))
+ return false;
+ const char *digits = key + 5;
+ if (!*digits || (*digits == '0' && digits[1]))
+ return false;
+ int i = 0;
+ for (const char *d = digits; *d; d++) {
+ if (*d < '0' || *d > '9')
+ return false;
+ i = i * 10 + (*d - '0');
+ if (i > 15)
+ return false;
+ }
+ *slot = &p->ansi[i];
+ *fallback = &def->ansi[i];
+ return true;
+}
+
+bool term_theme_set_key(struct term_theme *p, const char *key, const char *val)
+{
+ struct term_theme def;
+ term_theme_default(&def);
+ uint32_t *slot;
+ const uint32_t *fallback;
+ if (!palette_slot(p, &def, key, &slot, &fallback))
+ return false;
+ if (!val[0]) {
+ *slot = *fallback;
+ return true;
+ }
+ uint32_t v;
+ if (!term_color_parse(val, &v))
+ return false;
+ *slot = v;
+ return true;
+}
+
+static bool parse_hex_channel(const char *s, size_t len, uint8_t *out)
+{
+ if (len == 0 || len > 4)
+ return false;
+ unsigned v = 0;
+ for (size_t i = 0; i < len; i++) {
+ int d = term_hex_nib(s[i]);
+ if (d < 0)
+ return false;
+ v = (v << 4) | (unsigned)d;
+ }
+ static const unsigned max_for_len[5] = {0, 15, 255, 4095, 65535};
+ unsigned max = max_for_len[len];
+ *out = (uint8_t)((unsigned long)v * 255 / max);
+ return true;
+}
+
+static bool parse_fraction(const char *s, size_t len, uint8_t *out)
+{
+
+ if (len == 0)
+ return false;
+ bool seen_digit = false;
+ bool seen_dot = false;
+ unsigned integer = 0;
+ unsigned frac = 0;
+ unsigned frac_digits = 0;
+ size_t i = 0;
+ if (s[0] == '.') {
+ seen_dot = true;
+ i = 1;
+ }
+ for (; i < len; i++) {
+ char c = s[i];
+ if (c >= '0' && c <= '9') {
+ seen_digit = true;
+ if (!seen_dot) {
+ integer = integer * 10 + (unsigned)(c - '0');
+ if (integer > 1)
+ return false;
+ } else {
+ if (frac_digits < 9) {
+ frac = frac * 10 + (unsigned)(c - '0');
+ frac_digits++;
+ }
+ }
+ } else if (c == '.' && !seen_dot) {
+ seen_dot = true;
+ } else {
+ return false;
+ }
+ }
+ if (!seen_digit)
+ return false;
+ double value = (double)integer;
+ if (frac_digits > 0) {
+ double f = (double)frac;
+ for (unsigned k = 0; k < frac_digits; k++)
+ f /= 10.0;
+ value += f;
+ }
+ if (value > 1.0) value = 1.0;
+ *out = (uint8_t)(value * 255.0 + 0.5);
+ return true;
+}
+
+static const char *skip_leading_ws(const char *s)
+{
+ while (*s == ' ' || *s == '\t')
+ s++;
+ return s;
+}
+
+static size_t rtrimmed_len(const char *s)
+{
+ size_t n = strlen(s);
+ while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t'))
+ n--;
+ return n;
+}
+
+bool term_color_parse(const char *input, uint32_t *out)
+{
+ if (!input || !out)
+ return false;
+ const char *s = skip_leading_ws(input);
+ size_t len = rtrimmed_len(s);
+ if (len == 0)
+ return false;
+
+ if (s[0] == '#') {
+ const char *body = s + 1;
+ size_t blen = len - 1;
+ size_t cw = blen / 3;
+ if (blen != 3 && blen != 6 && blen != 9 && blen != 12)
+ return false;
+ uint8_t r, g, b;
+ if (!parse_hex_channel(body, cw, &r) ||
+ !parse_hex_channel(body + cw, cw, &g) ||
+ !parse_hex_channel(body + 2 * cw, cw, &b))
+ return false;
+ *out = term_rgb(r, g, b);
+ return true;
+ }
+
+ if (len >= 4 && (s[0] == 'r' || s[0] == 'R') &&
+ (s[1] == 'g' || s[1] == 'G') && (s[2] == 'b' || s[2] == 'B')) {
+ size_t i = 3;
+ bool intensity = false;
+ if ((s[i] == 'i' || s[i] == 'I')) {
+ intensity = true;
+ i++;
+ }
+ if (i >= len || s[i] != ':')
+ return false;
+ i++;
+
+ const char *channels[3];
+ size_t chlens[3];
+ int ci = 0;
+ size_t start = i;
+ for (;; i++) {
+ if (i == len || s[i] == '/') {
+ if (ci >= 3)
+ return false;
+ channels[ci] = s + start;
+ chlens[ci] = i - start;
+ ci++;
+ if (i == len)
+ break;
+ start = i + 1;
+ }
+ }
+ if (ci != 3)
+ return false;
+ uint8_t r, g, b;
+ if (intensity) {
+ if (!parse_fraction(channels[0], chlens[0], &r) ||
+ !parse_fraction(channels[1], chlens[1], &g) ||
+ !parse_fraction(channels[2], chlens[2], &b))
+ return false;
+ } else {
+ if (!parse_hex_channel(channels[0], chlens[0], &r) ||
+ !parse_hex_channel(channels[1], chlens[1], &g) ||
+ !parse_hex_channel(channels[2], chlens[2], &b))
+ return false;
+ }
+ *out = term_rgb(r, g, b);
+ return true;
+ }
+
+ if (len == 6 || len == 3) {
+ uint8_t r, g, b;
+ size_t cw = len / 3;
+ if (!parse_hex_channel(s, cw, &r) ||
+ !parse_hex_channel(s + cw, cw, &g) ||
+ !parse_hex_channel(s + 2 * cw, cw, &b))
+ return false;
+ *out = term_rgb(r, g, b);
+ return true;
+ }
+
+ return false;
+}
+
+struct lab { float l, a, b; };
+
+static struct lab lab_from_rgb(uint32_t argb)
+{
+ float r = srgb_linear[(argb >> 16) & 0xFF];
+ float g = srgb_linear[(argb >> 8) & 0xFF];
+ float b = srgb_linear[argb & 0xFF];
+
+ float x = (r * 0.4124564f + g * 0.3575761f + b * 0.1804375f) / 0.95047f;
+ float y = r * 0.2126729f + g * 0.7151522f + b * 0.0721750f;
+ float z = (r * 0.0193339f + g * 0.1191920f + b * 0.9503041f) / 1.08883f;
+
+ x = x > 0.008856f ? cbrtf(x) : 7.787f * x + 16.0f / 116.0f;
+ y = y > 0.008856f ? cbrtf(y) : 7.787f * y + 16.0f / 116.0f;
+ z = z > 0.008856f ? cbrtf(z) : 7.787f * z + 16.0f / 116.0f;
+
+ struct lab lab;
+ lab.l = 116.0f * y - 16.0f;
+ lab.a = 500.0f * (x - y);
+ lab.b = 200.0f * (y - z);
+ return lab;
+}
+
+static uint32_t lab_to_rgb(struct lab lab)
+{
+ float y = (lab.l + 16.0f) / 116.0f;
+ float x = lab.a / 500.0f + y;
+ float z = y - lab.b / 200.0f;
+
+ float x3 = x * x * x;
+ float y3 = y * y * y;
+ float z3 = z * z * z;
+ float xf = (x3 > 0.008856f ? x3 : (x - 16.0f / 116.0f) / 7.787f) * 0.95047f;
+ float yf = (y3 > 0.008856f ? y3 : (y - 16.0f / 116.0f) / 7.787f);
+ float zf = (z3 > 0.008856f ? z3 : (z - 16.0f / 116.0f) / 7.787f) * 1.08883f;
+
+ float r = xf * 3.2404542f - yf * 1.5371385f - zf * 0.4985314f;
+ float g = -xf * 0.9692660f + yf * 1.8760108f + zf * 0.0415560f;
+ float b = xf * 0.0556434f - yf * 0.2040259f + zf * 1.0572252f;
+
+ r = linf_to_srgbf(r);
+ g = linf_to_srgbf(g);
+ b = linf_to_srgbf(b);
+
+ if (r < 0.0f) r = 0.0f;
+ if (r > 1.0f) r = 1.0f;
+ if (g < 0.0f) g = 0.0f;
+ if (g > 1.0f) g = 1.0f;
+ if (b < 0.0f) b = 0.0f;
+ if (b > 1.0f) b = 1.0f;
+
+ uint8_t rb = (uint8_t)(r * 255.0f + 0.5f);
+ uint8_t gb = (uint8_t)(g * 255.0f + 0.5f);
+ uint8_t bb = (uint8_t)(b * 255.0f + 0.5f);
+ return term_rgb(rb, gb, bb);
+}
+
+static struct lab lab_lerp(float t, struct lab a, struct lab b)
+{
+ struct lab r;
+ r.l = a.l + t * (b.l - a.l);
+ r.a = a.a + t * (b.a - a.a);
+ r.b = a.b + t * (b.b - a.b);
+ return r;
+}
+
+/*
+ * Derive the 256-color palette from a theme. Entries 0-15 are the theme's
+ * ANSI colors. The 6x6x6 cube and the gray ramp interpolate in Lab space
+ * between the background, the ANSI colors, and the foreground, so they match
+ * the theme instead of the fixed xterm values.
+ */
+static void derive_palette(uint32_t *p, const struct term_theme *theme)
+{
+ struct lab base8[8];
+ base8[0] = lab_from_rgb(theme->bg);
+ for (int i = 1; i < 7; i++)
+ base8[i] = lab_from_rgb(theme->ansi[i]);
+ base8[7] = lab_from_rgb(theme->fg);
+
+ for (int i = 0; i < 16; i++)
+ p[i] = theme->ansi[i];
+ p[TERM_PALETTE_FG] = theme->fg;
+ p[TERM_PALETTE_BG] = theme->bg;
+ p[TERM_PALETTE_CURSOR] = theme->cursor;
+
+ bool is_light_theme = base8[7].l < base8[0].l;
+ if (is_light_theme) {
+ struct lab tmp = base8[0];
+ base8[0] = base8[7];
+ base8[7] = tmp;
+ }
+
+ int idx = 16;
+ for (int ri = 0; ri < 6; ri++) {
+ float tr = (float)ri / 5.0f;
+ struct lab c0 = lab_lerp(tr, base8[0], base8[1]);
+ struct lab c1 = lab_lerp(tr, base8[2], base8[3]);
+ struct lab c2 = lab_lerp(tr, base8[4], base8[5]);
+ struct lab c3 = lab_lerp(tr, base8[6], base8[7]);
+ for (int gi = 0; gi < 6; gi++) {
+ float tg = (float)gi / 5.0f;
+ struct lab c4 = lab_lerp(tg, c0, c1);
+ struct lab c5 = lab_lerp(tg, c2, c3);
+ for (int bi = 0; bi < 6; bi++)
+ p[idx++] = lab_to_rgb(lab_lerp((float)bi / 5.0f, c4, c5));
+ }
+ }
+
+ for (int i = 0; i < 24; i++) {
+ float t = (float)(i + 1) / 25.0f;
+ p[idx++] = lab_to_rgb(lab_lerp(t, base8[0], base8[7]));
+ }
+}
+
+static bool theme_is_zero(const struct term_theme *theme)
+{
+ static const struct term_theme zero;
+ return !memcmp(theme, &zero, sizeof(zero));
+}
+
+void term_palette_init(struct palette *p, const struct term_theme *theme)
+{
+ p->theme = theme && !theme_is_zero(theme) ? *theme : builtin_theme;
+ if (!p->theme.cursor)
+ p->theme.cursor = builtin_theme.cursor;
+ derive_palette(p->base, &p->theme);
+ memcpy(p->colors, p->base, sizeof(p->colors));
+ p->version++;
+}
+
+static bool valid_slot(int slot)
+{
+ return slot >= 0 && slot < TERM_PALETTE_SIZE;
+}
+
+void term_palette_set(struct palette *p, int slot, uint32_t argb)
+{
+ if (!valid_slot(slot) || p->colors[slot] == argb)
+ return;
+ p->colors[slot] = argb;
+ p->version++;
+}
+
+void term_palette_reset(struct palette *p, int slot)
+{
+ if (valid_slot(slot))
+ term_palette_set(p, slot, p->base[slot]);
+}
+
+void term_palette_reset_all(struct palette *p)
+{
+ if (memcmp(p->colors, p->base, sizeof(p->colors))) {
+ memcpy(p->colors, p->base, sizeof(p->colors));
+ p->version++;
+ }
+}
diff --git a/src/palette.h b/src/palette.h
new file mode 100644
index 0000000..5eef03e
--- /dev/null
+++ b/src/palette.h
@@ -0,0 +1,34 @@
+#ifndef TERM_PALETTE_H
+#define TERM_PALETTE_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "term.h"
+
+/*
+ * The colors of one terminal. base holds the colors that the theme gives;
+ * colors holds base plus the entries that the program changed with OSC 4,
+ * 10, 11, or 12. version changes whenever colors changes.
+ */
+struct palette {
+ uint32_t colors[TERM_PALETTE_SIZE];
+ uint32_t base[TERM_PALETTE_SIZE];
+ struct term_theme theme;
+ uint64_t version;
+};
+
+/* Set every entry from theme. An all-zero theme selects the built-in theme. */
+void term_palette_init(struct palette *p, const struct term_theme *theme);
+/* Change one entry (0-255, TERM_PALETTE_FG, _BG, or _CURSOR). */
+void term_palette_set(struct palette *p, int slot, uint32_t argb);
+/* Restore one entry, or all entries, to the theme color. */
+void term_palette_reset(struct palette *p, int slot);
+void term_palette_reset_all(struct palette *p);
+
+static inline uint32_t palette_resolve(const struct palette *p, term_color c)
+{
+ return term_color_is_slot(c) ? p->colors[c & 0x1FFu] : c;
+}
+
+#endif
diff --git a/src/pen.h b/src/pen.h
new file mode 100644
index 0000000..d7118b5
--- /dev/null
+++ b/src/pen.h
@@ -0,0 +1,165 @@
+#ifndef WSAP_TERM_PEN_H
+#define WSAP_TERM_PEN_H
+
+#include "term.h"
+#include <stdbool.h>
+#include <stdint.h>
+
+struct term_pen {
+ uint32_t fg;
+ uint32_t bg;
+ uint32_t ul;
+ uint8_t underline;
+ bool bold : 1;
+ bool faint : 1;
+ bool italic : 1;
+ bool strike : 1;
+ bool overline : 1;
+ bool inverse : 1;
+};
+
+/* The pen of a stored cell. A cell has no underline color, so neither has
+ * this. */
+struct pen_attr {
+ uint32_t fg;
+ uint32_t bg;
+ uint16_t flags;
+ uint16_t link;
+};
+
+static inline struct pen_attr pen_attr_default(void)
+{
+ return (struct pen_attr){ .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG };
+}
+
+static inline bool pen_attr_is_default(const struct pen_attr *a)
+{
+ return a->fg == TERM_COLOR_FG && a->bg == TERM_COLOR_BG && a->flags == 0 && a->link == 0;
+}
+
+static inline bool pen_attr_equal(const struct pen_attr *a, const struct pen_attr *b)
+{
+ return a->fg == b->fg && a->bg == b->bg && a->flags == b->flags && a->link == b->link;
+}
+
+static inline uint32_t term_rgb(int r, int g, int b)
+{
+ return term_color_rgb((uint8_t)r, (uint8_t)g, (uint8_t)b);
+}
+
+static inline void pen_reset(struct term_pen *p)
+{
+ p->fg = TERM_COLOR_FG;
+ p->bg = TERM_COLOR_BG;
+ p->ul = 0;
+ p->underline = 0;
+ p->bold = false;
+ p->faint = false;
+ p->italic = false;
+ p->strike = false;
+ p->overline = false;
+ p->inverse = false;
+}
+
+static inline int sgr_extended_color(const int *params, int nparams, uint16_t subparams, int i,
+ uint32_t *color, bool *assigned)
+{
+ *assigned = false;
+ if (i + 1 >= nparams)
+ return 0;
+ if (i + 2 < nparams && params[i + 1] == 5) {
+ int index = params[i + 2];
+ if (index >= 0 && index <= 255) {
+ *color = term_color_index(index);
+ *assigned = true;
+ }
+ return 2;
+ }
+ if (params[i + 1] != 2)
+ return 0;
+ int colon = 0;
+ while (i + 2 + colon < nparams && i + 2 + colon < 16 &&
+ (subparams & (1u << (i + 2 + colon))))
+ colon++;
+ int skip = colon >= 4 ? 1 : 0;
+ if (i + 4 + skip >= nparams)
+ return 0;
+ *color = term_rgb(params[i + 2 + skip], params[i + 3 + skip], params[i + 4 + skip]);
+ *assigned = true;
+ return 4 + skip;
+}
+
+static inline bool pen_set_standard_color(struct term_pen *p, int code)
+{
+ if ((unsigned)(code - 30) < 8)
+ p->fg = term_color_index(code - 30);
+ else if ((unsigned)(code - 40) < 8)
+ p->bg = term_color_index(code - 40);
+ else if ((unsigned)(code - 90) < 8)
+ p->fg = term_color_index(code - 90 + 8);
+ else if ((unsigned)(code - 100) < 8)
+ p->bg = term_color_index(code - 100 + 8);
+ else
+ return false;
+ return true;
+}
+
+static inline void sgr_apply(struct term_pen *pen, const int *params, int nparams, uint16_t subparams)
+{
+ if (nparams == 0) {
+ pen_reset(pen);
+ return;
+ }
+ for (int i = 0; i < nparams; i++) {
+ int p = params[i];
+ switch (p) {
+ case 0: pen_reset(pen); break;
+ case 1: pen->bold = true; break;
+ case 2: pen->faint = true; break;
+ case 3: pen->italic = true; break;
+ case 4: {
+ int style = 1;
+ if (i + 1 < nparams && (subparams & (1u << (i + 1))))
+ style = params[++i];
+ pen->underline = (uint8_t)(style <= 5 ? style : 1);
+ break;
+ }
+ case 7: pen->inverse = true; break;
+ case 9: pen->strike = true; break;
+ case 21: pen->underline = 2; break;
+ case 22: pen->bold = false; pen->faint = false; break;
+ case 23: pen->italic = false; break;
+ case 24: pen->underline = 0; break;
+ case 27: pen->inverse = false; break;
+ case 29: pen->strike = false; break;
+ case 38: {
+ uint32_t c; bool ok;
+ i += sgr_extended_color(params, nparams, subparams, i, &c, &ok);
+ if (ok) pen->fg = c;
+ break;
+ }
+ case 39: pen->fg = TERM_COLOR_FG; break;
+ case 48: {
+ uint32_t c; bool ok;
+ i += sgr_extended_color(params, nparams, subparams, i, &c, &ok);
+ if (ok) pen->bg = c;
+ break;
+ }
+ case 49: pen->bg = TERM_COLOR_BG; break;
+ case 53: pen->overline = true; break;
+ case 55: pen->overline = false; break;
+ case 58: {
+ uint32_t c; bool ok;
+ i += sgr_extended_color(params, nparams, subparams, i, &c, &ok);
+ if (ok) pen->ul = c;
+ break;
+ }
+ case 59: pen->ul = 0; break;
+ default:
+ pen_set_standard_color(pen, p);
+ break;
+ }
+ }
+}
+
+#endif
diff --git a/src/scan.c b/src/scan.c
new file mode 100644
index 0000000..497c95b
--- /dev/null
+++ b/src/scan.c
@@ -0,0 +1,11 @@
+#include "compat.h"
+#include "kernels.h"
+#include "term.h"
+
+TERM_HOT
+size_t term_scan_printable_span(const char *buf, size_t len, unsigned char hi)
+{
+ /* The only entry point that can run before the first terminal. */
+ term_isa_init();
+ return scan_printable_span(buf, len, hi);
+}
diff --git a/src/screen.c b/src/screen.c
new file mode 100644
index 0000000..681c42b
--- /dev/null
+++ b/src/screen.c
@@ -0,0 +1,1513 @@
+#include "screen.h"
+#include "osc/osc.h"
+#include <errno.h>
+#include <limits.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void *grow(struct grow_buf *b, size_t bytes)
+{
+ if (bytes > b->cap) {
+ size_t cap = b->cap ? b->cap : 256;
+ while (cap < bytes)
+ cap *= 2;
+ void *data = realloc(b->data, cap);
+ if (!data)
+ return NULL;
+ b->data = data;
+ b->cap = cap;
+ }
+ return b->data;
+}
+
+/* Make s an empty screen of cols x rows. It keeps its buffers when they are
+ * large enough, so a resize back and forth allocates nothing. */
+static bool screen_setup(struct screen *s, int cols, int rows, uint64_t *next_uid)
+{
+ int stride = (cols + 15) & ~15;
+ size_t text = TEXT_ROW_BYTES(stride), runs = TEXT_ROW_RUNS(stride);
+ size_t ascii = (size_t)rows * (size_t)cols;
+ size_t cells = (size_t)rows * (size_t)stride, bytes = ascii + (size_t)rows * (text + runs);
+ struct srow *slot = rows > s->slot_cap ? calloc((size_t)rows, sizeof(*slot)) : s->slot;
+ struct cell *cells_slab = cells > s->cells_cap ? calloc(cells, sizeof(*cells_slab)) : s->cells_slab;
+ uint8_t *text_slab = bytes > s->text_cap ? calloc(bytes, 1) : s->text_slab;
+ if (!slot || !cells_slab || !text_slab) {
+ if (slot != s->slot) free(slot);
+ if (cells_slab != s->cells_slab) free(cells_slab);
+ if (text_slab != s->text_slab) free(text_slab);
+ return false;
+ }
+ if (slot != s->slot) {
+ free(s->slot);
+ s->slot = slot;
+ s->slot_cap = rows;
+ }
+ if (cells_slab != s->cells_slab) {
+ free(s->cells_slab);
+ s->cells_slab = cells_slab;
+ s->cells_cap = cells;
+ }
+ if (text_slab != s->text_slab) {
+ free(s->text_slab);
+ s->text_slab = text_slab;
+ s->text_cap = bytes;
+ }
+ fill_cells(s->cells_slab, (int)cells, TERM_COLOR_FG, TERM_COLOR_BG);
+ s->rows = rows;
+ s->cols = cols;
+ s->top = 0;
+ s->stride = stride;
+ s->top_id = 0;
+ s->top_seg = 0;
+ for (int i = 0; i < rows; i++) {
+ struct srow *r = &s->slot[i];
+ *r = (struct srow){
+ .cells = s->cells_slab + (size_t)i * stride,
+ .ascii = s->text_slab + (size_t)i * cols,
+ .text = {
+ .text = s->text_slab + ascii + (size_t)i * (text + runs),
+ .storage = s->text_slab + ascii + (size_t)i * (text + runs),
+ .runs = { .data = s->text_slab + ascii + (size_t)i * (text + runs) + text,
+ .cap = (uint32_t)runs },
+ },
+ .uid = ++(*next_uid),
+ .version = 1,
+ .kind = ROW_CELLS,
+ .flags = RF_TAIL_PADDED,
+ };
+ }
+ return true;
+}
+
+static void screen_push_row_inner(struct term *t, struct srow *row)
+{
+ srow_materialize_text(row);
+ bool wrapped = (row->flags & RF_WRAPPED) != 0;
+ int cols = t->scr.cols;
+ if (row->kind == ROW_TEXT && !(wrapped && row->len < cols)) {
+ struct text_row *tr = &row->text;
+ /* Fill blanks are cells of the row, as a cells row stores them. */
+ int fill = (tr->fill_to < cols ? tr->fill_to : cols) - tr->cells;
+ if (fill > 0) {
+ text_row_unshare(tr);
+ memset(tr->text + tr->text_len, ' ', (size_t)fill);
+ text_commit(row, fill, fill, fill, HLINE_NARROW, tr->fill.fg, tr->fill.bg, 0);
+ }
+ term_hist_push_text(&t->hist, tr, wrapped);
+ return;
+ }
+ /* The rest of a wrapped row is part of the line: blanks up to the edge. */
+ if (TERM_EXPECT(wrapped && row->len < cols, 0)) {
+ srow_settle(row, cols);
+ if (!(row->flags & RF_TAIL_PADDED))
+ fill_cells(row->cells + row->len, cols - row->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ row->flags |= RF_TAIL_PADDED;
+ row->len = (uint16_t)cols;
+ }
+ term_hist_push_row(&t->hist, row->cells, row->len, wrapped, t->gstore);
+}
+
+void term_screen_push_row(struct term *t, struct srow *row)
+{
+ screen_push_row_inner(t, row);
+ /* The push can drop more rows than it adds, so the view learns the new
+ * history size here and not before. */
+ if (TERM_EXPECT(t->view.anchor.active, 0))
+ term_hist_view_note_push(&t->view, &t->hist, t->cols);
+}
+
+static void screen_free(struct screen *s)
+{
+ if (!s)
+ return;
+ free(s->slot);
+ free(s->cells_slab);
+ free(s->text_slab);
+ *s = (struct screen){ 0 };
+}
+
+/* Read the history count without changing its rows. */
+int term_screen_history_rows(const struct term *t)
+{
+ if (!t || !screen_has_history(t))
+ return 0;
+ uint64_t rows = term_hist_view_rows(&t->view, &t->hist, t->cols);
+ return rows > INT_MAX ? INT_MAX : (int)rows;
+}
+
+int term_history_rows(const struct term *t)
+{
+ if (!t)
+ return 0;
+ term_write_begin((struct term *)t);
+ int rows = term_screen_history_rows(t);
+ term_write_end((struct term *)t);
+ return rows;
+}
+
+const struct cell *term_ring_row(const struct term *t, int row)
+{
+ if (!t)
+ return NULL;
+ int total = term_screen_history_rows(t);
+ if (row < 0 || row >= total + t->rows)
+ return NULL;
+
+ if (row < total) {
+ uint64_t id;
+ int seg;
+ if (!term_hist_view_find_row(&t->view, &t->hist, row, t->cols, &id, &seg))
+ return NULL;
+ struct cell *out = t->view_row_scratch;
+ int got = term_hist_decode_segment(&t->hist, id, seg, t->cols, out, t->gstore);
+ if (got < t->cols) {
+ fill_cells(out + got, t->cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ int line_rows = term_hist_line_rows(&t->hist, id, t->cols);
+ if (seg + 1 < line_rows || (row + 1 == total && hist_is_open(&t->hist))) {
+ for (int x = got; x < t->cols; x++)
+ out[x].flags |= CELL_WRAP_PAD;
+ }
+ }
+ return out;
+ }
+
+ int y = row - total;
+ const struct screen *s = term_active_screen(t);
+ struct srow *srow = screen_row_at(s, y);
+ if (!srow)
+ return NULL;
+
+ struct cell *out = t->view_row_scratch;
+ if (srow->kind == ROW_CELLS) {
+ if (srow->len >= t->cols || (srow->flags & RF_TAIL_PADDED))
+ return srow->cells;
+ if (srow->len > 0)
+ memcpy(out, srow->cells, (size_t)srow->len * sizeof(struct cell));
+ fill_cells(out + srow->len, t->cols - srow->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ return out;
+ }
+
+ text_row_cells(srow, out, t->cols);
+ return out;
+}
+
+const struct cell *term_screen_view_row(const struct term *t, int y)
+{
+ if (!t || y < 0 || y >= t->rows)
+ return NULL;
+ uint64_t id;
+ int seg, scr_y;
+ if (term_hist_view_row_pos(&t->view, &t->hist, t->cols, t->rows, screen_top_line(t), y,
+ &id, &seg, &scr_y)) {
+ struct cell *out = t->view_row_scratch;
+ int got = term_hist_decode_segment(&t->hist, id, seg, t->cols, out, t->gstore);
+ if (got < t->cols) {
+ fill_cells(out + got, t->cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ int line_rows = term_hist_line_rows(&t->hist, id, t->cols);
+ if (seg + 1 < line_rows || (id + 1 == hist_end_id(&t->hist) && hist_is_open(&t->hist))) {
+ for (int x = got; x < t->cols; x++)
+ out[x].flags |= CELL_WRAP_PAD;
+ }
+ }
+ return out;
+ }
+ const struct screen *s = term_active_screen(t);
+ struct srow *srow = screen_row_at(s, scr_y);
+ if (!srow)
+ return NULL;
+ struct cell *out = t->view_row_scratch;
+ if (srow->kind == ROW_CELLS) {
+ if (srow->len >= t->cols || (srow->flags & RF_TAIL_PADDED))
+ return srow->cells;
+ if (srow->len > 0)
+ memcpy(out, srow->cells, (size_t)srow->len * sizeof(struct cell));
+ fill_cells(out + srow->len, t->cols - srow->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ return out;
+ }
+ text_row_cells(srow, out, t->cols);
+ return out;
+}
+
+static inline uint64_t mix64(uint64_t x)
+{
+ x ^= x >> 33;
+ x *= 0xff51afd7ed558ccdull;
+ x ^= x >> 33;
+ x *= 0xc4ceb9fe1a85ec53ull;
+ return x ^ (x >> 33);
+}
+
+uint64_t term_screen_view_row_key(const struct term *t, int y)
+{
+ if (!t || y < 0 || y >= t->rows)
+ return 0;
+ uint64_t id;
+ int seg, scr_y;
+ if (term_hist_view_row_pos(&t->view, &t->hist, t->cols, t->rows, screen_top_line(t), y,
+ &id, &seg, &scr_y)) {
+ uint64_t line_ver = (id + 1 == hist_end_id(&t->hist) && hist_is_open(&t->hist))
+ ? (term_hist_line_cells(&t->hist, id) << 32) : 0;
+ if (id == t->hist.base_cell_id)
+ line_ver ^= mix64(t->hist.base_cell);
+ return mix64(((uint64_t)id << 16) ^ (uint64_t)seg ^ line_ver ^ 0x517cc1b727220a95ull);
+ }
+ const struct screen *s = term_active_screen(t);
+ const struct srow *row = screen_row_at(s, scr_y);
+ if (!row)
+ return 0;
+ return mix64(row->uid ^ ((uint64_t)row->version << 32));
+}
+
+int term_view_offset(const struct term *t)
+{
+ if (!t)
+ return 0;
+ return term_hist_view_scroll_offset(&t->view, &t->hist, t->cols);
+}
+
+void term_scroll_to(struct term *t, int offset)
+{
+ if (!t)
+ return;
+ term_write_begin(t);
+ if (t->alternate || offset < 0)
+ offset = 0;
+ struct view_anchor old = t->view.anchor;
+ term_hist_view_scroll_to(&t->view, &t->hist, t->cols, screen_top_line(t), offset);
+ if (old.active != t->view.anchor.active || old.line_id != t->view.anchor.line_id ||
+ old.seg != t->view.anchor.seg)
+ t->full_gen++;
+ term_write_end(t);
+}
+
+void term_scroll(struct term *t, int lines)
+{
+ if (!t)
+ return;
+ /* Read the alternate flag inside the writer section. A read before
+ * term_write_begin races with a feed on another thread. */
+ term_write_begin(t);
+ if (t->alternate) {
+ term_write_end(t);
+ return;
+ }
+ struct view_anchor old = t->view.anchor;
+ term_hist_view_scroll(&t->view, &t->hist, t->cols, screen_top_line(t), lines);
+ if (old.active != t->view.anchor.active || old.line_id != t->view.anchor.line_id ||
+ old.seg != t->view.anchor.seg)
+ t->full_gen++;
+ term_write_end(t);
+}
+
+struct term_scrollback term_scrollback(const struct term *t)
+{
+ if (!t)
+ return (struct term_scrollback){ 0 };
+ term_write_begin((struct term *)t);
+ struct term_scrollback sb = { 0 };
+ if (!t->alternate) {
+ sb.offset = term_view_offset(t);
+ sb.total = term_screen_history_rows(t);
+ }
+ term_write_end((struct term *)t);
+ return sb;
+}
+
+void term_screen_erase_line(struct term *t, int mode)
+{
+ t->pending_wrap = false;
+ int p = phys_row(t, t->cursor_y);
+ if (mode == 0) {
+ erase_span(t, p, t->cursor_x, erase_width(t, p));
+ } else if (mode == 1) {
+ erase_span(t, p, 0, t->cursor_x + 1);
+ } else if (mode == 2) {
+ erase_span(t, p, 0, erase_width(t, p));
+ }
+}
+
+void term_screen_delete_chars(struct term *t, int n)
+{
+ if (t->cursor_x >= t->cols)
+ return;
+ t->pending_wrap = false;
+ int x = t->cursor_x;
+ int cols = t->cols;
+ if (n > cols - x)
+ n = cols - x;
+ int p = phys_row(t, t->cursor_y);
+ struct cell *row = row_cells(t, p);
+ struct srow *srow = term_row(t, t->cursor_y);
+ int len = srow->len;
+ clear_wide_edges(row, x, x + n, len);
+ if (len > cols) {
+ clear_cells_default(row + cols, len - cols);
+ len = cols;
+ }
+ if (len > x + n) {
+ memmove(&row[x], &row[x + n], (size_t)(len - (x + n)) * sizeof(struct cell));
+ clear_cells_default(&row[len - n], n);
+ srow->len = (uint16_t)(len - n);
+ } else if (len > x) {
+ clear_cells_default(&row[x], len - x);
+ srow->len = (uint16_t)x;
+ }
+ srow->flags &= (uint8_t)~RF_TAIL_PADDED;
+ bool pristine = using_default_colors(t);
+ if (!pristine) {
+ int from = cols - n;
+ if (from < x)
+ from = x;
+ term_row_fill_gap(srow, from);
+ fill_cells(&row[from], cols - from, t->cur_fg, t->cur_bg);
+ srow->len = (uint16_t)cols;
+ }
+ row_set_wrapped(srow, false);
+ repair_wide_row(row, x - 1, len, cols, srow->len);
+ srow->version++;
+}
+
+void term_screen_insert_chars(struct term *t, int n)
+{
+ if (t->cursor_x >= t->cols)
+ return;
+ t->pending_wrap = false;
+ int x = t->cursor_x;
+ int cols = t->cols;
+ if (n > cols - x)
+ n = cols - x;
+ int p = phys_row(t, t->cursor_y);
+ struct cell *row = row_cells(t, p);
+ struct srow *srow = term_row(t, t->cursor_y);
+ term_row_fill_gap(srow, x);
+ int len = srow->len;
+ if (x > 0 && x < len && (row[x].flags & CELL_TRAIL)) {
+ fill_cells(row + x - 1, 2, t->cur_fg, t->cur_bg);
+ srow->version++;
+ }
+ if (len > x) {
+ int shift = cols - (x + n);
+ if (shift > len - x)
+ shift = len - x;
+ if (shift > 0)
+ memmove(&row[x + n], &row[x], (size_t)shift * sizeof(struct cell));
+ }
+ fill_cells(&row[x], n, t->cur_fg, t->cur_bg);
+ bool pristine = using_default_colors(t);
+ int new_len = len + n > cols ? cols : len + n;
+ if (pristine && len <= x)
+ new_len = len;
+ else if (new_len < x + n)
+ new_len = x + n;
+ if (new_len < cols)
+ clear_cells_default(row + new_len, cols - new_len);
+ srow->len = (uint16_t)new_len;
+ srow->flags &= (uint8_t)~RF_TAIL_PADDED;
+ row_set_wrapped(srow, false);
+ int repair_end = x + n + 1 > len + 1 ? x + n + 1 : len + 1;
+ repair_wide_row(row, x - 1, repair_end, cols, srow->len);
+ srow->version++;
+}
+
+void term_screen_erase_all(struct term *t)
+{
+ struct screen *s = &t->scr;
+ bool pristine = using_default_colors(t);
+ for (int y = 0; y < s->rows; y++) {
+ int p = phys_row(t, y);
+ if (pristine && s->slot[p].len == 0 && !(s->slot[p].flags & RF_WRAPPED))
+ continue;
+ erase_row(t, p);
+ }
+ t->full_gen++;
+}
+
+#if defined(TERM_KERNEL_AVX2)
+/* The inner columns [1, len - 1): the callers probed both ends. */
+TERM_TARGET_AVX2
+static bool text_has_text_avx2(const uint8_t *text, int len)
+{
+ int x = 1;
+ const __m256i vspace = _mm256_set1_epi8(' ');
+ for (; x + 32 <= len - 1; x += 32) {
+ __m256i chunk = _mm256_loadu_si256((const __m256i *)(text + x));
+ __m256i diff = _mm256_subs_epu8(chunk, vspace);
+ if (!_mm256_testz_si256(diff, diff))
+ return true;
+ }
+ for (; x < len - 1; x++)
+ if (text[x] > ' ')
+ return true;
+ return false;
+}
+
+TERM_TARGET_AVX2
+static bool cells_has_text_avx2(const struct cell *cells, int len)
+{
+ int x = 1;
+ const __m256i vspace = _mm256_set1_epi32(0x20);
+ const __m256i mask = _mm256_setr_epi32(-1, 0, 0, 0, -1, 0, 0, 0);
+ for (; x + 4 <= len - 1; x += 4) {
+ __m256i v0 = _mm256_loadu_si256((const __m256i *)(cells + x));
+ __m256i v1 = _mm256_loadu_si256((const __m256i *)(cells + x + 2));
+ __m256i cmp0 = _mm256_cmpgt_epi32(v0, vspace);
+ __m256i cmp1 = _mm256_cmpgt_epi32(v1, vspace);
+ __m256i hits = _mm256_and_si256(_mm256_or_si256(cmp0, cmp1), mask);
+ if (!_mm256_testz_si256(hits, hits))
+ return true;
+ }
+ for (; x < len - 1; x++)
+ if (cells[x].c > ' ')
+ return true;
+ return false;
+}
+#endif
+
+static inline bool text_has_text(const uint8_t *text, int len)
+{
+ if (len <= 0)
+ return false;
+ /* Probe column 0 first. Most terminal lines start with visible text. */
+ if (text[0] > ' ')
+ return true;
+ if (len == 1)
+ return false;
+ /* Probe the line tail next. Indented lines often end with visible text. */
+ if (text[len - 1] > ' ')
+ return true;
+
+ int x = 1;
+#if defined(__aarch64__)
+ const uint8x16_t vspace = vdupq_n_u8(' ');
+ for (; x + 16 <= len - 1; x += 16) {
+ uint8x16_t chunk = vld1q_u8(text + x);
+ uint8x16_t cmp = vcgtq_u8(chunk, vspace);
+ if (vmaxvq_u8(cmp) != 0)
+ return true;
+ }
+#elif defined(__x86_64__)
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return text_has_text_avx2(text, len);
+#endif
+ const __m128i vspace = _mm_set1_epi8(' ');
+ for (; x + 16 <= len - 1; x += 16) {
+ __m128i chunk = _mm_loadu_si128((const __m128i *)(text + x));
+ __m128i diff = _mm_subs_epu8(chunk, vspace);
+ if (_mm_movemask_epi8(_mm_cmpeq_epi8(diff, _mm_setzero_si128())) != 0xFFFF)
+ return true;
+ }
+#endif
+ for (; x < len - 1; x++)
+ if (text[x] > ' ')
+ return true;
+ return false;
+}
+
+static inline bool cells_has_text(const struct cell *cells, int len)
+{
+ if (len <= 0)
+ return false;
+ /* Probe column 0 first. */
+ if (cells[0].c > ' ')
+ return true;
+ if (len == 1)
+ return false;
+ /* Probe the line tail next. */
+ if (cells[len - 1].c > ' ')
+ return true;
+
+ int x = 1;
+#if defined(__aarch64__)
+ const uint32x4_t vspace = vdupq_n_u32(' ');
+ for (; x + 4 <= len - 1; x += 4) {
+ uint32x4x4_t v = vld4q_u32((const uint32_t *)(cells + x));
+ uint32x4_t cmp = vcgtq_u32(v.val[0], vspace);
+ if (vmaxvq_u32(cmp) != 0)
+ return true;
+ }
+#elif defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return cells_has_text_avx2(cells, len);
+#endif
+ for (; x < len - 1; x++)
+ if (cells[x].c > ' ')
+ return true;
+ return false;
+}
+
+static inline bool srow_has_text(const struct srow *r, int cols)
+{
+ int len = r->len < cols ? r->len : cols;
+ if (len <= 0)
+ return false;
+ if (r->kind == ROW_TEXT)
+ return text_has_text(r->text.text, r->text.text_len < len ? r->text.text_len : len);
+ return cells_has_text(r->cells, len);
+}
+
+void term_screen_erase(struct term *t, int mode)
+{
+ if (mode == 3) {
+ term_hist_clear(&t->hist);
+ term_hist_view_clear(&t->view);
+ term_select_clear(t);
+ t->full_gen++;
+ return;
+ }
+ if (mode == 2) {
+ int last_active = -1;
+ bool tail_clean = using_default_colors(t);
+ int rows = t->rows;
+ int cols = t->cols;
+ const struct srow *slots = t->scr.slot;
+ int p = t->scr.top ? t->scr.top - 1 : rows - 1;
+ for (int y = rows - 1; y >= 0; y--) {
+ const struct srow *r = &slots[p];
+ p = p ? p - 1 : rows - 1;
+ if (TERM_EXPECT(r->len == 0, 1)) {
+ if (TERM_EXPECT((r->flags & RF_WRAPPED) != 0, 0))
+ tail_clean = false;
+ continue;
+ }
+ if (srow_has_text(r, cols)) {
+ last_active = y;
+ break;
+ }
+ tail_clean = false;
+ }
+ if (last_active >= 0 && !t->alternate) {
+ for (int y = 0; y <= last_active; y++)
+ term_screen_scroll_up(t);
+ if (tail_clean) {
+ term_hist_view_reset_bottom(&t->view);
+ return;
+ }
+ }
+ term_screen_erase_all(t);
+ term_hist_view_reset_bottom(&t->view);
+ t->full_gen++;
+ } else if (mode == 0) {
+ int cur_p = phys_row(t, t->cursor_y);
+ erase_span(t, cur_p, t->cursor_x, erase_width(t, cur_p));
+ bool pristine = using_default_colors(t);
+ for (int r = t->cursor_y + 1; r < t->rows; r++) {
+ int p = phys_row(t, r);
+ if (pristine && t->scr.slot[p].len == 0 && !(t->scr.slot[p].flags & RF_WRAPPED))
+ continue;
+ erase_row(t, p);
+ }
+ t->scr.slot[cur_p].version++;
+ } else if (mode == 1) {
+ bool pristine = using_default_colors(t);
+ for (int r = 0; r < t->cursor_y; r++) {
+ int p = phys_row(t, r);
+ if (pristine && t->scr.slot[p].len == 0 && !(t->scr.slot[p].flags & RF_WRAPPED))
+ continue;
+ erase_row(t, p);
+ }
+ int cur_p = phys_row(t, t->cursor_y);
+ erase_span(t, cur_p, 0, t->cursor_x + 1);
+ t->scr.slot[cur_p].version++;
+ }
+}
+
+void term_screen_scroll_region(struct term *t, int top, int bottom, int count,
+ bool down, bool history)
+{
+ if (count <= 0 || top > bottom || top < 0 || bottom >= t->rows)
+ return;
+ int height = bottom - top + 1;
+ if (count > height)
+ count = height;
+
+ if (!down && history && top == 0 && bottom == t->rows - 1) {
+ while (count-- > 0)
+ term_screen_scroll_up(t);
+ return;
+ }
+
+ if (!down && history && top == 0 && !t->alternate) {
+ for (int i = 0; i < count; i++) {
+ term_screen_scroll_up(t);
+ if (bottom + 1 < t->rows)
+ term_screen_scroll_region(t, bottom, t->rows - 1, 1, true, false);
+ }
+ return;
+ }
+
+ struct screen *s = term_active_screen(t);
+ bool def = using_default_colors(t);
+
+ if (count == height) {
+ for (int y = top; y <= bottom; y++) {
+ struct srow *dst = screen_get_row(s, y);
+ dst->kind = ROW_CELLS;
+ dst->uid = ++t->next_row_uid;
+ dst->version++;
+ if (def) {
+ dst->len = 0;
+ dst->flags = 0;
+ } else {
+ fill_cells(dst->cells, t->cols, t->cur_fg, t->cur_bg);
+ dst->len = (uint16_t)t->cols;
+ dst->flags = RF_TAIL_PADDED;
+ }
+ }
+ } else {
+ struct srow tmp[256];
+ struct srow *buf = (height <= 256) ? tmp : malloc((size_t)height * sizeof(struct srow));
+ if (!buf)
+ return;
+
+ for (int i = 0; i < height; i++)
+ buf[i] = *screen_get_row(s, top + i);
+
+ for (int i = 0; i < height; i++) {
+ int src_i = down ? i - count : i + count;
+ struct srow *dst = screen_get_row(s, top + i);
+ if (src_i >= 0 && src_i < height) {
+ *dst = buf[src_i];
+ dst->version++;
+ } else {
+ /* The row takes the buffers of a row that left the region. */
+ int dead_i = down ? (height - count + i) : (i - (height - count));
+ dst->cells = buf[dead_i].cells;
+ dst->ascii = buf[dead_i].ascii;
+ dst->text = buf[dead_i].text;
+ dst->kind = ROW_CELLS;
+ dst->uid = ++t->next_row_uid;
+ dst->version = 1;
+ if (def) {
+ dst->len = 0;
+ dst->flags = 0;
+ } else {
+ fill_cells(dst->cells, t->cols, t->cur_fg, t->cur_bg);
+ dst->len = (uint16_t)t->cols;
+ dst->flags = RF_TAIL_PADDED;
+ }
+ }
+ }
+ if (buf != tmp)
+ free(buf);
+ }
+
+ if (!t->alternate && t->prompt_y >= top && t->prompt_y <= bottom) {
+ t->prompt_y += down ? count : -count;
+ if (t->prompt_y < top || t->prompt_y > bottom) {
+ t->prompt_y = -1;
+ t->has_prompt = false;
+ }
+ }
+ t->full_gen++;
+}
+
+static bool term_resize_internal(struct term *t, int cols, int rows);
+
+void term_screen_set_alternate(struct term *t, bool enabled)
+{
+ if (t->alternate == enabled)
+ return;
+ if (enabled) {
+ if (!t->alt_allocated || t->alt_scr.cols != t->cols || t->alt_scr.rows != t->rows) {
+ if (!screen_setup(&t->alt_scr, t->cols, t->rows, &t->next_row_uid))
+ return;
+ t->alt_allocated = true;
+ } else {
+ for (int y = 0; y < t->alt_scr.rows; y++) {
+ struct srow *r = &t->alt_scr.slot[y];
+ r->kind = ROW_CELLS;
+ r->len = 0;
+ r->flags = 0;
+ r->uid = ++t->next_row_uid;
+ r->version++;
+ }
+ t->alt_scr.top = 0;
+ }
+ struct screen tmp_scr = t->scr;
+ t->scr = t->alt_scr;
+ t->alt_scr = tmp_scr;
+ t->normal_saved_cursor = term_cursor_save(t);
+ t->normal_saved_decsc = t->saved;
+ t->normal_cursor_visible = t->cursor_visible;
+ t->normal_cursor_style = (uint8_t)t->cursor_style;
+ t->normal_cursor_blink = t->cursor_blink;
+ t->normal_cols = t->cols;
+ t->normal_rows = t->rows;
+ t->alternate = true;
+ t->cursor_x = 0;
+ t->cursor_y = 0;
+ t->pending_wrap = false;
+ t->scroll_top = 0;
+ t->scroll_bottom = t->rows - 1;
+ t->left_margin = 0;
+ t->right_margin = t->cols - 1;
+ /* The alternate screen has no history to show. */
+ term_hist_view_reset_bottom(&t->view);
+ term_select_clear(t);
+ t->full_gen++;
+ } else {
+ int cols = t->cols;
+ int rows = t->rows;
+ struct screen tmp_scr = t->scr;
+ t->scr = t->alt_scr;
+ t->alt_scr = tmp_scr;
+ t->cols = t->scr.cols;
+ t->rows = t->scr.rows;
+ t->alternate = false;
+ t->saved = t->normal_saved_decsc;
+ t->cursor_visible = t->normal_cursor_visible;
+ t->cursor_style = (enum term_cursor_style)t->normal_cursor_style;
+ t->cursor_blink = t->normal_cursor_blink;
+ term_cursor_apply(t, &t->normal_saved_cursor);
+ t->cursor_x = t->normal_saved_cursor.x;
+ t->cursor_y = t->normal_saved_cursor.y;
+ t->pending_wrap = t->normal_saved_cursor.pending_wrap;
+ if (cols != t->cols || rows != t->rows)
+ term_resize_internal(t, cols, rows);
+ t->scroll_top = 0;
+ t->scroll_bottom = t->rows - 1;
+ t->left_margin = 0;
+ t->right_margin = t->cols - 1;
+ /* The alternate screen has no history to show. */
+ term_hist_view_reset_bottom(&t->view);
+ term_select_clear(t);
+ t->full_gen++;
+ }
+}
+
+static int scrollback_lines(int requested)
+{
+ if (requested == 0)
+ return TERM_SCROLLBACK_DEFAULT;
+ if (requested < 0)
+ return 0;
+ return requested > 1000000 ? 1000000 : requested;
+}
+
+static atomic_int tables_state;
+
+/* Build the shared lookup tables once. A second caller waits until the first
+ * is done. */
+static void tables_init(void)
+{
+ if (atomic_load_explicit(&tables_state, memory_order_acquire) == 2)
+ return;
+ int expected = 0;
+ if (atomic_compare_exchange_strong(&tables_state, &expected, 1)) {
+ term_isa_init();
+ term_text_tables_init();
+ term_snapshot_tables_init();
+ atomic_store_explicit(&tables_state, 2, memory_order_release);
+ return;
+ }
+ while (atomic_load_explicit(&tables_state, memory_order_acquire) != 2)
+ term_cpu_relax();
+}
+
+bool term_tables_ready(void)
+{
+ return atomic_load_explicit(&tables_state, memory_order_relaxed) == 2;
+}
+
+struct term *term_new(const struct term_options *opt)
+{
+ tables_init();
+ static const struct term_options defaults;
+ if (!opt)
+ opt = &defaults;
+ int cols = opt->size.x < 1 ? 80 : opt->size.x;
+ int rows = opt->size.y < 1 ? 24 : opt->size.y;
+ if (cols > TERM_MAX_COLS)
+ cols = TERM_MAX_COLS;
+
+ static _Atomic uint64_t next_serial = 1;
+ struct term *t = calloc(1, sizeof(*t));
+ if (!t)
+ return NULL;
+ atomic_init(&t->seq, 0);
+ atomic_init(&t->writer_tid, 0);
+ t->write_depth = 0;
+ t->serial = atomic_fetch_add_explicit(&next_serial, 1, memory_order_relaxed);
+ t->cols = cols;
+ t->rows = rows;
+ t->scroll_bottom = rows - 1;
+ t->right_margin = cols - 1;
+ t->prompt_y = -1;
+ t->sb_capacity = scrollback_lines(opt->scrollback);
+ t->cb = opt->callbacks;
+ term_palette_init(&t->palette, &opt->theme);
+
+ t->next_row_uid = 1;
+ if (!screen_setup(&t->scr, cols, rows, &t->next_row_uid)) {
+ free(t);
+ return NULL;
+ }
+ if (!term_hist_init(&t->hist, t->sb_capacity, 0)) {
+ screen_free(&t->scr);
+ free(t);
+ return NULL;
+ }
+ t->hist.wrap_cols = (uint16_t)cols;
+ term_hist_view_init(&t->view);
+
+ t->gstore = term_gstore_new(t);
+ t->view_scratch_cap = cols > 256 ? cols : 256;
+ t->view_row_scratch = calloc((size_t)t->view_scratch_cap, sizeof(struct cell));
+ t->tabs_cap = cols > 256 ? cols : 256;
+ t->tabs = calloc((size_t)t->tabs_cap, 1);
+
+ if (!t->gstore || !t->view_row_scratch || !t->tabs) {
+ term_free(t);
+ return NULL;
+ }
+
+ t->cur_fg = TERM_COLOR_FG;
+ t->cur_bg = TERM_COLOR_BG;
+ t->cursor_visible = true;
+ t->default_cursor_style = opt->cursor_style > TERM_CURSOR_BAR ? TERM_CURSOR_BLOCK : opt->cursor_style;
+ t->default_cursor_blink = !opt->cursor_steady;
+ t->cursor_style = t->default_cursor_style;
+ t->cursor_blink = t->default_cursor_blink;
+ t->autowrap = true;
+ t->charsets[0] = t->charsets[1] = 'B';
+ t->saved = term_saved_cursor_initial();
+ memcpy(t->tcap_name, "wsap_term", sizeof("wsap_term"));
+ term_tabs_default(t);
+ t->full_gen = 1;
+ for (int j = 0; j < TERM_WIDTH_SLOTS; j++)
+ t->text_width.lo[j] = 0xFFFFFFFFu;
+
+ return t;
+}
+
+void term_free(struct term *t)
+{
+ if (!t)
+ return;
+ screen_free(&t->scr);
+ screen_free(&t->alt_scr);
+ screen_free(&t->spare);
+ struct grow_buf *bufs[] = { &t->resize.row, &t->resize.rows, &t->resize.cells, &t->resize.line,
+ &t->resize.pull, &t->resize.rest, &t->resize.lens };
+ for (size_t i = 0; i < sizeof(bufs) / sizeof(bufs[0]); i++)
+ free(bufs[i]->data);
+ free(t->ascii_ends.data);
+ free(t->ascii_tail.data);
+ term_hist_free(&t->hist);
+ term_hist_view_free(&t->view);
+ free(t->view_row_scratch);
+ free(t->tabs);
+ term_osc_cleanup(t);
+ term_gstore_free(t->gstore);
+ free(t);
+}
+
+/* ---- Resize & Reflow ---- */
+
+struct reflow_target {
+ int cols, rows;
+ struct cell *cells;
+ int x;
+ int count;
+ int cursor_x, cursor_y, saved_x, saved_y, prompt_y;
+ bool cursor_set, saved_set, prompt_set, pending_wrap;
+
+ struct reflow_out {
+ int len;
+ bool wrapped;
+ } *out;
+ struct cell *out_cells;
+ int out_cap;
+ struct grow_buf *out_buf, *cells_buf; /* they keep their size across resizes */
+};
+
+static inline struct cell *reflow_out_row_cells(const struct reflow_target *r, int i)
+{
+ return r->out_cells + (size_t)i * (size_t)r->cols;
+}
+
+/* Returns false when a row buffer cannot grow. The caller then stops the
+ * reflow before the terminal changes. */
+static bool reflow_target_end_row(struct reflow_target *r, bool wrapped)
+{
+ if (r->count == r->out_cap) {
+ int cap = r->out_cap ? r->out_cap * 2 : (r->rows + 16 < 32 ? 32 : r->rows + 16);
+ struct reflow_out *out = grow(r->out_buf, (size_t)cap * sizeof(struct reflow_out));
+ if (!out)
+ return false;
+ r->out = out;
+ struct cell *cells = grow(r->cells_buf, (size_t)cap * (size_t)r->cols * sizeof(struct cell));
+ if (!cells)
+ return false;
+ r->out_cells = cells;
+ r->out_cap = cap;
+ }
+ int idx = r->count++;
+ struct reflow_out *dst = &r->out[idx];
+ struct cell *dst_cells = reflow_out_row_cells(r, idx);
+ int len = r->x > r->cols ? r->cols : r->x;
+ memcpy(dst_cells, r->cells, (size_t)len * sizeof(struct cell));
+ if (len < r->cols)
+ fill_cells(dst_cells + len, r->cols - len, TERM_COLOR_FG, TERM_COLOR_BG);
+ dst->len = len;
+ dst->wrapped = wrapped;
+ r->x = 0;
+ return true;
+}
+
+static bool reflow_append_span(struct reflow_target *r, const struct cell *cells,
+ int len, int cursor, int saved)
+{
+ for (int i = 0; i < len;) {
+ if (r->x == r->cols) {
+ if (!reflow_target_end_row(r, true))
+ return false;
+ continue;
+ }
+ int room = r->cols - r->x;
+ int take = len - i < room ? len - i : room;
+ if (i + take < len && (cells[i + take].flags & CELL_TRAIL)) {
+ if (--take == 0) {
+ r->cells[r->x] = (struct cell){' ', TERM_COLOR_FG, TERM_COLOR_BG, CELL_WRAP_PAD, 0};
+ r->x++;
+ if (!reflow_target_end_row(r, true))
+ return false;
+ continue;
+ }
+ }
+ if (cursor >= i && cursor < i + take) {
+ r->cursor_x = r->x + (cursor - i);
+ r->cursor_y = r->count;
+ r->cursor_set = true;
+ r->pending_wrap = false;
+ }
+ if (saved >= i && saved < i + take) {
+ r->saved_x = r->x + (saved - i);
+ r->saved_y = r->count;
+ r->saved_set = true;
+ }
+ memcpy(r->cells + r->x, cells + i, (size_t)take * sizeof(*cells));
+ r->x += take;
+ i += take;
+ }
+ return true;
+}
+
+static bool reflow_append_wide(struct reflow_target *r, const struct cell *cells, int len,
+ int cursor, int saved)
+{
+ for (int i = 0; i < len;) {
+ if (cells[i].flags & (CELL_WRAP_PAD | CELL_TRAIL)) {
+ int at = (cells[i].flags & CELL_TRAIL) && r->x > 0 ? r->x - 1 : r->x;
+ if (cursor == i) {
+ r->cursor_x = at;
+ r->cursor_y = r->count;
+ r->cursor_set = true;
+ r->pending_wrap = false;
+ }
+ if (saved == i) {
+ r->saved_x = at;
+ r->saved_y = r->count;
+ r->saved_set = true;
+ }
+ i++;
+ continue;
+ }
+ int source = (cells[i].flags & CELL_WIDE) && i + 1 < len &&
+ (cells[i + 1].flags & CELL_TRAIL) ? 2 : 1;
+ int n = (cells[i].flags & CELL_WIDE) && r->cols > 1 ? 2 : 1;
+ if (r->x + n > r->cols) {
+ if (r->x < r->cols) {
+ r->cells[r->x] = (struct cell){' ', TERM_COLOR_FG, TERM_COLOR_BG, CELL_WRAP_PAD, 0};
+ r->x++;
+ }
+ if (!reflow_target_end_row(r, true))
+ return false;
+ }
+ if (cursor >= i && cursor < i + source) {
+ r->cursor_x = r->x + (cursor - i < n ? cursor - i : 0);
+ r->cursor_y = r->count;
+ r->cursor_set = true;
+ r->pending_wrap = false;
+ }
+ if (saved >= i && saved < i + source) {
+ r->saved_x = r->x + (saved - i < n ? saved - i : 0);
+ r->saved_y = r->count;
+ r->saved_set = true;
+ }
+ write_cell_span(r->cells, r->x, cells[i], n);
+ r->x += n;
+ i += source;
+ }
+ return true;
+}
+
+static bool reflow_append(struct reflow_target *r, const struct cell *cells, int len,
+ int cursor, int saved)
+{
+ bool has_wide = false;
+ for (int i = 0; i < len; i++) {
+ if (cells[i].flags & (CELL_WIDE | CELL_TRAIL | CELL_WRAP_PAD)) {
+ has_wide = true;
+ break;
+ }
+ }
+ bool ok = has_wide ? reflow_append_wide(r, cells, len, cursor, saved)
+ : reflow_append_span(r, cells, len, cursor, saved);
+ if (!ok)
+ return false;
+
+ if (cursor == len) {
+ r->cursor_x = r->x == r->cols ? r->cols - 1 : r->x;
+ r->cursor_y = r->count;
+ r->pending_wrap = (r->x == r->cols);
+ r->cursor_set = true;
+ }
+ if (saved == len) {
+ r->saved_x = r->x == r->cols ? r->cols - 1 : r->x;
+ r->saved_y = r->count;
+ r->saved_set = true;
+ }
+ return true;
+}
+
+/* Move up to max_pull rows from the end of history to the top of dst. A line
+ * that does not fit whole leaves its first rows in history, still open. */
+static int pull_history_rows(struct term *t, struct screen *dst_scr, int cols, int max_pull)
+{
+ struct hist *h = &t->hist;
+ int pull = 0;
+ for (uint64_t id = hist_end_id(h); pull < max_pull && id > hist_first_id(h); id--)
+ pull += term_hist_line_rows(h, id - 1, cols);
+ if (pull > max_pull)
+ pull = max_pull;
+
+ int slot = pull;
+ while (slot > 0) {
+ uint64_t id = hist_end_id(h) - 1;
+ int line_rows = term_hist_line_rows(h, id, cols);
+ bool open = hist_is_open(h);
+ int take = slot < line_rows ? slot : line_rows;
+ int first = line_rows - take;
+ struct cell *pull_buf = grow(&t->resize.pull, (size_t)take * (size_t)cols * sizeof(*pull_buf));
+ int *pull_lens = grow(&t->resize.lens, (size_t)take * sizeof(*pull_lens));
+ if (pull_buf && pull_lens)
+ term_hist_decode_segments(h, id, first, take, cols, pull_buf, pull_lens, t->gstore);
+ for (int seg = line_rows - 1; seg >= first; seg--) {
+ struct srow *dst = &dst_scr->slot[--slot];
+ int idx = seg - first;
+ int got = (pull_buf && pull_lens) ? pull_lens[idx] : 0;
+ if (pull_buf)
+ memcpy(dst->cells, pull_buf + (size_t)idx * cols, (size_t)cols * sizeof(struct cell));
+ bool wrapped = seg + 1 < line_rows || open;
+ if (got < cols) {
+ fill_cells(dst->cells + got, cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ if (wrapped) {
+ for (int x = got; x < cols; x++)
+ dst->cells[x].flags |= CELL_WRAP_PAD;
+ got = cols;
+ }
+ }
+ dst->kind = ROW_CELLS;
+ dst->len = (uint16_t)got;
+ dst->flags = (wrapped ? RF_WRAPPED : 0) | RF_TAIL_PADDED;
+ dst->version = 1;
+ dst->uid = ++t->next_row_uid;
+ }
+ if (first == 0) {
+ term_hist_pop_line(h, NULL, 0, t->gstore);
+ continue;
+ }
+ struct cell *rest = grow(&t->resize.rest, (size_t)first * (size_t)cols * sizeof(*rest));
+ int *lens = grow(&t->resize.lens, (size_t)first * sizeof(*lens));
+ if (rest && lens)
+ term_hist_decode_segments(h, id, 0, first, cols, rest, lens, t->gstore);
+ term_hist_pop_line(h, NULL, 0, t->gstore);
+ if (rest && lens) {
+ for (int seg = 0; seg < first; seg++) {
+ struct cell *row = rest + (size_t)seg * cols;
+ if (lens[seg] < cols)
+ fill_cells(row + lens[seg], cols - lens[seg], TERM_COLOR_FG, TERM_COLOR_BG);
+ term_hist_push_row(h, row, cols, true, t->gstore);
+ }
+ }
+ }
+ return pull;
+}
+
+/* The spare screen, set up as cols x rows, for a resize to build into. */
+static bool resize_target(struct term *t, struct screen *out, int cols, int rows,
+ const struct screen *from)
+{
+ *out = t->spare;
+ t->spare = (struct screen){ 0 };
+ if (!screen_setup(out, cols, rows, &t->next_row_uid)) {
+ t->spare = *out;
+ return false;
+ }
+ out->top_id = from->top_id;
+ out->top_seg = from->top_seg;
+ return true;
+}
+
+static void copy_srow_resized(struct srow *dst, struct srow *src, int old_cols, int cols)
+{
+ int new_len = src->len < cols ? src->len : cols;
+ if (src->kind == ROW_TEXT && src->len <= cols) {
+ /* The text fits the new width: copy it into the new buffers. */
+ struct text_row *d = &dst->text;
+ const struct text_row *s = &src->text;
+ memcpy(d->text, s->text, s->text_len);
+ memcpy(d->runs.data, s->runs.data, s->runs.len);
+ d->text_len = s->text_len;
+ d->cells = s->cells;
+ d->wide = s->wide;
+ d->fill_to = s->fill_to < cols ? s->fill_to : (uint16_t)cols;
+ d->fill = s->fill;
+ d->flags = s->flags;
+ d->start = s->start;
+ d->runs.len = s->runs.len;
+ d->runs.base = s->runs.base;
+ d->runs.pen = s->runs.pen;
+ d->runs.count = s->runs.count;
+ dst->kind = ROW_TEXT;
+ dst->flags = (src->flags & (RF_WRAPPED | RF_WIDE)) | RF_TAIL_PADDED;
+ } else {
+ srow_settle(src, old_cols);
+ if (new_len > 0)
+ memcpy(dst->cells, src->cells, (size_t)new_len * sizeof(struct cell));
+ dst->kind = ROW_CELLS;
+ dst->flags = (src->flags & (RF_WRAPPED | RF_WIDE | RF_CLUSTER)) | RF_TAIL_PADDED;
+ }
+ dst->len = (uint16_t)new_len;
+ dst->uid = src->uid;
+ dst->version = src->version + 1;
+}
+
+static bool term_resize_internal(struct term *t, int cols, int rows)
+{
+ term_gstore_pin(t->gstore);
+
+ if (t->alternate || cols == t->scr.cols) {
+ struct screen *active_scr = term_active_screen(t);
+ int old_cols = active_scr->cols;
+ int old_rows = active_scr->rows;
+
+ if (rows < old_rows && t->cursor_y >= rows) {
+ int shift = t->cursor_y - rows + 1;
+ for (int i = 0; i < shift; i++)
+ term_screen_scroll_up(t);
+ t->cursor_y -= shift;
+ t->saved.y = t->saved.y >= shift ? t->saved.y - shift : 0;
+ }
+
+ struct screen new_scr;
+ if (!resize_target(t, &new_scr, cols, rows, active_scr)) {
+ term_gstore_unpin(t->gstore);
+ return false;
+ }
+
+ int pull = 0;
+ if (rows > old_rows && !t->alternate)
+ pull = pull_history_rows(t, &new_scr, cols, rows - old_rows);
+
+ int y = pull;
+ for (int i = 0; i < old_rows && y < rows; i++, y++) {
+ struct srow *old_r = screen_get_row(active_scr, i);
+ copy_srow_resized(&new_scr.slot[y], old_r, old_cols, cols);
+ }
+ int first_new = y;
+
+ t->spare = *active_scr;
+ *active_scr = new_scr;
+ t->cols = cols;
+ t->rows = rows;
+
+ if (rows != old_rows) {
+ for (y = first_new; y < rows; y++)
+ erase_row(t, phys_row(t, y));
+ t->cursor_y += pull;
+ t->saved.y += pull;
+ if (!t->alternate && t->prompt_y >= 0)
+ t->prompt_y += pull;
+ }
+
+ if (cols != old_cols)
+ t->pending_wrap = false;
+ if (t->cursor_x >= cols) t->cursor_x = cols - 1;
+ if (t->cursor_y >= rows) t->cursor_y = rows - 1;
+ if (t->saved.x >= cols) t->saved.x = cols - 1;
+ if (t->saved.y >= rows) t->saved.y = rows - 1;
+ if (!t->alternate && t->prompt_y >= rows) {
+ t->prompt_y = -1;
+ t->has_prompt = false;
+ }
+
+ t->scroll_top = 0;
+ t->scroll_bottom = rows - 1;
+ t->left_margin = 0;
+ t->right_margin = cols - 1;
+ term_hist_view_rewrap(&t->view, &t->hist, cols);
+ t->full_gen++;
+ term_gstore_unpin(t->gstore);
+ return true;
+ }
+
+ int old_cols = t->scr.cols;
+ int old_rows = t->scr.rows;
+
+ struct screen new_scr;
+ if (!resize_target(t, &new_scr, cols, rows, &t->scr)) {
+ term_gstore_unpin(t->gstore);
+ return false;
+ }
+
+ int active = old_rows;
+ while (active > t->cursor_y + 1 && active > t->saved.y + 1) {
+ struct srow *sr = screen_get_row(&t->scr, active - 1);
+ if (sr->len || (sr->flags & RF_WRAPPED))
+ break;
+ active--;
+ }
+
+ struct reflow_target r = {
+ .cols = cols,
+ .rows = rows,
+ .cells = grow(&t->resize.row, (size_t)cols * sizeof(struct cell)),
+ .out_buf = &t->resize.rows,
+ .cells_buf = &t->resize.cells,
+ };
+ if (!r.cells) {
+ t->spare = new_scr;
+ term_gstore_unpin(t->gstore);
+ return false;
+ }
+ /* The open history line reflows with the screen. It is read here and
+ * removed only after the reflow succeeds. The prompt line never rewraps. */
+ if (hist_is_open(&t->hist) && t->prompt_y != 0) {
+ uint64_t n = term_hist_line_cells(&t->hist, hist_end_id(&t->hist) - 1);
+ struct cell *pulled = n ? grow(&t->resize.pull, n * sizeof(*pulled)) : NULL;
+ if (n && !pulled)
+ goto reflow_failed;
+ int got = term_hist_peek_last_line(&t->hist, pulled, pulled ? (int)n : 0, t->gstore);
+ if (pulled && got > 0 && !reflow_append(&r, pulled, got, -1, -1))
+ goto reflow_failed;
+ }
+
+ size_t line_cap = (size_t)old_cols + (size_t)t->saved.x + 2;
+ struct cell *line_buf = grow(&t->resize.line, line_cap * sizeof(struct cell));
+
+ for (int y = 0; y < active; y++) {
+ struct srow *src = screen_get_row(&t->scr, y);
+ int len = src->len;
+ bool wrapped = (src->flags & RF_WRAPPED) != 0;
+
+ if (src->kind != ROW_CELLS)
+ srow_settle(src, old_cols);
+ const struct cell *cells = src->cells;
+
+ if (t->prompt_y >= 0 && y >= t->prompt_y) {
+ if (r.x && !reflow_target_end_row(&r, false))
+ goto reflow_failed;
+ if (y == t->prompt_y) {
+ r.prompt_y = r.count;
+ r.prompt_set = true;
+ }
+ if (y == t->cursor_y) {
+ r.cursor_y = r.count;
+ r.cursor_x = t->cursor_x < cols ? t->cursor_x : cols - 1;
+ r.pending_wrap = false;
+ r.cursor_set = true;
+ }
+ if (y == t->saved.y) {
+ r.saved_y = r.count;
+ r.saved_x = t->saved.x < cols ? t->saved.x : cols - 1;
+ r.saved_set = true;
+ }
+ int keep = len > cols ? cols : len;
+ memcpy(r.cells, cells, (size_t)keep * sizeof(struct cell));
+ r.x = keep;
+ if (!reflow_target_end_row(&r, wrapped))
+ goto reflow_failed;
+ continue;
+ }
+
+ if (wrapped && len < old_cols)
+ len = old_cols;
+ if (y == t->cursor_y) {
+ int pos = t->cursor_x + (t->pending_wrap ? 1 : 0);
+ if (len < pos)
+ len = pos;
+ }
+ if (y == t->saved.y && len <= t->saved.x)
+ len = t->saved.x + 1;
+
+ if (len > src->len && src->len < old_cols && !(src->flags & RF_TAIL_PADDED)) {
+ fill_cells(src->cells + src->len, old_cols - src->len,
+ TERM_COLOR_FG, TERM_COLOR_BG);
+ src->flags |= RF_TAIL_PADDED;
+ }
+
+ const struct cell *in_cells = cells;
+ if (len > old_cols && line_buf && (size_t)len <= line_cap) {
+ memcpy(line_buf, cells, (size_t)old_cols * sizeof(struct cell));
+ fill_cells(line_buf + old_cols, len - old_cols, TERM_COLOR_FG, TERM_COLOR_BG);
+ in_cells = line_buf;
+ } else if (len > old_cols) {
+ len = old_cols;
+ }
+
+ int cur_x = (y == t->cursor_y) ? t->cursor_x + (t->pending_wrap ? 1 : 0) : -1;
+ int sav_x = (y == t->saved.y) ? t->saved.x : -1;
+ if (!reflow_append(&r, in_cells, len, cur_x, sav_x))
+ goto reflow_failed;
+
+ if ((!wrapped || y + 1 == active) && !reflow_target_end_row(&r, false))
+ goto reflow_failed;
+ }
+ if (r.x && !reflow_target_end_row(&r, false))
+ goto reflow_failed;
+
+ /* The reflow succeeded. From here the history and the screen change, and
+ * nothing below can fail. */
+ if (hist_is_open(&t->hist)) {
+ if (t->prompt_y == 0)
+ term_hist_close_open_line(&t->hist);
+ else
+ term_hist_pop_line(&t->hist, NULL, 0, t->gstore);
+ }
+ term_hist_set_width(&t->hist, cols);
+
+ int visible = rows - (cols > old_cols ? old_rows - active : 0);
+ if (visible < 1)
+ visible = 1;
+
+ int screen_start = 0;
+ int pull = 0;
+ if (r.count > visible) {
+ screen_start = r.count - visible;
+ if (screen_start > r.cursor_y)
+ screen_start = r.cursor_y;
+ for (int i = 0; i < screen_start; i++) {
+ int push_len = (r.out[i].wrapped && r.out[i].len < cols) ? cols : r.out[i].len;
+ term_hist_push_row(&t->hist, reflow_out_row_cells(&r, i), push_len,
+ r.out[i].wrapped, t->gstore);
+ }
+ } else if (r.count < visible && t->hist.lines > 0) {
+ int want = visible - r.count;
+ if (want > rows - r.count)
+ want = rows - r.count;
+ pull = pull_history_rows(t, &new_scr, cols, want);
+ }
+
+ int out_idx = 0;
+ for (int i = screen_start; i < r.count && (pull + out_idx) < rows; i++) {
+ struct srow *dst = &new_scr.slot[pull + out_idx++];
+ int len = r.out[i].len;
+ memcpy(dst->cells, reflow_out_row_cells(&r, i), (size_t)len * sizeof(struct cell));
+ if (len < cols)
+ fill_cells(dst->cells + len, cols - len, TERM_COLOR_FG, TERM_COLOR_BG);
+ dst->kind = ROW_CELLS;
+ dst->len = (uint16_t)len;
+ dst->flags = (r.out[i].wrapped ? RF_WRAPPED : 0) | RF_TAIL_PADDED;
+ dst->version = 1;
+ dst->uid = ++t->next_row_uid;
+ }
+
+ t->spare = t->scr;
+ t->scr = new_scr;
+ t->cols = cols;
+ t->rows = rows;
+ t->cursor_x = r.cursor_x < cols ? r.cursor_x : cols - 1;
+ t->cursor_y = (r.cursor_y - screen_start) + pull;
+ if (t->cursor_y < 0) t->cursor_y = 0;
+ if (t->cursor_y >= rows) t->cursor_y = rows - 1;
+ t->pending_wrap = r.pending_wrap && t->autowrap;
+
+ if (t->prompt_y >= 0) {
+ int new_prompt = (r.prompt_y - screen_start) + pull;
+ if (!r.prompt_set || new_prompt >= rows) {
+ t->prompt_y = -1;
+ t->has_prompt = false;
+ } else {
+ t->prompt_y = new_prompt > 0 ? new_prompt : 0;
+ t->has_prompt = true;
+ }
+ }
+
+ if (r.saved_set && (r.saved_y - screen_start) + pull < rows) {
+ t->saved.x = r.saved_x < cols ? r.saved_x : cols - 1;
+ int new_saved_y = (r.saved_y - screen_start) + pull;
+ t->saved.y = new_saved_y < 0 ? 0 : new_saved_y;
+ } else {
+ t->saved.y = rows - 1;
+ }
+ if (t->saved.x >= cols) t->saved.x = cols - 1;
+ if (t->saved.y >= rows) t->saved.y = rows - 1;
+
+ t->scroll_top = 0;
+ t->scroll_bottom = rows - 1;
+ t->left_margin = 0;
+ t->right_margin = cols - 1;
+ term_hist_view_rewrap(&t->view, &t->hist, cols);
+ t->full_gen++;
+ term_gstore_unpin(t->gstore);
+ return true;
+
+reflow_failed:
+ t->spare = new_scr;
+ term_gstore_unpin(t->gstore);
+ return false;
+}
+
+bool term_resize(struct term *t, struct term_vector2i size)
+{
+ if (!t)
+ return false;
+ int cols = size.x;
+ int rows = size.y;
+ if (cols < 1 || rows < 1)
+ return false;
+ if (cols > TERM_MAX_COLS)
+ cols = TERM_MAX_COLS;
+ term_write_begin(t);
+ if (cols == t->cols && rows == t->rows) {
+ term_write_end(t);
+ return true;
+ }
+
+ if (cols > t->tabs_cap) {
+ int new_cap = cols + 64;
+ uint8_t *new_tabs = realloc(t->tabs, (size_t)new_cap);
+ if (!new_tabs) {
+ term_write_end(t);
+ return false;
+ }
+ memset(new_tabs + t->tabs_cap, 0, (size_t)(new_cap - t->tabs_cap));
+ int first_tab = (t->tabs_cap + 7) & ~7;
+ if (first_tab == 0)
+ first_tab = 8;
+ for (int c = first_tab; c < new_cap; c += 8)
+ new_tabs[c] = 1;
+ t->tabs = new_tabs;
+ t->tabs_cap = new_cap;
+ }
+ if (cols > t->view_scratch_cap) {
+ int new_cap = cols + 64;
+ struct cell *new_scratch = realloc(t->view_row_scratch, (size_t)new_cap * sizeof(struct cell));
+ if (!new_scratch) {
+ term_write_end(t);
+ return false;
+ }
+ memset(new_scratch + t->view_scratch_cap, 0, (size_t)(new_cap - t->view_scratch_cap) * sizeof(struct cell));
+ t->view_row_scratch = new_scratch;
+ t->view_scratch_cap = new_cap;
+ }
+
+ bool ok = term_resize_internal(t, cols, rows);
+ term_write_end(t);
+ return ok;
+}
diff --git a/src/screen.h b/src/screen.h
new file mode 100644
index 0000000..0110049
--- /dev/null
+++ b/src/screen.h
@@ -0,0 +1,453 @@
+#ifndef TERM_SCREEN_H
+#define TERM_SCREEN_H
+
+#include <assert.h>
+#include "compat.h"
+#include "internal.h"
+#include "kernels.h"
+#include "simd.h"
+
+void term_screen_scroll_region(struct term *t, int top, int bottom, int count, bool down, bool history);
+void term_screen_set_alternate(struct term *t, bool enabled);
+void term_screen_erase(struct term *t, int mode);
+void term_screen_erase_all(struct term *t);
+void term_screen_erase_line(struct term *t, int mode);
+void term_screen_delete_chars(struct term *t, int count);
+void term_screen_insert_chars(struct term *t, int count);
+
+const struct cell *term_ring_row(const struct term *t, int row);
+const struct cell *term_screen_view_row(const struct term *t, int y);
+uint64_t term_screen_view_row_key(const struct term *t, int y);
+/* The selected columns [c0, c1] of each view row; c1 < c0 when none. */
+void term_selection_view_spans(const struct term *t, int *c0, int *c1);
+
+int term_screen_history_rows(const struct term *t);
+
+/* Rows of history above the live screen. The view owns the offset. */
+int term_view_offset(const struct term *t);
+
+static inline struct screen *term_active_screen(const struct term *t)
+{
+ return (struct screen *)&t->scr;
+}
+
+static inline bool screen_has_history(const struct term *t)
+{
+ return !t->alternate && t->hist.line_limit > 0;
+}
+
+static inline int phys_row(const struct term *t, int y)
+{
+ int p = t->scr.top + y;
+ return p >= t->scr.rows ? p - t->scr.rows : p;
+}
+
+static inline void init_ascii_text_row(struct text_row *tr, uint8_t *text, uint16_t len)
+{
+ tr->text = text;
+ tr->text_len = tr->cells = len;
+ tr->wide = tr->fill_to = 0;
+ tr->flags = HLINE_NARROW;
+ tr->runs.len = 0;
+ tr->runs.count = len;
+#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__)
+ static_assert(sizeof(struct pen_attr) == 12,
+ "a 16-byte store writes one pen and 4 spare bytes");
+ static_assert(offsetof(struct hist_runs, base) + 12 == offsetof(struct hist_runs, pen),
+ "the spare bytes of the store at base land on pen");
+ static_assert(offsetof(struct hist_runs, pen) + 16 <= sizeof(struct hist_runs),
+ "the store at pen stays inside hist_runs");
+ static_assert(offsetof(struct text_row, start) + 16 <= offsetof(struct text_row, runs),
+ "the store at start stays before runs");
+#endif
+ /* The extra lanes cover unused fill data and run padding. */
+#if defined(__x86_64__) || defined(_M_X64)
+ __m128i pen = _mm_setr_epi32(TERM_COLOR_FG, TERM_COLOR_BG, 0, TERM_COLOR_FG);
+ _mm_storeu_si128((__m128i *)&tr->start, pen);
+ _mm_storeu_si128((__m128i *)&tr->runs.base, pen);
+ _mm_storeu_si128((__m128i *)&tr->runs.pen, pen);
+#elif defined(__aarch64__)
+ uint32x4_t pen = { TERM_COLOR_FG, TERM_COLOR_BG, 0, TERM_COLOR_FG };
+ vst1q_u32((uint32_t *)&tr->start, pen);
+ vst1q_u32((uint32_t *)&tr->runs.base, pen);
+ vst1q_u32((uint32_t *)&tr->runs.pen, pen);
+#else
+ tr->start = tr->runs.base = tr->runs.pen = pen_attr_default();
+#endif
+}
+
+static inline void srow_materialize_text(struct srow *row)
+{
+ if (row->kind == ROW_ASCII) {
+ init_ascii_text_row(&row->text, row->text.text, row->len);
+ row->kind = ROW_TEXT;
+ }
+}
+
+static inline struct srow *screen_get_row(const struct screen *s, int y)
+{
+ int p = s->top + y;
+ struct srow *row = &s->slot[p >= s->rows ? p - s->rows : p];
+ srow_materialize_text(row);
+ return row;
+}
+
+/* The row at y, or NULL when y is outside the screen. A view row that
+ * resolves to no screen row must not index the ring. */
+static inline struct srow *screen_row_at(const struct screen *s, int y)
+{
+ if (y < 0 || y >= s->rows)
+ return NULL;
+ return screen_get_row(s, y);
+}
+
+static inline struct srow *term_row(const struct term *t, int y)
+{
+ int p = t->scr.top + y;
+ struct srow *row = &t->scr.slot[p >= t->scr.rows ? p - t->scr.rows : p];
+ srow_materialize_text(row);
+ return row;
+}
+
+static inline struct srow *term_screen_row(const struct term *t, int y)
+{
+ return term_row(t, y);
+}
+
+/* Write the cells of a text row to out: its text, then its fill blanks.
+ * Return the cells written. */
+static inline int text_row_decode(const struct srow *row, struct cell *out, int cols)
+{
+ srow_materialize_text((struct srow *)row);
+ const struct text_row *tr = &row->text;
+ int n = term_hist_text_decode(tr, out, cols, NULL);
+ int fill = tr->fill_to < cols ? tr->fill_to : cols;
+ if (n < fill) {
+ fill_cells(out + n, fill - n, tr->fill.fg, tr->fill.bg);
+ n = fill;
+ }
+ return n;
+}
+
+/* The cells of a text row, cols wide, for a reader. */
+static inline void text_row_cells(const struct srow *row, struct cell *out, int cols)
+{
+ int n = text_row_decode(row, out, cols);
+ if (n < cols)
+ fill_cells(out + n, cols - n, TERM_COLOR_FG, TERM_COLOR_BG);
+}
+
+/* Turn a text row into a cells row. Only the write core does this. Its
+ * cells past the text are still blank when RF_TAIL_PADDED says so. */
+static inline void srow_settle(struct srow *row, int cols)
+{
+ srow_materialize_text(row);
+ if (row->kind == ROW_TEXT) {
+ int n = text_row_decode(row, row->cells, cols);
+ if (n < cols && !(row->flags & RF_TAIL_PADDED))
+ fill_cells(row->cells + n, cols - n, TERM_COLOR_FG, TERM_COLOR_BG);
+ row->flags |= RF_TAIL_PADDED;
+ row->kind = ROW_CELLS;
+ }
+}
+
+/*
+ * The cursor row as a row that text can append to: a text row with the
+ * cursor at its end, or an empty row on the bottom line of a full-screen
+ * region with history, which becomes a text row. Rows there scroll into
+ * history, so they keep the history format. NULL when neither holds. The
+ * caller has handled a pending wrap, insert mode, and margins.
+ */
+static inline struct srow *text_append_row(struct term *t)
+{
+ struct srow *row = term_row(t, t->cursor_y);
+ if (row->kind == ROW_TEXT) {
+ text_row_unshare(&row->text);
+ if (t->cursor_x != row->text.cells)
+ return NULL;
+ if (row->text.cells)
+ return row;
+ } else if (row->len != 0 || t->cursor_x != 0 || t->cursor_y != t->rows - 1 ||
+ t->scroll_top != 0 || t->scroll_bottom != t->rows - 1 || !screen_has_history(t)) {
+ return NULL;
+ }
+ /* A row that continues a text row starts from that row's last pen, so
+ * history can copy its runs. */
+ struct pen_attr start = pen_attr_default();
+ if (t->cursor_y > 0) {
+ const struct srow *prev = term_row(t, t->cursor_y - 1);
+ if (prev->kind == ROW_TEXT && (prev->flags & RF_WRAPPED))
+ start = text_row_last_pen(&prev->text);
+ }
+ if (row->kind == ROW_TEXT) {
+ uint32_t sl = pen_sl(&start);
+ pen_set(&row->text.start, start.fg, start.bg, sl);
+ pen_set(&row->text.runs.base, start.fg, start.bg, sl);
+ pen_set(&row->text.runs.pen, start.fg, start.bg, sl);
+ return row;
+ }
+ row->kind = ROW_TEXT;
+ text_row_start(&row->text, &start);
+ return row;
+}
+
+/* Take nbytes bytes that a writer put at the end of the row text: chars
+ * characters in cells columns, with the pen fg, bg, sl (flags | link << 16).
+ * Runs count characters. */
+static inline void text_commit(struct srow *row, int nbytes, int chars, int cells, uint8_t hflags,
+ uint32_t fg, uint32_t bg, uint32_t sl)
+{
+ struct text_row *tr = &row->text;
+ tr->text_len = (uint16_t)(tr->text_len + nbytes);
+ tr->cells = (uint16_t)(tr->cells + cells);
+ tr->wide = (uint16_t)(tr->wide + cells - chars);
+ tr->flags = (uint8_t)((tr->flags & hflags & HLINE_NARROW) |
+ ((tr->flags | hflags) & ~HLINE_NARROW) |
+ ((sl >> 16) ? HLINE_HAS_LINK : 0));
+ hist_runs_add_pen(&tr->runs, fg, bg, sl, (uint32_t)chars);
+ if (tr->cells > row->len)
+ row->len = tr->cells;
+ row->version++;
+ if (hflags & HLINE_HAS_WIDE)
+ row->flags |= RF_WIDE;
+}
+
+static inline void text_append(struct srow *row, const uint8_t *bytes, int nbytes, int chars,
+ int cells, uint8_t hflags, uint32_t fg, uint32_t bg, uint32_t sl)
+{
+ bytes_copy_small(row->text.text + row->text.text_len, (const char *)bytes, nbytes);
+ text_commit(row, nbytes, chars, cells, hflags, fg, bg, sl);
+}
+
+static inline struct cell *row_cells(const struct term *t, int p)
+{
+ struct srow *row = &t->scr.slot[p];
+ if (row->kind != ROW_CELLS)
+ srow_settle(row, t->cols);
+ return row->cells;
+}
+
+/* Screen rows carry no line ids. A row's logical line follows from the
+ * history tail and the RF_WRAPPED chain above it, so a scroll, a wrap change,
+ * or a resize can never leave a stale id behind. */
+
+/* The logical line of screen row 0, and the row's segment in it. */
+static inline struct line_pos screen_top_line(const struct term *t)
+{
+ if (!screen_has_history(t))
+ return (struct line_pos){ t->scr.top_id, t->scr.top_seg };
+ uint64_t end = hist_end_id(&t->hist);
+ if (!hist_is_open(&t->hist))
+ return (struct line_pos){ end, 0 };
+ return (struct line_pos){ end - 1, (uint32_t)term_hist_line_rows(&t->hist, end - 1, t->cols) };
+}
+
+static inline struct line_pos line_pos_next(struct line_pos p, const struct srow *row)
+{
+ return (row->flags & RF_WRAPPED) ? (struct line_pos){ p.id, p.seg + 1 }
+ : (struct line_pos){ p.id + 1, 0 };
+}
+
+/* The oldest line that history or the screen still holds. */
+static inline uint64_t first_retained_line(const struct term *t)
+{
+ if (screen_has_history(t) && t->hist.lines)
+ return hist_first_id(&t->hist);
+ return screen_top_line(t).id;
+}
+
+static inline void row_set_wrapped(struct srow *row, bool wrapped)
+{
+ row->flags = wrapped ? (row->flags | RF_WRAPPED) : (row->flags & ~RF_WRAPPED);
+}
+
+static inline void clear_wide_edges(struct cell *row, int from, int to, int len)
+{
+ if (from >= to) return;
+ if (from > 0 && from < len && (row[from].flags & CELL_TRAIL))
+ fill_cells(row + from - 1, 1, row[from - 1].fg, row[from - 1].bg);
+ if (to < len && (row[to].flags & CELL_TRAIL))
+ fill_cells(row + to, 1, row[to].fg, row[to].bg);
+}
+
+static inline void repair_wide_row(struct cell *row, int from, int to, int cols, int len)
+{
+ if (from < 0) from = 0;
+ if (to > len) to = len;
+ if (to > cols) to = cols;
+ for (int x = from; x < to; x++) {
+ if ((row[x].flags & CELL_TRAIL) &&
+ (x == 0 || !(row[x - 1].flags & CELL_WIDE)))
+ fill_cells(row + x, 1, row[x].fg, row[x].bg);
+ if ((row[x].flags & CELL_WIDE) && cols > 1 &&
+ (x + 1 == cols || x + 1 >= len || !(row[x + 1].flags & CELL_TRAIL)))
+ fill_cells(row + x, 1, row[x].fg, row[x].bg);
+ }
+}
+
+static inline bool using_default_colors(const struct term *t)
+{
+ return t->cur_fg == TERM_COLOR_FG && t->cur_bg == TERM_COLOR_BG;
+}
+
+static inline void mark_row_blank(struct term *t, int p, uint32_t fg, uint32_t bg)
+{
+ struct srow *row = &t->scr.slot[p];
+ uint8_t tail_padded = ((row->kind == ROW_CELLS && row->len == 0) || row->kind == ROW_TEXT)
+ ? (row->flags & RF_TAIL_PADDED) : 0;
+ row->kind = ROW_CELLS;
+ row->len = 0;
+ row->flags = tail_padded;
+ row->version++;
+ (void)fg;
+ (void)bg;
+}
+
+static inline void row_fill_gap(struct term *t, int p, struct cell *row, int x)
+{
+ struct screen *s = term_active_screen(t);
+ struct srow *r = &s->slot[p];
+ if (x > r->len) {
+ if (!(r->flags & RF_TAIL_PADDED))
+ fill_cells(row + r->len, x - r->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ r->len = (uint16_t)x;
+ }
+}
+
+static inline void term_row_fill_gap(struct srow *row, int x)
+{
+ if (x > row->len) {
+ if (!(row->flags & RF_TAIL_PADDED))
+ fill_cells(row->cells + row->len, x - row->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ row->len = (uint16_t)x;
+ }
+}
+
+static inline void write_cell_span(struct cell *row, int x,
+ struct cell cell, int span)
+{
+ row[x] = cell;
+ if (span == 2) {
+ row[x + 1] = cell;
+ row[x + 1].c = 0;
+ row[x + 1].flags = (uint16_t)((cell.flags & ~CELL_WIDE) | CELL_TRAIL);
+ }
+}
+
+static inline void erase_span(struct term *t, int p, int from, int to)
+{
+ bool pristine = using_default_colors(t);
+ struct screen *s = term_active_screen(t);
+ struct srow *row = &s->slot[p];
+ int len = row->len;
+ if (to > TERM_MAX_COLS) to = TERM_MAX_COLS;
+ if (pristine && to > len) to = len;
+ if (from >= to) return;
+
+ if (pristine && from == 0 && to >= len) {
+ mark_row_blank(t, p, t->cur_fg, t->cur_bg);
+ return;
+ }
+ struct cell *cells = row_cells(t, p);
+ if (from > 0 && from < len && (cells[from].flags & CELL_TRAIL)) from--;
+ if (to < len && (cells[to].flags & CELL_TRAIL)) to++;
+ if (to >= t->cols)
+ row_set_wrapped(row, false);
+
+ if (!pristine) {
+ row_fill_gap(t, p, cells, from);
+ fill_cells(cells + from, to - from, t->cur_fg, t->cur_bg);
+ if (to > len) {
+ row->len = (uint16_t)to;
+ if (to < t->cols)
+ row->flags &= ~RF_TAIL_PADDED;
+ }
+ row->version++;
+ return;
+ }
+ if (to < len) {
+ fill_cells(cells + from, to - from, t->cur_fg, t->cur_bg);
+ row->version++;
+ return;
+ }
+ if (from == 0) {
+ mark_row_blank(t, p, t->cur_fg, t->cur_bg);
+ } else {
+ row->len = (uint16_t)from;
+ row->flags &= ~RF_TAIL_PADDED;
+ row->version++;
+ }
+}
+
+static inline int erase_width(const struct term *t, int p)
+{
+ const struct screen *s = term_active_screen(t);
+ return s->slot[p].len > t->cols ? s->slot[p].len : t->cols;
+}
+
+static inline void erase_row(struct term *t, int p)
+{
+ erase_span(t, p, 0, erase_width(t, p));
+}
+
+static inline void clamp_selection_after_scroll(struct term *t)
+{
+ if (TERM_EXPECT(!t->selection, 1))
+ return;
+ uint64_t first_id = first_retained_line(t);
+ if (t->sel_end.line_id < first_id) {
+ t->selection = false;
+ } else if (t->sel_start.line_id < first_id) {
+ t->sel_start.line_id = first_id;
+ t->sel_start.cell_off = 0;
+ }
+}
+
+/* Move a row that leaves the top of the normal screen into history. */
+void term_screen_push_row(struct term *t, struct srow *row);
+
+/* Count a row that leaves the top of a screen without history. */
+static inline void screen_drop_top(struct screen *s, const struct srow *row)
+{
+ struct line_pos p = line_pos_next((struct line_pos){ s->top_id, s->top_seg }, row);
+ s->top_id = p.id;
+ s->top_seg = p.seg;
+}
+
+/* Clear a row that enters at the bottom. With a colored pen it is a text row
+ * that is all fill: blanks in the pen colors, which text can append over. */
+static inline void screen_new_row(struct term *t, struct srow *row)
+{
+ row->uid = ++t->next_row_uid;
+ row->version++;
+ row->flags = ((row->kind == ROW_CELLS && row->len == 0) || row->kind == ROW_TEXT)
+ ? (row->flags & RF_TAIL_PADDED) : 0;
+ if (TERM_EXPECT(using_default_colors(t), 1)) {
+ row->kind = ROW_CELLS;
+ row->len = 0;
+ } else {
+ row->kind = ROW_TEXT;
+ text_row_start(&row->text, &(struct pen_attr){ .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG });
+ row->text.fill_to = (uint16_t)t->scr.cols;
+ row->text.fill = (struct pen_attr){ .fg = t->cur_fg, .bg = t->cur_bg };
+ row->len = (uint16_t)t->scr.cols;
+ }
+}
+
+static inline void term_screen_scroll_up(struct term *t)
+{
+ struct screen *s = &t->scr;
+ struct srow *row = &s->slot[s->top];
+ if (TERM_EXPECT(screen_has_history(t), 1))
+ term_screen_push_row(t, row);
+ else
+ screen_drop_top(s, row);
+ if (TERM_EXPECT(t->prompt_y > 0, 0) && !t->alternate)
+ t->prompt_y--;
+ s->top = s->top + 1 == s->rows ? 0 : s->top + 1;
+ clamp_selection_after_scroll(t);
+ screen_new_row(t, row);
+ t->full_gen++;
+}
+
+#endif
diff --git a/src/selection.c b/src/selection.c
new file mode 100644
index 0000000..6f7336f
--- /dev/null
+++ b/src/selection.c
@@ -0,0 +1,824 @@
+#include "screen.h"
+#include "osc/osc.h"
+#include "unicode/grapheme.h"
+#include "unicode/utf8.h"
+#include <stdlib.h>
+#include <string.h>
+
+/* Copies a heap string into buf and frees it. Returns the length without the NUL. */
+static size_t text_out(char *text, char *buf, size_t cap)
+{
+ size_t n = term_copy_out(text, buf, cap);
+ free(text);
+ return n;
+}
+
+static char *link_at_text(const struct term *t, struct term_vector2i cell)
+{
+ int col = cell.x, row = cell.y;
+ if (!t || col < 0 || col >= t->cols || row < 0 || row >= t->rows) return NULL;
+ const char *link = term_osc8_url_at(t, col, row);
+ if (link) return term_strdup(link);
+ const struct cell *cells = term_screen_view_row(t, row);
+ if (!cells) return NULL;
+ int start = col, end = col;
+ while (start > 0 && cells[start - 1].c > ' ' && cells[start - 1].c < 127) start--;
+ while (end < t->cols && cells[end].c > ' ' && cells[end].c < 127) end++;
+ char *text = malloc((size_t)(end - start) + 1);
+ if (!text) return NULL;
+ for (int c = start; c < end; c++) text[c - start] = cells[c].c;
+ text[end - start] = 0;
+ size_t scheme = 8;
+ char *url = strstr(text, "https://");
+ if (!url) {
+ scheme = 7;
+ url = strstr(text, "http://");
+ if (!url) url = strstr(text, "file://");
+ }
+ char *out = NULL;
+ if (url && col >= start + (int)(url - text)) {
+ size_t n = strlen(url);
+ while (n && strchr(".,;:!?)]}>\"'", url[n - 1])) url[--n] = 0;
+ if (col < start + (int)(url - text) + (int)n && n > scheme)
+ out = term_strdup(url);
+ }
+ free(text);
+ return out;
+}
+
+size_t term_link_at(const struct term *t, struct term_vector2i cell, char *buf, size_t cap)
+{
+ if (!t)
+ return 0;
+ term_write_begin((struct term *)t);
+ char *text = link_at_text(t, cell);
+ term_write_end((struct term *)t);
+ return text_out(text, buf, cap);
+}
+
+void term_select_clear(struct term *t)
+{
+ if (t) {
+ term_write_begin(t);
+ t->selection = false;
+ term_write_end(t);
+ }
+}
+
+static inline int word_class(uint32_t c)
+{
+ if (c <= ' ') return 0;
+ if (c < 128) {
+ static const uint64_t mask[2] = {
+ 0x03FF000000000000ULL,
+ 0x07FFFFFE87FFFFFEULL
+ };
+ return (mask[c >> 6] & (1ULL << (c & 63))) ? 1 : 2;
+ }
+ return 1;
+}
+
+static inline int sel_point_cmp(struct term_sel_point a, struct term_sel_point b)
+{
+ if (a.line_id < b.line_id) return -1;
+ if (a.line_id > b.line_id) return 1;
+ if (a.cell_off < b.cell_off) return -1;
+ if (a.cell_off > b.cell_off) return 1;
+ return 0;
+}
+
+/*
+ * The lines that one call reads. History holds every row of its closed
+ * lines and the first rows of the open line. The screen holds the rest; pos
+ * gives the line and segment of each screen row.
+ */
+struct lines {
+ const struct term *t;
+ int cols;
+ bool hist;
+ uint64_t hist_end;
+ uint64_t first;
+ struct line_pos *pos;
+ struct line_pos stack[256];
+};
+
+static void lines_init(struct lines *L, const struct term *t)
+{
+ L->t = t;
+ L->cols = t->cols;
+ L->hist = screen_has_history(t);
+ L->hist_end = L->hist ? hist_end_id(&t->hist) : 0;
+ L->first = first_retained_line(t);
+ L->pos = t->rows <= 256 ? L->stack : malloc((size_t)t->rows * sizeof(*L->pos));
+ if (!L->pos)
+ L->pos = L->stack;
+ int rows = L->pos == L->stack && t->rows > 256 ? 256 : t->rows;
+ struct line_pos p = screen_top_line(t);
+ for (int y = 0; y < rows; y++) {
+ L->pos[y] = p;
+ p = line_pos_next(p, screen_get_row(term_active_screen(t), y));
+ }
+}
+
+static void lines_free(struct lines *L)
+{
+ if (L->pos != L->stack)
+ free(L->pos);
+}
+
+static inline int lines_rows(const struct lines *L)
+{
+ return L->pos == L->stack && L->t->rows > 256 ? 256 : L->t->rows;
+}
+
+static inline bool in_hist(const struct lines *L, uint64_t id)
+{
+ return L->hist && id >= hist_first_id(&L->t->hist) && id < L->hist_end;
+}
+
+static inline bool is_open_line(const struct lines *L, uint64_t id)
+{
+ return L->hist && id + 1 == L->hist_end && hist_is_open(&L->t->hist);
+}
+
+static inline int hist_segs(const struct lines *L, uint64_t id)
+{
+ return in_hist(L, id) ? term_hist_line_rows(&L->t->hist, id, L->cols) : 0;
+}
+
+static int screen_row_of(const struct lines *L, uint64_t id, int seg)
+{
+ int rows = lines_rows(L);
+ for (int y = 0; y < rows; y++)
+ if (L->pos[y].id == id && (int)L->pos[y].seg == seg)
+ return y;
+ return -1;
+}
+
+/* Segments of line id across history and the screen. */
+static int line_total_segs(const struct lines *L, uint64_t id)
+{
+ int segs = hist_segs(L, id);
+ int rows = lines_rows(L);
+ for (int y = 0; y < rows; y++)
+ if (L->pos[y].id == id && (int)L->pos[y].seg + 1 > segs)
+ segs = (int)L->pos[y].seg + 1;
+ return segs;
+}
+
+/* The cells of one segment, padded to the width. */
+static const struct cell *line_seg_cells(const struct lines *L, uint64_t id, int seg,
+ struct cell *scratch, bool *out_wrapped)
+{
+ const struct term *t = L->t;
+ struct cell *buf = scratch ? scratch : t->view_row_scratch;
+ int hrows = hist_segs(L, id);
+ if (seg < hrows) {
+ int got = term_hist_decode_segment(&t->hist, id, seg, L->cols, buf, t->gstore);
+ bool wrapped = seg + 1 < hrows || is_open_line(L, id);
+ if (got < L->cols) {
+ fill_cells(buf + got, L->cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ if (wrapped)
+ for (int x = got; x < L->cols; x++)
+ buf[x].flags |= CELL_WRAP_PAD;
+ }
+ if (out_wrapped)
+ *out_wrapped = wrapped;
+ return buf;
+ }
+ int y = screen_row_of(L, id, seg);
+ if (y < 0)
+ return NULL;
+ const struct srow *srow = screen_get_row(term_active_screen(t), y);
+ if (out_wrapped)
+ *out_wrapped = (srow->flags & RF_WRAPPED) != 0;
+ if (srow->kind == ROW_CELLS) {
+ if (srow->len >= L->cols || (srow->flags & RF_TAIL_PADDED))
+ return srow->cells;
+ if (srow->len > 0)
+ memcpy(buf, srow->cells, (size_t)srow->len * sizeof(struct cell));
+ fill_cells(buf + srow->len, L->cols - srow->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ return buf;
+ }
+ text_row_cells(srow, buf, L->cols);
+ return buf;
+}
+
+/* A line whose segments may not hold exactly cols cells each. */
+static bool line_has_wide(const struct lines *L, uint64_t id)
+{
+ const struct term *t = L->t;
+ if (in_hist(L, id)) {
+ if (term_hist_line_flags(&t->hist, id) & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER))
+ return true;
+ if (is_open_line(L, id) &&
+ term_hist_line_cells(&t->hist, id) != (uint64_t)hist_segs(L, id) * (uint64_t)L->cols)
+ return true;
+ }
+ int rows = lines_rows(L);
+ for (int y = 0; y < rows; y++) {
+ if (L->pos[y].id != id)
+ continue;
+ const struct srow *r = screen_get_row(term_active_screen(t), y);
+ if (r->flags & RF_WIDE)
+ return true;
+ if (r->kind == ROW_CELLS && r->len >= L->cols && L->cols > 1 &&
+ (r->cells[L->cols - 1].flags & CELL_WRAP_PAD))
+ return true;
+ }
+ return false;
+}
+
+static inline int seg_display_cols(int cols, const struct cell *cells, bool wrapped)
+{
+ if (cells && wrapped && cols > 1 && (cells[cols - 1].flags & CELL_WRAP_PAD))
+ return cols - 1;
+ return cols;
+}
+
+/* The display column where segment seg of line id starts. */
+static uint32_t line_seg_start_off(const struct lines *L, uint64_t id, int seg)
+{
+ if (seg <= 0)
+ return 0;
+ if (!line_has_wide(L, id))
+ return (uint32_t)seg * (uint32_t)L->cols;
+
+ const struct term *t = L->t;
+ int hrows = hist_segs(L, id);
+ if (in_hist(L, id)) {
+ if (seg <= hrows) {
+ uint64_t base = id == t->hist.base_cell_id ? t->hist.base_cell : 0;
+ return (uint32_t)(term_hist_segment_cell(&t->hist, id, seg, L->cols) - base);
+ }
+ }
+
+ uint32_t off = 0;
+ if (in_hist(L, id) && hrows > 0) {
+ uint64_t base = id == t->hist.base_cell_id ? t->hist.base_cell : 0;
+ off = (uint32_t)(term_hist_segment_cell(&t->hist, id, hrows, L->cols) - base);
+ }
+ struct cell stack_buf[512];
+ struct cell *scratch = L->cols <= 512 ? stack_buf : malloc((size_t)L->cols * sizeof(*scratch));
+ for (int s = hrows; s < seg; s++) {
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(L, id, s, scratch, &wrapped);
+ off += (uint32_t)seg_display_cols(L->cols, cells, wrapped);
+ }
+ if (scratch != stack_buf)
+ free(scratch);
+ return off;
+}
+
+/* The line and segment of view row y. */
+static bool view_row_line(const struct lines *L, int y, uint64_t *id, int *seg)
+{
+ const struct term *t = L->t;
+ int y_scr;
+ if (term_hist_view_row_pos(&t->view, &t->hist, L->cols, t->rows, screen_top_line(t), y,
+ id, seg, &y_scr))
+ return true;
+ if (y_scr >= lines_rows(L))
+ y_scr = lines_rows(L) - 1;
+ *id = L->pos[y_scr].id;
+ *seg = (int)L->pos[y_scr].seg;
+ return true;
+}
+
+static struct term_sel_point view_to_sel_point(const struct lines *L, int x, int y)
+{
+ const struct term *t = L->t;
+ if (x < 0) x = 0;
+ if (x >= L->cols) x = L->cols - 1;
+ if (y < 0) y = 0;
+ if (y >= t->rows) y = t->rows - 1;
+
+ uint64_t id;
+ int seg;
+ if (!view_row_line(L, y, &id, &seg))
+ return (struct term_sel_point){ .line_id = L->first };
+ const struct cell *cells = line_seg_cells(L, id, seg, NULL, NULL);
+ if (cells)
+ while (x > 0 && (cells[x].flags & CELL_WRAP_PAD))
+ x--;
+ return (struct term_sel_point){
+ .line_id = id,
+ .cell_off = line_seg_start_off(L, id, seg) + (uint32_t)x,
+ };
+}
+
+static uint32_t line_total_display_cols(const struct lines *L, uint64_t id, int nsegs)
+{
+ if (nsegs <= 0)
+ return 0;
+ uint32_t start = line_seg_start_off(L, id, nsegs - 1);
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(L, id, nsegs - 1, NULL, &wrapped);
+ return start + (uint32_t)seg_display_cols(L->cols, cells, wrapped);
+}
+
+static struct term_sel_point sel_point_next(const struct lines *L, struct term_sel_point pt)
+{
+ int nsegs = line_total_segs(L, pt.line_id);
+ if (nsegs <= 0)
+ return pt;
+ if (pt.cell_off + 1 < (uint64_t)(nsegs - 1) * (L->cols > 1 ? (uint64_t)(L->cols - 1) : 1ULL) ||
+ pt.cell_off + 1 < line_total_display_cols(L, pt.line_id, nsegs)) {
+ pt.cell_off++;
+ return pt;
+ }
+ uint64_t max_id = L->pos[lines_rows(L) - 1].id;
+ for (uint64_t next = pt.line_id + 1; next <= max_id; next++)
+ if (line_total_segs(L, next) > 0)
+ return (struct term_sel_point){ .line_id = next };
+ pt.cell_off++;
+ return pt;
+}
+
+static struct term_sel_point sel_point_prev(const struct lines *L, struct term_sel_point pt)
+{
+ if (pt.cell_off > 0) {
+ pt.cell_off--;
+ return pt;
+ }
+ for (uint64_t prev = pt.line_id; prev-- > L->first; ) {
+ int nsegs = line_total_segs(L, prev);
+ if (nsegs <= 0)
+ continue;
+ uint32_t cols = line_total_display_cols(L, prev, nsegs);
+ return (struct term_sel_point){ .line_id = prev, .cell_off = cols > 0 ? cols - 1 : 0 };
+ }
+ return pt;
+}
+
+static void unit_span(const struct lines *L, int x, int y, bool right_half,
+ enum term_select_unit unit,
+ struct term_sel_point *first, struct term_sel_point *last)
+{
+ const struct term *t = L->t;
+ int cols = L->cols;
+ if (x < 0) x = 0;
+ if (x >= cols) x = cols - 1;
+ if (y < 0) y = 0;
+ if (y >= t->rows) y = t->rows - 1;
+
+ if (unit == TERM_SELECT_CELL) {
+ struct term_sel_point pt = view_to_sel_point(L, x, y);
+ if (right_half)
+ pt = sel_point_next(L, pt);
+ *first = *last = pt;
+ return;
+ }
+ if (unit == TERM_SELECT_LINE) {
+ *first = view_to_sel_point(L, 0, y);
+ *last = sel_point_next(L, view_to_sel_point(L, cols - 1, y));
+ return;
+ }
+
+ const struct cell *row = term_screen_view_row(t, y);
+ if (row)
+ while (x > 0 && (row[x].flags & CELL_WRAP_PAD))
+ x--;
+ int left = x, right = x;
+ int kind = row ? word_class(row[x].c) : 0;
+ while (left > 0 && row && word_class(row[left - 1].c) == kind)
+ left--;
+ while (right + 1 < cols && row && !(row[right + 1].flags & CELL_WRAP_PAD) &&
+ word_class(row[right + 1].c) == kind)
+ right++;
+ *first = view_to_sel_point(L, left, y);
+ *last = sel_point_next(L, view_to_sel_point(L, right, y));
+}
+
+/* Clamp the selection to the retained lines. A read does not change it. */
+static bool selection_bounds(const struct lines *L, struct term_sel_point *start,
+ struct term_sel_point *end)
+{
+ const struct term *t = L->t;
+ if (!t->selection || t->sel_end.line_id < L->first)
+ return false;
+ *start = t->sel_start;
+ *end = t->sel_end;
+ if (start->line_id < L->first)
+ *start = (struct term_sel_point){ .line_id = L->first };
+ return sel_point_cmp(*start, *end) <= 0;
+}
+
+static bool line_locate_cell(const struct lines *L, uint64_t id, uint32_t cell_off,
+ int *out_seg, int *out_col)
+{
+ int nsegs = line_total_segs(L, id);
+ if (nsegs <= 0)
+ return false;
+ if (!line_has_wide(L, id)) {
+ int seg = (int)(cell_off / (uint32_t)L->cols);
+ if (seg >= nsegs)
+ return false;
+ if (out_seg)
+ *out_seg = seg;
+ if (out_col)
+ *out_col = (int)(cell_off % (uint32_t)L->cols);
+ return true;
+ }
+
+ int hrows = hist_segs(L, id);
+ if (in_hist(L, id) && hrows > 0) {
+ uint32_t last_start = line_seg_start_off(L, id, hrows - 1);
+ if (cell_off >= last_start) {
+ if (hrows == nsegs) {
+ if (cell_off < last_start + (uint32_t)L->cols) {
+ if (out_seg)
+ *out_seg = hrows - 1;
+ if (out_col)
+ *out_col = (int)(cell_off - last_start);
+ return true;
+ }
+ return false;
+ }
+ } else {
+ uint64_t base = id == L->t->hist.base_cell_id ? L->t->hist.base_cell : 0;
+ int seg = term_hist_cell_segment(&L->t->hist, id, base + (uint64_t)cell_off, L->cols);
+ if (seg < hrows) {
+ uint32_t seg_start = line_seg_start_off(L, id, seg);
+ if (cell_off >= seg_start) {
+ if (out_seg)
+ *out_seg = seg;
+ if (out_col)
+ *out_col = (int)(cell_off - seg_start);
+ return true;
+ }
+ }
+ }
+ }
+
+ int start_seg = (in_hist(L, id) && hrows > 0 && hrows < nsegs) ? hrows - 1 : hrows;
+ uint32_t start = line_seg_start_off(L, id, start_seg);
+ for (int seg = start_seg; seg < nsegs; seg++) {
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(L, id, seg, NULL, &wrapped);
+ uint32_t w = (uint32_t)seg_display_cols(L->cols, cells, wrapped);
+ if (cell_off < start + w) {
+ if (out_seg)
+ *out_seg = seg;
+ if (out_col)
+ *out_col = (int)(cell_off - start);
+ return true;
+ }
+ start += w;
+ }
+ return false;
+}
+
+static void select_range(struct term *t, const struct lines *L,
+ struct term_sel_point start, struct term_sel_point end)
+{
+ if (sel_point_cmp(start, end) > 0) {
+ t->selection = false;
+ return;
+ }
+ int seg = 0, col = 0;
+ if (line_locate_cell(L, start.line_id, start.cell_off, &seg, &col)) {
+ const struct cell *cells = line_seg_cells(L, start.line_id, seg, NULL, NULL);
+ if (cells && col > 0 && (cells[col].flags & CELL_TRAIL))
+ start = sel_point_prev(L, start);
+ }
+ if (line_locate_cell(L, end.line_id, end.cell_off, &seg, &col)) {
+ const struct cell *cells = line_seg_cells(L, end.line_id, seg, NULL, NULL);
+ if (cells && col + 1 < L->cols && (cells[col].flags & CELL_WIDE))
+ end = sel_point_next(L, end);
+ }
+ t->sel_start = start;
+ t->sel_end = end;
+ t->selection = true;
+}
+
+void term_select_start(struct term *t, struct term_vector2i cell, bool right_half,
+ enum term_select_unit unit)
+{
+ if (!t) return;
+ term_write_begin(t);
+ struct lines L;
+ lines_init(&L, t);
+ t->select_unit = unit;
+ unit_span(&L, cell.x, cell.y, right_half, unit, &t->sel_anchor_first, &t->sel_anchor_last);
+ t->selection = false;
+ lines_free(&L);
+ term_select_extend(t, cell, right_half);
+ term_write_end(t);
+}
+
+void term_select_extend(struct term *t, struct term_vector2i cell, bool right_half)
+{
+ if (!t) return;
+ term_write_begin(t);
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point first, last;
+ unit_span(&L, cell.x, cell.y, right_half, t->select_unit, &first, &last);
+ if (sel_point_cmp(first, t->sel_anchor_first) > 0) first = t->sel_anchor_first;
+ if (sel_point_cmp(last, t->sel_anchor_last) < 0) last = t->sel_anchor_last;
+ if (sel_point_cmp(first, last) == 0)
+ t->selection = false;
+ else
+ select_range(t, &L, first, sel_point_prev(&L, last));
+ lines_free(&L);
+ term_write_end(t);
+}
+
+bool term_has_selection(const struct term *t)
+{
+ if (!t)
+ return false;
+ term_write_begin((struct term *)t);
+ bool has = t->selection && t->sel_end.line_id >= first_retained_line(t);
+ term_write_end((struct term *)t);
+ return has;
+}
+
+void term_selection_view_spans(const struct term *t, int *c0, int *c1)
+{
+ int rows = t->rows, cols = t->cols;
+ for (int y = 0; y < rows; y++) {
+ c0[y] = cols;
+ c1[y] = -1;
+ }
+ if (!t->selection)
+ return;
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point start, end;
+ if (!selection_bounds(&L, &start, &end)) {
+ lines_free(&L);
+ return;
+ }
+ uint64_t prev_id = UINT64_MAX;
+ int prev_seg = -1;
+ uint32_t prev_end = 0;
+
+ for (int y = 0; y < rows; y++) {
+ uint64_t id;
+ int seg;
+ if (!view_row_line(&L, y, &id, &seg) || id < start.line_id || id > end.line_id)
+ continue;
+ uint32_t seg_start;
+ if (id == prev_id && seg == prev_seg + 1)
+ seg_start = prev_end;
+ else
+ seg_start = line_seg_start_off(&L, id, seg);
+
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(&L, id, seg, NULL, &wrapped);
+ uint32_t seg_cols = (uint32_t)seg_display_cols(cols, cells, wrapped);
+ uint32_t seg_end = seg_start + seg_cols;
+ prev_id = id;
+ prev_seg = seg;
+ prev_end = seg_end;
+
+ uint32_t s = 0, e = (uint32_t)(cols - 1);
+ if (id == start.line_id) {
+ if (start.cell_off >= seg_end)
+ continue;
+ s = start.cell_off > seg_start ? start.cell_off - seg_start : 0;
+ }
+ if (id == end.line_id) {
+ if (end.cell_off < seg_start)
+ continue;
+ if (end.cell_off < seg_end && end.cell_off - seg_start < (uint32_t)cols)
+ e = end.cell_off - seg_start;
+ }
+ c0[y] = (int)s;
+ c1[y] = (int)e;
+ }
+ lines_free(&L);
+}
+
+static bool text_reserve(char **text, size_t *cap, size_t need)
+{
+ if (need <= *cap)
+ return true;
+ size_t next = *cap * 2 > need ? *cap * 2 : need;
+ char *grown = realloc(*text, next);
+ if (!grown) {
+ free(*text);
+ *text = NULL;
+ return false;
+ }
+ *text = grown;
+ *cap = next;
+ return true;
+}
+
+static char *range_text_inner(const struct term *t,
+ const struct term_sel_point *bounds)
+{
+ if (!t || (!bounds && !t->selection)) return NULL;
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point start, end;
+ if (bounds) {
+ start = bounds[0];
+ end = bounds[1];
+ } else if (!selection_bounds(&L, &start, &end)) {
+ lines_free(&L);
+ return NULL;
+ }
+
+ size_t cap = 4096, n = 0;
+ char *text = malloc(cap);
+ for (uint64_t id = start.line_id; text && id <= end.line_id; id++) {
+ int nsegs = line_total_segs(&L, id);
+ int start_seg = 0;
+ if (id == start.line_id) {
+ if (!line_locate_cell(&L, id, start.cell_off, &start_seg, NULL))
+ start_seg = 0;
+ }
+ int end_seg = nsegs - 1;
+ if (id == end.line_id) {
+ if (!line_locate_cell(&L, id, end.cell_off, &end_seg, NULL))
+ end_seg = nsegs - 1;
+ }
+ uint32_t next_start = line_seg_start_off(&L, id, start_seg);
+ int hrows = hist_segs(&L, id);
+ enum { BATCH_SEGS = 64 };
+ struct cell *batch_cells = NULL;
+ int batch_lens[BATCH_SEGS];
+ int batch_start = -1, batch_len = 0;
+
+ for (int seg = start_seg; text && seg <= end_seg && seg < nsegs; seg++) {
+ uint32_t seg_start = next_start;
+ bool row_wrapped = false;
+ const struct cell *cells = NULL;
+ if (seg < hrows) {
+ if (seg >= batch_start + batch_len) {
+ batch_start = seg;
+ int take = hrows - seg;
+ if (take > BATCH_SEGS)
+ take = BATCH_SEGS;
+ if (seg + take - 1 > end_seg)
+ take = end_seg - seg + 1;
+ if (!batch_cells)
+ batch_cells = malloc((size_t)BATCH_SEGS * (size_t)t->cols * sizeof(struct cell));
+ batch_len = batch_cells ? term_hist_decode_segments(&t->hist, id, batch_start, take,
+ t->cols, batch_cells, batch_lens,
+ t->gstore) : 0;
+ }
+ if (batch_cells && seg >= batch_start && seg < batch_start + batch_len) {
+ int idx = seg - batch_start;
+ struct cell *row_buf = batch_cells + (size_t)idx * (size_t)t->cols;
+ int got = batch_lens[idx];
+ row_wrapped = seg + 1 < hrows || is_open_line(&L, id);
+ if (got < t->cols) {
+ fill_cells(row_buf + got, t->cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ if (row_wrapped)
+ for (int x = got; x < t->cols; x++)
+ row_buf[x].flags |= CELL_WRAP_PAD;
+ }
+ cells = row_buf;
+ }
+ }
+ if (!cells)
+ cells = line_seg_cells(&L, id, seg, NULL, &row_wrapped);
+ if (!cells) {
+ next_start = seg_start + (uint32_t)t->cols;
+ continue;
+ }
+ uint32_t seg_end = seg_start + (uint32_t)seg_display_cols(t->cols, cells, row_wrapped);
+ next_start = seg_end;
+
+ uint32_t s = 0, e = (uint32_t)(t->cols - 1);
+ if (id == start.line_id) {
+ if (start.cell_off >= seg_end)
+ continue;
+ s = start.cell_off > seg_start ? start.cell_off - seg_start : 0;
+ }
+ if (id == end.line_id) {
+ if (end.cell_off < seg_start)
+ continue;
+ if (end.cell_off < seg_end && end.cell_off - seg_start < (uint32_t)t->cols)
+ e = end.cell_off - seg_start;
+ }
+ int c0 = (int)s, c1 = (int)e;
+ if (c1 < c0)
+ continue;
+ bool is_last = id == end.line_id && (end.cell_off < seg_end || seg == nsegs - 1);
+ bool wrapped = !is_last && row_wrapped;
+ if (!wrapped)
+ while (c1 >= c0 && cells[c1].c <= ' ')
+ c1--;
+
+ if (!text_reserve(&text, &cap, n + (size_t)(c1 - c0 + 1) + 1))
+ break;
+ int c = c0;
+ for (; c + 3 <= c1; c += 4) {
+#if defined(__aarch64__)
+ uint32x4x4_t block = vld4q_u32((const uint32_t *)&cells[c]);
+ uint32x4_t cp = block.val[0];
+ uint32x4_t valid = vandq_u32(
+ vcltq_u32(vsubq_u32(cp, vdupq_n_u32(1)), vdupq_n_u32(127)),
+ vceqq_u32(vandq_u32(block.val[3],
+ vdupq_n_u32(CELL_TRAIL | CELL_WRAP_PAD)), vdupq_n_u32(0)));
+ if (!vminvq_u32(valid)) break;
+ uint16x4_t half = vmovn_u32(cp);
+ uint8x8_t bytes = vmovn_u16(vcombine_u16(half, half));
+ uint32_t packed = vget_lane_u32(vreinterpret_u32_u8(bytes), 0);
+#else
+ uint32_t a = cells[c].c, b = cells[c + 1].c;
+ uint32_t d = cells[c + 2].c, f = cells[c + 3].c;
+ unsigned flags = (unsigned)cells[c].flags | cells[c + 1].flags |
+ cells[c + 2].flags | cells[c + 3].flags;
+ if ((a - 1) >= 127 || (b - 1) >= 127 || (d - 1) >= 127 || (f - 1) >= 127 ||
+ (flags & (CELL_TRAIL | CELL_WRAP_PAD))) break;
+ uint32_t packed = a | (b << 8) | (d << 16) | (f << 24);
+#endif
+ memcpy(text + n, &packed, 4);
+ n += 4;
+ }
+ for (; c <= c1; c++) {
+ uint32_t cp = cells[c].c;
+ if (cells[c].flags & (CELL_TRAIL | CELL_WRAP_PAD))
+ continue;
+ /* One cell expands to GRAPHEME_MAX_LEN code points of up
+ * to 4 bytes each. Leave room for all of them. */
+ if (!text_reserve(&text, &cap, n + (size_t)GRAPHEME_MAX_LEN * 4 + 1))
+ break;
+ if (cp > 0 && cp < 128) {
+ text[n++] = (char)cp;
+ } else {
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int len = term_gstore_expand(t->gstore, cp ? cp : ' ', cps, GRAPHEME_MAX_LEN);
+ for (int k = 0; k < len; k++)
+ n += (size_t)utf8_encode(cps[k], text + n);
+ }
+ }
+ if (text && !is_last && !wrapped && text_reserve(&text, &cap, n + 2))
+ text[n++] = '\n';
+ }
+ free(batch_cells);
+ }
+ if (text)
+ text[n] = 0;
+ lines_free(&L);
+ return text;
+}
+
+static char *history_text(struct term *t, int max_rows)
+{
+ if (!t)
+ return NULL;
+ term_write_begin(t);
+ term_hist_prepare_rows(&t->hist, t->cols);
+ if (!term_hist_view_sync(&t->view, &t->hist, t->cols)) {
+ term_write_end(t);
+ return NULL;
+ }
+ int end_row = term_screen_history_rows(t) - term_view_offset(t);
+ if (max_rows <= 0 || end_row <= 0) {
+ term_write_end(t);
+ return term_strdup("");
+ }
+ int begin_row = end_row > max_rows ? end_row - max_rows : 0;
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point bounds[2] = {0};
+ int first_seg, last_seg;
+ char *text = NULL;
+ if (term_hist_view_find_row(&t->view, &t->hist, begin_row, t->cols,
+ &bounds[0].line_id, &first_seg) &&
+ term_hist_view_find_row(&t->view, &t->hist, end_row - 1, t->cols,
+ &bounds[1].line_id, &last_seg)) {
+ bounds[0].cell_off = line_seg_start_off(&L, bounds[0].line_id, first_seg);
+ uint32_t last_start = line_seg_start_off(&L, bounds[1].line_id, last_seg);
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(&L, bounds[1].line_id,
+ last_seg, NULL, &wrapped);
+ bounds[1].cell_off = last_start +
+ (uint32_t)seg_display_cols(t->cols, cells, wrapped) - 1;
+ text = range_text_inner(t, bounds);
+ if (text && !wrapped) {
+ size_t n = strlen(text);
+ size_t cap = n + 1;
+ if (text_reserve(&text, &cap, n + 2)) {
+ text[n] = '\n';
+ text[n + 1] = '\0';
+ }
+ }
+ }
+ lines_free(&L);
+ term_write_end(t);
+ return text;
+}
+
+size_t term_history_text(struct term *t, int max_rows, char *buf, size_t cap)
+{
+ return text_out(history_text(t, max_rows), buf, cap);
+}
+
+size_t term_selection_text(const struct term *t, char *buf, size_t cap)
+{
+ if (!t)
+ return 0;
+ term_write_begin((struct term *)t);
+ char *text = range_text_inner(t, NULL);
+ term_write_end((struct term *)t);
+ return text_out(text, buf, cap);
+}
diff --git a/src/simd.h b/src/simd.h
new file mode 100644
index 0000000..0238500
--- /dev/null
+++ b/src/simd.h
@@ -0,0 +1,117 @@
+#ifndef TERM_SIMD_H
+#define TERM_SIMD_H
+
+#include "compat.h"
+
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#if defined(__x86_64__) || defined(_M_X64)
+#include <immintrin.h>
+#endif
+#if defined(__aarch64__)
+#include <arm_neon.h>
+#endif
+
+/*
+ * x86-64 kernels come in three levels: base (SSE2), AVX2 (x86-64-v3), and
+ * AVX-512 (x86-64-v4 with VBMI and VBMI2). GCC and Clang compile every
+ * level with target attributes, and term_isa_init() picks one at run time.
+ * Other compilers compile the levels that their flags enable.
+ *
+ * A kernel at a level is a function marked TERM_TARGET_AVX2 or
+ * TERM_TARGET_AVX512. It exists when TERM_KERNEL_AVX2 or TERM_KERNEL_AVX512
+ * is defined. Call it only when term_use_avx2() or term_use_avx512() is
+ * true. When the compiler flags already enable a level, these checks are
+ * constants and the kernels inline into their callers.
+ */
+#if defined(__AVX512F__) && defined(__AVX512BW__) && defined(__AVX512CD__) && \
+ defined(__AVX512DQ__) && defined(__AVX512VL__) && defined(__AVX512VBMI__) && \
+ defined(__AVX512VBMI2__) && defined(__AVX2__) && defined(__BMI2__)
+#define TERM_ISA_FLOOR 2
+#elif defined(__AVX2__) && defined(__BMI__) && defined(__BMI2__) && defined(__FMA__) && \
+ defined(__LZCNT__)
+#define TERM_ISA_FLOOR 1
+#else
+#define TERM_ISA_FLOOR 0
+#endif
+
+/* Clang 18 to 21 also need evex512 for 512-bit vectors. GCC and Clang 22
+ * reject the name. Apple Clang 17 is LLVM 19. */
+#if defined(__clang__) && ((defined(__apple_build_version__) && __clang_major__ >= 17) || \
+ (!defined(__apple_build_version__) && __clang_major__ >= 18 && __clang_major__ <= 21))
+#define TERM_EVEX512 ",evex512"
+#else
+#define TERM_EVEX512 ""
+#endif
+
+#if (defined(__x86_64__) || defined(_M_X64)) && defined(TERM_COMPILER_GNU)
+#define TERM_TARGET_AVX2 \
+ __attribute__((target("avx2,bmi,bmi2,fma,lzcnt,popcnt,movbe,f16c")))
+#define TERM_TARGET_AVX512 \
+ __attribute__((target("avx2,bmi,bmi2,fma,lzcnt,popcnt,movbe,f16c,avx512f,avx512bw," \
+ "avx512cd,avx512dq,avx512vl,avx512vbmi,avx512vbmi2" TERM_EVEX512)))
+#define TERM_KERNEL_AVX2 1
+#define TERM_KERNEL_AVX512 1
+#define TERM_ISA_RUNTIME 1
+#else
+#define TERM_TARGET_AVX2
+#define TERM_TARGET_AVX512
+#if TERM_ISA_FLOOR >= 1
+#define TERM_KERNEL_AVX2 1
+#endif
+#if TERM_ISA_FLOOR >= 2
+#define TERM_KERNEL_AVX512 1
+#endif
+#endif
+
+enum { TERM_ISA_BASE, TERM_ISA_AVX2, TERM_ISA_AVX512 };
+
+/* The level in use. Zero, the base level, until term_isa_init() runs. It
+ * is a plain int, so the compiler can hoist the checks out of loops: it is
+ * written once, before the release that publishes it, and read only after
+ * that. */
+extern int term_isa_level;
+
+/* Pick the level once. Any thread can call it. term_new() calls it before
+ * the first terminal exists. */
+void term_isa_init(void);
+
+static inline bool term_use_avx2(void)
+{
+#if TERM_ISA_FLOOR >= 1
+ return true;
+#elif defined(TERM_ISA_RUNTIME)
+ return term_isa_level >= TERM_ISA_AVX2;
+#else
+ return false;
+#endif
+}
+
+static inline bool term_use_avx512(void)
+{
+#if TERM_ISA_FLOOR >= 2
+ return true;
+#elif defined(TERM_ISA_RUNTIME)
+ return term_isa_level >= TERM_ISA_AVX512;
+#else
+ return false;
+#endif
+}
+
+#if defined(__aarch64__)
+static inline uint16_t term_lane_bits(uint8x16_t m)
+{
+ const uint8x16_t idx = {1, 2, 4, 8, 16, 32, 64, 128,
+ 1, 2, 4, 8, 16, 32, 64, 128};
+ uint8x16_t s = vandq_u8(m, idx);
+ s = vpaddq_u8(s, s);
+ s = vpaddq_u8(s, s);
+ s = vpaddq_u8(s, s);
+ return (uint16_t)(vgetq_lane_u8(s, 0) | (vgetq_lane_u8(s, 1) << 8));
+}
+#endif
+
+#endif
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;
+}
diff --git a/src/term.c b/src/term.c
new file mode 100644
index 0000000..583bdd8
--- /dev/null
+++ b/src/term.c
@@ -0,0 +1,125 @@
+#include "internal.h"
+#include "screen.h"
+
+_Thread_local char term_thread_anchor;
+
+const char *term_unicode_version(void)
+{
+ return UNICODE_DATA_VERSION;
+}
+
+void term_lock(struct term *t)
+{
+ term_write_begin(t);
+}
+
+void term_unlock(struct term *t)
+{
+ term_write_end(t);
+}
+
+struct term_vector2i term_size(const struct term *t)
+{
+ if (!t)
+ return (struct term_vector2i){ 0 };
+ term_write_begin((struct term *)t);
+ struct term_vector2i size = { .x = t->cols, .y = t->rows };
+ term_write_end((struct term *)t);
+ return size;
+}
+
+struct term_modes term_modes(const struct term *t)
+{
+ if (!t)
+ return (struct term_modes){ 0 };
+ term_write_begin((struct term *)t);
+ struct term_modes m = {
+ .keyboard = t->keyboard[t->alternate].flags,
+ .mouse = (enum term_mouse)t->mouse_mode,
+ .mouse_encoding = (enum term_mouse_encoding)t->mouse_encoding,
+ .grid = { .x = t->cols, .y = t->rows },
+ .app_cursor = t->application_cursor,
+ .app_keypad = t->application_keypad,
+ .backarrow_bs = t->backarrow_sends_bs,
+ .bracketed_paste = t->bracketed_paste,
+ .focus_events = t->focus_events,
+ .alternate = t->alternate,
+ .reverse_video = t->reverse_video,
+ .synchronized = t->synchronized_output,
+ };
+ term_write_end((struct term *)t);
+ return m;
+}
+
+struct term_cursor term_cursor(const struct term *t)
+{
+ if (!t)
+ return (struct term_cursor){ .visible = true };
+ term_write_begin((struct term *)t);
+ struct term_cursor c = {
+ .pos = { .x = t->cursor_x, .y = t->cursor_y },
+ .style = t->cursor_style,
+ .visible = t->cursor_visible,
+ .blink = t->cursor_blink,
+ };
+ term_write_end((struct term *)t);
+ return c;
+}
+
+void term_set_cursor_default(struct term *t, enum term_cursor_style style, bool steady)
+{
+ if (!t)
+ return;
+ term_write_begin(t);
+ if (style > TERM_CURSOR_BAR)
+ style = TERM_CURSOR_BLOCK;
+ if (t->cursor_style != style || t->cursor_blink == steady) {
+ struct srow *cur = term_row(t, t->cursor_y);
+ if (cur) cur->version++;
+ }
+ t->default_cursor_style = t->cursor_style = style;
+ t->default_cursor_blink = t->cursor_blink = !steady;
+ term_write_end(t);
+}
+
+void term_set_theme(struct term *t, const struct term_theme *theme)
+{
+ if (!t)
+ return;
+ term_write_begin(t);
+ struct palette next = { .version = t->palette.version };
+ term_palette_init(&next, theme);
+ if (memcmp(&next.theme, &t->palette.theme, sizeof(next.theme)))
+ t->palette = next;
+ term_write_end(t);
+}
+
+void term_focus(struct term *t, bool focused)
+{
+ if (!t)
+ return;
+ term_write_begin(t);
+ if (t->focus_events)
+ send_response(t, focused ? "\033[I" : "\033[O");
+ term_write_end(t);
+}
+
+size_t term_cwd(const struct term *t, char *buf, size_t cap)
+{
+ if (!t)
+ return term_copy_out(NULL, buf, cap);
+ term_write_begin((struct term *)t);
+ size_t n = term_copy_out(t->cwd, buf, cap);
+ term_write_end((struct term *)t);
+ return n;
+}
+
+size_t term_pointer_shape(const struct term *t, char *buf, size_t cap)
+{
+ if (!t)
+ return term_copy_out(NULL, buf, cap);
+ term_write_begin((struct term *)t);
+ size_t n = term_copy_out(t->pointer_shape, buf, cap);
+ term_write_end((struct term *)t);
+ return n;
+}
diff --git a/src/text.c b/src/text.c
new file mode 100644
index 0000000..9f73bd3
--- /dev/null
+++ b/src/text.c
@@ -0,0 +1,350 @@
+#include "compat.h"
+#include "core.h"
+#include "kernels.h"
+#include "text_run.h"
+#include "screen.h"
+#include "escape.h"
+#include "palette.h"
+#include "unicode/grapheme.h"
+#include "unicode/unicode.h"
+#include "text.h"
+#include "unicode/utf8.h"
+#include <stdio.h>
+#include <string.h>
+
+static void dispatch_c1_control(struct term *t, uint32_t cp)
+{
+ static const char c1_map[32] = {
+ [4] = 'D', [5] = 'E', [8] = 'H', [13] = 'M',
+ [16] = 'P', [27] = '[', [28] = '\\', [29] = ']' };
+ char ch = c1_map[cp - 0x80];
+ if (!ch) return;
+ t->state = ST_ESC;
+ term_escape_byte(t, (unsigned char)ch);
+}
+
+static inline void emit_codepoint(struct term *t, uint32_t cp, int klass)
+{
+ static const uint32_t dec_graphics[] = {
+ 0x00a0,
+ 0x25c6,0x2592,0x2409,0x240c,0x240d,0x240a,0x00b0,0x00b1,
+ 0x2424,0x240b,0x2518,0x2510,0x250c,0x2514,0x253c,0x23ba,
+ 0x23bb,0x2500,0x23bc,0x23bd,0x251c,0x2524,0x2534,0x252c,
+ 0x2502,0x2264,0x2265,0x03c0,0x2260,0x00a3,0x00b7 };
+ if (cp >= 0x80 && cp <= 0x9f) {
+ dispatch_c1_control(t, cp);
+ return;
+ }
+ if (t->graphics && cp >= '_' && cp <= '~') {
+ cp = dec_graphics[cp - '_'];
+ klass = -1;
+ }
+ term_sc_print_glyph(t, cp, klass, term_current_flags(t));
+}
+
+static inline void emit_decoded(struct term *t, uint32_t cp)
+{
+ int klass = term_text_class(cp);
+ if (!term_sc_extend_cluster(t, cp, klass))
+ emit_codepoint(t, cp, klass);
+}
+
+/* A byte that cannot continue a UTF-8 sequence ends it: the unfinished
+ * sequence prints U+FFFD (Unicode 3.9, maximal subparts). */
+void term_text_utf8_interrupt(struct term *t)
+{
+ if (TERM_EXPECT(t->utf8_state == UTF8_ACCEPT, 1))
+ return;
+ t->utf8_state = UTF8_ACCEPT;
+ emit_decoded(t, 0xFFFD);
+}
+
+void term_text_ground(struct term *t, unsigned char ch)
+{
+ if (ch >= 0x20 && ch < 0x7f && t->utf8_state == UTF8_ACCEPT) {
+ emit_codepoint(t, ch, 1);
+ return;
+ }
+
+ if (ch < 0x20 || ch == 0x7f)
+ term_text_utf8_interrupt(t);
+
+ if (TERM_EXPECT(ch == 0x1b, 0)) {
+ term_escape_begin(t);
+ } else if (ch == '\n' || ch == 0x0b || ch == 0x0c) {
+ /* VT and FF act as LF (xterm). */
+ if (term_lnm(t)) sc_cr(t);
+ sc_linefeed(t, 1);
+ } else if (ch == '\r') {
+ sc_cr(t);
+ } else if (ch == '\b') {
+ sc_bs(t);
+ } else if (ch == '\t') {
+ term_sc_tab(t, 1);
+ } else if (ch == 0x07) {
+ if (t->cb.bell)
+ t->cb.bell(t->cb.ud);
+ } else if (ch == 0x0e) {
+ t->charset_active = 1;
+ t->graphics = t->charsets[1] == '0';
+ } else if (ch == 0x0f) {
+ t->charset_active = 0;
+ t->graphics = t->charsets[0] == '0';
+ } else if (ch >= 0x20 && ch != 0x7f) {
+ uint32_t before = t->utf8_state;
+ uint32_t st = utf8_decode(&t->utf8_state, &t->utf8_codep, ch);
+ if (st == UTF8_ACCEPT) {
+ emit_decoded(t, t->utf8_codep);
+ } else if (st == UTF8_REJECT) {
+ t->utf8_state = UTF8_ACCEPT;
+ emit_decoded(t, 0xFFFD);
+ /* The byte that ended an unfinished sequence starts anew. */
+ if (before != UTF8_ACCEPT)
+ term_text_ground(t, ch);
+ }
+ }
+}
+
+static inline int decode_ground_utf8(struct term *t, const char *buf, size_t len,
+ uint32_t *cp)
+{
+ return utf8_decode_ground(&t->utf8_state, &t->utf8_codep, buf, len, cp);
+}
+
+static inline bool can_stream_bottom(const struct term *t)
+{
+ return t->state == ST_GROUND && !t->graphics && !t->insert_mode && t->autowrap &&
+ !t->lr_margin_mode &&
+ t->utf8_state == UTF8_ACCEPT && !t->inverse &&
+ t->scroll_top == 0 && t->scroll_bottom == t->rows - 1 &&
+ t->cursor_y == t->rows - 1 &&
+ !t->alternate && t->sb_capacity > 0 &&
+ using_default_colors(t);
+}
+
+static inline bool try_text_run(struct term *t, const char *buf, size_t len,
+ size_t *idx)
+{
+ size_t i = *idx;
+ if (t->pending_wrap || i + 4 > len ||
+ ((unsigned char)buf[i] >= 0x80 &&
+ ((unsigned char)buf[i] < 0xc2 || (unsigned char)buf[i] > 0xf4)) ||
+ t->last_char == 0x200D || grapheme_is_handle(t->last_char))
+ return false;
+ if ((unsigned char)buf[i] >= 0x80) {
+ uint32_t cp;
+ int bytes = utf8_decode_one(buf + i, len - i, &cp);
+ if (!bytes || term_text_class(cp) <= 0)
+ return false;
+ }
+ if (t->cursor_x != 0 &&
+ t->cursor_x != term_row(t, t->cursor_y)->len)
+ return false;
+ uint32_t fg = t->inverse ? t->cur_bg : t->cur_fg;
+ uint32_t bg = t->inverse ? t->cur_fg : t->cur_bg;
+ uint32_t flags = term_flags_pad(t);
+ bool wrote = false;
+ do {
+ struct srow *text = t->insert_mode || t->lr_margin_mode ? NULL : text_append_row(t);
+ if (text) {
+ int x = t->cursor_x, avail = t->cols - x;
+ struct text_run_bytes r = term_text_run_bytes(text->text.text + text->text.text_len,
+ buf + i, (int)(len - i), avail,
+ t->last_char, &t->text_width);
+ if (!r.bytes)
+ break;
+ wrote = true;
+ text_commit(text, r.out, r.chars, r.cols,
+ (uint8_t)((r.narrow ? HLINE_NARROW : 0) | (r.wide ? HLINE_HAS_WIDE : 0)),
+ fg, bg, flags);
+ t->last_char = r.last;
+ sc_text_advance(t, x, r.cols, avail);
+ i += (size_t)r.bytes;
+ } else {
+ struct span span = sc_span_begin(t, t->cols);
+ if (span.avail < 2)
+ break;
+ struct text_run_result r = term_text_run(span.cells, buf + i, (int)(len - i),
+ span.avail, fg, bg, flags, t->last_char,
+ &t->text_width);
+ if (!r.bytes)
+ break;
+ wrote = true;
+ sc_span_end(t, &span, r.cols, 0, r.last);
+ i += (size_t)r.bytes;
+ }
+ bool newline = false;
+ if (i < len && buf[i] == '\r') {
+ sc_cr(t);
+ i++;
+ if (i < len && buf[i] == '\n') {
+ sc_linefeed(t, 1);
+ i++;
+ newline = true;
+ }
+ } else if (i < len && buf[i] == '\n') {
+ if (term_lnm(t)) sc_cr(t);
+ sc_linefeed(t, 1);
+ i++;
+ newline = true;
+ }
+ if (!newline || i + 4 > len || t->pending_wrap ||
+ t->last_char == 0x200D || grapheme_is_handle(t->last_char) ||
+ (t->cursor_x != 0 &&
+ t->cursor_x != term_row(t, t->cursor_y)->len))
+ break;
+ } while (true);
+ if (wrote) *idx = i - 1;
+ return wrote;
+}
+
+static void term_feed_inner(struct term *t, const char *buf, size_t len, unsigned flags)
+{
+ if (!t || !buf || len == 0)
+ return;
+ t->transport_newline = (flags & TERM_FEED_ONLCR) != 0;
+
+ for (size_t i = 0; i < len; i++) {
+ while (t->state != ST_GROUND) {
+ if (t->state == ST_CSI)
+ i += term_escape_csi(t, buf + i, len - i);
+ else if (t->state == ST_OSC || t->state == ST_DCS || t->state == ST_IGNORE)
+ i += term_escape_str(t, buf + i, len - i);
+ else
+ term_escape_byte(t, (unsigned char)buf[i++]);
+ if (i == len) return;
+ }
+
+ unsigned char ch = (unsigned char)buf[i];
+ if ((ch < 0x20 || ch == 0x7f) && t->state == ST_GROUND)
+ term_text_utf8_interrupt(t);
+
+ if (TERM_EXPECT(ch < 0x20 && t->state == ST_GROUND &&
+ ch != 0x1b && ch != '\n' && ch != '\r' && ch != '\b' && ch != '\t' &&
+ ch != 0x0e && ch != 0x0f && ch != 0x07 && ch != 0x0b &&
+ ch != 0x0c, 0)) {
+ continue;
+ }
+
+ if (TERM_EXPECT(ch == 0x1b && t->state == ST_GROUND &&
+ t->utf8_state == UTF8_ACCEPT && i + 1 < len &&
+ buf[i + 1] == '[' && !t->insert_mode &&
+ !t->lr_margin_mode && !t->graphics, 0)) {
+ int fast = term_try_fast_csi(t, buf + i + 2, len - i - 2);
+ if (fast > 0) {
+ i += 1 + (size_t)fast;
+ continue;
+ }
+ t->state = ST_CSI;
+ csi_parser_reset(&t->csi_parser);
+ i += 1 + term_escape_csi(t, buf + i + 2, len - i - 2);
+ continue;
+ }
+
+ if (TERM_EXPECT(t->state == ST_GROUND && ch == '\n', 0)) {
+ size_t end = i + 1 + scan_byte_run(buf + i + 1, len - i - 1, '\n');
+ if (term_lnm(t)) sc_cr(t);
+ sc_linefeed(t, end - i);
+ i = end - 1;
+ continue;
+ }
+
+ if (TERM_EXPECT(t->state == ST_GROUND && ch == '\r' &&
+ i + 1 < len && buf[i + 1] == '\n', 0)) {
+ size_t end = i + 2 + scan_crlf_pairs(buf + i + 2, len - i - 2);
+ sc_cr(t);
+ sc_linefeed(t, (end - i) / 2);
+ i = end - 1;
+ continue;
+ }
+
+ if (TERM_EXPECT(ch >= 0x20 && ch < 0x7f && !t->insert_mode && !t->lr_margin_mode &&
+ t->state == ST_GROUND && !t->graphics &&
+ t->utf8_state == UTF8_ACCEPT, 1)) {
+ int avail = t->pending_wrap ? t->cols : t->cols - t->cursor_x;
+ int rem = (int)(len - i);
+ if (rem > avail) rem = avail;
+ int run = 1;
+ if (rem > 1 && (unsigned char)buf[i + 1] >= 0x20 &&
+ (unsigned char)buf[i + 1] < 0x7f)
+ run += scan_ascii_run(buf + i + 1, rem - 1);
+ if (TERM_EXPECT(can_stream_bottom(t), 1) &&
+ (run == t->cols ||
+ (i + (size_t)run < len &&
+ (buf[i + run] == '\r' || buf[i + run] == '\n')))) {
+ struct srow *row = term_row(t, t->cursor_y);
+ if (t->pending_wrap ||
+ (t->cursor_x == 0 && row && row->len == 0)) {
+ size_t done = term_sc_bulk_ascii_lines(t, buf + i, len - i);
+ if (done > 0) {
+ i += done - 1;
+ continue;
+ }
+ }
+ }
+ if (i + (size_t)run < len && (unsigned char)buf[i + run] >= 0x80) {
+ uint32_t next_cp;
+ if (utf8_decode_one(buf + i + run, len - i - (size_t)run, &next_cp) &&
+ term_text_class(next_cp) > 0 &&
+ try_text_run(t, buf, len, &i))
+ continue;
+ }
+ sc_print_ascii(t, buf + i, run);
+ i += (size_t)run;
+ if (i < len && buf[i] == '\r') {
+ sc_cr(t);
+ i++;
+ if (i < len && buf[i] == '\n') {
+ sc_linefeed(t, 1);
+ i++;
+ }
+ } else if (i < len && buf[i] == '\n') {
+ if (term_lnm(t)) sc_cr(t);
+ sc_linefeed(t, 1);
+ i++;
+ }
+ i--;
+ continue;
+ }
+
+ if (TERM_EXPECT(ch == '\r' && t->state == ST_GROUND, 0)) {
+ sc_cr(t);
+ continue;
+ }
+
+ if (TERM_EXPECT(ch >= 0x80 && t->state == ST_GROUND &&
+ t->utf8_state == UTF8_ACCEPT && !t->graphics, 1)) {
+ if (!t->insert_mode && !t->lr_margin_mode &&
+ try_text_run(t, buf, len, &i))
+ continue;
+ uint32_t cp;
+ int cp_len = decode_ground_utf8(t, buf + i, len - i, &cp);
+ if (cp)
+ emit_decoded(t, cp);
+ i += (size_t)(cp_len - 1);
+ continue;
+ }
+
+ if (TERM_EXPECT(t->state == ST_GROUND, 1))
+ term_text_ground(t, ch);
+ else
+ term_escape_byte(t, ch);
+ }
+}
+
+void term_feed(struct term *t, const char *buf, size_t len, unsigned flags)
+{
+ if (!t || !buf || len == 0)
+ return;
+ term_write_begin(t);
+ term_feed_inner(t, buf, len, flags);
+ term_write_end(t);
+}
+
+void term_text_repeat(struct term *t, int count)
+{
+ int screen = t->cols * t->rows;
+ if (count > screen) count = screen;
+ for (int j = 0; j < count && t->last_char; j++)
+ term_sc_print_glyph(t, t->last_char, -1, term_current_flags(t));
+}
diff --git a/src/text.h b/src/text.h
new file mode 100644
index 0000000..9caaf80
--- /dev/null
+++ b/src/text.h
@@ -0,0 +1,10 @@
+#ifndef TERM_TEXT_H
+#define TERM_TEXT_H
+#include "internal.h"
+
+void term_text_ground(struct term *t, unsigned char ch);
+/* End an unfinished UTF-8 sequence: it prints U+FFFD. */
+void term_text_utf8_interrupt(struct term *t);
+void term_text_repeat(struct term *t, int count);
+
+#endif
diff --git a/src/text_run.c b/src/text_run.c
new file mode 100644
index 0000000..3901cbe
--- /dev/null
+++ b/src/text_run.c
@@ -0,0 +1,682 @@
+#include <assert.h>
+#include <stdalign.h>
+#include "text_run.h"
+#include "compat.h"
+#include "kernels.h"
+#include "simd.h"
+#include "unicode/utf8.h"
+
+static uint8_t unicode_fast_class[65536];
+static int8_t unicode_width_bmp[65536];
+
+/* Runs of code points with one fast class, for the AVX-512 width cache. */
+#define TERM_WIDTH_RUNS_MAX 1024
+static uint16_t width_run_lo[TERM_WIDTH_RUNS_MAX];
+static uint16_t width_run_hi[TERM_WIDTH_RUNS_MAX];
+static uint8_t width_run_class[TERM_WIDTH_RUNS_MAX];
+static int width_run_count;
+
+void term_text_tables_init(void)
+{
+ for (uint32_t cp = 0; cp < 65536; cp++) {
+ int w = unicode_width(cp);
+ unicode_fast_class[cp] = (w == 1 || w == 2) && !cluster_contextual(cp)
+ ? (uint8_t)w : 0;
+ unicode_width_bmp[cp] = (int8_t)w;
+ }
+ for (uint32_t cp = 0; cp < 65536; ) {
+ uint8_t k = unicode_fast_class[cp];
+ uint32_t lo = cp;
+ while (cp < 65536 && unicode_fast_class[cp] == k) cp++;
+ if (width_run_count == TERM_WIDTH_RUNS_MAX) {
+ width_run_count = 0;
+ break;
+ }
+ width_run_lo[width_run_count] = (uint16_t)lo;
+ width_run_hi[width_run_count] = (uint16_t)(cp - 1);
+ width_run_class[width_run_count] = k;
+ width_run_count++;
+ }
+}
+
+static TERM_MAYBE_UNUSED int width_run_find(uint32_t cp)
+{
+ int lo = 0, hi = width_run_count - 1;
+ while (lo < hi) {
+ int mid = lo + (hi - lo + 1) / 2;
+ if (width_run_lo[mid] > cp) hi = mid - 1;
+ else lo = mid;
+ }
+ return width_run_count > 0 ? lo : -1;
+}
+
+static TERM_MAYBE_UNUSED uint16_t width_cache_covers(const struct width_cache *wc,
+ const uint32_t *cp)
+{
+ uint16_t covered = 0;
+ for (int j = 0; j < TERM_WIDTH_SLOTS; j++)
+ for (int l = 0; l < 16; l++)
+ if (cp[l] - wc->lo[j] <= wc->span[j])
+ covered |= (uint16_t)(1u << l);
+ return covered;
+}
+
+static TERM_MAYBE_UNUSED bool width_cache_extend(struct width_cache *wc, const uint32_t *cp,
+ uint16_t covered)
+{
+ unsigned miss = (unsigned)~covered & 0xFFFFu;
+ int r = width_run_find(cp[miss ? term_ctz32(miss) : 0]);
+ if (r < 0) return false;
+ uint8_t k = width_run_class[r];
+ if (k != 1 && k != 2) return false;
+ if (k != wc->klass) {
+ for (int j = 0; j < TERM_WIDTH_SLOTS; j++) {
+ wc->lo[j] = 0xFFFFFFFFu;
+ wc->span[j] = 0;
+ }
+ wc->klass = k;
+ wc->next = 0;
+ }
+ for (int added = 0; added < TERM_WIDTH_SLOTS; added++) {
+ wc->lo[wc->next] = width_run_lo[r];
+ wc->span[wc->next] = (uint32_t)(width_run_hi[r] - width_run_lo[r]);
+ wc->next = (uint8_t)((wc->next + 1) % TERM_WIDTH_SLOTS);
+ covered = width_cache_covers(wc, cp);
+ if (covered == 0xFFFF) return true;
+ r = width_run_find(cp[term_ctz32((unsigned)~covered & 0xFFFFu)]);
+ if (r < 0 || width_run_class[r] != k) return false;
+ }
+ return false;
+}
+
+int term_text_class(uint32_t cp)
+{
+ assert(term_tables_ready());
+ return TERM_EXPECT(cp < 65536, 1) ? (int)unicode_width_bmp[cp] : unicode_width(cp);
+}
+
+#if defined(TERM_KERNEL_AVX512)
+
+static const uint8_t utf8_3byte_shuffle[64] = {
+ 2, 1, 0, 48, 5, 4, 3, 48, 8, 7, 6, 48, 11, 10, 9, 48,
+ 14, 13, 12, 48, 17, 16, 15, 48, 20, 19, 18, 48, 23, 22, 21, 48,
+ 26, 25, 24, 48, 29, 28, 27, 48, 32, 31, 30, 48, 35, 34, 33, 48,
+ 38, 37, 36, 48, 41, 40, 39, 48, 44, 43, 42, 48, 47, 46, 45, 48,
+};
+
+static const uint8_t utf8_3byte_bits[64] = {
+ 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0,
+ 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0,
+ 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0,
+ 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0,
+ 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0, 0xF0, 0xC0, 0xC0,
+ 0xF0, 0xC0, 0xC0, 0xF0,
+};
+static const uint8_t utf8_3byte_want[64] = {
+ 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80,
+ 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80,
+ 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80,
+ 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80,
+ 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0xE0, 0x80, 0x80,
+ 0xE0, 0x80, 0x80, 0xE0,
+};
+#define UTF8_3BYTE_SPAN 0x0000FFFFFFFFFFFFull
+
+TERM_TARGET_AVX512
+static inline __m512i utf8_3byte_decode(__m512i packed)
+{
+ __m512i b2 = _mm512_and_si512(packed, _mm512_set1_epi32(0x003F));
+ __m512i b1 = _mm512_and_si512(_mm512_srli_epi32(packed, 2), _mm512_set1_epi32(0x0FC0));
+ __m512i b0 = _mm512_and_si512(_mm512_srli_epi32(packed, 4), _mm512_set1_epi32(0xF000));
+ return _mm512_ternarylogic_epi32(b2, b1, b0, 0xFE);
+}
+
+struct width_ranges {
+ __m512i lo[TERM_WIDTH_SLOTS];
+ __m512i span[TERM_WIDTH_SLOTS];
+};
+
+TERM_TARGET_AVX512
+static inline struct width_ranges width_ranges_load(const struct width_cache *wc)
+{
+ struct width_ranges r;
+ for (int j = 0; j < TERM_WIDTH_SLOTS; j++) {
+ r.lo[j] = _mm512_set1_epi32((int)wc->lo[j]);
+ r.span[j] = _mm512_set1_epi32((int)wc->span[j]);
+ }
+ return r;
+}
+
+TERM_TARGET_AVX512
+static inline __mmask16 width_ranges_cover(const struct width_ranges *r, __m512i cps)
+{
+ return _mm512_cmple_epu32_mask(_mm512_sub_epi32(cps, r->lo[0]), r->span[0])
+ | _mm512_cmple_epu32_mask(_mm512_sub_epi32(cps, r->lo[1]), r->span[1])
+ | _mm512_cmple_epu32_mask(_mm512_sub_epi32(cps, r->lo[2]), r->span[2])
+ | _mm512_cmple_epu32_mask(_mm512_sub_epi32(cps, r->lo[3]), r->span[3]);
+}
+
+TERM_TARGET_AVX512
+static inline void store_wide_cells(uint64_t * restrict dst, __m512i base, __m512i cps)
+{
+#define TERM_WIDE_PAIR(k) _mm512_storeu_si512((void *)(dst + 8 * (k)), \
+ _mm512_mask_permutexvar_epi32(base, 0x0101, \
+ _mm512_setr_epi32((k) * 2, 0, 0, 0, 0, 0, 0, 0, \
+ (k) * 2 + 1, 0, 0, 0, 0, 0, 0, 0), cps))
+ TERM_WIDE_PAIR(0); TERM_WIDE_PAIR(1); TERM_WIDE_PAIR(2); TERM_WIDE_PAIR(3);
+ TERM_WIDE_PAIR(4); TERM_WIDE_PAIR(5); TERM_WIDE_PAIR(6); TERM_WIDE_PAIR(7);
+#undef TERM_WIDE_PAIR
+}
+
+TERM_TARGET_AVX512
+static inline void store_narrow_cells(uint64_t * restrict dst, __m512i base, __m512i cps)
+{
+#define TERM_NARROW_QUAD(k) _mm512_storeu_si512((void *)(dst + 8 * (k)), \
+ _mm512_mask_permutexvar_epi32(base, 0x1111, \
+ _mm512_setr_epi32((k) * 4, 0, 0, 0, (k) * 4 + 1, 0, 0, 0, \
+ (k) * 4 + 2, 0, 0, 0, (k) * 4 + 3, 0, 0, 0), cps))
+ TERM_NARROW_QUAD(0); TERM_NARROW_QUAD(1); TERM_NARROW_QUAD(2); TERM_NARROW_QUAD(3);
+#undef TERM_NARROW_QUAD
+}
+
+TERM_TARGET_AVX512
+static TERM_NOINLINE int
+decode_3byte_blocks(uint64_t * restrict dst, const char * restrict buf, int len, int cols_left,
+ __m512i base_wide_pair, __m512i base_narrow_quad,
+ struct width_cache * restrict wc,
+ int * restrict out_cols, uint32_t * restrict last_cp)
+{
+ struct width_ranges ranges = width_ranges_load(wc);
+ __m512i shuffle = _mm512_loadu_si512(utf8_3byte_shuffle);
+ __m512i want = _mm512_loadu_si512(utf8_3byte_want);
+ __m512i bits = _mm512_loadu_si512(utf8_3byte_bits);
+ uint32_t last = *last_cp;
+ int i = 0, cx = 0;
+
+ while (i + 48 <= len && cx + 32 <= cols_left &&
+ ((unsigned char)buf[i + 45] & UTF8_LEAD3_MASK) == UTF8_LEAD3_TAG) {
+ __m512i in = _mm512_maskz_loadu_epi8(UTF8_3BYTE_SPAN, buf + i);
+ if (TERM_EXPECT(_mm512_test_epi8_mask(_mm512_xor_si512(in, want), bits) &
+ UTF8_3BYTE_SPAN, 0))
+ break;
+
+ __m512i cps = utf8_3byte_decode(_mm512_permutexvar_epi8(shuffle, in));
+ if (_mm512_cmplt_epu32_mask(cps, _mm512_set1_epi32((int)UTF8_3BYTE_MIN)) ||
+ _mm512_cmple_epu32_mask(
+ _mm512_sub_epi32(cps, _mm512_set1_epi32((int)UTF8_SURROGATE_LO)),
+ _mm512_set1_epi32((int)(UTF8_SURROGATE_HI - UTF8_SURROGATE_LO))))
+ break;
+ __mmask16 covered = width_ranges_cover(&ranges, cps);
+ if (TERM_EXPECT(covered != 0xFFFF || wc->klass == 0, 0)) {
+ uint32_t cp[16];
+ _mm512_storeu_si512(cp, cps);
+ if (!width_cache_extend(wc, cp, covered))
+ break;
+ ranges = width_ranges_load(wc);
+ }
+
+ if (wc->klass == 2) {
+ store_wide_cells(dst, base_wide_pair, cps);
+ dst += 64;
+ cx += 32;
+ } else {
+ store_narrow_cells(dst, base_narrow_quad, cps);
+ dst += 32;
+ cx += 16;
+ }
+ last = (uint32_t)_mm_extract_epi32(_mm512_extracti32x4_epi32(cps, 3), 3);
+ i += 48;
+ }
+
+ *out_cols = cx;
+ *last_cp = last;
+ return i;
+}
+
+/* decode_3byte_blocks() that writes the history format: a narrow block
+ * copies its 48 bytes, a wide block puts 0xF5 before each character. */
+TERM_TARGET_AVX512
+static TERM_NOINLINE int
+text_3byte_blocks(uint8_t * restrict dst, const char * restrict buf, int len, int cols_left,
+ struct width_cache * restrict wc, int * restrict out_cols,
+ int * restrict out_bytes, bool * restrict wide, uint32_t * restrict last_cp)
+{
+ static const uint8_t spread[64] = {
+ 0, 0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11,
+ 0, 12, 13, 14, 0, 15, 16, 17, 0, 18, 19, 20, 0, 21, 22, 23,
+ 0, 24, 25, 26, 0, 27, 28, 29, 0, 30, 31, 32, 0, 33, 34, 35,
+ 0, 36, 37, 38, 0, 39, 40, 41, 0, 42, 43, 44, 0, 45, 46, 47,
+ };
+ struct width_ranges ranges = width_ranges_load(wc);
+ __m512i shuffle = _mm512_loadu_si512(utf8_3byte_shuffle);
+ __m512i want = _mm512_loadu_si512(utf8_3byte_want);
+ __m512i bits = _mm512_loadu_si512(utf8_3byte_bits);
+ __m512i idx = _mm512_loadu_si512(spread);
+ __m512i prefix = _mm512_set1_epi8((char)0xF5);
+ uint32_t last = *last_cp;
+ int i = 0, cx = 0, o = 0;
+
+ while (i + 48 <= len && cx + 32 <= cols_left &&
+ ((unsigned char)buf[i + 45] & UTF8_LEAD3_MASK) == UTF8_LEAD3_TAG) {
+ __m512i in = _mm512_maskz_loadu_epi8(UTF8_3BYTE_SPAN, buf + i);
+ if (TERM_EXPECT(_mm512_test_epi8_mask(_mm512_xor_si512(in, want), bits) &
+ UTF8_3BYTE_SPAN, 0))
+ break;
+ __m512i cps = utf8_3byte_decode(_mm512_permutexvar_epi8(shuffle, in));
+ if (_mm512_cmplt_epu32_mask(cps, _mm512_set1_epi32((int)UTF8_3BYTE_MIN)) ||
+ _mm512_cmple_epu32_mask(
+ _mm512_sub_epi32(cps, _mm512_set1_epi32((int)UTF8_SURROGATE_LO)),
+ _mm512_set1_epi32((int)(UTF8_SURROGATE_HI - UTF8_SURROGATE_LO))))
+ break;
+ __mmask16 covered = width_ranges_cover(&ranges, cps);
+ if (TERM_EXPECT(covered != 0xFFFF || wc->klass == 0, 0)) {
+ uint32_t cp[16];
+ _mm512_storeu_si512(cp, cps);
+ if (!width_cache_extend(wc, cp, covered))
+ break;
+ ranges = width_ranges_load(wc);
+ }
+ if (wc->klass == 2) {
+ __m512i out = _mm512_mask_mov_epi8(_mm512_permutexvar_epi8(idx, in),
+ 0x1111111111111111ull, prefix);
+ _mm512_storeu_si512((void *)(dst + o), out);
+ o += 64;
+ cx += 32;
+ *wide = true;
+ } else {
+ _mm512_mask_storeu_epi8(dst + o, UTF8_3BYTE_SPAN, in);
+ o += 48;
+ cx += 16;
+ }
+ last = (uint32_t)_mm_extract_epi32(_mm512_extracti32x4_epi32(cps, 3), 3);
+ i += 48;
+ }
+ *out_cols = cx;
+ *out_bytes = o;
+ *last_cp = last;
+ return i;
+}
+#endif
+
+struct text_run_bytes term_text_run_bytes(uint8_t *restrict dst, const char *restrict buf, int len,
+ int max_cols, uint32_t last, struct width_cache *restrict wc)
+{
+ if (TERM_EXPECT(last == 0x200D || grapheme_is_handle(last), 0))
+ return (struct text_run_bytes){ .last = last, .narrow = true };
+ /* Unlike term_text_run(), this fills up to the edge and up to the end of
+ * buf: the cells path writes those characters one by one with the same
+ * result, and a text row should not settle for them. */
+ int i = 0, cx = 0, o = 0, chars = 0;
+ bool wide = false, narrow = true;
+ (void)wc;
+#if defined(TERM_KERNEL_AVX512)
+ bool blocks = term_use_avx512();
+#endif
+ while (i < len && cx < max_cols) {
+ unsigned char c = (unsigned char)buf[i];
+ if (c >= 0x20 && c < 0x7f) {
+ int room = max_cols - cx < len - i ? max_cols - cx : len - i;
+ int n = 1;
+ while (n < room && n < 16 && (unsigned char)(buf[i + n] - 0x20) < 0x5f)
+ n++;
+ if (n == 16 && n < room)
+ n += (int)scan_printable_span(buf + i + 16, (size_t)(room - 16), 0x7f);
+ bytes_copy_small(dst + o, buf + i, n);
+ i += n;
+ o += n;
+ cx += n;
+ chars += n;
+ last = (unsigned char)buf[i - 1];
+ continue;
+ }
+ if (c < 0x20)
+ break;
+#if defined(TERM_KERNEL_AVX512)
+ if ((c & UTF8_LEAD3_MASK) == UTF8_LEAD3_TAG && len - i >= 48 && max_cols - cx >= 32 &&
+ ((unsigned char)buf[i + 21] & UTF8_LEAD3_MASK) == UTF8_LEAD3_TAG && blocks) {
+ int cols, bytes;
+ bool block_wide = false;
+ int used = text_3byte_blocks(dst + o, buf + i, len - i, max_cols - cx, wc,
+ &cols, &bytes, &block_wide, &last);
+ if (used) {
+ i += used;
+ cx += cols;
+ o += bytes;
+ chars += used / 3;
+ wide |= block_wide;
+ narrow = false;
+ continue;
+ }
+ }
+#endif
+ uint32_t cp;
+ int bytes = utf8_decode_one(buf + i, (size_t)(len - i), &cp);
+ if (!bytes)
+ break;
+ int w = bytes < 4 ? unicode_fast_class[cp] : term_text_class(cp);
+ if (w <= 0 || (bytes == 4 && can_extend_cluster(last, cp, w)) || cx + w > max_cols)
+ break;
+ if (w == 2) {
+ dst[o++] = 0xF5;
+ wide = true;
+ }
+ bytes_copy_small(dst + o, buf + i, bytes);
+ o += bytes;
+ cx += w;
+ chars++;
+ narrow = false;
+ last = cp;
+ i += bytes;
+ }
+ return (struct text_run_bytes){ i, cx, o, chars, last, wide, narrow };
+}
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static inline __m512i wide_pair_base(uint32_t fg, uint32_t bg, uint32_t flags)
+{
+ return _mm512_setr_epi32(
+ 0, fg, bg, flags | CELL_WIDE,
+ 0, fg, bg, (flags & ~CELL_WIDE) | CELL_TRAIL,
+ 0, fg, bg, flags | CELL_WIDE,
+ 0, fg, bg, (flags & ~CELL_WIDE) | CELL_TRAIL);
+}
+
+TERM_TARGET_AVX512
+static inline int try_3byte_blocks(uint64_t * restrict dst, const char * restrict buf,
+ int len, int cols_left, uint32_t fg, uint32_t bg,
+ uint32_t flags, struct width_cache * restrict wc,
+ int * restrict out_cols, uint32_t * restrict last_cp)
+{
+ *out_cols = 0;
+ if (len < 48 || cols_left < 32) return 0;
+ if (((unsigned char)buf[0] & UTF8_LEAD3_MASK) != UTF8_LEAD3_TAG) return 0;
+ if (((unsigned char)buf[21] & UTF8_LEAD3_MASK) != UTF8_LEAD3_TAG) return 0;
+ if (((unsigned char)buf[45] & UTF8_LEAD3_MASK) != UTF8_LEAD3_TAG) return 0;
+ __m512i base_narrow_quad = _mm512_broadcast_i32x4(_mm_set_epi32((int)flags, (int)bg, (int)fg, 0));
+ return decode_3byte_blocks(dst, buf, len, cols_left, wide_pair_base(fg, bg, flags),
+ base_narrow_quad, wc, out_cols, last_cp);
+}
+
+/* Two wide cells side by side: cp and next_cp. */
+TERM_TARGET_AVX512
+static inline void store_wide_pair(uint64_t *dst, uint32_t fg, uint32_t bg, uint32_t flags,
+ uint32_t cp, uint32_t next_cp)
+{
+ __m512i cells = _mm512_mask_set1_epi32(wide_pair_base(fg, bg, flags), 0x0001, (int)cp);
+ cells = _mm512_mask_set1_epi32(cells, 0x0100, (int)next_cp);
+ _mm512_storeu_si512((void *)dst, cells);
+}
+#endif
+
+struct cell_bases {
+#if defined(__x86_64__) || defined(_M_X64)
+ __m128i cell, wide, trail;
+#elif defined(__aarch64__)
+ uint32x4_t cell, wide, trail;
+#else
+ uint64_t fg_shifted, high, high_wide, high_trail;
+#endif
+};
+
+static inline struct cell_bases cell_bases_make(uint32_t fg, uint32_t bg, uint32_t flags)
+{
+ struct cell_bases b;
+#if defined(__x86_64__) || defined(_M_X64)
+ b.cell = _mm_set_epi32(flags, bg, fg, 0);
+ b.wide = _mm_set_epi32(flags | CELL_WIDE, bg, fg, 0);
+ b.trail = _mm_set_epi32((flags & ~CELL_WIDE) | CELL_TRAIL, bg, fg, 0);
+#elif defined(__aarch64__)
+ b.cell = (uint32x4_t){ 0, fg, bg, (uint32_t)flags };
+ b.wide = (uint32x4_t){ 0, fg, bg, (uint32_t)(flags | CELL_WIDE) };
+ b.trail = (uint32x4_t){ 0, fg, bg,
+ (uint32_t)((flags & ~CELL_WIDE) | CELL_TRAIL) };
+#else
+ b.fg_shifted = (uint64_t)fg << 32;
+ b.high = ((uint64_t)flags << 32) | bg;
+ b.high_wide = ((uint64_t)(flags | CELL_WIDE) << 32) | bg;
+ b.high_trail = ((uint64_t)((flags & ~CELL_WIDE) | CELL_TRAIL) << 32) | bg;
+#endif
+ return b;
+}
+
+static inline uint64_t *store_cell(uint64_t * restrict dst,
+ const struct cell_bases *b, uint32_t cp)
+{
+#if defined(__x86_64__) || defined(_M_X64)
+ _mm_storeu_si128((__m128i *)dst, _mm_or_si128(b->cell, _mm_cvtsi32_si128(cp)));
+#elif defined(__aarch64__)
+ vst1q_u32((uint32_t *)(void *)dst, vsetq_lane_u32(cp, b->cell, 0));
+#else
+ dst[0] = b->fg_shifted | cp;
+ dst[1] = b->high;
+#endif
+ return dst + 2;
+}
+
+static inline uint64_t *store_cell_wide(uint64_t * restrict dst,
+ const struct cell_bases *b, uint32_t cp)
+{
+#if defined(__x86_64__) || defined(_M_X64)
+ _mm_storeu_si128((__m128i *)dst, _mm_or_si128(b->wide, _mm_cvtsi32_si128(cp)));
+ _mm_storeu_si128((__m128i *)(dst + 2), b->trail);
+#elif defined(__aarch64__)
+ vst1q_u32((uint32_t *)(void *)dst, vsetq_lane_u32(cp, b->wide, 0));
+ vst1q_u32((uint32_t *)(void *)(dst + 2), b->trail);
+#else
+ dst[0] = b->fg_shifted | cp;
+ dst[1] = b->high_wide;
+ dst[2] = b->fg_shifted;
+ dst[3] = b->high_trail;
+#endif
+ return dst + 4;
+}
+
+#if defined(TERM_KERNEL_AVX2)
+/*
+ * Decode runs of 3-byte UTF-8 four code points at a time. A quad stops the
+ * run when a byte is not a 3-byte sequence, a code point is overlong or a
+ * surrogate, or a code point has no fast width class. The scalar loop then
+ * handles that code point, so both paths store the same cells.
+ */
+TERM_TARGET_AVX2
+static inline int decode_3byte_quads(uint64_t **dst_io, const char *buf, int len,
+ int cols_left, const struct cell_bases *b,
+ int *out_cols, uint32_t *last)
+{
+ const __m128i want = _mm_setr_epi8((char)0xE0, (char)0x80, (char)0x80,
+ (char)0xE0, (char)0x80, (char)0x80,
+ (char)0xE0, (char)0x80, (char)0x80,
+ (char)0xE0, (char)0x80, (char)0x80, 0, 0, 0, 0);
+ const __m128i bits = _mm_setr_epi8((char)0xF0, (char)0xC0, (char)0xC0,
+ (char)0xF0, (char)0xC0, (char)0xC0,
+ (char)0xF0, (char)0xC0, (char)0xC0,
+ (char)0xF0, (char)0xC0, (char)0xC0, 0, 0, 0, 0);
+ const __m128i shuffle = _mm_setr_epi8(2, 1, 0, -1, 5, 4, 3, -1,
+ 8, 7, 6, -1, 11, 10, 9, -1);
+ uint64_t *dst = *dst_io;
+ int i = 0, cx = 0;
+ while (i + 16 <= len && cx + 8 <= cols_left) {
+ __m128i in = _mm_loadu_si128((const __m128i *)(buf + i));
+ __m128i ok = _mm_cmpeq_epi8(_mm_and_si128(_mm_xor_si128(in, want), bits),
+ _mm_setzero_si128());
+ if (_mm_movemask_epi8(ok) != 0xFFFF)
+ break;
+ __m128i packed = _mm_shuffle_epi8(in, shuffle);
+ __m128i cps = _mm_or_si128(
+ _mm_or_si128(_mm_and_si128(packed, _mm_set1_epi32(0x003F)),
+ _mm_and_si128(_mm_srli_epi32(packed, 2), _mm_set1_epi32(0x0FC0))),
+ _mm_and_si128(_mm_srli_epi32(packed, 4), _mm_set1_epi32(0xF000)));
+ __m128i bad = _mm_or_si128(
+ _mm_cmplt_epi32(cps, _mm_set1_epi32((int)UTF8_3BYTE_MIN)),
+ _mm_cmpeq_epi32(_mm_and_si128(cps, _mm_set1_epi32(0xF800)),
+ _mm_set1_epi32((int)UTF8_SURROGATE_LO)));
+ if (_mm_movemask_epi8(bad))
+ break;
+ alignas(16) uint32_t cp[4];
+ _mm_store_si128((__m128i *)cp, cps);
+ uint8_t k0 = unicode_fast_class[cp[0]], k1 = unicode_fast_class[cp[1]];
+ uint8_t k2 = unicode_fast_class[cp[2]], k3 = unicode_fast_class[cp[3]];
+ if (!k0 || !k1 || !k2 || !k3)
+ break;
+ const uint8_t k[4] = { k0, k1, k2, k3 };
+ for (int j = 0; j < 4; j++) {
+ if (k[j] == 1) {
+ dst = store_cell(dst, b, cp[j]);
+ cx++;
+ } else {
+ dst = store_cell_wide(dst, b, cp[j]);
+ cx += 2;
+ }
+ }
+ *last = cp[3];
+ i += 12;
+ }
+ *dst_io = dst;
+ *out_cols = cx;
+ return i;
+}
+#endif
+
+/* One body for every level. isa is a constant in each copy below, so each
+ * copy keeps only its own kernels. */
+static inline TERM_ALWAYS_INLINE struct text_run_result
+text_run_body(struct cell *restrict row, const char *restrict buf, int len, int max_cols,
+ uint32_t fg, uint32_t bg, uint32_t flags, uint32_t last,
+ struct width_cache *restrict wc, int isa)
+{
+ int i = 0;
+ int cx = 0;
+ if (TERM_EXPECT(last == 0x200D || grapheme_is_handle(last), 0))
+ return (struct text_run_result){0, 0, last};
+
+ const struct cell_bases bases = cell_bases_make(fg, bg, flags);
+ uint64_t *dst = (uint64_t *)row;
+ (void)wc;
+ (void)isa;
+
+#if defined(TERM_KERNEL_AVX512)
+ if (isa == TERM_ISA_AVX512) {
+ int cols;
+ i += try_3byte_blocks(dst, buf, len, max_cols, fg, bg, flags, wc, &cols, &last);
+ cx += cols;
+ dst += 2 * cols;
+ }
+#endif
+
+ while (i + 4 <= len && cx + 2 <= max_cols) {
+ unsigned char c = (unsigned char)buf[i];
+ if (c >= 0x20 && c < 0x7f) {
+ int ascii_at = cx;
+ dst = store_cell(dst, &bases, c);
+ last = c;
+ cx++;
+ i++;
+ while (i + 4 <= len && cx + 2 <= max_cols) {
+ unsigned char next_c = (unsigned char)buf[i];
+ if (next_c < 0x20 || next_c >= 0x7f) break;
+ dst = store_cell(dst, &bases, next_c);
+ last = next_c;
+ cx++;
+ i++;
+ }
+#if defined(TERM_KERNEL_AVX512)
+ if (isa == TERM_ISA_AVX512 && ascii_at == 0) {
+ int cols;
+ i += try_3byte_blocks(dst, buf + i, len - i, max_cols - cx,
+ fg, bg, flags, wc, &cols, &last);
+ cx += cols;
+ dst += 2 * cols;
+ }
+#endif
+ (void)ascii_at;
+ continue;
+ }
+ if (c < 0x20) {
+ break;
+ }
+
+#if defined(TERM_KERNEL_AVX2)
+ if (isa == TERM_ISA_AVX2 &&
+ TERM_EXPECT((c & UTF8_LEAD3_MASK) == UTF8_LEAD3_TAG && i + 16 <= len &&
+ ((unsigned char)buf[i + 9] & UTF8_LEAD3_MASK) == UTF8_LEAD3_TAG, 0)) {
+ int cols;
+ int used = decode_3byte_quads(&dst, buf + i, len - i, max_cols - cx,
+ &bases, &cols, &last);
+ i += used;
+ cx += cols;
+ if (used)
+ continue;
+ }
+#endif
+ uint32_t cp;
+ int bytes = utf8_decode_one(buf + i, (size_t)(len - i), &cp);
+ if (!bytes) break;
+ int w = bytes < 4 ? unicode_fast_class[cp] : term_text_class(cp);
+ if (w <= 0 || (bytes == 4 && can_extend_cluster(last, cp, w))) break;
+#if defined(TERM_KERNEL_AVX512)
+ if (isa == TERM_ISA_AVX512 && bytes == 3 && w == 2 && i + 6 <= len && cx + 4 <= max_cols) {
+ uint32_t next_cp;
+ if (utf8_decode_one(buf + i + 3, (size_t)(len - i - 3), &next_cp) == 3 &&
+ unicode_fast_class[next_cp] == 2) {
+ store_wide_pair(dst, fg, bg, flags, cp, next_cp);
+ dst += 8;
+ last = next_cp;
+ cx += 4;
+ i += 6;
+ continue;
+ }
+ }
+#endif
+ if (w == 1) {
+ dst = store_cell(dst, &bases, cp);
+ cx++;
+ } else {
+ dst = store_cell_wide(dst, &bases, cp);
+ cx += 2;
+ }
+ last = cp;
+ i += bytes;
+ }
+
+ return (struct text_run_result){i, cx, last};
+}
+
+#define TEXT_RUN_PARAMS struct cell *restrict row, const char *restrict buf, int len, \
+ int max_cols, uint32_t fg, uint32_t bg, uint32_t flags, uint32_t last, \
+ struct width_cache *restrict wc
+#define TEXT_RUN_ARGS row, buf, len, max_cols, fg, bg, flags, last, wc
+
+#if defined(TERM_KERNEL_AVX512)
+TERM_TARGET_AVX512
+static TERM_NOINLINE struct text_run_result text_run_avx512(TEXT_RUN_PARAMS)
+{
+ return text_run_body(TEXT_RUN_ARGS, TERM_ISA_AVX512);
+}
+#endif
+
+#if defined(TERM_KERNEL_AVX2)
+TERM_TARGET_AVX2
+static TERM_NOINLINE struct text_run_result text_run_avx2(TEXT_RUN_PARAMS)
+{
+ return text_run_body(TEXT_RUN_ARGS, TERM_ISA_AVX2);
+}
+#endif
+
+static TERM_NOINLINE struct text_run_result text_run_base(TEXT_RUN_PARAMS)
+{
+ return text_run_body(TEXT_RUN_ARGS, TERM_ISA_BASE);
+}
+
+struct text_run_result term_text_run(TEXT_RUN_PARAMS)
+{
+#if defined(TERM_KERNEL_AVX512)
+ if (term_use_avx512())
+ return text_run_avx512(TEXT_RUN_ARGS);
+#endif
+#if defined(TERM_KERNEL_AVX2)
+ if (term_use_avx2())
+ return text_run_avx2(TEXT_RUN_ARGS);
+#endif
+ return text_run_base(TEXT_RUN_ARGS);
+}
diff --git a/src/text_run.h b/src/text_run.h
new file mode 100644
index 0000000..d546062
--- /dev/null
+++ b/src/text_run.h
@@ -0,0 +1,90 @@
+#ifndef TERM_TEXT_RUN_H
+#define TERM_TEXT_RUN_H
+
+#include "cell.h"
+#include "unicode/unicode.h"
+#include "unicode/grapheme.h"
+
+#define UTF8_LEAD_MIN 0xc2
+#define UTF8_LEAD_MAX 0xf4
+#define UTF8_LEAD3_TAG 0xE0
+#define UTF8_LEAD3_MASK 0xF0
+
+#define TERM_WIDTH_SLOTS 4
+struct width_cache {
+ uint32_t lo[TERM_WIDTH_SLOTS];
+ uint32_t span[TERM_WIDTH_SLOTS];
+ uint8_t klass;
+ uint8_t next;
+};
+
+/* term_new() builds the shared tables before any terminal can use them. */
+bool term_tables_ready(void);
+void term_text_tables_init(void);
+void term_snapshot_tables_init(void);
+
+int term_text_class(uint32_t cp);
+
+struct text_run_result {
+ int bytes;
+ int cols;
+ uint32_t last;
+};
+
+struct text_run_result term_text_run(struct cell *restrict row, const char *restrict buf,
+ int len, int max_cols, uint32_t fg, uint32_t bg,
+ uint32_t flags, uint32_t last, struct width_cache *restrict wc);
+
+struct text_run_bytes {
+ int bytes; /* input used */
+ int cols;
+ int out; /* bytes written */
+ int chars;
+ uint32_t last;
+ bool wide, narrow; /* narrow: every character was one ASCII byte */
+};
+
+/* term_text_run() with the same rules, that writes the text in the history
+ * format instead of cells: raw UTF-8, a wide character after 0xF5. dst has
+ * room for 4 bytes per column. */
+struct text_run_bytes term_text_run_bytes(uint8_t *restrict dst, const char *restrict buf, int len,
+ int max_cols, uint32_t last, struct width_cache *restrict wc);
+
+static inline bool joins_cluster(uint32_t cp, int klass)
+{
+ if (cp < 0x0300)
+ return false;
+ if (klass == 0 || grapheme_emoji_modifier(cp))
+ return true;
+
+ return klass == 1 && unicode_spacing_mark(cp);
+}
+
+static inline bool can_extend_cluster(uint32_t last_char, uint32_t cp, int klass)
+{
+ if (klass == 0) return true;
+ if (last_char == 0x200D || grapheme_is_handle(last_char)) return true;
+ if (cp - 0x1F1E6u <= 0x19u && grapheme_regional_indicator(cp))
+ return true;
+ return joins_cluster(cp, klass);
+}
+
+static inline bool cluster_contextual(uint32_t cp)
+{
+ return unicode_spacing_mark(cp) || grapheme_emoji_modifier(cp) ||
+ (cp - 0x1F1E6u <= 0x19u && grapheme_regional_indicator(cp));
+}
+
+static inline bool cluster_joins(const uint32_t *cps, int len,
+ uint32_t cp, int klass)
+{
+ if (joins_cluster(cp, klass))
+ return true;
+ if (cps[len - 1] == 0x200D)
+ return unicode_extended_pictographic(cp);
+ return len == 1 && cp - 0x1F1E6u <= 0x19u &&
+ grapheme_regional_indicator(cp) &&
+ grapheme_regional_indicator(cps[0]);
+}
+
+#endif