diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 17:49:34 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 17:49:34 +0300 |
| commit | 4384435941b8c7658856b381d071aee2e0035e89 (patch) | |
| tree | 5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /src | |
term: move the term core to subprojects/term
The core now builds, tests, and benchmarks alone, as a Meson subproject
with no dependency on the main tree.
- include/term/ holds term.h and the header-only unicode/. src/ holds
the core and its private headers. The main tree gets only
include/term through term_dep, so it cannot reach a private header.
- The term tests, the term benchmarks, the Kitty parser suite, and
tools/amalgamate.py and tools/generate_unicode.py move with it. The
tests run as term:<name>.
- The main build passes its -march (arch_flag) and b_ndebug to the
subproject. A directory configured before this commit needs
meson configure -Dterm:arch_flag=-march=... once.
- The term tests compile with -UNDEBUG. They do work inside assert.
- The subproject exports term_test_dep with term_api.h, for the main
tree's render, thumbnail, color, and selection tests.
- The Kitty bridge used struct vector2i and has not compiled since the
public API redesign. It builds again. 2 of its 14 tests fail: REP
after a tab, and a cursor report after a resize.
- dirgraph.py drops the term and unicode layers. Its self-test uses pty.
- The symbols test ignores the __odr_asan symbols that ASan adds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/cell.h | 46 | ||||
| -rw-r--r-- | src/compat.h | 171 | ||||
| -rw-r--r-- | src/core.c | 1311 | ||||
| -rw-r--r-- | src/core.h | 385 | ||||
| -rw-r--r-- | src/csi.h | 109 | ||||
| -rw-r--r-- | src/escape.c | 1116 | ||||
| -rw-r--r-- | src/escape.h | 10 | ||||
| -rw-r--r-- | src/fastcsi.c | 378 | ||||
| -rw-r--r-- | src/gstore.c | 380 | ||||
| -rw-r--r-- | src/gstore.h | 36 | ||||
| -rw-r--r-- | src/hist.c | 3256 | ||||
| -rw-r--r-- | src/hist.h | 321 | ||||
| -rw-r--r-- | src/hist_view.c | 366 | ||||
| -rw-r--r-- | src/hist_view.h | 60 | ||||
| -rw-r--r-- | src/input.c | 771 | ||||
| -rw-r--r-- | src/internal.h | 411 | ||||
| -rw-r--r-- | src/isa.c | 78 | ||||
| -rw-r--r-- | src/kernels.h | 790 | ||||
| -rw-r--r-- | src/meson.build | 27 | ||||
| -rw-r--r-- | src/osc/clipboard.c | 76 | ||||
| -rw-r--r-- | src/osc/dispatch.c | 227 | ||||
| -rw-r--r-- | src/osc/link.c | 260 | ||||
| -rw-r--r-- | src/osc/meson.build | 6 | ||||
| -rw-r--r-- | src/osc/misc.c | 69 | ||||
| -rw-r--r-- | src/osc/osc.h | 30 | ||||
| -rw-r--r-- | src/palette.c | 492 | ||||
| -rw-r--r-- | src/palette.h | 34 | ||||
| -rw-r--r-- | src/pen.h | 165 | ||||
| -rw-r--r-- | src/scan.c | 11 | ||||
| -rw-r--r-- | src/screen.c | 1513 | ||||
| -rw-r--r-- | src/screen.h | 453 | ||||
| -rw-r--r-- | src/selection.c | 824 | ||||
| -rw-r--r-- | src/simd.h | 117 | ||||
| -rw-r--r-- | src/snapshot.c | 346 | ||||
| -rw-r--r-- | src/term.c | 125 | ||||
| -rw-r--r-- | src/text.c | 350 | ||||
| -rw-r--r-- | src/text.h | 10 | ||||
| -rw-r--r-- | src/text_run.c | 682 | ||||
| -rw-r--r-- | src/text_run.h | 90 |
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 |
