summaryrefslogtreecommitdiff
path: root/src/selection.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/selection.c')
-rw-r--r--src/selection.c824
1 files changed, 824 insertions, 0 deletions
diff --git a/src/selection.c b/src/selection.c
new file mode 100644
index 0000000..6f7336f
--- /dev/null
+++ b/src/selection.c
@@ -0,0 +1,824 @@
+#include "screen.h"
+#include "osc/osc.h"
+#include "unicode/grapheme.h"
+#include "unicode/utf8.h"
+#include <stdlib.h>
+#include <string.h>
+
+/* Copies a heap string into buf and frees it. Returns the length without the NUL. */
+static size_t text_out(char *text, char *buf, size_t cap)
+{
+ size_t n = term_copy_out(text, buf, cap);
+ free(text);
+ return n;
+}
+
+static char *link_at_text(const struct term *t, struct term_vector2i cell)
+{
+ int col = cell.x, row = cell.y;
+ if (!t || col < 0 || col >= t->cols || row < 0 || row >= t->rows) return NULL;
+ const char *link = term_osc8_url_at(t, col, row);
+ if (link) return term_strdup(link);
+ const struct cell *cells = term_screen_view_row(t, row);
+ if (!cells) return NULL;
+ int start = col, end = col;
+ while (start > 0 && cells[start - 1].c > ' ' && cells[start - 1].c < 127) start--;
+ while (end < t->cols && cells[end].c > ' ' && cells[end].c < 127) end++;
+ char *text = malloc((size_t)(end - start) + 1);
+ if (!text) return NULL;
+ for (int c = start; c < end; c++) text[c - start] = cells[c].c;
+ text[end - start] = 0;
+ size_t scheme = 8;
+ char *url = strstr(text, "https://");
+ if (!url) {
+ scheme = 7;
+ url = strstr(text, "http://");
+ if (!url) url = strstr(text, "file://");
+ }
+ char *out = NULL;
+ if (url && col >= start + (int)(url - text)) {
+ size_t n = strlen(url);
+ while (n && strchr(".,;:!?)]}>\"'", url[n - 1])) url[--n] = 0;
+ if (col < start + (int)(url - text) + (int)n && n > scheme)
+ out = term_strdup(url);
+ }
+ free(text);
+ return out;
+}
+
+size_t term_link_at(const struct term *t, struct term_vector2i cell, char *buf, size_t cap)
+{
+ if (!t)
+ return 0;
+ term_write_begin((struct term *)t);
+ char *text = link_at_text(t, cell);
+ term_write_end((struct term *)t);
+ return text_out(text, buf, cap);
+}
+
+void term_select_clear(struct term *t)
+{
+ if (t) {
+ term_write_begin(t);
+ t->selection = false;
+ term_write_end(t);
+ }
+}
+
+static inline int word_class(uint32_t c)
+{
+ if (c <= ' ') return 0;
+ if (c < 128) {
+ static const uint64_t mask[2] = {
+ 0x03FF000000000000ULL,
+ 0x07FFFFFE87FFFFFEULL
+ };
+ return (mask[c >> 6] & (1ULL << (c & 63))) ? 1 : 2;
+ }
+ return 1;
+}
+
+static inline int sel_point_cmp(struct term_sel_point a, struct term_sel_point b)
+{
+ if (a.line_id < b.line_id) return -1;
+ if (a.line_id > b.line_id) return 1;
+ if (a.cell_off < b.cell_off) return -1;
+ if (a.cell_off > b.cell_off) return 1;
+ return 0;
+}
+
+/*
+ * The lines that one call reads. History holds every row of its closed
+ * lines and the first rows of the open line. The screen holds the rest; pos
+ * gives the line and segment of each screen row.
+ */
+struct lines {
+ const struct term *t;
+ int cols;
+ bool hist;
+ uint64_t hist_end;
+ uint64_t first;
+ struct line_pos *pos;
+ struct line_pos stack[256];
+};
+
+static void lines_init(struct lines *L, const struct term *t)
+{
+ L->t = t;
+ L->cols = t->cols;
+ L->hist = screen_has_history(t);
+ L->hist_end = L->hist ? hist_end_id(&t->hist) : 0;
+ L->first = first_retained_line(t);
+ L->pos = t->rows <= 256 ? L->stack : malloc((size_t)t->rows * sizeof(*L->pos));
+ if (!L->pos)
+ L->pos = L->stack;
+ int rows = L->pos == L->stack && t->rows > 256 ? 256 : t->rows;
+ struct line_pos p = screen_top_line(t);
+ for (int y = 0; y < rows; y++) {
+ L->pos[y] = p;
+ p = line_pos_next(p, screen_get_row(term_active_screen(t), y));
+ }
+}
+
+static void lines_free(struct lines *L)
+{
+ if (L->pos != L->stack)
+ free(L->pos);
+}
+
+static inline int lines_rows(const struct lines *L)
+{
+ return L->pos == L->stack && L->t->rows > 256 ? 256 : L->t->rows;
+}
+
+static inline bool in_hist(const struct lines *L, uint64_t id)
+{
+ return L->hist && id >= hist_first_id(&L->t->hist) && id < L->hist_end;
+}
+
+static inline bool is_open_line(const struct lines *L, uint64_t id)
+{
+ return L->hist && id + 1 == L->hist_end && hist_is_open(&L->t->hist);
+}
+
+static inline int hist_segs(const struct lines *L, uint64_t id)
+{
+ return in_hist(L, id) ? term_hist_line_rows(&L->t->hist, id, L->cols) : 0;
+}
+
+static int screen_row_of(const struct lines *L, uint64_t id, int seg)
+{
+ int rows = lines_rows(L);
+ for (int y = 0; y < rows; y++)
+ if (L->pos[y].id == id && (int)L->pos[y].seg == seg)
+ return y;
+ return -1;
+}
+
+/* Segments of line id across history and the screen. */
+static int line_total_segs(const struct lines *L, uint64_t id)
+{
+ int segs = hist_segs(L, id);
+ int rows = lines_rows(L);
+ for (int y = 0; y < rows; y++)
+ if (L->pos[y].id == id && (int)L->pos[y].seg + 1 > segs)
+ segs = (int)L->pos[y].seg + 1;
+ return segs;
+}
+
+/* The cells of one segment, padded to the width. */
+static const struct cell *line_seg_cells(const struct lines *L, uint64_t id, int seg,
+ struct cell *scratch, bool *out_wrapped)
+{
+ const struct term *t = L->t;
+ struct cell *buf = scratch ? scratch : t->view_row_scratch;
+ int hrows = hist_segs(L, id);
+ if (seg < hrows) {
+ int got = term_hist_decode_segment(&t->hist, id, seg, L->cols, buf, t->gstore);
+ bool wrapped = seg + 1 < hrows || is_open_line(L, id);
+ if (got < L->cols) {
+ fill_cells(buf + got, L->cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ if (wrapped)
+ for (int x = got; x < L->cols; x++)
+ buf[x].flags |= CELL_WRAP_PAD;
+ }
+ if (out_wrapped)
+ *out_wrapped = wrapped;
+ return buf;
+ }
+ int y = screen_row_of(L, id, seg);
+ if (y < 0)
+ return NULL;
+ const struct srow *srow = screen_get_row(term_active_screen(t), y);
+ if (out_wrapped)
+ *out_wrapped = (srow->flags & RF_WRAPPED) != 0;
+ if (srow->kind == ROW_CELLS) {
+ if (srow->len >= L->cols || (srow->flags & RF_TAIL_PADDED))
+ return srow->cells;
+ if (srow->len > 0)
+ memcpy(buf, srow->cells, (size_t)srow->len * sizeof(struct cell));
+ fill_cells(buf + srow->len, L->cols - srow->len, TERM_COLOR_FG, TERM_COLOR_BG);
+ return buf;
+ }
+ text_row_cells(srow, buf, L->cols);
+ return buf;
+}
+
+/* A line whose segments may not hold exactly cols cells each. */
+static bool line_has_wide(const struct lines *L, uint64_t id)
+{
+ const struct term *t = L->t;
+ if (in_hist(L, id)) {
+ if (term_hist_line_flags(&t->hist, id) & (HLINE_HAS_WIDE | HLINE_HAS_CLUSTER))
+ return true;
+ if (is_open_line(L, id) &&
+ term_hist_line_cells(&t->hist, id) != (uint64_t)hist_segs(L, id) * (uint64_t)L->cols)
+ return true;
+ }
+ int rows = lines_rows(L);
+ for (int y = 0; y < rows; y++) {
+ if (L->pos[y].id != id)
+ continue;
+ const struct srow *r = screen_get_row(term_active_screen(t), y);
+ if (r->flags & RF_WIDE)
+ return true;
+ if (r->kind == ROW_CELLS && r->len >= L->cols && L->cols > 1 &&
+ (r->cells[L->cols - 1].flags & CELL_WRAP_PAD))
+ return true;
+ }
+ return false;
+}
+
+static inline int seg_display_cols(int cols, const struct cell *cells, bool wrapped)
+{
+ if (cells && wrapped && cols > 1 && (cells[cols - 1].flags & CELL_WRAP_PAD))
+ return cols - 1;
+ return cols;
+}
+
+/* The display column where segment seg of line id starts. */
+static uint32_t line_seg_start_off(const struct lines *L, uint64_t id, int seg)
+{
+ if (seg <= 0)
+ return 0;
+ if (!line_has_wide(L, id))
+ return (uint32_t)seg * (uint32_t)L->cols;
+
+ const struct term *t = L->t;
+ int hrows = hist_segs(L, id);
+ if (in_hist(L, id)) {
+ if (seg <= hrows) {
+ uint64_t base = id == t->hist.base_cell_id ? t->hist.base_cell : 0;
+ return (uint32_t)(term_hist_segment_cell(&t->hist, id, seg, L->cols) - base);
+ }
+ }
+
+ uint32_t off = 0;
+ if (in_hist(L, id) && hrows > 0) {
+ uint64_t base = id == t->hist.base_cell_id ? t->hist.base_cell : 0;
+ off = (uint32_t)(term_hist_segment_cell(&t->hist, id, hrows, L->cols) - base);
+ }
+ struct cell stack_buf[512];
+ struct cell *scratch = L->cols <= 512 ? stack_buf : malloc((size_t)L->cols * sizeof(*scratch));
+ for (int s = hrows; s < seg; s++) {
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(L, id, s, scratch, &wrapped);
+ off += (uint32_t)seg_display_cols(L->cols, cells, wrapped);
+ }
+ if (scratch != stack_buf)
+ free(scratch);
+ return off;
+}
+
+/* The line and segment of view row y. */
+static bool view_row_line(const struct lines *L, int y, uint64_t *id, int *seg)
+{
+ const struct term *t = L->t;
+ int y_scr;
+ if (term_hist_view_row_pos(&t->view, &t->hist, L->cols, t->rows, screen_top_line(t), y,
+ id, seg, &y_scr))
+ return true;
+ if (y_scr >= lines_rows(L))
+ y_scr = lines_rows(L) - 1;
+ *id = L->pos[y_scr].id;
+ *seg = (int)L->pos[y_scr].seg;
+ return true;
+}
+
+static struct term_sel_point view_to_sel_point(const struct lines *L, int x, int y)
+{
+ const struct term *t = L->t;
+ if (x < 0) x = 0;
+ if (x >= L->cols) x = L->cols - 1;
+ if (y < 0) y = 0;
+ if (y >= t->rows) y = t->rows - 1;
+
+ uint64_t id;
+ int seg;
+ if (!view_row_line(L, y, &id, &seg))
+ return (struct term_sel_point){ .line_id = L->first };
+ const struct cell *cells = line_seg_cells(L, id, seg, NULL, NULL);
+ if (cells)
+ while (x > 0 && (cells[x].flags & CELL_WRAP_PAD))
+ x--;
+ return (struct term_sel_point){
+ .line_id = id,
+ .cell_off = line_seg_start_off(L, id, seg) + (uint32_t)x,
+ };
+}
+
+static uint32_t line_total_display_cols(const struct lines *L, uint64_t id, int nsegs)
+{
+ if (nsegs <= 0)
+ return 0;
+ uint32_t start = line_seg_start_off(L, id, nsegs - 1);
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(L, id, nsegs - 1, NULL, &wrapped);
+ return start + (uint32_t)seg_display_cols(L->cols, cells, wrapped);
+}
+
+static struct term_sel_point sel_point_next(const struct lines *L, struct term_sel_point pt)
+{
+ int nsegs = line_total_segs(L, pt.line_id);
+ if (nsegs <= 0)
+ return pt;
+ if (pt.cell_off + 1 < (uint64_t)(nsegs - 1) * (L->cols > 1 ? (uint64_t)(L->cols - 1) : 1ULL) ||
+ pt.cell_off + 1 < line_total_display_cols(L, pt.line_id, nsegs)) {
+ pt.cell_off++;
+ return pt;
+ }
+ uint64_t max_id = L->pos[lines_rows(L) - 1].id;
+ for (uint64_t next = pt.line_id + 1; next <= max_id; next++)
+ if (line_total_segs(L, next) > 0)
+ return (struct term_sel_point){ .line_id = next };
+ pt.cell_off++;
+ return pt;
+}
+
+static struct term_sel_point sel_point_prev(const struct lines *L, struct term_sel_point pt)
+{
+ if (pt.cell_off > 0) {
+ pt.cell_off--;
+ return pt;
+ }
+ for (uint64_t prev = pt.line_id; prev-- > L->first; ) {
+ int nsegs = line_total_segs(L, prev);
+ if (nsegs <= 0)
+ continue;
+ uint32_t cols = line_total_display_cols(L, prev, nsegs);
+ return (struct term_sel_point){ .line_id = prev, .cell_off = cols > 0 ? cols - 1 : 0 };
+ }
+ return pt;
+}
+
+static void unit_span(const struct lines *L, int x, int y, bool right_half,
+ enum term_select_unit unit,
+ struct term_sel_point *first, struct term_sel_point *last)
+{
+ const struct term *t = L->t;
+ int cols = L->cols;
+ if (x < 0) x = 0;
+ if (x >= cols) x = cols - 1;
+ if (y < 0) y = 0;
+ if (y >= t->rows) y = t->rows - 1;
+
+ if (unit == TERM_SELECT_CELL) {
+ struct term_sel_point pt = view_to_sel_point(L, x, y);
+ if (right_half)
+ pt = sel_point_next(L, pt);
+ *first = *last = pt;
+ return;
+ }
+ if (unit == TERM_SELECT_LINE) {
+ *first = view_to_sel_point(L, 0, y);
+ *last = sel_point_next(L, view_to_sel_point(L, cols - 1, y));
+ return;
+ }
+
+ const struct cell *row = term_screen_view_row(t, y);
+ if (row)
+ while (x > 0 && (row[x].flags & CELL_WRAP_PAD))
+ x--;
+ int left = x, right = x;
+ int kind = row ? word_class(row[x].c) : 0;
+ while (left > 0 && row && word_class(row[left - 1].c) == kind)
+ left--;
+ while (right + 1 < cols && row && !(row[right + 1].flags & CELL_WRAP_PAD) &&
+ word_class(row[right + 1].c) == kind)
+ right++;
+ *first = view_to_sel_point(L, left, y);
+ *last = sel_point_next(L, view_to_sel_point(L, right, y));
+}
+
+/* Clamp the selection to the retained lines. A read does not change it. */
+static bool selection_bounds(const struct lines *L, struct term_sel_point *start,
+ struct term_sel_point *end)
+{
+ const struct term *t = L->t;
+ if (!t->selection || t->sel_end.line_id < L->first)
+ return false;
+ *start = t->sel_start;
+ *end = t->sel_end;
+ if (start->line_id < L->first)
+ *start = (struct term_sel_point){ .line_id = L->first };
+ return sel_point_cmp(*start, *end) <= 0;
+}
+
+static bool line_locate_cell(const struct lines *L, uint64_t id, uint32_t cell_off,
+ int *out_seg, int *out_col)
+{
+ int nsegs = line_total_segs(L, id);
+ if (nsegs <= 0)
+ return false;
+ if (!line_has_wide(L, id)) {
+ int seg = (int)(cell_off / (uint32_t)L->cols);
+ if (seg >= nsegs)
+ return false;
+ if (out_seg)
+ *out_seg = seg;
+ if (out_col)
+ *out_col = (int)(cell_off % (uint32_t)L->cols);
+ return true;
+ }
+
+ int hrows = hist_segs(L, id);
+ if (in_hist(L, id) && hrows > 0) {
+ uint32_t last_start = line_seg_start_off(L, id, hrows - 1);
+ if (cell_off >= last_start) {
+ if (hrows == nsegs) {
+ if (cell_off < last_start + (uint32_t)L->cols) {
+ if (out_seg)
+ *out_seg = hrows - 1;
+ if (out_col)
+ *out_col = (int)(cell_off - last_start);
+ return true;
+ }
+ return false;
+ }
+ } else {
+ uint64_t base = id == L->t->hist.base_cell_id ? L->t->hist.base_cell : 0;
+ int seg = term_hist_cell_segment(&L->t->hist, id, base + (uint64_t)cell_off, L->cols);
+ if (seg < hrows) {
+ uint32_t seg_start = line_seg_start_off(L, id, seg);
+ if (cell_off >= seg_start) {
+ if (out_seg)
+ *out_seg = seg;
+ if (out_col)
+ *out_col = (int)(cell_off - seg_start);
+ return true;
+ }
+ }
+ }
+ }
+
+ int start_seg = (in_hist(L, id) && hrows > 0 && hrows < nsegs) ? hrows - 1 : hrows;
+ uint32_t start = line_seg_start_off(L, id, start_seg);
+ for (int seg = start_seg; seg < nsegs; seg++) {
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(L, id, seg, NULL, &wrapped);
+ uint32_t w = (uint32_t)seg_display_cols(L->cols, cells, wrapped);
+ if (cell_off < start + w) {
+ if (out_seg)
+ *out_seg = seg;
+ if (out_col)
+ *out_col = (int)(cell_off - start);
+ return true;
+ }
+ start += w;
+ }
+ return false;
+}
+
+static void select_range(struct term *t, const struct lines *L,
+ struct term_sel_point start, struct term_sel_point end)
+{
+ if (sel_point_cmp(start, end) > 0) {
+ t->selection = false;
+ return;
+ }
+ int seg = 0, col = 0;
+ if (line_locate_cell(L, start.line_id, start.cell_off, &seg, &col)) {
+ const struct cell *cells = line_seg_cells(L, start.line_id, seg, NULL, NULL);
+ if (cells && col > 0 && (cells[col].flags & CELL_TRAIL))
+ start = sel_point_prev(L, start);
+ }
+ if (line_locate_cell(L, end.line_id, end.cell_off, &seg, &col)) {
+ const struct cell *cells = line_seg_cells(L, end.line_id, seg, NULL, NULL);
+ if (cells && col + 1 < L->cols && (cells[col].flags & CELL_WIDE))
+ end = sel_point_next(L, end);
+ }
+ t->sel_start = start;
+ t->sel_end = end;
+ t->selection = true;
+}
+
+void term_select_start(struct term *t, struct term_vector2i cell, bool right_half,
+ enum term_select_unit unit)
+{
+ if (!t) return;
+ term_write_begin(t);
+ struct lines L;
+ lines_init(&L, t);
+ t->select_unit = unit;
+ unit_span(&L, cell.x, cell.y, right_half, unit, &t->sel_anchor_first, &t->sel_anchor_last);
+ t->selection = false;
+ lines_free(&L);
+ term_select_extend(t, cell, right_half);
+ term_write_end(t);
+}
+
+void term_select_extend(struct term *t, struct term_vector2i cell, bool right_half)
+{
+ if (!t) return;
+ term_write_begin(t);
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point first, last;
+ unit_span(&L, cell.x, cell.y, right_half, t->select_unit, &first, &last);
+ if (sel_point_cmp(first, t->sel_anchor_first) > 0) first = t->sel_anchor_first;
+ if (sel_point_cmp(last, t->sel_anchor_last) < 0) last = t->sel_anchor_last;
+ if (sel_point_cmp(first, last) == 0)
+ t->selection = false;
+ else
+ select_range(t, &L, first, sel_point_prev(&L, last));
+ lines_free(&L);
+ term_write_end(t);
+}
+
+bool term_has_selection(const struct term *t)
+{
+ if (!t)
+ return false;
+ term_write_begin((struct term *)t);
+ bool has = t->selection && t->sel_end.line_id >= first_retained_line(t);
+ term_write_end((struct term *)t);
+ return has;
+}
+
+void term_selection_view_spans(const struct term *t, int *c0, int *c1)
+{
+ int rows = t->rows, cols = t->cols;
+ for (int y = 0; y < rows; y++) {
+ c0[y] = cols;
+ c1[y] = -1;
+ }
+ if (!t->selection)
+ return;
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point start, end;
+ if (!selection_bounds(&L, &start, &end)) {
+ lines_free(&L);
+ return;
+ }
+ uint64_t prev_id = UINT64_MAX;
+ int prev_seg = -1;
+ uint32_t prev_end = 0;
+
+ for (int y = 0; y < rows; y++) {
+ uint64_t id;
+ int seg;
+ if (!view_row_line(&L, y, &id, &seg) || id < start.line_id || id > end.line_id)
+ continue;
+ uint32_t seg_start;
+ if (id == prev_id && seg == prev_seg + 1)
+ seg_start = prev_end;
+ else
+ seg_start = line_seg_start_off(&L, id, seg);
+
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(&L, id, seg, NULL, &wrapped);
+ uint32_t seg_cols = (uint32_t)seg_display_cols(cols, cells, wrapped);
+ uint32_t seg_end = seg_start + seg_cols;
+ prev_id = id;
+ prev_seg = seg;
+ prev_end = seg_end;
+
+ uint32_t s = 0, e = (uint32_t)(cols - 1);
+ if (id == start.line_id) {
+ if (start.cell_off >= seg_end)
+ continue;
+ s = start.cell_off > seg_start ? start.cell_off - seg_start : 0;
+ }
+ if (id == end.line_id) {
+ if (end.cell_off < seg_start)
+ continue;
+ if (end.cell_off < seg_end && end.cell_off - seg_start < (uint32_t)cols)
+ e = end.cell_off - seg_start;
+ }
+ c0[y] = (int)s;
+ c1[y] = (int)e;
+ }
+ lines_free(&L);
+}
+
+static bool text_reserve(char **text, size_t *cap, size_t need)
+{
+ if (need <= *cap)
+ return true;
+ size_t next = *cap * 2 > need ? *cap * 2 : need;
+ char *grown = realloc(*text, next);
+ if (!grown) {
+ free(*text);
+ *text = NULL;
+ return false;
+ }
+ *text = grown;
+ *cap = next;
+ return true;
+}
+
+static char *range_text_inner(const struct term *t,
+ const struct term_sel_point *bounds)
+{
+ if (!t || (!bounds && !t->selection)) return NULL;
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point start, end;
+ if (bounds) {
+ start = bounds[0];
+ end = bounds[1];
+ } else if (!selection_bounds(&L, &start, &end)) {
+ lines_free(&L);
+ return NULL;
+ }
+
+ size_t cap = 4096, n = 0;
+ char *text = malloc(cap);
+ for (uint64_t id = start.line_id; text && id <= end.line_id; id++) {
+ int nsegs = line_total_segs(&L, id);
+ int start_seg = 0;
+ if (id == start.line_id) {
+ if (!line_locate_cell(&L, id, start.cell_off, &start_seg, NULL))
+ start_seg = 0;
+ }
+ int end_seg = nsegs - 1;
+ if (id == end.line_id) {
+ if (!line_locate_cell(&L, id, end.cell_off, &end_seg, NULL))
+ end_seg = nsegs - 1;
+ }
+ uint32_t next_start = line_seg_start_off(&L, id, start_seg);
+ int hrows = hist_segs(&L, id);
+ enum { BATCH_SEGS = 64 };
+ struct cell *batch_cells = NULL;
+ int batch_lens[BATCH_SEGS];
+ int batch_start = -1, batch_len = 0;
+
+ for (int seg = start_seg; text && seg <= end_seg && seg < nsegs; seg++) {
+ uint32_t seg_start = next_start;
+ bool row_wrapped = false;
+ const struct cell *cells = NULL;
+ if (seg < hrows) {
+ if (seg >= batch_start + batch_len) {
+ batch_start = seg;
+ int take = hrows - seg;
+ if (take > BATCH_SEGS)
+ take = BATCH_SEGS;
+ if (seg + take - 1 > end_seg)
+ take = end_seg - seg + 1;
+ if (!batch_cells)
+ batch_cells = malloc((size_t)BATCH_SEGS * (size_t)t->cols * sizeof(struct cell));
+ batch_len = batch_cells ? term_hist_decode_segments(&t->hist, id, batch_start, take,
+ t->cols, batch_cells, batch_lens,
+ t->gstore) : 0;
+ }
+ if (batch_cells && seg >= batch_start && seg < batch_start + batch_len) {
+ int idx = seg - batch_start;
+ struct cell *row_buf = batch_cells + (size_t)idx * (size_t)t->cols;
+ int got = batch_lens[idx];
+ row_wrapped = seg + 1 < hrows || is_open_line(&L, id);
+ if (got < t->cols) {
+ fill_cells(row_buf + got, t->cols - got, TERM_COLOR_FG, TERM_COLOR_BG);
+ if (row_wrapped)
+ for (int x = got; x < t->cols; x++)
+ row_buf[x].flags |= CELL_WRAP_PAD;
+ }
+ cells = row_buf;
+ }
+ }
+ if (!cells)
+ cells = line_seg_cells(&L, id, seg, NULL, &row_wrapped);
+ if (!cells) {
+ next_start = seg_start + (uint32_t)t->cols;
+ continue;
+ }
+ uint32_t seg_end = seg_start + (uint32_t)seg_display_cols(t->cols, cells, row_wrapped);
+ next_start = seg_end;
+
+ uint32_t s = 0, e = (uint32_t)(t->cols - 1);
+ if (id == start.line_id) {
+ if (start.cell_off >= seg_end)
+ continue;
+ s = start.cell_off > seg_start ? start.cell_off - seg_start : 0;
+ }
+ if (id == end.line_id) {
+ if (end.cell_off < seg_start)
+ continue;
+ if (end.cell_off < seg_end && end.cell_off - seg_start < (uint32_t)t->cols)
+ e = end.cell_off - seg_start;
+ }
+ int c0 = (int)s, c1 = (int)e;
+ if (c1 < c0)
+ continue;
+ bool is_last = id == end.line_id && (end.cell_off < seg_end || seg == nsegs - 1);
+ bool wrapped = !is_last && row_wrapped;
+ if (!wrapped)
+ while (c1 >= c0 && cells[c1].c <= ' ')
+ c1--;
+
+ if (!text_reserve(&text, &cap, n + (size_t)(c1 - c0 + 1) + 1))
+ break;
+ int c = c0;
+ for (; c + 3 <= c1; c += 4) {
+#if defined(__aarch64__)
+ uint32x4x4_t block = vld4q_u32((const uint32_t *)&cells[c]);
+ uint32x4_t cp = block.val[0];
+ uint32x4_t valid = vandq_u32(
+ vcltq_u32(vsubq_u32(cp, vdupq_n_u32(1)), vdupq_n_u32(127)),
+ vceqq_u32(vandq_u32(block.val[3],
+ vdupq_n_u32(CELL_TRAIL | CELL_WRAP_PAD)), vdupq_n_u32(0)));
+ if (!vminvq_u32(valid)) break;
+ uint16x4_t half = vmovn_u32(cp);
+ uint8x8_t bytes = vmovn_u16(vcombine_u16(half, half));
+ uint32_t packed = vget_lane_u32(vreinterpret_u32_u8(bytes), 0);
+#else
+ uint32_t a = cells[c].c, b = cells[c + 1].c;
+ uint32_t d = cells[c + 2].c, f = cells[c + 3].c;
+ unsigned flags = (unsigned)cells[c].flags | cells[c + 1].flags |
+ cells[c + 2].flags | cells[c + 3].flags;
+ if ((a - 1) >= 127 || (b - 1) >= 127 || (d - 1) >= 127 || (f - 1) >= 127 ||
+ (flags & (CELL_TRAIL | CELL_WRAP_PAD))) break;
+ uint32_t packed = a | (b << 8) | (d << 16) | (f << 24);
+#endif
+ memcpy(text + n, &packed, 4);
+ n += 4;
+ }
+ for (; c <= c1; c++) {
+ uint32_t cp = cells[c].c;
+ if (cells[c].flags & (CELL_TRAIL | CELL_WRAP_PAD))
+ continue;
+ /* One cell expands to GRAPHEME_MAX_LEN code points of up
+ * to 4 bytes each. Leave room for all of them. */
+ if (!text_reserve(&text, &cap, n + (size_t)GRAPHEME_MAX_LEN * 4 + 1))
+ break;
+ if (cp > 0 && cp < 128) {
+ text[n++] = (char)cp;
+ } else {
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ int len = term_gstore_expand(t->gstore, cp ? cp : ' ', cps, GRAPHEME_MAX_LEN);
+ for (int k = 0; k < len; k++)
+ n += (size_t)utf8_encode(cps[k], text + n);
+ }
+ }
+ if (text && !is_last && !wrapped && text_reserve(&text, &cap, n + 2))
+ text[n++] = '\n';
+ }
+ free(batch_cells);
+ }
+ if (text)
+ text[n] = 0;
+ lines_free(&L);
+ return text;
+}
+
+static char *history_text(struct term *t, int max_rows)
+{
+ if (!t)
+ return NULL;
+ term_write_begin(t);
+ term_hist_prepare_rows(&t->hist, t->cols);
+ if (!term_hist_view_sync(&t->view, &t->hist, t->cols)) {
+ term_write_end(t);
+ return NULL;
+ }
+ int end_row = term_screen_history_rows(t) - term_view_offset(t);
+ if (max_rows <= 0 || end_row <= 0) {
+ term_write_end(t);
+ return term_strdup("");
+ }
+ int begin_row = end_row > max_rows ? end_row - max_rows : 0;
+ struct lines L;
+ lines_init(&L, t);
+ struct term_sel_point bounds[2] = {0};
+ int first_seg, last_seg;
+ char *text = NULL;
+ if (term_hist_view_find_row(&t->view, &t->hist, begin_row, t->cols,
+ &bounds[0].line_id, &first_seg) &&
+ term_hist_view_find_row(&t->view, &t->hist, end_row - 1, t->cols,
+ &bounds[1].line_id, &last_seg)) {
+ bounds[0].cell_off = line_seg_start_off(&L, bounds[0].line_id, first_seg);
+ uint32_t last_start = line_seg_start_off(&L, bounds[1].line_id, last_seg);
+ bool wrapped = false;
+ const struct cell *cells = line_seg_cells(&L, bounds[1].line_id,
+ last_seg, NULL, &wrapped);
+ bounds[1].cell_off = last_start +
+ (uint32_t)seg_display_cols(t->cols, cells, wrapped) - 1;
+ text = range_text_inner(t, bounds);
+ if (text && !wrapped) {
+ size_t n = strlen(text);
+ size_t cap = n + 1;
+ if (text_reserve(&text, &cap, n + 2)) {
+ text[n] = '\n';
+ text[n + 1] = '\0';
+ }
+ }
+ }
+ lines_free(&L);
+ term_write_end(t);
+ return text;
+}
+
+size_t term_history_text(struct term *t, int max_rows, char *buf, size_t cap)
+{
+ return text_out(history_text(t, max_rows), buf, cap);
+}
+
+size_t term_selection_text(const struct term *t, char *buf, size_t cap)
+{
+ if (!t)
+ return 0;
+ term_write_begin((struct term *)t);
+ char *text = range_text_inner(t, NULL);
+ term_write_end((struct term *)t);
+ return text_out(text, buf, cap);
+}