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