summaryrefslogtreecommitdiff
path: root/src/escape.c
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /src/escape.c
term: move the term core to subprojects/term
The core now builds, tests, and benchmarks alone, as a Meson subproject with no dependency on the main tree. - include/term/ holds term.h and the header-only unicode/. src/ holds the core and its private headers. The main tree gets only include/term through term_dep, so it cannot reach a private header. - The term tests, the term benchmarks, the Kitty parser suite, and tools/amalgamate.py and tools/generate_unicode.py move with it. The tests run as term:<name>. - The main build passes its -march (arch_flag) and b_ndebug to the subproject. A directory configured before this commit needs meson configure -Dterm:arch_flag=-march=... once. - The term tests compile with -UNDEBUG. They do work inside assert. - The subproject exports term_test_dep with term_api.h, for the main tree's render, thumbnail, color, and selection tests. - The Kitty bridge used struct vector2i and has not compiled since the public API redesign. It builds again. 2 of its 14 tests fail: REP after a tab, and a cursor report after a resize. - dirgraph.py drops the term and unicode layers. Its self-test uses pty. - The symbols test ignores the __odr_asan symbols that ASan adds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/escape.c')
-rw-r--r--src/escape.c1116
1 files changed, 1116 insertions, 0 deletions
diff --git a/src/escape.c b/src/escape.c
new file mode 100644
index 0000000..119010c
--- /dev/null
+++ b/src/escape.c
@@ -0,0 +1,1116 @@
+#include "escape.h"
+#include "compat.h"
+#include "core.h"
+#include "kernels.h"
+#include "pen.h"
+#include "screen.h"
+#include "text.h"
+#include "osc/osc.h"
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static int sgr_color_param(const struct term *t, char *buf, size_t sz, int base, uint32_t c)
+{
+ if (term_color_is_slot(c) && (c & 0x1FFu) < 256) {
+ int i = (int)(c & 0xFFu);
+ /* The underline color has no short form. */
+ if (i < 16 && base != 50)
+ return snprintf(buf, sz, ";%d", i < 8 ? base + i : base + 60 + i - 8);
+ return snprintf(buf, sz, ";%d:5:%d", base + 8, i);
+ }
+ c = palette_resolve(&t->palette, c);
+ return snprintf(buf, sz, ";%d:2:%d:%d:%d", base + 8,
+ (int)((c >> 16) & 0xff), (int)((c >> 8) & 0xff), (int)(c & 0xff));
+}
+
+static int sgr_serialize(const struct term *t, char *buf, size_t sz)
+{
+ int n = 0;
+ if (!(t->bold || t->faint || t->italic || t->underline || t->strike ||
+ t->overline || t->inverse || t->cur_fg != TERM_COLOR_FG ||
+ t->cur_bg != TERM_COLOR_BG || t->cur_ul))
+ return 0;
+#define SGR_ADD(expr) \
+ do { \
+ n += (expr); \
+ if ((size_t)n >= sz) return sz ? (int)sz - 1 : 0; \
+ } while (0)
+ SGR_ADD(snprintf(buf + n, sz - (size_t)n, "0"));
+ if (t->bold) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";1"));
+ if (t->faint) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";2"));
+ if (t->italic) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";3"));
+ if (t->underline == 1) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";4"));
+ else if (t->underline) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";4:%d", t->underline));
+ if (t->inverse) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";7"));
+ if (t->strike) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";9"));
+ if (t->overline) SGR_ADD(snprintf(buf + n, sz - (size_t)n, ";53"));
+ if (t->cur_fg != TERM_COLOR_FG) SGR_ADD(sgr_color_param(t, buf + n, sz - (size_t)n, 30, t->cur_fg));
+ if (t->cur_bg != TERM_COLOR_BG) SGR_ADD(sgr_color_param(t, buf + n, sz - (size_t)n, 40, t->cur_bg));
+ if (t->cur_ul) SGR_ADD(sgr_color_param(t, buf + n, sz - (size_t)n, 50, t->cur_ul));
+#undef SGR_ADD
+ return n;
+}
+
+static int dcs_decode_hex(const char *s, int len, char *out, int cap)
+{
+ if (len & 1) return -1;
+ int n = 0;
+ for (int i = 0; i < len; i += 2) {
+ int hi = term_hex_nib(s[i]), lo = term_hex_nib(s[i + 1]);
+ if (hi < 0 || lo < 0 || n >= cap) return -1;
+ out[n++] = (char)(hi << 4 | lo);
+ }
+ return n;
+}
+
+static const char *term_tcap(const struct term *t, const char *name)
+{
+ static const struct { const char *name, *value; } caps[] = {
+ { "Co", "16777216" },
+ { "RGB", "8/8/8" },
+ { "colors", "16777216" },
+ { "kDC", "\x1b[3;2~" }, { "kDC3", "\x1b[3;3~" },
+ { "kDC4", "\x1b[3;4~" }, { "kDC5", "\x1b[3;5~" },
+ { "kDC6", "\x1b[3;6~" }, { "kDC7", "\x1b[3;7~" },
+ { "kDN", "\x1b[1;2B" }, { "kDN3", "\x1b[1;3B" },
+ { "kDN4", "\x1b[1;4B" }, { "kDN5", "\x1b[1;5B" },
+ { "kDN6", "\x1b[1;6B" }, { "kDN7", "\x1b[1;7B" },
+ { "kEND", "\x1b[1;2F" }, { "kEND3", "\x1b[1;3F" },
+ { "kEND4", "\x1b[1;4F" }, { "kEND5", "\x1b[1;5F" },
+ { "kEND6", "\x1b[1;6F" }, { "kEND7", "\x1b[1;7F" },
+ { "kHOM", "\x1b[1;2H" }, { "kHOM3", "\x1b[1;3H" },
+ { "kHOM4", "\x1b[1;4H" }, { "kHOM5", "\x1b[1;5H" },
+ { "kHOM6", "\x1b[1;6H" }, { "kHOM7", "\x1b[1;7H" },
+ { "kIC", "\x1b[2;2~" }, { "kIC3", "\x1b[2;3~" },
+ { "kIC4", "\x1b[2;4~" }, { "kIC5", "\x1b[2;5~" },
+ { "kIC6", "\x1b[2;6~" }, { "kIC7", "\x1b[2;7~" },
+ { "kLFT", "\x1b[1;2D" }, { "kLFT3", "\x1b[1;3D" },
+ { "kLFT4", "\x1b[1;4D" }, { "kLFT5", "\x1b[1;5D" },
+ { "kLFT6", "\x1b[1;6D" }, { "kLFT7", "\x1b[1;7D" },
+ { "kNXT", "\x1b[6;2~" }, { "kNXT3", "\x1b[6;3~" },
+ { "kNXT4", "\x1b[6;4~" }, { "kNXT5", "\x1b[6;5~" },
+ { "kNXT6", "\x1b[6;6~" }, { "kNXT7", "\x1b[6;7~" },
+ { "kPRV", "\x1b[5;2~" }, { "kPRV3", "\x1b[5;3~" },
+ { "kPRV4", "\x1b[5;4~" }, { "kPRV5", "\x1b[5;5~" },
+ { "kPRV6", "\x1b[5;6~" }, { "kPRV7", "\x1b[5;7~" },
+ { "kRIT", "\x1b[1;2C" }, { "kRIT3", "\x1b[1;3C" },
+ { "kRIT4", "\x1b[1;4C" }, { "kRIT5", "\x1b[1;5C" },
+ { "kRIT6", "\x1b[1;6C" }, { "kRIT7", "\x1b[1;7C" },
+ { "kUP", "\x1b[1;2A" }, { "kUP3", "\x1b[1;3A" },
+ { "kUP4", "\x1b[1;4A" }, { "kUP5", "\x1b[1;5A" },
+ { "kUP6", "\x1b[1;6A" }, { "kUP7", "\x1b[1;7A" },
+ { "kbs", "\x7f" }, { "kcbt", "\x1b[Z" },
+ { "kcub1", "\x1bOD" }, { "kcud1", "\x1bOB" },
+ { "kcuf1", "\x1bOC" }, { "kcuu1", "\x1bOA" },
+ { "kdch1", "\x1b[3~" }, { "kend", "\x1bOF" },
+ { "kent", "\x1bOM" }, { "khome", "\x1bOH" },
+ { "kich1", "\x1b[2~" }, { "kind", "\x1b[1;2B" },
+ { "kmous", "\x1b[<" }, { "knp", "\x1b[6~" },
+ { "kpp", "\x1b[5~" }, { "kri", "\x1b[1;2A" },
+ { "kxIN", "\x1b[I" }, { "kxOUT", "\x1b[O" },
+ { "kf1", "\x1bOP" }, { "kf2", "\x1bOQ" },
+ { "kf3", "\x1bOR" }, { "kf4", "\x1bOS" },
+ { "kf5", "\x1b[15~" }, { "kf6", "\x1b[17~" },
+ { "kf7", "\x1b[18~" }, { "kf8", "\x1b[19~" },
+ { "kf9", "\x1b[20~" }, { "kf10", "\x1b[21~" },
+ { "kf11", "\x1b[23~" }, { "kf12", "\x1b[24~" },
+ { "kf13", "\x1b[1;2P" }, { "kf14", "\x1b[1;2Q" },
+ { "kf15", "\x1b[1;2R" }, { "kf16", "\x1b[1;2S" },
+ { "kf17", "\x1b[15;2~" }, { "kf18", "\x1b[17;2~" },
+ { "kf19", "\x1b[18;2~" }, { "kf20", "\x1b[19;2~" },
+ { "kf21", "\x1b[20;2~" }, { "kf22", "\x1b[21;2~" },
+ { "kf23", "\x1b[23;2~" }, { "kf24", "\x1b[24;2~" },
+ { "kf25", "\x1b[1;5P" }, { "kf26", "\x1b[1;5Q" },
+ { "kf27", "\x1b[1;5R" }, { "kf28", "\x1b[1;5S" },
+ { "kf29", "\x1b[15;5~" }, { "kf30", "\x1b[17;5~" },
+ { "kf31", "\x1b[18;5~" }, { "kf32", "\x1b[19;5~" },
+ { "kf33", "\x1b[20;5~" }, { "kf34", "\x1b[21;5~" },
+ { "kf35", "\x1b[23;5~" }, { "kf36", "\x1b[24;5~" },
+ { "kf37", "\x1b[1;6P" }, { "kf38", "\x1b[1;6Q" },
+ { "kf39", "\x1b[1;6R" }, { "kf40", "\x1b[1;6S" },
+ { "kf41", "\x1b[15;6~" }, { "kf42", "\x1b[17;6~" },
+ { "kf43", "\x1b[18;6~" }, { "kf44", "\x1b[19;6~" },
+ { "kf45", "\x1b[20;6~" }, { "kf46", "\x1b[21;6~" },
+ { "kf47", "\x1b[23;6~" }, { "kf48", "\x1b[24;6~" },
+ };
+ if (!strcmp(name, "TN") || !strcmp(name, "name"))
+ return t->tcap_name;
+ for (size_t i = 0; i < sizeof(caps) / sizeof(caps[0]); i++)
+ if (!strcmp(name, caps[i].name))
+ return caps[i].value;
+ return NULL;
+}
+
+struct resp_buf {
+ char *s;
+ size_t len, cap;
+ bool failed;
+};
+
+static void resp_put(struct resp_buf *r, const char *s, size_t n)
+{
+ if (r->failed)
+ return;
+ if (r->len + n + 1 > r->cap) {
+ size_t cap = r->cap ? r->cap : 256;
+ while (cap < r->len + n + 1)
+ cap *= 2;
+ char *p = realloc(r->s, cap);
+ if (!p) {
+ r->failed = true;
+ return;
+ }
+ r->s = p;
+ r->cap = cap;
+ }
+ memcpy(r->s + r->len, s, n);
+ r->len += n;
+ r->s[r->len] = '\0';
+}
+
+static void dcs_xtgettcap(struct term *t, const char *buf, int len)
+{
+ static const char hex[] = "0123456789abcdef";
+ struct resp_buf r = {0};
+ resp_put(&r, "\033P0+r", 5);
+ char *name = malloc((size_t)len / 2 + 1);
+ if (!name) {
+ free(r.s);
+ return;
+ }
+ int i = 0;
+ bool first = true;
+ while (i < len) {
+ int span = 0;
+ while (i + span < len && buf[i + span] != ';') span++;
+ int n = dcs_decode_hex(buf + i, span, name, len / 2);
+ if (n < 0)
+ break;
+ name[n] = 0;
+ const char *cap = term_tcap(t, name);
+ if (first && r.s) { r.s[2] = cap ? '1' : '0'; first = false; }
+ resp_put(&r, buf + i, (size_t)span);
+ if (!cap) break;
+ resp_put(&r, "=", 1);
+ for (const char *c = cap; *c; c++) {
+ unsigned char b = (unsigned char)*c;
+ char pair[2] = { hex[b >> 4], hex[b & 15] };
+ resp_put(&r, pair, 2);
+ }
+ i += span;
+ if (i < len) resp_put(&r, &buf[i++], 1);
+ }
+ free(name);
+ resp_put(&r, "\033\\", 2);
+ if (!r.failed)
+ send_response(t, r.s);
+ free(r.s);
+}
+
+static int dcs_param_int(const char **p, const char *end)
+{
+ int v = 0, any = 0;
+ while (*p < end && **p >= '0' && **p <= '9') {
+ v = v < 214748364 ? v * 10 + (**p - '0') : INT_MAX;
+ (*p)++;
+ any = 1;
+ }
+ if (*p < end && **p == ';') (*p)++;
+ return any ? v : -1;
+}
+
+static int dcs_param_chr(const char **p, const char *end)
+{
+ if (*p >= end) return -1;
+ int v = (unsigned char)*(*p)++;
+ if (*p < end && **p == ';') (*p)++;
+ return v;
+}
+
+static void restore_deccir(struct term *t, const char *p, const char *end)
+{
+ int row = dcs_param_int(&p, end);
+ if (row <= 0 || row > t->rows) return;
+ int col = dcs_param_int(&p, end);
+ if (col <= 0 || col > t->cols) return;
+ if (dcs_param_int(&p, end) != 1) return;
+ sc_move(t, col - 1, row - 1);
+ int rend = dcs_param_chr(&p, end);
+ if ((rend & 0xf0) != 0x40) return;
+ t->bold = rend & 1;
+ t->underline = (rend & 2) ? 1 : 0;
+ t->inverse = (rend & 8) != 0;
+ int attr = dcs_param_chr(&p, end);
+ if ((attr & 0xf0) != 0x40) return;
+ int flag = dcs_param_chr(&p, end);
+ if ((flag & 0xf0) != 0x40) return;
+ t->autowrap = (flag & 8) != 0;
+ t->origin_mode = (flag & 1) != 0;
+ int curss = (flag & 4) ? 3 : (flag & 2) ? 2 : 0;
+ int gl = dcs_param_chr(&p, end) - '0';
+ if (gl < 0 || gl > 3) return;
+ int gr = dcs_param_chr(&p, end) - '0';
+ if (gr < 0 || gr > 3) return;
+ t->charset_active = gl < 2 ? gl : (curss ? 1 : 0);
+ if (dcs_param_chr(&p, end) < 0) return;
+ for (int i = 0; i < 4 && p < end; i++) {
+ char d = *p++;
+ if ((d == '%' || d == '&' || d == '"') && p < end) d = *p++;
+ if (i < 2) t->charsets[i] = d;
+ }
+ t->graphics = t->charsets[t->charset_active] == '0';
+}
+
+static void restore_dectabsr(struct term *t, const char *p, const char *end)
+{
+ term_tabs_clear(t);
+ int stop = 0, any = 0;
+ for (; p < end; p++) {
+ if (*p >= '0' && *p <= '9') {
+ stop = stop < 214748364 ? stop * 10 + (*p - '0') : INT_MAX;
+ any = 1;
+ } else if (*p == '/') {
+ if (any && --stop >= 0 && stop < t->tabs_cap) t->tabs[stop] = 1;
+ stop = any = 0;
+ } else {
+ return;
+ }
+ }
+ if (any && --stop >= 0 && stop < t->tabs_cap) t->tabs[stop] = 1;
+}
+
+/* Grows the DCS string to hold extra more bytes, up to TERM_OSC_MAX.
+ * False when nothing more fits. */
+static bool dcs_reserve(struct term *t, size_t extra)
+{
+ size_t need = (size_t)t->dcs_len + extra;
+ if (need <= t->dcs_cap)
+ return true;
+ if (t->dcs_cap >= TERM_OSC_MAX)
+ return false;
+ size_t cap = t->dcs_cap ? t->dcs_cap : 256;
+ while (cap < need && cap < TERM_OSC_MAX)
+ cap *= 2;
+ if (cap > TERM_OSC_MAX)
+ cap = TERM_OSC_MAX;
+ char *p = realloc(t->dcs_buf, cap);
+ if (!p)
+ return false;
+ t->dcs_buf = p;
+ t->dcs_cap = cap;
+ return need <= cap;
+}
+
+static void dcs_dispatch(struct term *t)
+{
+ if (term_str_was_truncated(t))
+ return;
+ const char *buf = t->dcs_buf;
+ int len = t->dcs_len;
+ char resp[512];
+ if (len < 2) return;
+ if (buf[0] == '+' && buf[1] == 'q') {
+ dcs_xtgettcap(t, buf + 2, len - 2);
+ } else if (buf[0] == '+' && buf[1] == 'p') {
+ int n = dcs_decode_hex(buf + 2, len - 2, t->tcap_name, sizeof(t->tcap_name) - 1);
+ if (n >= 0) t->tcap_name[n] = 0;
+ } else if (buf[0] == '+' && buf[1] == 'Q') {
+ int span = 0;
+ while (span < len - 2 && buf[2 + span] != ';') span++;
+ char *name = malloc((size_t)span / 2 + 1);
+ if (name && span && dcs_decode_hex(buf + 2, span, name, span / 2) >= 0) {
+ struct resp_buf r = {0};
+ resp_put(&r, "\033P0+R", 5);
+ resp_put(&r, buf + 2, (size_t)span);
+ resp_put(&r, "\033\\", 2);
+ if (!r.failed)
+ send_response(t, r.s);
+ free(r.s);
+ }
+ free(name);
+ } else if (buf[0] == '$' && buf[1] == 'q') {
+ const char *spec = buf + 2;
+ int slen = len - 2, p;
+ if (slen == 2 && spec[0] == ' ' && spec[1] == 'q') {
+ int shape = t->cursor_style == TERM_CURSOR_BLOCK ? (t->cursor_blink ? 1 : 2)
+ : t->cursor_style == TERM_CURSOR_UNDERLINE ? (t->cursor_blink ? 3 : 4)
+ : (t->cursor_blink ? 5 : 6);
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d q\033\\", shape);
+ } else if (slen == 1 && spec[0] == 'm') {
+ char sgr[256] = "";
+ sgr_serialize(t, sgr, sizeof(sgr));
+ p = snprintf(resp, sizeof(resp), "\033P1$r%sm\033\\", sgr);
+ } else if (slen == 1 && spec[0] == 'r') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d;%dr\033\\",
+ t->scroll_top + 1, t->scroll_bottom + 1);
+ } else if (slen == 1 && spec[0] == 's') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d;%ds\033\\",
+ t->left_margin + 1, t->right_margin + 1);
+ } else if (slen == 1 && spec[0] == 't') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%dt\033\\", t->rows);
+ } else if (slen == 2 && spec[0] == '"' && spec[1] == 'p') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r64;1\"p\033\\");
+ } else if (slen == 2 && spec[0] == '"' && spec[1] == 'q') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r0\"q\033\\");
+ } else if (slen == 2 && spec[0] == '$' && spec[1] == '|') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d$|\033\\", t->cols);
+ } else if (slen == 2 && spec[0] == '$' && spec[1] == '}') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r0$}\033\\");
+ } else if (slen == 2 && spec[0] == '$' && spec[1] == '~') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r0$~\033\\");
+ } else if (slen == 2 && spec[0] == '*' && spec[1] == 'x') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d*x\033\\", t->decsace);
+ } else if (slen == 2 && spec[0] == '*' && spec[1] == '|') {
+ p = snprintf(resp, sizeof(resp), "\033P1$r%d*|\033\\", t->rows);
+ } else {
+ p = snprintf(resp, sizeof(resp), "\033P0$r\033\\");
+ }
+ (void)p;
+ send_response(t, resp);
+ } else if (buf[0] == '=' && len >= 3 && buf[len - 1] == 's') {
+ const char *p = buf + 1, *end = buf + len;
+ int mode = dcs_param_int(&p, end);
+ if (mode == 1) t->synchronized_output = true;
+ else if (mode == 2) t->synchronized_output = false;
+ } else if (buf[0] >= '0' && buf[0] <= '9') {
+ const char *p = buf, *end = buf + len;
+ int ps = dcs_param_int(&p, end);
+ while (p < end && *p == ';') { p++; dcs_param_int(&p, end); }
+ if (end - p >= 2 && p[0] == '$' && p[1] == 't') {
+ if (ps == 1) restore_deccir(t, p + 2, end);
+ else if (ps == 2) restore_dectabsr(t, p + 2, end);
+ }
+ }
+}
+
+static void soft_reset(struct term *t)
+{
+ t->application_cursor = t->application_keypad = t->backarrow_sends_bs = false;
+ t->insert_mode = t->origin_mode = t->reverse_video = t->lnm = false;
+ t->focus_events = t->synchronized_output = t->lr_margin_mode = false;
+ t->mouse_mode = t->mouse_encoding = 0;
+ t->bracketed_paste = false;
+ t->autowrap = t->cursor_visible = true;
+ t->cursor_blink = t->default_cursor_blink;
+ t->cursor_style = t->default_cursor_style;
+ t->scroll_top = 0; t->scroll_bottom = t->rows - 1;
+ t->left_margin = 0; t->right_margin = t->cols - 1;
+ t->decsace = 0;
+ t->cur_link = 0;
+ memset(t->keyboard, 0, sizeof(t->keyboard));
+ reset_sgr(t);
+}
+
+static inline void sgr_set_slot(struct term *t, int code, uint32_t c)
+{
+ if (code == 38) t->cur_fg = c;
+ else if (code == 48) t->cur_bg = c;
+ else t->cur_ul = c;
+}
+
+static inline void handle_sgr(struct term *t, const struct csi *csi)
+{
+ if (csi->nparams == 0) {
+ reset_sgr(t);
+ return;
+ }
+
+ if (csi->nparams == 1 && !csi->subparams) {
+ int p = csi->params[0];
+ if (p == 0) { reset_sgr(t); return; }
+ if ((unsigned)(p - 30) < 8) { t->cur_fg = term_color_index(p - 30); return; }
+ if ((unsigned)(p - 40) < 8) { t->cur_bg = term_color_index(p - 40); return; }
+ if ((unsigned)(p - 90) < 8) { t->cur_fg = term_color_index(p - 90 + 8); return; }
+ if ((unsigned)(p - 100) < 8) { t->cur_bg = term_color_index(p - 100 + 8); return; }
+ if (p == 1) { t->bold = true; return; }
+ if (p == 39) { t->cur_fg = TERM_COLOR_FG; return; }
+ if (p == 49) { t->cur_bg = TERM_COLOR_BG; return; }
+ }
+
+ if (!csi->subparams) {
+ if (csi->nparams == 5 && csi->params[1] == 2 &&
+ (csi->params[0] == 38 || csi->params[0] == 48)) {
+ sgr_set_slot(t, csi->params[0],
+ term_rgb(csi->params[2], csi->params[3], csi->params[4]));
+ return;
+ }
+
+ if (csi->nparams == 10 && csi->params[1] == 2 && csi->params[6] == 2 &&
+ (csi->params[0] == 38 || csi->params[0] == 48) &&
+ csi->params[5] == (csi->params[0] == 38 ? 48 : 38)) {
+ sgr_set_slot(t, csi->params[0],
+ term_rgb(csi->params[2], csi->params[3], csi->params[4]));
+ sgr_set_slot(t, csi->params[5],
+ term_rgb(csi->params[7], csi->params[8], csi->params[9]));
+ return;
+ }
+
+ if (csi->nparams == 3 && csi->params[1] == 5 &&
+ (csi->params[0] == 38 || csi->params[0] == 48) &&
+ (unsigned)csi->params[2] < 256) {
+ sgr_set_slot(t, csi->params[0], term_color_index(csi->params[2]));
+ return;
+ }
+
+ if (csi->nparams == 6 && csi->params[1] == 5 && csi->params[4] == 5 &&
+ (csi->params[0] == 38 || csi->params[0] == 48) &&
+ csi->params[3] == (csi->params[0] == 38 ? 48 : 38) &&
+ (unsigned)csi->params[2] < 256 && (unsigned)csi->params[5] < 256) {
+ sgr_set_slot(t, csi->params[0], term_color_index(csi->params[2]));
+ sgr_set_slot(t, csi->params[3], term_color_index(csi->params[5]));
+ return;
+ }
+ }
+
+ struct term_pen pen = {
+ .fg = t->cur_fg, .bg = t->cur_bg, .ul = t->cur_ul,
+ .underline = t->underline,
+ .bold = t->bold, .faint = t->faint, .italic = t->italic,
+ .strike = t->strike, .overline = t->overline, .inverse = t->inverse,
+ };
+ sgr_apply(&pen, csi->params, csi->nparams, csi->subparams);
+ t->cur_fg = pen.fg;
+ t->cur_bg = pen.bg;
+ t->cur_ul = pen.ul;
+ t->underline = pen.underline;
+ t->bold = pen.bold;
+ t->faint = pen.faint;
+ t->italic = pen.italic;
+ t->strike = pen.strike;
+ t->overline = pen.overline;
+ t->inverse = pen.inverse;
+}
+
+#define TERM_PRIV_MODE_FIELDS(X) \
+ X(1, application_cursor) \
+ X(12, cursor_blink) \
+ X(25, cursor_visible) \
+ X(67, backarrow_sends_bs) \
+ X(1004, focus_events) \
+ X(2004, bracketed_paste) \
+ X(2026, synchronized_output)
+
+#define TERM_ANSI_MODE_FIELDS(X) \
+ X(4, insert_mode) \
+ X(20, lnm)
+
+static int mode_get(const struct term *t, int mode, bool priv)
+{
+ if (priv) {
+ switch (mode) {
+#define MODE_FIELD_GET(n, f) case n: return t->f;
+ TERM_PRIV_MODE_FIELDS(MODE_FIELD_GET)
+#undef MODE_FIELD_GET
+ case 5: return t->reverse_video;
+ case 6: return t->origin_mode;
+ case 7: return t->autowrap;
+ case 47: case 1047: case 1049: return t->alternate;
+ case 69: return t->lr_margin_mode;
+ default: break;
+ }
+ if (mode_is_mouse(mode)) return t->mouse_mode == mode;
+ if (mode_is_mouse_enc(mode)) return t->mouse_encoding == mode;
+ return -1;
+ }
+ switch (mode) {
+#define MODE_FIELD_GET(n, f) case n: return t->f;
+ TERM_ANSI_MODE_FIELDS(MODE_FIELD_GET)
+#undef MODE_FIELD_GET
+ }
+ return -1;
+}
+
+static int mode_status(const struct term *t, int mode, bool priv)
+{
+ int set = mode_get(t, mode, priv);
+ return set < 0 ? 0 : set ? 1 : 2;
+}
+
+static int cursor_top(const struct term *t) { return t->origin_mode ? t->scroll_top : 0; }
+static int cursor_bottom(const struct term *t) { return t->origin_mode ? t->scroll_bottom : t->rows - 1; }
+static int cursor_left(const struct term *t) { return t->origin_mode && t->lr_margin_mode ? t->left_margin : 0; }
+static int cursor_right(const struct term *t) { return t->origin_mode && t->lr_margin_mode ? t->right_margin : t->cols - 1; }
+
+static void mode_set(struct term *t, int mode, bool enabled, bool priv)
+{
+ if (!priv) {
+ switch (mode) {
+#define MODE_FIELD_SET(n, f) case n: t->f = enabled; break;
+ TERM_ANSI_MODE_FIELDS(MODE_FIELD_SET)
+#undef MODE_FIELD_SET
+ }
+ return;
+ }
+ switch (mode) {
+#define MODE_FIELD_SET(n, f) case n: t->f = enabled; return;
+ TERM_PRIV_MODE_FIELDS(MODE_FIELD_SET)
+#undef MODE_FIELD_SET
+ default: break;
+ }
+ if (mode == 5) {
+ term_sc_set_reverse_video(t, enabled);
+ } else if (mode == 6) {
+ t->origin_mode = enabled;
+ sc_move(t, 0, enabled ? t->scroll_top : 0);
+ } else if (mode == 7) {
+ term_sc_set_autowrap(t, enabled);
+ } else if (mode == 1048) {
+ if (enabled) t->saved = term_cursor_save(t);
+ else sc_restore_cursor(t, &t->saved);
+ } else if (mode == 47 || mode == 1047 || mode == 1049) {
+ term_screen_set_alternate(t, enabled);
+ } else if (mode_is_mouse(mode)) {
+ t->mouse_mode = enabled ? mode : 0;
+ } else if (mode == 69) {
+ t->lr_margin_mode = enabled;
+ t->left_margin = 0; t->right_margin = t->cols - 1;
+ sc_move(t, 0, cursor_top(t));
+ } else if (mode_is_mouse_enc(mode)) {
+ t->mouse_encoding = enabled ? mode : 0;
+ }
+}
+
+/* Vertical moves stop at a margin when they start inside the scroll
+ * region (xterm, DEC STD 070). */
+static inline void move_v(struct term *t, int n, bool col0)
+{
+ int top = t->cursor_y >= t->scroll_top ? t->scroll_top : 0;
+ int bottom = t->cursor_y <= t->scroll_bottom ? t->scroll_bottom : t->rows - 1;
+ int y = n < 0 ? (t->cursor_y - top >= -n ? t->cursor_y + n : top)
+ : (n < bottom - t->cursor_y ? t->cursor_y + n : bottom);
+ sc_move(t, col0 ? cursor_left(t) : t->cursor_x, y);
+}
+static inline void move_h(struct term *t, int n, bool margin_aware)
+{
+ int right = margin_aware ? cursor_right(t) : t->cols - 1;
+ int x = n < 0 ? (t->cursor_x >= -n ? t->cursor_x + n : 0)
+ : (n < right - t->cursor_x ? t->cursor_x + n : right);
+ sc_move(t, x, t->cursor_y);
+}
+
+static inline TERM_ALWAYS_INLINE void dispatch_csi(struct term *t, const struct csi *csi)
+{
+ if (TERM_EXPECT(csi->final == 'm', 1)) {
+ if (TERM_EXPECT(!csi->prefix && !csi->intermediate, 1))
+ handle_sgr(t, csi);
+ return;
+ }
+ if (TERM_EXPECT(csi->final == 'H' || csi->final == 'f', 0)) {
+ if (TERM_EXPECT(!csi->prefix && !csi->intermediate && !csi->subparams, 1)) {
+ int64_t r = (int64_t)(csi->params[0] > 0 ? csi->params[0] - 1 : 0) + cursor_top(t);
+ if (r > cursor_bottom(t)) r = cursor_bottom(t);
+ int64_t c = (int64_t)(csi->nparams > 1 && csi->params[1] > 0 ? csi->params[1] - 1 : 0) + cursor_left(t);
+ if (c > cursor_right(t)) c = cursor_right(t);
+ sc_move(t, (int)c, (int)r);
+ }
+ return;
+ }
+
+ if (csi->intermediate) {
+ bool plain = (csi->final == 'q' && csi->intermediate == ' ') ||
+ (csi->final == 'p' && csi->intermediate == '!') ||
+ (csi->final == 'x' && csi->intermediate == '*');
+ if (plain) {
+ if (csi->prefix || csi->subparams) return;
+ } else if (csi->final == 'p' && csi->intermediate == '$') {
+ if (csi->subparams) return;
+ } else if (csi->final == 'r' && csi->intermediate == '$') {
+ if (csi->prefix) return;
+ } else {
+ return;
+ }
+ }
+ if (csi->prefix) {
+ if (csi->final != 'u' &&
+ !(csi->final == 'c' && (csi->prefix == '>' || csi->prefix == '=')) &&
+ !(csi->final == 'q' && csi->prefix == '>' && !csi->intermediate) &&
+ !(csi->final == 'p' && csi->intermediate == '$' && csi->prefix == '?') &&
+ !(csi->final == 'W' && csi->prefix == '?') &&
+ !((csi->final == 'h' || csi->final == 'l') && csi->prefix == '?')) return;
+ }
+ if (csi->subparams && !(csi->intermediate == '$' && csi->final == 'r')) return;
+ switch (csi->final) {
+ case 'r': {
+ if (csi->intermediate == '$') {
+ term_sc_deccara(t, csi);
+ break;
+ }
+ if (csi->prefix || csi->intermediate) break;
+ int top = csi_count(csi) - 1;
+ int bottom = csi->nparams > 1 && csi->params[1] ? csi->params[1] - 1 : t->rows - 1;
+ if (top < bottom && bottom < t->rows) {
+ t->scroll_top = top;
+ t->scroll_bottom = bottom;
+ sc_move(t, 0, t->origin_mode ? top : 0);
+ }
+ break;
+ }
+ case 'L':
+ case 'M':
+ if (csi->prefix || t->cursor_y < t->scroll_top || t->cursor_y > t->scroll_bottom) break;
+ term_sc_scroll_region(t, t->cursor_y, t->scroll_bottom, csi_count(csi), csi->final == 'L');
+ /* IL and DL move the cursor to the left margin (xterm). */
+ sc_move(t, t->lr_margin_mode ? t->left_margin : 0, t->cursor_y);
+ break;
+ case 'S':
+ case 'T':
+ if (csi->prefix || csi->nparams > 1) break;
+ term_sc_scroll(t, csi_count(csi), csi->final == 'T');
+ break;
+ case 'd': {
+ int64_t r = (int64_t)csi_count(csi) - 1 + cursor_top(t);
+ if (r > cursor_bottom(t)) r = cursor_bottom(t);
+ sc_move(t, t->cursor_x, (int)r);
+ break;
+ }
+ case 'b':
+ term_text_repeat(t, csi_count(csi));
+ break;
+ case 'A': move_v(t, -csi_count(csi), false); break;
+ case 'B': move_v(t, csi_count(csi), false); break;
+ case 'E': move_v(t, csi_count(csi), true); break;
+ case 'F': move_v(t, -csi_count(csi), true); break;
+ case 'e': move_v(t, csi_count(csi), false); break;
+ case 'a': move_h(t, csi_count(csi), true); break;
+ case 'C': move_h(t, csi_count(csi), false); break;
+ case 'D': move_h(t, -csi_count(csi), false); break;
+ case 'G':
+ case '`': {
+ int64_t x = (int64_t)csi_count(csi) - 1 + cursor_left(t);
+ sc_move(t, (int)(x > cursor_right(t) ? cursor_right(t) : x), t->cursor_y);
+ break;
+ }
+ case 's':
+ if (!csi->prefix && t->lr_margin_mode && csi->nparams > 1) {
+ int left = csi_count(csi) - 1;
+ int right = csi->params[1] > 0 ? csi->params[1] - 1 : t->cols - 1;
+ if (left < right && right < t->cols) {
+ t->left_margin = left; t->right_margin = right;
+ sc_move(t, t->origin_mode ? left : 0, cursor_top(t));
+ }
+ } else if (!csi->prefix) {
+ t->saved = term_cursor_save(t);
+ }
+ break;
+ case 'I':
+ case 'Z':
+ term_sc_tab(t, csi->final == 'I' ? csi_count(csi) : -csi_count(csi));
+ break;
+ case 'g':
+ if (csi->params[0] == 0) t->tabs[t->cursor_x] = 0;
+ else if (csi->params[0] == 3) term_tabs_clear(t);
+ break;
+ case 'W':
+ if (csi->prefix == '?' && csi->params[0] == 5) {
+ term_tabs_clear(t);
+ term_tabs_default(t);
+ }
+ break;
+ case 'n':
+ if (!csi->prefix && csi->params[0] == 5) send_response(t, "\033[0n");
+ else if (!csi->prefix && csi->params[0] == 6) {
+ /* Report the cursor cell, also with a pending wrap (xterm). */
+ char reply[48];
+ snprintf(reply, sizeof(reply), "\033[%d;%dR", t->cursor_y - cursor_top(t) + 1,
+ t->cursor_x - cursor_left(t) + 1);
+ send_response(t, reply);
+ }
+ break;
+ case 'J':
+ sc_erase_display(t, csi->params[0]);
+ break;
+ case 'X': {
+ int n = csi_count(csi);
+ sc_erase_span(t, t->cursor_y, t->cursor_x,
+ n > t->cols - t->cursor_x ? t->cols : t->cursor_x + n);
+ break;
+ }
+ case 'x':
+ if (csi->intermediate == '*')
+ t->decsace = (uint8_t)csi->params[0];
+ break;
+ case 'K':
+ sc_erase_line(t, csi->params[0]);
+ break;
+ case 'P':
+ sc_delete_chars(t, csi_count(csi));
+ break;
+ case '@':
+ sc_insert_chars(t, csi_count(csi));
+ break;
+ case 'c': {
+ if (csi->prefix == '>')
+ send_response(t, "\x1b[>0;10;0c");
+ else if (!csi->prefix && csi->params[0] == 0)
+ send_response(t, "\x1b[?62;22c");
+ break;
+ }
+ case 't':
+ if (!csi->prefix && csi->params[0] == 14) {
+ char reply[32];
+ snprintf(reply, sizeof(reply), "\033[4;%d;%dt",
+ t->rows * 20, t->cols * 10);
+ send_response(t, reply);
+ }
+ break;
+ case 'u': {
+ if (!csi->prefix) { sc_restore_cursor(t, &t->saved); break; }
+ if (csi->nparams > (csi->prefix == '=' ? 2 : 1)) break;
+ struct keyboard_mode *k = &t->keyboard[t->alternate];
+ unsigned flags = (unsigned)csi->params[0] & 31;
+ if (csi->prefix == '?') {
+ char reply[32];
+ snprintf(reply, sizeof(reply), "\033[?%uu", k->flags);
+ send_response(t, reply);
+ } else if (csi->prefix == '>') {
+ if (k->depth == KEYBOARD_STACK_SIZE) {
+ 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 (csi->prefix == '<') {
+ unsigned count = (unsigned)csi_count(csi);
+ if (count > k->depth) { k->depth = 0; k->flags = 0; }
+ else { k->depth -= count; k->flags = k->saved[k->depth]; }
+ } else if (csi->prefix == '=') {
+ int mode = csi->nparams > 1 && csi->params[1] ? csi->params[1] : 1;
+ if (mode == 1) k->flags = flags;
+ else if (mode == 2) k->flags |= flags;
+ else if (mode == 3) k->flags &= ~flags;
+ }
+ break;
+ }
+ case 'h':
+ case 'l': {
+ bool enabled = csi->final == 'h';
+ bool priv = csi->prefix == '?';
+ for (int p = 0; p < csi->nparams; p++)
+ mode_set(t, csi->params[p], enabled, priv);
+ break;
+ }
+ case 'q': {
+ if (csi->prefix == '>' && !csi->intermediate &&
+ (!csi->nparams || !csi->params[0])) {
+ send_response(t, "\033P>|wsap_term\033\\");
+ } else if (!csi->prefix && csi->intermediate == ' ') {
+ term_sc_set_cursor_style(t, csi->nparams > 0 ? csi->params[0] : 0);
+ }
+ break;
+ }
+ case 'p':
+ if (csi->intermediate == '!') {
+ soft_reset(t);
+ sc_move(t, 0, 0);
+ } else if (csi->intermediate == '$' && csi->nparams >= 1) {
+ char reply[32];
+ snprintf(reply, sizeof(reply), "\033[%s%d;%d$y",
+ csi->prefix == '?' ? "?" : "", csi->params[0],
+ mode_status(t, csi->params[0], csi->prefix == '?'));
+ send_response(t, reply);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+/* Formats one trace line and hands it to the host's trace callback. */
+static void esc_tracef(const struct term *t, const char *fmt, ...)
+{
+ char line[512];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(line, sizeof line, fmt, ap);
+ va_end(ap);
+ t->cb.trace(t->cb.ud, line);
+}
+
+/* The guard keeps formatting off the parser path when tracing is off. */
+#define ESC_TRACE(t, ...) \
+ do { if ((t)->cb.trace) esc_tracef((t), __VA_ARGS__); } while (0)
+
+static void dispatch_esc(struct term *t, unsigned char ch)
+{
+ t->state = ST_GROUND;
+ switch (ch) {
+ case '=':
+ t->application_keypad = true;
+ break;
+ case '>':
+ t->application_keypad = false;
+ break;
+ case 'H':
+ t->tabs[t->cursor_x] = 1;
+ break;
+ case 'c':
+ if (t->alternate) term_screen_set_alternate(t, false);
+ soft_reset(t);
+ term_sc_reset(t);
+ break;
+ case '[':
+ ESC_TRACE(t, "ESC[ → ST_CSI");
+ t->state = ST_CSI;
+ csi_parser_reset(&t->csi_parser);
+ break;
+ case ']':
+ ESC_TRACE(t, "ESC] → ST_OSC");
+ t->state = ST_OSC;
+ t->osc_len = 0;
+ t->str_truncated = false;
+ break;
+ case 'P':
+ ESC_TRACE(t, "ESCP → ST_DCS");
+ t->state = ST_DCS;
+ t->dcs_len = 0;
+ t->str_truncated = false;
+ break;
+ case '_':
+ case '^':
+ case 'X':
+ t->state = ST_IGNORE;
+ break;
+ case '#':
+ t->state = ST_DECALN;
+ break;
+ case '%':
+ t->state = ST_DOCS;
+ break;
+ case '(':
+ case ')':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '/':
+ t->charset_designation = (char)ch;
+ t->state = ST_CHARSET;
+ break;
+ case 'D':
+ case 'E':
+ if (ch == 'E') sc_cr(t);
+ sc_linefeed(t, 1);
+ break;
+ case 'M':
+ if (t->cursor_y == t->scroll_top)
+ term_sc_scroll_region(t, t->scroll_top, t->scroll_bottom, 1, true);
+ else
+ sc_move(t, t->cursor_x, t->cursor_y > 0 ? t->cursor_y - 1 : 0);
+ break;
+ case '7':
+ t->saved = term_cursor_save(t);
+ break;
+ case '8':
+ sc_restore_cursor(t, &t->saved);
+ break;
+ default:
+ break;
+ }
+}
+
+void term_escape_byte(struct term *t, unsigned char ch)
+{
+ if (ch >= 0x80 && t->state != ST_OSC && t->state != ST_DCS &&
+ t->state != ST_IGNORE) {
+ ESC_TRACE(t, " 0x%02x in state %d → abort → ST_GROUND", ch, t->state);
+ t->osc_len = 0;
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ return;
+ }
+ switch (t->state) {
+ case ST_ESC:
+ ESC_TRACE(t, " ESC+0x%02x('%c')", ch, ch >= 0x20 && ch < 0x7f ? ch : '.');
+ if (ch < 0x20) {
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ } else {
+ dispatch_esc(t, ch);
+ }
+ break;
+ case ST_CSI:
+ if (ch == 0x1b) {
+ ESC_TRACE(t, " CSI interrupted by ESC");
+ t->state = ST_ESC;
+ } else if (ch == 0x18 || ch == 0x1a) {
+ ESC_TRACE(t, " CSI cancelled by 0x%02x", ch);
+ t->state = ST_GROUND;
+ } else if (ch < 0x20) {
+ /* Other C0 controls run inside the sequence (ECMA-48 5.5,
+ * xterm). */
+ term_text_ground(t, ch);
+ }
+ break;
+ case ST_CHARSET:
+ if (t->charset_designation == '(' || t->charset_designation == ')') {
+ t->charsets[t->charset_designation == ')'] = (char)ch;
+ t->graphics = t->charsets[t->charset_active] == '0';
+ }
+ t->state = ST_GROUND;
+ break;
+ case ST_DOCS:
+ if (ch != 'G' && ch != '@' && ch != '/')
+ term_text_ground(t, '%');
+ t->state = ST_GROUND;
+ if (ch != 'G' && ch != '@' && ch != '/')
+ term_text_ground(t, ch);
+ break;
+ case ST_DECALN:
+ t->state = ST_GROUND;
+ if (ch == '8')
+ term_sc_decaln(t);
+ break;
+ case ST_IGNORE:
+ if (ch == 0x07 || ch == 0x18 || ch == 0x1a)
+ t->state = ST_GROUND;
+ else if (ch == 0x1b)
+ t->state = ST_IGNORE_ESC;
+ break;
+ case ST_IGNORE_ESC:
+ if (ch == '\\' || ch == 0x07 || ch == 0x18 || ch == 0x1a)
+ t->state = ST_GROUND;
+ else {
+ t->state = ST_ESC;
+ term_escape_byte(t, ch);
+ }
+ break;
+ case ST_OSC:
+ if (ch == 0x07) {
+ term_osc_finish(t);
+ t->state = ST_GROUND;
+ } else if (ch == 0x1b) {
+ t->state = ST_STR_ESC;
+ } else if (ch < 0x20) {
+ t->osc_len = 0;
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ } else if (term_osc_room(t) || term_osc_grow(t)) {
+ *term_osc_tail(t) = (char)ch;
+ term_osc_commit(t, 1);
+ } else {
+ t->str_truncated = true;
+ }
+ break;
+ case ST_DCS:
+ if (ch == 0x07) {
+ dcs_dispatch(t);
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ } else if (ch == 0x1b) {
+ t->state = ST_DCS_ESC;
+ } else if (ch < 0x20) {
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ term_text_ground(t, ch);
+ } else if (dcs_reserve(t, 1)) {
+ t->dcs_buf[t->dcs_len++] = (char)ch;
+ } else {
+ t->str_truncated = true;
+ }
+ break;
+ case ST_DCS_ESC:
+ if (ch == '\\') {
+ dcs_dispatch(t);
+ t->dcs_len = 0;
+ t->state = ST_GROUND;
+ } else {
+ t->dcs_len = 0;
+ t->state = ST_ESC;
+ term_escape_byte(t, ch);
+ }
+ break;
+ case ST_STR_ESC:
+ if (ch == '\\') {
+ term_osc_finish(t);
+ t->state = ST_GROUND;
+ } else {
+ t->osc_len = 0;
+ t->dcs_len = 0;
+ t->state = ST_ESC;
+ term_escape_byte(t, ch);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static inline size_t str_appendable_span(const char *buf, size_t len)
+{
+ return scan_printable_span(buf, len, 0x80);
+}
+
+size_t term_escape_str(struct term *t, const char *buf, size_t len)
+{
+ if (t->state == ST_IGNORE) {
+ size_t take = str_appendable_span(buf, len);
+ if (take < len)
+ term_escape_byte(t, (unsigned char)buf[take++]);
+ return take;
+ }
+ int is_osc = t->state == ST_OSC;
+ size_t take = str_appendable_span(buf, len);
+ if (is_osc) {
+ size_t room = term_osc_room(t);
+ if (take > room && term_osc_grow(t))
+ room = term_osc_room(t);
+ if (room) {
+ if (take > room)
+ take = room;
+ memcpy(term_osc_tail(t), buf, take);
+ term_osc_commit(t, take);
+ } else {
+ t->str_truncated = true;
+ }
+ } else {
+ dcs_reserve(t, take);
+ size_t room = t->dcs_cap - (size_t)t->dcs_len;
+ if (room) {
+ if (take > room)
+ take = room;
+ memcpy(t->dcs_buf + t->dcs_len, buf, take);
+ t->dcs_len += (int)take;
+ } else {
+ t->str_truncated = true;
+ }
+ }
+ if (take < len)
+ term_escape_byte(t, (unsigned char)buf[take++]);
+ return take;
+}
+
+size_t term_escape_csi(struct term *t, const char *buf, size_t len)
+{
+ size_t consumed;
+ enum csi_result result = csi_read(&t->csi_parser, buf, len, &consumed);
+ if (result == CSI_CONTROL) {
+ term_escape_byte(t, (unsigned char)buf[consumed++]);
+ } else if (result == CSI_DISPATCH || result == CSI_DISCARD) {
+ if (t->cb.trace) {
+ const struct csi *c = &t->csi_parser.csi;
+ char params[256] = "-1";
+ if (c->nparams) {
+ size_t n = 0;
+ for (int j = 0; j < c->nparams && n < sizeof params; j++)
+ n += (size_t)snprintf(params + n, sizeof params - n,
+ j ? ";%d" : "%d", c->params[j]);
+ }
+ esc_tracef(t, " CSI complete: %s%c p=[%s] prefix='%c' inter='%c'",
+ result == CSI_DISCARD ? "INVALID " : "", c->final, params,
+ c->prefix ? c->prefix : ' ', c->intermediate ? c->intermediate : ' ');
+ }
+ if (result == CSI_DISPATCH)
+ dispatch_csi(t, &t->csi_parser.csi);
+ t->state = ST_GROUND;
+ }
+ return consumed;
+}
+
+void term_escape_begin(struct term *t)
+{
+ ESC_TRACE(t, "0x1b → ST_ESC");
+ t->state = ST_ESC;
+}