summaryrefslogtreecommitdiff
path: root/src/screen.h
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /src/screen.h
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/screen.h')
-rw-r--r--src/screen.h453
1 files changed, 453 insertions, 0 deletions
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