/* * Reference model of the terminal. See model.h. * * Sources of truth, in order: * - xterm and ECMA-48 for controls, escape sequences, and modes. * - Unicode for UTF-8: each maximal invalid subpart becomes one U+FFFD. * - This terminal's design where xterm has no rule: reflow on resize, the * row length that reflow keeps, history, and grapheme clusters. * * The model is slow on purpose. It has no lazy rows, no rings, and no SIMD. * * Known differences from xterm follow. For each one the model matches the * current terminal, and the macro names the phase that removes it. */ #include "model.h" #include "term/unicode/unicode.h" #include #include #include #include #include /* Phase 6: the history limit counts rows. It must count logical lines. */ #define KNOWN_HISTORY_COUNTS_ROWS 0 /* Phase 6: a column resize on the alternate screen also cuts the saved * normal screen and the history to the new width. */ #define KNOWN_ALT_RESIZE_CUTS_NORMAL 0 #define MAX_TABS 4096 #define STR_KEEP 1024 struct cell { uint32_t cp[GRAPHEME_MAX_LEN]; uint8_t n, width; uint16_t attrs; term_color fg, bg; bool pad; /* the blank that a wide glyph left at a row end */ }; /* * A row. len is the length that reflow keeps: it covers every cell that * was written or erased in a color, and a default erase at the end of the * row shortens it. Cells at or after len are default blanks. */ struct row { int len; bool wrapped; /* autowrap continued this row on the next one */ struct cell c[]; }; struct pen { term_color fg, bg, ul; uint16_t attrs; /* TERM_ATTR_* style bits and the underline style */ bool inverse; }; /* What DECSC saves. */ struct saved { int x, y; bool pending, origin; struct pen pen; char g[2]; int gl; }; struct keyboard { unsigned flags, saved[32], depth; }; enum { GROUND, ESC, CSI, STR, STR_ESC, CHARSET, HASH, DOCS }; enum { OSC, DCS, IGNORE }; struct model { int cols, rows, limit; struct row **scr; struct row **hist; int hist_len, hist_cap, hist_lines; /* The normal screen while the alternate screen is active. */ bool alternate; struct row **normal; int normal_cols, normal_rows; struct saved normal_cursor, normal_saved; bool normal_visible, normal_blink; int normal_style; int x, y; bool pending; struct pen pen; char g[2]; int gl; struct saved saved; bool autowrap, origin, insert, lnm, reverse, app_cursor, app_keypad; bool backarrow, bracketed, focus, sync, visible, blink; int style, mouse, mouse_enc, decsace; int top, bottom; bool tabs[MAX_TABS]; struct keyboard keyboard[2]; /* The last printed glyph, for REP and cluster joins. */ uint32_t last[GRAPHEME_MAX_LEN]; int last_n; int prompt; /* screen row of the last OSC 133;A, or -1 */ unsigned flags; int state, str_kind; char slot; int param[16], nparam, csi_phase; uint16_t sub; char prefix, inter; char str[STR_KEEP]; size_t str_len; int need; uint32_t acc; uint8_t lo, hi; char *reply; size_t reply_len, reply_cap; struct model_cell **out; int out_cap, out_cols; }; /* ---- cells and rows ---- */ static struct cell blank(term_color fg, term_color bg) { return (struct cell){ .cp = { ' ' }, .n = 1, .width = 1, .fg = fg, .bg = bg }; } static struct cell default_blank(void) { return blank(TERM_COLOR_FG, TERM_COLOR_BG); } static struct row *row_new(int cols) { struct row *r = malloc(sizeof(*r) + (size_t)cols * sizeof(struct cell)); if (!r) abort(); r->len = 0; r->wrapped = false; for (int x = 0; x < cols; x++) r->c[x] = default_blank(); return r; } /* Cut len to n. The cells after it become default blanks. */ static void row_cut(struct row *r, int n, int cols) { if (n >= r->len) return; for (int x = n; x < r->len && x < cols; x++) r->c[x] = default_blank(); r->len = n; } static bool pristine(const struct model *m) { return m->pen.fg == TERM_COLOR_FG && m->pen.bg == TERM_COLOR_BG; } static bool is_ri(uint32_t cp) { return cp - 0x1F1E6u <= 0x19u && grapheme_regional_indicator(cp); } /* ---- replies ---- */ static void __attribute__((format(printf, 2, 3))) reply(struct model *m, const char *fmt, ...) { char tmp[256]; va_list args; va_start(args, fmt); int n = vsnprintf(tmp, sizeof(tmp), fmt, args); va_end(args); if (n < 0) return; if (m->reply_len + (size_t)n + 1 > m->reply_cap) { m->reply_cap = (m->reply_len + (size_t)n + 1) * 2; m->reply = realloc(m->reply, m->reply_cap); if (!m->reply) abort(); } memcpy(m->reply + m->reply_len, tmp, (size_t)n); m->reply_len += (size_t)n; } /* ---- history ---- */ static int history_limit(int requested) { return requested == 0 ? TERM_SCROLLBACK_DEFAULT : requested < 0 ? 0 : requested; } static void hist_reflow_tail(struct model *m); static void hist_push(struct model *m, struct row *r) { if (!m->limit) { free(r); return; } if (KNOWN_HISTORY_COUNTS_ROWS) { if (m->hist_len == m->limit) { free(m->hist[0]); memmove(m->hist, m->hist + 1, (size_t)(m->hist_len - 1) * sizeof(*m->hist)); m->hist_len--; } } else { bool new_line = (m->hist_len == 0 || !m->hist[m->hist_len - 1]->wrapped); if (new_line) { if (m->hist_lines == m->limit) { int evict_rows = 1; while (evict_rows < m->hist_len && m->hist[evict_rows - 1]->wrapped) evict_rows++; for (int i = 0; i < evict_rows; i++) free(m->hist[i]); memmove(m->hist, m->hist + evict_rows, (size_t)(m->hist_len - evict_rows) * sizeof(*m->hist)); m->hist_len -= evict_rows; m->hist_lines--; } m->hist_lines++; } } if (m->hist_len == m->hist_cap) { m->hist_cap = m->hist_cap ? m->hist_cap * 2 : 64; m->hist = realloc(m->hist, (size_t)m->hist_cap * sizeof(*m->hist)); if (!m->hist) abort(); } m->hist[m->hist_len++] = r; if (!KNOWN_HISTORY_COUNTS_ROWS) hist_reflow_tail(m); } static void term_hist_clear(struct model *m) { for (int i = 0; i < m->hist_len; i++) free(m->hist[i]); m->hist_len = 0; m->hist_lines = 0; } /* ---- scrolling ---- */ static void erase_row(struct model *m, int y); /* Scroll the whole screen up one row. On the normal screen the top row * goes to history. */ static void scroll_up_full(struct model *m) { struct row *top = m->scr[0]; memmove(m->scr, m->scr + 1, (size_t)(m->rows - 1) * sizeof(*m->scr)); m->scr[m->rows - 1] = row_new(m->cols); erase_row(m, m->rows - 1); if (m->alternate) { free(top); return; } if (m->prompt > 0) m->prompt--; hist_push(m, top); } static void scroll_region(struct model *m, int top, int bottom, int n, bool down, bool history) { int height = bottom - top + 1; if (n > height) n = height; if (!down && history && top == 0 && bottom == m->rows - 1) { for (int i = 0; i < n; i++) scroll_up_full(m); return; } /* A region at the top of the normal screen saves the rows that leave * it. The rows below the region stay in place. */ if (!down && history && top == 0 && !m->alternate) { for (int i = 0; i < n; i++) { scroll_up_full(m); scroll_region(m, bottom, m->rows - 1, 1, true, false); } return; } for (int i = 0; i < n; i++) { if (down) { free(m->scr[bottom]); memmove(m->scr + top + 1, m->scr + top, (size_t)(height - 1) * sizeof(*m->scr)); m->scr[top] = row_new(m->cols); erase_row(m, top); } else { free(m->scr[top]); memmove(m->scr + top, m->scr + top + 1, (size_t)(height - 1) * sizeof(*m->scr)); m->scr[bottom] = row_new(m->cols); erase_row(m, bottom); } } if (!m->alternate && m->prompt >= top && m->prompt <= bottom) { m->prompt += down ? n : -n; if (m->prompt < top || m->prompt > bottom) m->prompt = -1; } } static void line_feed(struct model *m) { if (m->y == m->bottom) { scroll_region(m, m->top, m->bottom, 1, false, true); } else if (m->y < m->rows - 1) { m->y++; } } static void reverse_index(struct model *m) { if (m->y == m->top) scroll_region(m, m->top, m->bottom, 1, true, false); else if (m->y > 0) m->y--; } /* ---- erasing ---- */ /* An erase that covers half of a wide glyph erases all of it. Erased cells * take the pen colors without inverse and lose their attributes. */ static void erase_cells(struct model *m, int y, int from, int to) { struct row *r = m->scr[y]; if (to > m->cols) to = m->cols; bool plain = pristine(m); if (plain && to > r->len) to = r->len; if (from >= to) return; if (from > 0 && r->c[from].width == 0) from--; if (to < r->len && r->c[to].width == 0) to++; if (to >= m->cols) r->wrapped = false; if (!plain) { for (int x = from; x < to; x++) r->c[x] = blank(m->pen.fg, m->pen.bg); if (to > r->len) r->len = to; return; } if (to < r->len) { for (int x = from; x < to; x++) r->c[x] = default_blank(); return; } row_cut(r, from, m->cols); if (from == 0) r->wrapped = false; } static void erase_row(struct model *m, int y) { erase_cells(m, y, 0, m->cols); } /* The last screen row that shows a glyph other than a space. */ static int last_text_row(const struct model *m) { for (int y = m->rows - 1; y >= 0; y--) for (int x = 0; x < m->cols; x++) { const struct cell *c = &m->scr[y]->c[x]; if (c->n > 1 || (c->n == 1 && c->cp[0] > ' ')) return y; } return -1; } static void erase_display(struct model *m, int mode) { if (mode == 3) { term_hist_clear(m); return; } if (mode == 2) { /* The normal screen keeps its text: rows up to the last one with * text move to history first. */ int last = m->alternate ? -1 : last_text_row(m); for (int i = 0; i <= last; i++) scroll_up_full(m); for (int y = 0; y < m->rows; y++) erase_row(m, y); } else if (mode == 0) { erase_cells(m, m->y, m->x, m->cols); for (int y = m->y + 1; y < m->rows; y++) erase_row(m, y); } else if (mode == 1) { for (int y = 0; y < m->y; y++) erase_row(m, y); erase_cells(m, m->y, 0, m->x + 1); } } static void erase_line(struct model *m, int mode) { if (mode == 0) erase_cells(m, m->y, m->x, m->cols); else if (mode == 1) erase_cells(m, m->y, 0, m->x + 1); else if (mode == 2) erase_cells(m, m->y, 0, m->cols); } /* ---- wide glyph repair ---- */ /* A write to [from, to) splits the wide glyphs at its edges. Their other * halves become blanks that keep their colors. */ static void split_edges(struct model *m, struct row *r, int from, int to) { if (from >= to) return; if (from > 0 && from < r->len && r->c[from].width == 0) r->c[from - 1] = blank(r->c[from - 1].fg, r->c[from - 1].bg); if (to < r->len && r->c[to].width == 0) r->c[to] = blank(r->c[to].fg, r->c[to].bg); (void)m; } /* Blank every half of a wide glyph in [from, to) that lost its partner. */ static void repair_row(struct model *m, struct row *r, int from, int to) { if (from < 0) from = 0; if (to > m->cols) to = m->cols; for (int x = from; x < to; x++) { struct cell *c = &r->c[x]; if (c->width == 0 && (x == 0 || r->c[x - 1].width != 2)) *c = blank(c->fg, c->bg); if (c->width == 2 && m->cols > 1 && (x + 1 == m->cols || r->c[x + 1].width != 0)) *c = blank(c->fg, c->bg); } } /* Writing at x past len first fills the gap with default blanks. */ static void fill_gap(struct row *r, int x) { if (x > r->len) r->len = x; } /* ---- printing ---- */ static uint16_t presentation(const uint32_t *cps, int n) { bool pictographic = unicode_extended_pictographic(cps[0]); bool emoji = false; for (int i = 1; i < n; i++) { if (cps[i] == 0xFE0F) return TERM_ATTR_EMOJI; if (cps[i] == 0xFE0E) return TERM_ATTR_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 ? TERM_ATTR_EMOJI : 0; } static void wrap_to_next_row(struct model *m) { m->scr[m->y]->wrapped = true; m->x = 0; line_feed(m); m->pending = false; } /* Put one glyph at the cursor with the pen colors and the given style. */ static void put_glyph(struct model *m, const uint32_t *cps, int n, uint16_t attrs) { if (n > 1) attrs = (uint16_t)((attrs & ~(TERM_ATTR_EMOJI | TERM_ATTR_TEXT)) | presentation(cps, n)); int width = unicode_width(cps[0]) == 2 ? 2 : 1; if (attrs & TERM_ATTR_EMOJI) width = 2; if (attrs & TERM_ATTR_TEXT) width = 1; if (m->pending) wrap_to_next_row(m); int right = m->cols - 1; struct row *r = m->scr[m->y]; /* A wide glyph that does not fit wraps and leaves a pad blank. */ if (width == 2 && m->cols > 1 && m->x == right && m->autowrap) { fill_gap(r, m->x); split_edges(m, r, m->x, right + 1); r->c[m->x] = default_blank(); r->c[m->x].pad = true; r->len = m->cols; wrap_to_next_row(m); r = m->scr[m->y]; } int x = m->x; int span = width > right + 1 - x ? 1 : width; fill_gap(r, x); if (m->insert) { int move = right + 1 - x - span; if (move > 0) memmove(r->c + x + span, r->c + x, (size_t)move * sizeof(struct cell)); r->len = r->len + span > m->cols ? m->cols : r->len + span; repair_row(m, r, right, right + 1); } split_edges(m, r, x, x + span); struct cell c = { .n = (uint8_t)n, .width = (uint8_t)span, .attrs = attrs, .fg = m->pen.inverse ? m->pen.bg : m->pen.fg, .bg = m->pen.inverse ? m->pen.fg : m->pen.bg, }; memcpy(c.cp, cps, (size_t)n * sizeof(*cps)); r->c[x] = c; if (span == 2) { r->c[x + 1] = c; r->c[x + 1].n = 0; r->c[x + 1].width = 0; memset(r->c[x + 1].cp, 0, sizeof(c.cp)); } if (x + span > r->len) r->len = x + span; if (x + span <= right) { m->x = x + span; } else { m->x = right; m->pending = m->autowrap; } } /* Marks, emoji modifiers, and spacing marks extend the glyph before them. */ static bool joins(uint32_t cp, int width) { if (cp < 0x300) return false; return width == 0 || grapheme_emoji_modifier(cp) || (width == 1 && unicode_spacing_mark(cp)); } static bool cluster_joins(const uint32_t *cps, int n, uint32_t cp, int width) { if (joins(cp, width)) return true; if (cps[n - 1] == 0x200D) return unicode_extended_pictographic(cp); return n == 1 && is_ri(cp) && is_ri(cps[0]); } /* * Try to add cp to the glyph left of the cursor. True when cp was used: * joined, or dropped because it has no glyph to join. The glyph keeps its * colors and style and is placed again, so it can grow to two cells. */ static bool extend_cluster(struct model *m, uint32_t cp, int width) { bool after_join = m->last_n > 1 || (m->last_n == 1 && m->last[0] == 0x200D); if (!(width == 0 || after_join || is_ri(cp) || joins(cp, width))) return false; struct row *r = m->scr[m->y]; int x = m->pending ? m->cols - 1 : m->x - 1; struct cell *prev = NULL; if (x >= 0 && x < r->len) { if (x > 0 && r->c[x].width == 0) x--; prev = &r->c[x]; } if (!prev || !(prev->n > 1 || (prev->n == 1 && prev->cp[0] > ' '))) return joins(cp, width); if (!cluster_joins(prev->cp, prev->n, cp, width)) return false; if (prev->n >= GRAPHEME_MAX_LEN) return true; struct cell old = *prev; uint32_t cps[GRAPHEME_MAX_LEN]; memcpy(cps, old.cp, (size_t)old.n * sizeof(*cps)); cps[old.n] = cp; int n = old.n + 1; int span = old.width == 2 && x + 1 < m->cols ? 2 : 1; for (int i = 0; i < span; i++) r->c[x + i] = blank(old.fg, old.bg); if (r->len == x + span) row_cut(r, x, m->cols); m->x = x; m->pending = false; memcpy(m->last, cps, (size_t)n * sizeof(*cps)); m->last_n = n; put_glyph(m, cps, n, (uint16_t)(old.attrs & ~(TERM_ATTR_EMOJI | TERM_ATTR_TEXT))); r = m->scr[m->y]; x = m->pending ? m->cols - 1 : m->x - 1; if (x > 0 && r->c[x].width == 0) x--; r->c[x].fg = old.fg; r->c[x].bg = old.bg; if (r->c[x].width == 2 && x + 1 < m->cols) { r->c[x + 1].fg = old.fg; r->c[x + 1].bg = old.bg; } return true; } static void esc_dispatch(struct model *m, unsigned char ch); static void emit(struct model *m, uint32_t cp) { static const uint32_t dec_graphics[] = { 0x00a0, 0x25c6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, 0x00b1, 0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0x23ba, 0x23bb, 0x2500, 0x23bc, 0x23bd, 0x251c, 0x2524, 0x2534, 0x252c, 0x2502, 0x2264, 0x2265, 0x03c0, 0x2260, 0x00a3, 0x00b7, }; /* C1 controls in UTF-8 form act as their ESC forms. */ if (cp >= 0x80 && cp <= 0x9f) { static const char c1[32] = { [4] = 'D', [5] = 'E', [8] = 'H', [13] = 'M', [16] = 'P', [27] = '[', [28] = '\\', [29] = ']', }; if (c1[cp - 0x80]) { m->state = GROUND; esc_dispatch(m, (unsigned char)c1[cp - 0x80]); } return; } if (m->g[m->gl] == '0' && cp >= '_' && cp <= '~') cp = dec_graphics[cp - '_']; m->last[0] = cp; m->last_n = 1; put_glyph(m, &cp, 1, m->pen.attrs); } static void print_cp(struct model *m, uint32_t cp) { if (cp < 0x80) { emit(m, cp); return; } int width = unicode_width(cp); if (!extend_cluster(m, cp, width)) emit(m, cp); } /* ---- cursor ---- */ static int top_limit(const struct model *m) { return m->origin ? m->top : 0; } static int bottom_limit(const struct model *m) { return m->origin ? m->bottom : m->rows - 1; } static void move_to(struct model *m, int x, int y) { m->pending = false; m->x = x < 0 ? 0 : x >= m->cols ? m->cols - 1 : x; m->y = y < 0 ? 0 : y >= m->rows ? m->rows - 1 : y; } /* CUU, CUD, CNL, CPL, and VPR stop at a margin when they start inside the * scroll region (xterm, DEC STD 070). */ static void move_v(struct model *m, int n, bool col0) { int lo = 0, hi = m->rows - 1; if (m->origin || (m->y >= m->top && m->y <= m->bottom)) { lo = m->top; hi = m->bottom; } else if (m->y < m->top) { hi = m->bottom; } else { lo = m->top; } long y = (long)m->y + n; if (n < 0 && m->y >= lo && y < lo) y = lo; if (n > 0 && m->y <= hi && y > hi) y = hi; move_to(m, col0 ? 0 : m->x, (int)(y < 0 ? 0 : y > m->rows - 1 ? m->rows - 1 : y)); } static void move_h(struct model *m, int n) { long x = (long)m->x + n; move_to(m, (int)(x < 0 ? 0 : x > m->cols - 1 ? m->cols - 1 : x), m->y); } static struct saved save_cursor(const struct model *m) { return (struct saved){ .x = m->x, .y = m->y, .pending = m->pending, .origin = m->origin, .pen = m->pen, .g = { m->g[0], m->g[1] }, .gl = m->gl, }; } static void restore_cursor(struct model *m, const struct saved *s) { m->pen = s->pen; m->origin = s->origin; m->g[0] = s->g[0]; m->g[1] = s->g[1]; m->gl = s->gl; move_to(m, s->x, s->y); m->pending = s->pending; } static struct saved initial_saved(void) { return (struct saved){ .pen = { .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG }, .g = { 'B', 'B' }, }; } static void tabs_default(struct model *m) { memset(m->tabs, 0, sizeof(m->tabs)); for (int c = 8; c < MAX_TABS; c += 8) m->tabs[c] = true; } /* ---- insert and delete ---- */ static void delete_chars(struct model *m, int n) { struct row *r = m->scr[m->y]; int x = m->x, cols = m->cols; if (n > cols - x) n = cols - x; int len = r->len; split_edges(m, r, x, x + n); if (len > x + n) { memmove(r->c + x, r->c + x + n, (size_t)(len - x - n) * sizeof(struct cell)); for (int i = len - n; i < len; i++) r->c[i] = default_blank(); r->len = len - n; } else if (len > x) { row_cut(r, x, cols); } if (!pristine(m)) { int from = cols - n < x ? x : cols - n; fill_gap(r, from); for (int i = from; i < cols; i++) r->c[i] = blank(m->pen.fg, m->pen.bg); r->len = cols; } r->wrapped = false; repair_row(m, r, x - 1, len); } static void insert_chars(struct model *m, int n) { struct row *r = m->scr[m->y]; int x = m->x, cols = m->cols; if (n > cols - x) n = cols - x; fill_gap(r, x); int len = r->len; if (x > 0 && r->c[x].width == 0) { r->c[x - 1] = blank(m->pen.fg, m->pen.bg); r->c[x] = blank(m->pen.fg, m->pen.bg); } if (len > x) { int move = len - x; if (move > cols - x - n) move = cols - x - n; if (move > 0) memmove(r->c + x + n, r->c + x, (size_t)move * sizeof(struct cell)); } for (int i = x; i < x + n; i++) r->c[i] = blank(m->pen.fg, m->pen.bg); int new_len = len + n > cols ? cols : len + n; if (pristine(m) && len <= x) new_len = len; else if (new_len < x + n) new_len = x + n; for (int i = new_len; i < cols; i++) r->c[i] = default_blank(); r->len = new_len; r->wrapped = false; int end = x + n + 1 > len + 1 ? x + n + 1 : len + 1; repair_row(m, r, x - 1, end); } /* ---- SGR ---- */ static void pen_reset(struct pen *p) { *p = (struct pen){ .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG }; } static void set_underline(uint16_t *attrs, int style) { if (style > 5) style = 1; *attrs = (uint16_t)((*attrs & ~TERM_ATTR_UNDERLINE_MASK) | (unsigned)style << TERM_ATTR_UNDERLINE_SHIFT); } /* 38, 48, and 58. Returns the parameters it used after the first. */ static int extended_color(const struct model *m, int i, term_color *color, bool *ok) { *ok = false; if (i + 1 >= m->nparam) return 0; if (i + 2 < m->nparam && m->param[i + 1] == 5) { int index = m->param[i + 2]; if (index >= 0 && index <= 255) { *color = term_color_index(index); *ok = true; } return 2; } if (m->param[i + 1] != 2) return 0; int colon = 0; while (i + 2 + colon < m->nparam && i + 2 + colon < 16 && (m->sub & (1u << (i + 2 + colon)))) colon++; int skip = colon >= 4 ? 1 : 0; if (i + 4 + skip >= m->nparam) return 0; *color = term_color_rgb((uint8_t)m->param[i + 2 + skip], (uint8_t)m->param[i + 3 + skip], (uint8_t)m->param[i + 4 + skip]); *ok = true; return 4 + skip; } static void sgr(struct model *m) { struct pen *p = &m->pen; if (m->nparam == 0) { pen_reset(p); return; } for (int i = 0; i < m->nparam; i++) { int v = m->param[i]; term_color color; bool ok; switch (v) { case 0: pen_reset(p); break; case 1: p->attrs |= TERM_ATTR_BOLD; break; case 2: p->attrs |= TERM_ATTR_FAINT; break; case 3: p->attrs |= TERM_ATTR_ITALIC; break; case 4: { int style = 1; if (i + 1 < m->nparam && (m->sub & (1u << (i + 1)))) style = m->param[++i]; set_underline(&p->attrs, style); break; } case 7: p->inverse = true; break; case 9: p->attrs |= TERM_ATTR_STRIKE; break; case 21: set_underline(&p->attrs, 2); break; case 22: p->attrs &= (uint16_t)~(TERM_ATTR_BOLD | TERM_ATTR_FAINT); break; case 23: p->attrs &= (uint16_t)~TERM_ATTR_ITALIC; break; case 24: set_underline(&p->attrs, 0); break; case 27: p->inverse = false; break; case 29: p->attrs &= (uint16_t)~TERM_ATTR_STRIKE; break; case 38: i += extended_color(m, i, &color, &ok); if (ok) p->fg = color; break; case 39: p->fg = TERM_COLOR_FG; break; case 48: i += extended_color(m, i, &color, &ok); if (ok) p->bg = color; break; case 49: p->bg = TERM_COLOR_BG; break; case 53: p->attrs |= TERM_ATTR_OVERLINE; break; case 55: p->attrs &= (uint16_t)~TERM_ATTR_OVERLINE; break; case 58: i += extended_color(m, i, &color, &ok); if (ok) p->ul = color; break; case 59: p->ul = 0; break; default: if (v >= 30 && v <= 37) p->fg = term_color_index(v - 30); else if (v >= 40 && v <= 47) p->bg = term_color_index(v - 40); else if (v >= 90 && v <= 97) p->fg = term_color_index(v - 90 + 8); else if (v >= 100 && v <= 107) p->bg = term_color_index(v - 100 + 8); break; } } } /* The DECRQSS reply body for SGR. */ static void sgr_report(const struct model *m, char *out, size_t cap) { const struct pen *p = &m->pen; size_t n = 0; out[0] = 0; #define ADD(...) do { int k = snprintf(out + n, cap - n, __VA_ARGS__); if (k > 0) n += (size_t)k; } while (0) int ul = (int)((p->attrs & TERM_ATTR_UNDERLINE_MASK) >> TERM_ATTR_UNDERLINE_SHIFT); if (!(p->attrs & (TERM_ATTR_BOLD | TERM_ATTR_FAINT | TERM_ATTR_ITALIC | TERM_ATTR_STRIKE | TERM_ATTR_OVERLINE | TERM_ATTR_UNDERLINE_MASK)) && !p->inverse && p->fg == TERM_COLOR_FG && p->bg == TERM_COLOR_BG && !p->ul) return; ADD("0"); if (p->attrs & TERM_ATTR_BOLD) ADD(";1"); if (p->attrs & TERM_ATTR_FAINT) ADD(";2"); if (p->attrs & TERM_ATTR_ITALIC) ADD(";3"); if (ul == 1) ADD(";4"); else if (ul) ADD(";4:%d", ul); if (p->inverse) ADD(";7"); if (p->attrs & TERM_ATTR_STRIKE) ADD(";9"); if (p->attrs & TERM_ATTR_OVERLINE) ADD(";53"); const term_color colors[3] = { p->fg, p->bg, p->ul }; const bool set[3] = { p->fg != TERM_COLOR_FG, p->bg != TERM_COLOR_BG, p->ul != 0 }; for (int k = 0; k < 3; k++) { if (!set[k]) continue; int base = 30 + 10 * k; term_color c = colors[k]; if (term_color_is_slot(c) && (c & 0x1FFu) < 256) { int i = (int)(c & 0xFFu); if (i < 16 && k < 2) ADD(";%d", i < 8 ? base + i : base + 60 + i - 8); else ADD(";%d:5:%d", base + 8, i); } else { ADD(";%d:2:%d:%d:%d", base + 8, (int)((c >> 16) & 0xff), (int)((c >> 8) & 0xff), (int)(c & 0xff)); } } #undef ADD } /* DECCARA: change attributes in a rectangle, or in a stream of cells when * DECSACE is not 2. A rectangle edge that splits a wide glyph changes both * of its halves. */ static void cara_cell(struct model *m, struct cell *c) { if (m->nparam <= 4) { c->fg = TERM_COLOR_FG; c->bg = TERM_COLOR_BG; c->attrs &= TERM_ATTR_EMOJI | TERM_ATTR_TEXT | TERM_ATTR_LINK; return; } for (int i = 4; i < m->nparam; i++) { int v = m->param[i]; term_color color; bool ok; switch (v) { case 0: c->fg = TERM_COLOR_FG; c->bg = TERM_COLOR_BG; c->attrs &= TERM_ATTR_EMOJI | TERM_ATTR_TEXT | TERM_ATTR_LINK; break; case 1: c->attrs |= TERM_ATTR_BOLD; break; case 2: c->attrs |= TERM_ATTR_FAINT; break; case 3: c->attrs |= TERM_ATTR_ITALIC; break; case 4: { int style = 1; if (i + 1 < m->nparam && (m->sub & (1u << (i + 1)))) style = m->param[++i]; set_underline(&c->attrs, style); break; } case 7: { term_color t = c->fg; c->fg = c->bg; c->bg = t; break; } case 9: c->attrs |= TERM_ATTR_STRIKE; break; case 21: set_underline(&c->attrs, 2); break; case 22: c->attrs &= (uint16_t)~(TERM_ATTR_BOLD | TERM_ATTR_FAINT); break; case 23: c->attrs &= (uint16_t)~TERM_ATTR_ITALIC; break; case 24: set_underline(&c->attrs, 0); break; case 29: c->attrs &= (uint16_t)~TERM_ATTR_STRIKE; break; case 38: i += extended_color(m, i, &color, &ok); if (ok) c->fg = color; break; case 39: c->fg = TERM_COLOR_FG; break; case 48: i += extended_color(m, i, &color, &ok); if (ok) c->bg = color; break; case 49: c->bg = TERM_COLOR_BG; break; case 53: c->attrs |= TERM_ATTR_OVERLINE; break; case 55: c->attrs &= (uint16_t)~TERM_ATTR_OVERLINE; break; case 58: i += extended_color(m, i, &color, &ok); break; default: if (v >= 30 && v <= 37) c->fg = term_color_index(v - 30); else if (v >= 40 && v <= 47) c->bg = term_color_index(v - 40); else if (v >= 90 && v <= 97) c->fg = term_color_index(v - 90 + 8); else if (v >= 100 && v <= 107) c->bg = term_color_index(v - 100 + 8); break; } } } static void cara_row(struct model *m, int y, int x0, int x1) { struct row *r = m->scr[y]; if (x0 > 0 && r->c[x0].width == 0) x0--; if (x1 + 1 < m->cols && r->c[x1].width == 2) x1++; fill_gap(r, x1 + 1); for (int x = x0; x <= x1; x++) cara_cell(m, &r->c[x]); } static void deccara(struct model *m) { int top = m->param[0] > 0 ? m->param[0] - 1 : 0; int left = m->nparam > 1 && m->param[1] > 0 ? m->param[1] - 1 : 0; int bottom = m->nparam > 2 && m->param[2] > 0 ? m->param[2] - 1 : m->rows - 1; int right = m->nparam > 3 && m->param[3] > 0 ? m->param[3] - 1 : m->cols - 1; if (top > bottom || left > right || top >= m->rows || left >= m->cols) return; if (bottom >= m->rows) bottom = m->rows - 1; if (right >= m->cols) right = m->cols - 1; if (m->decsace == 2) { for (int y = top; y <= bottom; y++) cara_row(m, y, left, right); return; } cara_row(m, top, left, top == bottom ? right : m->cols - 1); for (int y = top + 1; y < bottom; y++) cara_row(m, y, 0, m->cols - 1); if (bottom > top) cara_row(m, bottom, 0, right); } /* ---- screens and resize ---- */ static void free_rows(struct row **rows, int n) { if (!rows) return; for (int i = 0; i < n; i++) free(rows[i]); free(rows); } static struct row **rows_new(int n, int cols) { struct row **rows = calloc((size_t)n, sizeof(*rows)); if (!rows) abort(); for (int i = 0; i < n; i++) rows[i] = row_new(cols); return rows; } /* Copy a row into a new width. Cells past the width drop. */ static struct row *row_resized(const struct row *r, int old_cols, int cols) { struct row *n = row_new(cols); int keep = old_cols < cols ? old_cols : cols; memcpy(n->c, r->c, (size_t)keep * sizeof(struct cell)); n->len = r->len < cols ? r->len : cols; n->wrapped = r->wrapped; return n; } struct reflow { int cols, rows; struct row **out; int count, cap; struct row *cur; int x; bool cursor_set, saved_set, prompt_set; int cursor_x, cursor_y, saved_x, saved_y, prompt; bool pending; }; static void reflow_end_row(struct reflow *r, bool wrapped) { if (!r->cur) r->cur = row_new(r->cols); r->cur->len = r->x > r->cols ? r->cols : r->x; r->cur->wrapped = wrapped; if (r->count == r->cap) { r->cap = r->cap ? r->cap * 2 : 64; r->out = realloc(r->out, (size_t)r->cap * sizeof(*r->out)); if (!r->out) abort(); } r->out[r->count++] = r->cur; r->cur = NULL; r->x = 0; } static struct cell *reflow_dst(struct reflow *r) { if (!r->cur) r->cur = row_new(r->cols); return r->cur->c; } /* * Append one logical line. Pads drop and wide glyphs wrap again. A cursor * keeps its cell. A cursor on a pad moves to the next glyph. */ static void reflow_append(struct reflow *r, const struct cell *cells, int len, int cursor, int saved) { for (int i = 0; i < len;) { const struct cell *c = &cells[i]; if (c->pad || c->width == 0) { int at = c->pad ? r->x : r->x - 1; if (cursor == i) { r->cursor_x = at < 0 ? 0 : at; r->cursor_y = r->count; r->cursor_set = true; r->pending = false; } if (saved == i) { r->saved_x = at < 0 ? 0 : at; r->saved_y = r->count; r->saved_set = true; } i++; continue; } int n = c->width == 2 && r->cols > 1 ? 2 : 1; if (r->x + n > r->cols) { if (r->x < r->cols) { reflow_dst(r)[r->x] = default_blank(); reflow_dst(r)[r->x].pad = true; r->x++; } reflow_end_row(r, true); } if (cursor == i) { r->cursor_x = r->x; r->cursor_y = r->count; r->cursor_set = true; r->pending = false; } if (saved == i) { r->saved_x = r->x; r->saved_y = r->count; r->saved_set = true; } struct cell *dst = reflow_dst(r); dst[r->x] = *c; if (n == 2) { dst[r->x + 1] = *c; dst[r->x + 1].n = 0; dst[r->x + 1].width = 0; memset(dst[r->x + 1].cp, 0, sizeof(c->cp)); } r->x += n; i++; } if (cursor == len) { r->cursor_y = r->count; r->cursor_x = r->x == r->cols ? r->cols - 1 : r->x; r->pending = r->x == r->cols; r->cursor_set = true; } if (saved == len) { r->saved_y = r->count; r->saved_x = r->x == r->cols ? r->cols - 1 : r->x; r->saved_set = true; } } static void hist_reflow_tail(struct model *m) { if (m->hist_len <= 0) return; int start = m->hist_len - 1; while (start > 0 && m->hist[start - 1]->wrapped) start--; bool line_open = m->hist[m->hist_len - 1]->wrapped; struct reflow rf = { .cols = m->cols, .rows = m->rows }; for (int i = start; i < m->hist_len; i++) { const struct row *src = m->hist[i]; int len = src->wrapped && src->len < m->cols ? m->cols : src->len; reflow_append(&rf, src->c, len, -1, -1); } if (rf.x || rf.count == 0) reflow_end_row(&rf, false); free(rf.cur); if (line_open) { struct row *last = rf.out[rf.count - 1]; last->wrapped = true; if (last->len < m->cols) { for (int x = last->len; x < m->cols; x++) { last->c[x] = default_blank(); last->c[x].pad = true; } last->len = m->cols; } } for (int i = start; i < m->hist_len; i++) free(m->hist[i]); m->hist_len = start; while (m->hist_len + rf.count > m->hist_cap) { m->hist_cap = m->hist_cap ? m->hist_cap * 2 : 64; m->hist = realloc(m->hist, (size_t)m->hist_cap * sizeof(*m->hist)); if (!m->hist) abort(); } for (int i = 0; i < rf.count; i++) m->hist[m->hist_len++] = rf.out[i]; free(rf.out); } /* * Reflow the normal screen and its history to a new width. Logical lines * wrap again. Lines at or after the prompt row (OSC 133;A) are cut, not * wrapped. Blank rows below the cursor drop. Rows more than one screen * below the cursor drop. */ static void reflow(struct model *m, int cols, int rows) { int old_cols = m->cols, old_rows = m->rows; int active = old_rows; while (active > m->y + 1 && active > m->saved.y + 1 && !m->scr[active - 1]->len && !m->scr[active - 1]->wrapped) active--; struct reflow r = { .cols = cols, .rows = rows }; int total_in = m->hist_len + active; struct cell *line = malloc(((size_t)old_cols + (size_t)m->saved.x + 2) * sizeof(*line)); if (!line) abort(); for (int i = 0; i < total_in; i++) { int y = i - m->hist_len; const struct row *src = i < m->hist_len ? m->hist[i] : m->scr[y]; int len = src->len; bool wrapped = src->wrapped; if (m->prompt >= 0 && y >= m->prompt) { if (r.x) reflow_end_row(&r, false); if (y == m->prompt) { r.prompt = r.count; r.prompt_set = true; } if (y == m->y) { r.cursor_y = r.count; r.cursor_x = m->x < cols ? m->x : cols - 1; r.pending = false; r.cursor_set = true; } if (y == m->saved.y) { r.saved_y = r.count; r.saved_x = m->saved.x < cols ? m->saved.x : cols - 1; r.saved_set = true; } int keep = len > cols ? cols : len; memcpy(reflow_dst(&r), src->c, (size_t)keep * sizeof(struct cell)); r.x = keep; reflow_end_row(&r, wrapped); continue; } if (wrapped && len < old_cols) len = old_cols; if (y == m->y && len < m->x + m->pending) len = m->x + m->pending; if (y == m->saved.y && len <= m->saved.x) len = m->saved.x + 1; for (int x = 0; x < len; x++) line[x] = x < old_cols ? src->c[x] : default_blank(); reflow_append(&r, line, len, y == m->y ? m->x + m->pending : -1, y == m->saved.y ? m->saved.x : -1); if (!wrapped || i + 1 == total_in) reflow_end_row(&r, false); } free(line); if (r.x) reflow_end_row(&r, false); free(r.cur); int visible = rows - (cols > old_cols ? old_rows - active : 0); if (visible < 1) visible = 1; int start = r.count > visible ? r.count - visible : 0; if (start > r.cursor_y) start = r.cursor_y; int history = start; if (KNOWN_HISTORY_COUNTS_ROWS) { if (history > m->limit) history = m->limit; } else { int lines = 0; int first = start; for (int i = start - 1; i >= 0; i--) { if (i == start - 1 || !r.out[i]->wrapped) { if (lines == m->limit) { first = i + 1; break; } lines++; } if (i == 0) first = 0; } history = start - first; } m->cols = cols; term_hist_clear(m); for (int i = start - history; i < start; i++) { hist_push(m, r.out[i]); r.out[i] = NULL; } free_rows(m->scr, old_rows); m->scr = calloc((size_t)rows, sizeof(*m->scr)); if (!m->scr) abort(); for (int y = 0; y < rows; y++) { if (start + y < r.count) { m->scr[y] = r.out[start + y]; r.out[start + y] = NULL; } else { m->scr[y] = row_new(cols); } } for (int i = 0; i < r.count; i++) free(r.out[i]); free(r.out); m->rows = rows; m->x = r.cursor_x < cols ? r.cursor_x : cols - 1; m->y = r.cursor_y - start; m->pending = r.pending && m->autowrap; if (m->prompt >= 0) { if (!r.prompt_set || r.prompt - start >= rows) m->prompt = -1; else m->prompt = r.prompt > start ? r.prompt - start : 0; } /* A saved cursor on a row below the new screen moves to the last row * and keeps its column. */ if (r.saved_set && r.saved_y - start < rows) { m->saved.x = r.saved_x < cols ? r.saved_x : cols - 1; m->saved.y = r.saved_y < start ? 0 : r.saved_y - start; } else { m->saved.y = rows - 1; } if (m->saved.x >= cols) m->saved.x = cols - 1; if (m->saved.y >= rows) m->saved.y = rows - 1; } /* Resize without reflow: the alternate screen, or the same width. */ static void resize_plain(struct model *m, int cols, int rows) { int old_cols = m->cols, old_rows = m->rows; if (cols != old_cols) { for (int y = 0; y < old_rows; y++) { struct row *n = row_resized(m->scr[y], old_cols, cols); free(m->scr[y]); m->scr[y] = n; } if (KNOWN_ALT_RESIZE_CUTS_NORMAL && m->alternate) { for (int y = 0; y < m->normal_rows; y++) if (m->normal[y]->len > cols) row_cut(m->normal[y], cols, m->normal_cols); for (int i = 0; i < m->hist_len; i++) if (m->hist[i]->len > cols) row_cut(m->hist[i], cols, m->normal_cols); } m->cols = cols; } if (rows < old_rows && m->y >= rows) { int shift = m->y - rows + 1; for (int i = 0; i < shift; i++) scroll_up_full(m); m->y -= shift; m->saved.y = m->saved.y >= shift ? m->saved.y - shift : 0; } if (rows != old_rows) { /* Growth pulls rows back from history above the old rows. New * blank rows take the pen colors. */ int pull = 0; if (rows > old_rows && !m->alternate) pull = m->hist_len < rows - old_rows ? m->hist_len : rows - old_rows; struct row **scr = calloc((size_t)rows, sizeof(*scr)); if (!scr) abort(); int y = 0; for (int i = 0; i < pull; i++) scr[y++] = m->hist[m->hist_len - pull + i]; for (int i = 0; i < old_rows; i++) { if (y < rows) scr[y++] = m->scr[i]; else free(m->scr[i]); } int first_new = y; while (y < rows) scr[y++] = row_new(cols); free(m->scr); m->scr = scr; m->rows = rows; for (y = first_new; y < rows; y++) erase_row(m, y); m->hist_len -= pull; m->hist_lines = 0; for (int i = 0; i < m->hist_len; i++) if (i == 0 || !m->hist[i - 1]->wrapped) m->hist_lines++; m->y += pull; m->saved.y += pull; if (m->prompt >= 0) m->prompt += pull; } /* A shrink that cuts the prompt row off the normal screen ends the * prompt. */ if (!m->alternate && m->prompt >= rows) m->prompt = -1; if (cols != old_cols) m->pending = false; if (m->x >= cols) m->x = cols - 1; if (m->y >= rows) m->y = rows - 1; if (m->saved.x >= cols) m->saved.x = cols - 1; if (m->saved.y >= rows) m->saved.y = rows - 1; } static bool resize(struct model *m, int cols, int rows) { if (cols > TERM_MAX_COLS) cols = TERM_MAX_COLS; if (cols == m->cols && rows == m->rows) return true; if (cols > MAX_TABS) return false; m->top = 0; m->bottom = rows - 1; if (cols != m->cols && !m->alternate) reflow(m, cols, rows); else resize_plain(m, cols, rows); return true; } static void set_alternate(struct model *m, bool on) { if (m->alternate == on) return; if (on) { m->normal = m->scr; m->normal_cols = m->cols; m->normal_rows = m->rows; m->normal_cursor = save_cursor(m); m->normal_saved = m->saved; m->normal_visible = m->visible; m->normal_style = m->style; m->normal_blink = m->blink; m->scr = rows_new(m->rows, m->cols); m->alternate = true; m->x = m->y = 0; m->pending = false; } else { int cols = m->cols, rows = m->rows; free_rows(m->scr, m->rows); m->scr = m->normal; m->normal = NULL; m->cols = m->normal_cols; m->rows = m->normal_rows; m->alternate = false; m->saved = m->normal_saved; m->visible = m->normal_visible; m->style = m->normal_style; m->blink = m->normal_blink; restore_cursor(m, &m->normal_cursor); m->x = m->normal_cursor.x; m->y = m->normal_cursor.y; m->pending = m->normal_cursor.pending; resize(m, cols, rows); } m->top = 0; m->bottom = m->rows - 1; } /* ---- modes and resets ---- */ static void soft_reset(struct model *m) { m->app_cursor = m->app_keypad = m->backarrow = false; m->insert = m->origin = m->reverse = m->lnm = false; m->focus = m->sync = false; m->mouse = m->mouse_enc = 0; m->bracketed = false; m->autowrap = m->visible = true; m->blink = true; m->style = TERM_CURSOR_BLOCK; m->top = 0; m->bottom = m->rows - 1; m->decsace = 0; memset(m->keyboard, 0, sizeof(m->keyboard)); pen_reset(&m->pen); } static void full_reset(struct model *m) { set_alternate(m, false); soft_reset(m); m->x = m->y = 0; m->pending = false; m->saved = initial_saved(); m->last_n = 0; m->g[0] = m->g[1] = 'B'; m->gl = 0; m->prompt = -1; tabs_default(m); for (int y = 0; y < m->rows; y++) erase_row(m, y); } static int mode_get(const struct model *m, int mode, bool priv) { if (!priv) { if (mode == 4) return m->insert; if (mode == 20) return m->lnm; return -1; } switch (mode) { case 1: return m->app_cursor; case 12: return m->blink; case 25: return m->visible; case 67: return m->backarrow; case 1004: return m->focus; case 2004: return m->bracketed; case 2026: return m->sync; case 5: return m->reverse; case 6: return m->origin; case 7: return m->autowrap; case 47: case 1047: case 1049: return m->alternate; case 69: return 0; case 9: case 1000: case 1001: case 1002: case 1003: return m->mouse == mode; case 1005: case 1006: case 1015: case 1016: return m->mouse_enc == mode; default: return -1; } } static void mode_set(struct model *m, int mode, bool on, bool priv) { if (!priv) { if (mode == 4) m->insert = on; else if (mode == 20) m->lnm = on; return; } switch (mode) { case 1: m->app_cursor = on; break; case 12: m->blink = on; break; case 25: m->visible = on; break; case 67: m->backarrow = on; break; case 1004: m->focus = on; break; case 2004: m->bracketed = on; break; case 2026: m->sync = on; break; case 5: m->reverse = on; break; case 6: m->origin = on; move_to(m, 0, on ? m->top : 0); break; case 7: m->autowrap = on; m->pending = false; break; case 1048: if (on) m->saved = save_cursor(m); else restore_cursor(m, &m->saved); break; case 47: case 1047: case 1049: set_alternate(m, on); break; case 9: case 1000: case 1001: case 1002: case 1003: m->mouse = on ? mode : 0; break; case 1005: case 1006: case 1015: case 1016: m->mouse_enc = on ? mode : 0; break; default: break; } } /* ---- CSI ---- */ static int count(const struct model *m) { return m->param[0] > 0 ? m->param[0] : 1; } static bool csi_allowed(const struct model *m, unsigned char final) { char prefix = m->prefix, inter = m->inter; if (final == 'm') return !prefix && !inter; if (final == 'H' || final == 'f') return !prefix && !inter && !m->sub; if (inter) { bool plain = (final == 'q' && inter == ' ') || (final == 'p' && inter == '!') || (final == 'x' && inter == '*'); if (plain) { if (prefix || m->sub) return false; } else if (final == 'p' && inter == '$') { if (m->sub) return false; } else if (final == 'r' && inter == '$') { if (prefix) return false; } else { return false; } } if (prefix && final != 'u' && !(final == 'c' && (prefix == '>' || prefix == '=')) && !(final == 'q' && prefix == '>' && !inter) && !(final == 'p' && inter == '$' && prefix == '?') && !(final == 'W' && prefix == '?') && !((final == 'h' || final == 'l') && prefix == '?')) return false; if (m->sub && !(inter == '$' && final == 'r')) return false; return true; } static void keyboard_csi(struct model *m) { if (m->nparam > (m->prefix == '=' ? 2 : 1)) return; struct keyboard *k = &m->keyboard[m->alternate]; unsigned flags = (unsigned)m->param[0] & 31; if (m->prefix == '?') { reply(m, "\033[?%uu", k->flags); } else if (m->prefix == '>') { if (k->depth == 32) { memmove(k->saved, k->saved + 1, sizeof(k->saved) - sizeof(k->saved[0])); k->depth--; } k->saved[k->depth++] = k->flags; k->flags = flags; } else if (m->prefix == '<') { unsigned n = (unsigned)count(m); if (n > k->depth) { k->depth = 0; k->flags = 0; } else { k->depth -= n; k->flags = k->saved[k->depth]; } } else if (m->prefix == '=') { int mode = m->nparam > 1 && m->param[1] ? m->param[1] : 1; if (mode == 1) k->flags = flags; else if (mode == 2) k->flags |= flags; else if (mode == 3) k->flags &= ~flags; } } static void csi_dispatch(struct model *m, unsigned char final) { if (!csi_allowed(m, final)) return; int n = count(m); switch (final) { case 'm': sgr(m); break; case 'H': case 'f': { long y = (long)(m->param[0] > 0 ? m->param[0] - 1 : 0) + top_limit(m); if (y > bottom_limit(m)) y = bottom_limit(m); long x = m->nparam > 1 && m->param[1] > 0 ? m->param[1] - 1 : 0; move_to(m, (int)(x > m->cols - 1 ? m->cols - 1 : x), (int)y); break; } case 'r': { if (m->inter == '$') { deccara(m); break; } int top = n - 1; int bottom = m->nparam > 1 && m->param[1] ? m->param[1] - 1 : m->rows - 1; if (top < bottom && bottom < m->rows) { m->top = top; m->bottom = bottom; move_to(m, 0, m->origin ? top : 0); } break; } case 'L': case 'M': if (m->y < m->top || m->y > m->bottom) break; scroll_region(m, m->y, m->bottom, n, final == 'L', false); move_to(m, 0, m->y); break; case 'S': case 'T': if (m->nparam > 1) break; m->pending = false; scroll_region(m, m->top, m->bottom, n, final == 'T', true); break; case 'd': { long y = (long)n - 1 + top_limit(m); move_to(m, m->x, (int)(y > bottom_limit(m) ? bottom_limit(m) : y)); break; } case 'b': { int times = n > m->cols * m->rows ? m->cols * m->rows : n; for (int i = 0; i < times && m->last_n; i++) put_glyph(m, m->last, m->last_n, m->pen.attrs); break; } case 'A': move_v(m, -n, false); break; case 'B': case 'e': move_v(m, n, false); break; case 'E': move_v(m, n, true); break; case 'F': move_v(m, -n, true); break; case 'a': case 'C': move_h(m, n); break; case 'D': move_h(m, -n); break; case 'G': case '`': { long x = (long)n - 1; move_to(m, (int)(x > m->cols - 1 ? m->cols - 1 : x), m->y); break; } case 's': if (!m->prefix) m->saved = save_cursor(m); break; case 'u': if (!m->prefix) restore_cursor(m, &m->saved); else keyboard_csi(m); break; case 'I': case 'Z': { int times = n > m->cols ? m->cols : n, dir = final == 'I' ? 1 : -1; while (times-- > 0) { int x = m->x + dir; while (x > 0 && x < m->cols - 1 && !m->tabs[x]) x += dir; m->x = x < 0 ? 0 : x >= m->cols ? m->cols - 1 : x; } m->pending = false; break; } case 'g': if (m->param[0] == 0) m->tabs[m->x] = false; else if (m->param[0] == 3) memset(m->tabs, 0, sizeof(m->tabs)); break; case 'W': if (m->prefix == '?' && m->param[0] == 5) tabs_default(m); break; case 'n': if (m->prefix) break; if (m->param[0] == 5) reply(m, "\033[0n"); else if (m->param[0] == 6) reply(m, "\033[%d;%dR", m->y - top_limit(m) + 1, m->x + 1); break; case 'J': m->pending = false; erase_display(m, m->param[0]); break; case 'K': m->pending = false; erase_line(m, m->param[0]); break; case 'X': { m->pending = false; int k = n > m->cols - m->x ? m->cols - m->x : n; erase_cells(m, m->y, m->x, m->x + k); break; } case 'x': if (m->inter == '*') m->decsace = m->param[0]; break; case 'P': m->pending = false; delete_chars(m, n); break; case '@': m->pending = false; insert_chars(m, n); break; case 'c': if (m->prefix == '>') reply(m, "\033[>0;10;0c"); else if (!m->prefix && m->param[0] == 0) reply(m, "\033[?62;22c"); break; case 't': if (!m->prefix && m->param[0] == 14) reply(m, "\033[4;%d;%dt", m->rows * 20, m->cols * 10); break; case 'h': case 'l': for (int i = 0; i < m->nparam; i++) mode_set(m, m->param[i], final == 'h', m->prefix == '?'); break; case 'q': if (m->prefix == '>' && !m->inter && (!m->nparam || !m->param[0])) { reply(m, "\033P>|wsap_term\033\\"); } else if (!m->prefix && m->inter == ' ') { int ps = m->nparam ? m->param[0] : 0; if (ps == 0) { m->style = TERM_CURSOR_BLOCK; m->blink = true; } else if (ps <= 6) { m->style = ps < 3 ? TERM_CURSOR_BLOCK : ps < 5 ? TERM_CURSOR_UNDERLINE : TERM_CURSOR_BAR; m->blink = ps & 1; } } break; case 'p': if (m->inter == '!') { soft_reset(m); move_to(m, 0, 0); } else if (m->inter == '$' && m->nparam >= 1) { int set = mode_get(m, m->param[0], m->prefix == '?'); reply(m, "\033[%s%d;%d$y", m->prefix == '?' ? "?" : "", m->param[0], set < 0 ? 0 : set ? 1 : 2); } break; default: break; } } /* ---- strings ---- */ static void osc_finish(struct model *m) { const char *s = m->str; size_t len = m->str_len; if (!m->alternate && len >= 5 && !strncmp(s, "133;", 4) && (len == 5 || s[5] == ';')) { if (s[4] == 'A') m->prompt = m->y; if (s[4] == 'C' || s[4] == 'D') m->prompt = -1; } } static void dcs_finish(struct model *m) { const char *s = m->str; size_t len = m->str_len; if (len < 2 || s[0] != '$' || s[1] != 'q') return; const char *spec = s + 2; size_t n = len - 2; char body[256]; if (n == 2 && !memcmp(spec, " q", 2)) { int shape = m->style == TERM_CURSOR_BLOCK ? (m->blink ? 1 : 2) : m->style == TERM_CURSOR_UNDERLINE ? (m->blink ? 3 : 4) : (m->blink ? 5 : 6); reply(m, "\033P1$r%d q\033\\", shape); } else if (n == 1 && spec[0] == 'm') { sgr_report(m, body, sizeof(body)); reply(m, "\033P1$r%sm\033\\", body); } else if (n == 1 && spec[0] == 'r') { reply(m, "\033P1$r%d;%dr\033\\", m->top + 1, m->bottom + 1); } else if (n == 1 && spec[0] == 's') { reply(m, "\033P1$r%d;%ds\033\\", 1, m->cols); } else if (n == 1 && spec[0] == 't') { reply(m, "\033P1$r%dt\033\\", m->rows); } else if (n == 2 && !memcmp(spec, "\"p", 2)) { reply(m, "\033P1$r64;1\"p\033\\"); } else if (n == 2 && !memcmp(spec, "\"q", 2)) { reply(m, "\033P1$r0\"q\033\\"); } else if (n == 2 && !memcmp(spec, "$|", 2)) { reply(m, "\033P1$r%d$|\033\\", m->cols); } else if (n == 2 && !memcmp(spec, "$}", 2)) { reply(m, "\033P1$r0$}\033\\"); } else if (n == 2 && !memcmp(spec, "$~", 2)) { reply(m, "\033P1$r0$~\033\\"); } else if (n == 2 && !memcmp(spec, "*x", 2)) { reply(m, "\033P1$r%d*x\033\\", m->decsace); } else if (n == 2 && !memcmp(spec, "*|", 2)) { reply(m, "\033P1$r%d*|\033\\", m->rows); } else { reply(m, "\033P0$r\033\\"); } } static void str_add(struct model *m, unsigned char c) { if (m->str_len < STR_KEEP) m->str[m->str_len] = (char)c; m->str_len++; } /* A string longer than STR_KEEP keeps its true length, so it matches no * short request. */ static void str_finish(struct model *m) { if (m->str_kind == OSC) osc_finish(m); else if (m->str_kind == DCS) dcs_finish(m); m->str_len = 0; } /* ---- the byte parser ---- */ static void esc_dispatch(struct model *m, unsigned char ch) { m->state = GROUND; switch (ch) { case '=': m->app_keypad = true; break; case '>': m->app_keypad = false; break; case 'H': m->tabs[m->x] = true; break; case 'c': full_reset(m); break; case '[': m->state = CSI; m->nparam = 0; m->param[0] = 0; m->sub = 0; m->prefix = m->inter = 0; m->csi_phase = 0; break; case ']': m->state = STR; m->str_kind = OSC; m->str_len = 0; break; case 'P': m->state = STR; m->str_kind = DCS; m->str_len = 0; break; case '_': case '^': case 'X': m->state = STR; m->str_kind = IGNORE; m->str_len = 0; break; case '#': m->state = HASH; break; case '%': m->state = DOCS; break; case '(': case ')': case '*': case '+': case '-': case '.': case '/': m->slot = (char)ch; m->state = CHARSET; break; case 'D': case 'E': m->pending = false; if (ch == 'E') m->x = 0; line_feed(m); break; case 'M': m->pending = false; reverse_index(m); break; case '7': m->saved = save_cursor(m); break; case '8': restore_cursor(m, &m->saved); break; default: break; } } static void utf8_interrupt(struct model *m) { if (m->need) { m->need = 0; print_cp(m, 0xFFFD); } } static void control(struct model *m, unsigned char c) { switch (c) { case 0x1b: m->state = ESC; break; case '\n': case 0x0b: case 0x0c: m->pending = false; if (m->lnm || (m->flags & TERM_FEED_ONLCR)) m->x = 0; line_feed(m); break; case '\r': m->pending = false; m->x = 0; break; case '\b': m->pending = false; if (m->x > 0) m->x--; break; case '\t': { m->pending = false; int x = m->x + 1; while (x < m->cols - 1 && !m->tabs[x]) x++; m->x = x < m->cols ? x : m->cols - 1; break; } case 0x0e: m->gl = 1; break; case 0x0f: m->gl = 0; break; default: break; } } /* Ground-state bytes: printable ASCII, C0 controls, DEL, and UTF-8. Each * maximal invalid UTF-8 subpart prints one U+FFFD. */ static void ground(struct model *m, unsigned char c) { if (c < 0x20 || c == 0x7f) { utf8_interrupt(m); if (c < 0x20) control(m, c); return; } if (m->need) { if (c >= m->lo && c <= m->hi) { m->acc = m->acc << 6 | (c & 0x3fu); m->lo = 0x80; m->hi = 0xbf; if (--m->need == 0) print_cp(m, m->acc); return; } utf8_interrupt(m); } if (c < 0x80) { print_cp(m, c); return; } m->lo = 0x80; m->hi = 0xbf; if (c >= 0xc2 && c <= 0xdf) { m->need = 1; m->acc = c & 0x1fu; } else if (c == 0xe0) { m->need = 2; m->acc = c & 0x0fu; m->lo = 0xa0; } else if (c == 0xed) { m->need = 2; m->acc = c & 0x0fu; m->hi = 0x9f; } else if (c >= 0xe1 && c <= 0xef) { m->need = 2; m->acc = c & 0x0fu; } else if (c == 0xf0) { m->need = 3; m->acc = c & 0x07u; m->lo = 0x90; } else if (c == 0xf4) { m->need = 3; m->acc = c & 0x07u; m->hi = 0x8f; } else if (c >= 0xf1 && c <= 0xf3) { m->need = 3; m->acc = c & 0x07u; } else print_cp(m, 0xFFFD); } static void csi_byte(struct model *m, unsigned char c) { if (c == 0x7f) return; if (c >= 0x40) { if (m->csi_phase != 3) csi_dispatch(m, c); m->state = GROUND; return; } if (m->csi_phase == 3) return; if (c >= '0' && c <= ';') { if (m->csi_phase == 2) { m->csi_phase = 3; return; } m->csi_phase = 1; if (!m->nparam) m->nparam = 1; int *p = &m->param[m->nparam - 1]; if (c <= '9') { *p = *p < 214748364 ? *p * 10 + (c - '0') : 0x7fffffff; } else { if (m->nparam == 16) { m->csi_phase = 3; return; } if (c == ':') m->sub |= (uint16_t)(1u << m->nparam); m->param[m->nparam++] = 0; } } else if (c >= '<') { if (m->csi_phase != 0 || m->prefix) m->csi_phase = 3; else m->prefix = (char)c; } else { if (m->csi_phase == 2) { m->csi_phase = 3; } else { m->csi_phase = 2; m->inter = (char)c; } } } static void feed_byte(struct model *m, unsigned char c) { /* A byte of 0x80 or more cancels any sequence except a string. */ if (c >= 0x80 && m->state != GROUND && m->state != STR) { m->state = GROUND; ground(m, c); return; } switch (m->state) { case GROUND: ground(m, c); break; case ESC: if (c < 0x20) { m->state = GROUND; ground(m, c); } else { esc_dispatch(m, c); } break; case CSI: /* ESC restarts, CAN and SUB cancel, other C0 controls run inside * the sequence (ECMA-48 5.5, xterm). */ if (c == 0x1b) m->state = ESC; else if (c == 0x18 || c == 0x1a) m->state = GROUND; else if (c < 0x20) control(m, c); else csi_byte(m, c); break; case CHARSET: if (m->slot == '(' || m->slot == ')') m->g[m->slot == ')'] = (char)c; m->state = GROUND; break; case HASH: m->state = GROUND; if (c == '8') { for (int y = 0; y < m->rows; y++) { struct row *r = m->scr[y]; for (int x = 0; x < m->cols; x++) { r->c[x] = default_blank(); r->c[x].cp[0] = 'E'; } r->len = m->cols; r->wrapped = false; } move_to(m, 0, 0); } break; case DOCS: m->state = GROUND; if (c != 'G' && c != '@' && c != '/') { ground(m, '%'); ground(m, c); } break; case STR: if (m->str_kind == IGNORE) { if (c == 0x07 || c == 0x18 || c == 0x1a) m->state = GROUND; else if (c == 0x1b) m->state = STR_ESC; break; } if (c == 0x07) { str_finish(m); m->state = GROUND; } else if (c == 0x1b) { m->state = STR_ESC; } else if (c < 0x20) { m->str_len = 0; m->state = GROUND; ground(m, c); } else { str_add(m, c); } break; case STR_ESC: if (m->str_kind == IGNORE) { if (c == '\\' || c == 0x07 || c == 0x18 || c == 0x1a) { m->state = GROUND; } else { m->state = ESC; feed_byte(m, c); } break; } if (c == '\\') { str_finish(m); m->state = GROUND; } else { m->str_len = 0; m->state = ESC; feed_byte(m, c); } break; } } /* ---- public ---- */ struct model *model_new(struct term_vector2i size, int history) { struct model *m = calloc(1, sizeof(*m)); if (!m) abort(); m->cols = size.x < 1 ? 80 : size.x; m->rows = size.y < 1 ? 24 : size.y; m->limit = history_limit(history); m->scr = rows_new(m->rows, m->cols); m->autowrap = m->visible = m->blink = true; m->style = TERM_CURSOR_BLOCK; m->bottom = m->rows - 1; m->g[0] = m->g[1] = 'B'; m->saved = initial_saved(); m->prompt = -1; pen_reset(&m->pen); tabs_default(m); (void)KNOWN_HISTORY_COUNTS_ROWS; return m; } void model_free(struct model *m) { if (!m) return; free_rows(m->scr, m->rows); free_rows(m->normal, m->normal_rows); term_hist_clear(m); free(m->hist); free(m->reply); for (int i = 0; i < m->out_cap; i++) free(m->out[i]); free(m->out); free(m); } void model_feed(struct model *m, const char *s, size_t len, unsigned flags) { m->flags = flags; for (size_t i = 0; i < len; i++) feed_byte(m, (unsigned char)s[i]); } bool model_resize(struct model *m, struct term_vector2i size) { if (size.x < 1 || size.y < 1) return false; return resize(m, size.x, size.y); } struct term_vector2i model_size(const struct model *m) { return (struct term_vector2i){ .x = m->cols, .y = m->rows }; } int model_history(const struct model *m) { return m->alternate ? 0 : m->hist_len; } const struct model_cell *model_row(const struct model *m, int row) { int hist = model_history(m); if (row < 0 || (row >= hist && row - hist >= m->rows)) return NULL; struct model *mm = (struct model *)m; const struct row *r = row < hist ? m->hist[row] : m->scr[row - hist]; if (mm->out_cols != m->cols) { for (int i = 0; i < mm->out_cap; i++) { free(mm->out[i]); mm->out[i] = NULL; } mm->out_cols = m->cols; } if (row >= mm->out_cap) { int cap = mm->out_cap ? mm->out_cap : 16; while (cap <= row) { if (cap > INT_MAX / 2) abort(); cap *= 2; } struct model_cell **out = realloc(mm->out, (size_t)cap * sizeof(*out)); if (!out) abort(); memset(out + mm->out_cap, 0, (size_t)(cap - mm->out_cap) * sizeof(*out)); mm->out = out; mm->out_cap = cap; } if (!mm->out[row]) { mm->out[row] = calloc((size_t)m->cols, sizeof(*mm->out[row])); if (!mm->out[row]) abort(); } for (int x = 0; x < m->cols; x++) { const struct cell *c = &r->c[x]; struct model_cell *o = &mm->out[row][x]; memcpy(o->cp, c->cp, sizeof(o->cp)); o->n = c->n; o->width = c->width; o->attrs = c->attrs; o->fg = c->fg; o->bg = c->bg; } return mm->out[row]; } struct term_cursor model_cursor(const struct model *m) { return (struct term_cursor){ .pos = { .x = m->x, .y = m->y }, .style = (enum term_cursor_style)m->style, .visible = m->visible, .blink = m->blink, }; } struct term_modes model_modes(const struct model *m) { return (struct term_modes){ .keyboard = m->keyboard[m->alternate].flags, .mouse = (enum term_mouse)m->mouse, .mouse_encoding = (enum term_mouse_encoding)m->mouse_enc, .app_cursor = m->app_cursor, .app_keypad = m->app_keypad, .backarrow_bs = m->backarrow, .bracketed_paste = m->bracketed, .focus_events = m->focus, .alternate = m->alternate, .reverse_video = m->reverse, .synchronized = m->sync, }; } const char *model_replies(const struct model *m, size_t *len) { *len = m->reply_len; return m->reply ? m->reply : ""; } void model_take_replies(struct model *m) { m->reply_len = 0; }