summaryrefslogtreecommitdiff
path: root/src/screen.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/screen.c')
-rw-r--r--src/screen.c1513
1 files changed, 1513 insertions, 0 deletions
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;
+}