aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/vterm/state.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nvim/vterm/state.c')
-rw-r--r--src/nvim/vterm/state.c2467
1 files changed, 2467 insertions, 0 deletions
diff --git a/src/nvim/vterm/state.c b/src/nvim/vterm/state.c
new file mode 100644
index 0000000000..0e43107347
--- /dev/null
+++ b/src/nvim/vterm/state.c
@@ -0,0 +1,2467 @@
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "nvim/grid.h"
+#include "nvim/mbyte.h"
+#include "nvim/vterm/encoding.h"
+#include "nvim/vterm/parser.h"
+#include "nvim/vterm/pen.h"
+#include "nvim/vterm/state.h"
+#include "nvim/vterm/vterm.h"
+#include "nvim/vterm/vterm_internal_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/state.c.generated.h"
+#endif
+
+#define strneq(a, b, n) (strncmp(a, b, n) == 0)
+
+// Some convenient wrappers to make callback functions easier
+
+static void putglyph(VTermState *state, const schar_T schar, int width, VTermPos pos)
+{
+ VTermGlyphInfo info = {
+ .schar = schar,
+ .width = width,
+ .protected_cell = state->protected_cell,
+ .dwl = state->lineinfo[pos.row].doublewidth,
+ .dhl = state->lineinfo[pos.row].doubleheight,
+ };
+
+ if (state->callbacks && state->callbacks->putglyph) {
+ if ((*state->callbacks->putglyph)(&info, pos, state->cbdata)) {
+ return;
+ }
+ }
+
+ DEBUG_LOG("libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row);
+}
+
+static void updatecursor(VTermState *state, VTermPos *oldpos, int cancel_phantom)
+{
+ if (state->pos.col == oldpos->col && state->pos.row == oldpos->row) {
+ return;
+ }
+
+ if (cancel_phantom) {
+ state->at_phantom = 0;
+ }
+
+ if (state->callbacks && state->callbacks->movecursor) {
+ if ((*state->callbacks->movecursor)(state->pos, *oldpos, state->mode.cursor_visible,
+ state->cbdata)) {
+ return;
+ }
+ }
+}
+
+static void erase(VTermState *state, VTermRect rect, int selective)
+{
+ if (rect.end_col == state->cols) {
+ // If we're erasing the final cells of any lines, cancel the continuation marker on the
+ // subsequent line
+ for (int row = rect.start_row + 1; row < rect.end_row + 1 && row < state->rows; row++) {
+ state->lineinfo[row].continuation = 0;
+ }
+ }
+
+ if (state->callbacks && state->callbacks->erase) {
+ if ((*state->callbacks->erase)(rect, selective, state->cbdata)) {
+ return;
+ }
+ }
+}
+
+static VTermState *vterm_state_new(VTerm *vt)
+{
+ VTermState *state = vterm_allocator_malloc(vt, sizeof(VTermState));
+
+ state->vt = vt;
+
+ state->rows = vt->rows;
+ state->cols = vt->cols;
+
+ state->mouse_col = 0;
+ state->mouse_row = 0;
+ state->mouse_buttons = 0;
+
+ state->mouse_protocol = MOUSE_X10;
+
+ state->callbacks = NULL;
+ state->cbdata = NULL;
+
+ state->selection.callbacks = NULL;
+ state->selection.user = NULL;
+ state->selection.buffer = NULL;
+
+ vterm_state_newpen(state);
+
+ state->bold_is_highbright = 0;
+
+ state->combine_pos.row = -1;
+
+ state->tabstops = vterm_allocator_malloc(state->vt, ((size_t)state->cols + 7) / 8);
+
+ state->lineinfos[BUFIDX_PRIMARY] = vterm_allocator_malloc(state->vt,
+ (size_t)state->rows *
+ sizeof(VTermLineInfo));
+ // TODO(vterm): Make an 'enable' function
+ state->lineinfos[BUFIDX_ALTSCREEN] = vterm_allocator_malloc(state->vt,
+ (size_t)state->rows *
+ sizeof(VTermLineInfo));
+ state->lineinfo = state->lineinfos[BUFIDX_PRIMARY];
+
+ state->encoding_utf8.enc = vterm_lookup_encoding(ENC_UTF8, 'u');
+ if (*state->encoding_utf8.enc->init) {
+ (*state->encoding_utf8.enc->init)(state->encoding_utf8.enc, state->encoding_utf8.data);
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(state->key_encoding_stacks); i++) {
+ struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[i];
+ for (size_t j = 0; j < ARRAY_SIZE(stack->items); j++) {
+ memset(&stack->items[j], 0, sizeof(stack->items[j]));
+ }
+
+ stack->size = 1;
+ }
+
+ return state;
+}
+
+void vterm_state_free(VTermState *state)
+{
+ vterm_allocator_free(state->vt, state->tabstops);
+ vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_PRIMARY]);
+ if (state->lineinfos[BUFIDX_ALTSCREEN]) {
+ vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_ALTSCREEN]);
+ }
+ vterm_allocator_free(state->vt, state);
+}
+
+static void scroll(VTermState *state, VTermRect rect, int downward, int rightward)
+{
+ if (!downward && !rightward) {
+ return;
+ }
+
+ int rows = rect.end_row - rect.start_row;
+ if (downward > rows) {
+ downward = rows;
+ } else if (downward < -rows) {
+ downward = -rows;
+ }
+
+ int cols = rect.end_col - rect.start_col;
+ if (rightward > cols) {
+ rightward = cols;
+ } else if (rightward < -cols) {
+ rightward = -cols;
+ }
+
+ // Update lineinfo if full line
+ if (rect.start_col == 0 && rect.end_col == state->cols && rightward == 0) {
+ int height = rect.end_row - rect.start_row - abs(downward);
+
+ if (downward > 0) {
+ memmove(state->lineinfo + rect.start_row,
+ state->lineinfo + rect.start_row + downward,
+ (size_t)height * sizeof(state->lineinfo[0]));
+ for (int row = rect.end_row - downward; row < rect.end_row; row++) {
+ state->lineinfo[row] = (VTermLineInfo){ 0 };
+ }
+ } else {
+ memmove(state->lineinfo + rect.start_row - downward,
+ state->lineinfo + rect.start_row,
+ (size_t)height * sizeof(state->lineinfo[0]));
+ for (int row = rect.start_row; row < rect.start_row - downward; row++) {
+ state->lineinfo[row] = (VTermLineInfo){ 0 };
+ }
+ }
+ }
+
+ if (state->callbacks && state->callbacks->scrollrect) {
+ if ((*state->callbacks->scrollrect)(rect, downward, rightward, state->cbdata)) {
+ return;
+ }
+ }
+
+ if (state->callbacks) {
+ vterm_scroll_rect(rect, downward, rightward,
+ state->callbacks->moverect, state->callbacks->erase, state->cbdata);
+ }
+}
+
+static void linefeed(VTermState *state)
+{
+ if (state->pos.row == SCROLLREGION_BOTTOM(state) - 1) {
+ VTermRect rect = {
+ .start_row = state->scrollregion_top,
+ .end_row = SCROLLREGION_BOTTOM(state),
+ .start_col = SCROLLREGION_LEFT(state),
+ .end_col = SCROLLREGION_RIGHT(state),
+ };
+
+ scroll(state, rect, 1, 0);
+ } else if (state->pos.row < state->rows - 1) {
+ state->pos.row++;
+ }
+}
+
+static void set_col_tabstop(VTermState *state, int col)
+{
+ uint8_t mask = (uint8_t)(1 << (col & 7));
+ state->tabstops[col >> 3] |= mask;
+}
+
+static void clear_col_tabstop(VTermState *state, int col)
+{
+ uint8_t mask = (uint8_t)(1 << (col & 7));
+ state->tabstops[col >> 3] &= ~mask;
+}
+
+static int is_col_tabstop(VTermState *state, int col)
+{
+ uint8_t mask = (uint8_t)(1 << (col & 7));
+ return state->tabstops[col >> 3] & mask;
+}
+
+static int is_cursor_in_scrollregion(const VTermState *state)
+{
+ if (state->pos.row < state->scrollregion_top
+ || state->pos.row >= SCROLLREGION_BOTTOM(state)) {
+ return 0;
+ }
+ if (state->pos.col < SCROLLREGION_LEFT(state)
+ || state->pos.col >= SCROLLREGION_RIGHT(state)) {
+ return 0;
+ }
+
+ return 1;
+}
+
+static void tab(VTermState *state, int count, int direction)
+{
+ while (count > 0) {
+ if (direction > 0) {
+ if (state->pos.col >= THISROWWIDTH(state) - 1) {
+ return;
+ }
+
+ state->pos.col++;
+ } else if (direction < 0) {
+ if (state->pos.col < 1) {
+ return;
+ }
+
+ state->pos.col--;
+ }
+
+ if (is_col_tabstop(state, state->pos.col)) {
+ count--;
+ }
+ }
+}
+
+#define NO_FORCE 0
+#define FORCE 1
+
+#define DWL_OFF 0
+#define DWL_ON 1
+
+#define DHL_OFF 0
+#define DHL_TOP 1
+#define DHL_BOTTOM 2
+
+static void set_lineinfo(VTermState *state, int row, int force, int dwl, int dhl)
+{
+ VTermLineInfo info = state->lineinfo[row];
+
+ if (dwl == DWL_OFF) {
+ info.doublewidth = DWL_OFF;
+ } else if (dwl == DWL_ON) {
+ info.doublewidth = DWL_ON;
+ }
+ // else -1 to ignore
+
+ if (dhl == DHL_OFF) {
+ info.doubleheight = DHL_OFF;
+ } else if (dhl == DHL_TOP) {
+ info.doubleheight = DHL_TOP;
+ } else if (dhl == DHL_BOTTOM) {
+ info.doubleheight = DHL_BOTTOM;
+ }
+
+ if ((state->callbacks
+ && state->callbacks->setlineinfo
+ && (*state->callbacks->setlineinfo)(row, &info, state->lineinfo + row, state->cbdata))
+ || force) {
+ state->lineinfo[row] = info;
+ }
+}
+
+static int on_text(const char bytes[], size_t len, void *user)
+{
+ VTermState *state = user;
+
+ VTermPos oldpos = state->pos;
+
+ uint32_t *codepoints = (uint32_t *)(state->vt->tmpbuffer);
+ size_t maxpoints = (state->vt->tmpbuffer_len) / sizeof(uint32_t);
+
+ int npoints = 0;
+ size_t eaten = 0;
+
+ VTermEncodingInstance *encoding =
+ state->gsingle_set ? &state->encoding[state->gsingle_set]
+ : !(bytes[eaten] & 0x80) ? &state->encoding[state->gl_set]
+ : state->vt->mode.utf8 ? &state->encoding_utf8
+ : &state->encoding[state->
+ gr_set];
+
+ (*encoding->enc->decode)(encoding->enc, encoding->data,
+ codepoints, &npoints, state->gsingle_set ? 1 : (int)maxpoints,
+ bytes, &eaten, len);
+
+ // There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet for even a single codepoint
+ if (!npoints) {
+ return (int)eaten;
+ }
+
+ if (state->gsingle_set && npoints) {
+ state->gsingle_set = 0;
+ }
+
+ int i = 0;
+ GraphemeState grapheme_state = GRAPHEME_STATE_INIT;
+ size_t grapheme_len = 0;
+ bool recombine = false;
+
+ // See if the cursor has moved since
+ if (state->pos.row == state->combine_pos.row
+ && state->pos.col == state->combine_pos.col + state->combine_width) {
+ // This is a combining char. that needs to be merged with the previous glyph output
+ if (utf_iscomposing((int)state->grapheme_last, (int)codepoints[i], &state->grapheme_state)) {
+ // Find where we need to append these combining chars
+ grapheme_len = state->grapheme_len;
+ grapheme_state = state->grapheme_state;
+ state->pos.col = state->combine_pos.col;
+ recombine = true;
+ } else {
+ DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n");
+ }
+ }
+
+ while (i < npoints) {
+ // Try to find combining characters following this
+ do {
+ if (grapheme_len < sizeof(state->grapheme_buf) - 4) {
+ grapheme_len += (size_t)utf_char2bytes((int)codepoints[i],
+ state->grapheme_buf + grapheme_len);
+ }
+ i++;
+ } while (i < npoints && utf_iscomposing((int)codepoints[i - 1], (int)codepoints[i],
+ &grapheme_state));
+
+ int width = utf_ptr2cells_len(state->grapheme_buf, (int)grapheme_len);
+
+ if (state->at_phantom || state->pos.col + width > THISROWWIDTH(state)) {
+ linefeed(state);
+ state->pos.col = 0;
+ state->at_phantom = 0;
+ state->lineinfo[state->pos.row].continuation = 1;
+ }
+
+ if (state->mode.insert && !recombine) {
+ // TODO(vterm): This will be a little inefficient for large bodies of text, as it'll have to
+ // 'ICH' effectively before every glyph. We should scan ahead and ICH as many times as
+ // required
+ VTermRect rect = {
+ .start_row = state->pos.row,
+ .end_row = state->pos.row + 1,
+ .start_col = state->pos.col,
+ .end_col = THISROWWIDTH(state),
+ };
+ scroll(state, rect, 0, -1);
+ }
+
+ schar_T sc = schar_from_buf(state->grapheme_buf, grapheme_len);
+ putglyph(state, sc, width, state->pos);
+
+ if (i == npoints) {
+ // End of the buffer. Save the chars in case we have to combine with more on the next call
+ state->grapheme_len = grapheme_len;
+ state->grapheme_last = codepoints[i - 1];
+ state->grapheme_state = grapheme_state;
+ state->combine_width = width;
+ state->combine_pos = state->pos;
+ } else {
+ grapheme_len = 0;
+ recombine = false;
+ }
+
+ if (state->pos.col + width >= THISROWWIDTH(state)) {
+ if (state->mode.autowrap) {
+ state->at_phantom = 1;
+ }
+ } else {
+ state->pos.col += width;
+ }
+ }
+
+ updatecursor(state, &oldpos, 0);
+
+#ifdef DEBUG
+ if (state->pos.row < 0 || state->pos.row >= state->rows
+ || state->pos.col < 0 || state->pos.col >= state->cols) {
+ fprintf(stderr, "Position out of bounds after text: (%d,%d)\n",
+ state->pos.row, state->pos.col);
+ abort();
+ }
+#endif
+
+ return (int)eaten;
+}
+
+static int on_control(uint8_t control, void *user)
+{
+ VTermState *state = user;
+
+ VTermPos oldpos = state->pos;
+
+ switch (control) {
+ case 0x07: // BEL - ECMA-48 8.3.3
+ if (state->callbacks && state->callbacks->bell) {
+ (*state->callbacks->bell)(state->cbdata);
+ }
+ break;
+
+ case 0x08: // BS - ECMA-48 8.3.5
+ if (state->pos.col > 0) {
+ state->pos.col--;
+ }
+ break;
+
+ case 0x09: // HT - ECMA-48 8.3.60
+ tab(state, 1, +1);
+ break;
+
+ case 0x0a: // LF - ECMA-48 8.3.74
+ case 0x0b: // VT
+ case 0x0c: // FF
+ linefeed(state);
+ if (state->mode.newline) {
+ state->pos.col = 0;
+ }
+ break;
+
+ case 0x0d: // CR - ECMA-48 8.3.15
+ state->pos.col = 0;
+ break;
+
+ case 0x0e: // LS1 - ECMA-48 8.3.76
+ state->gl_set = 1;
+ break;
+
+ case 0x0f: // LS0 - ECMA-48 8.3.75
+ state->gl_set = 0;
+ break;
+
+ case 0x84: // IND - DEPRECATED but implemented for completeness
+ linefeed(state);
+ break;
+
+ case 0x85: // NEL - ECMA-48 8.3.86
+ linefeed(state);
+ state->pos.col = 0;
+ break;
+
+ case 0x88: // HTS - ECMA-48 8.3.62
+ set_col_tabstop(state, state->pos.col);
+ break;
+
+ case 0x8d: // RI - ECMA-48 8.3.104
+ if (state->pos.row == state->scrollregion_top) {
+ VTermRect rect = {
+ .start_row = state->scrollregion_top,
+ .end_row = SCROLLREGION_BOTTOM(state),
+ .start_col = SCROLLREGION_LEFT(state),
+ .end_col = SCROLLREGION_RIGHT(state),
+ };
+
+ scroll(state, rect, -1, 0);
+ } else if (state->pos.row > 0) {
+ state->pos.row--;
+ }
+ break;
+
+ case 0x8e: // SS2 - ECMA-48 8.3.141
+ state->gsingle_set = 2;
+ break;
+
+ case 0x8f: // SS3 - ECMA-48 8.3.142
+ state->gsingle_set = 3;
+ break;
+
+ default:
+ if (state->fallbacks && state->fallbacks->control) {
+ if ((*state->fallbacks->control)(control, state->fbdata)) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ updatecursor(state, &oldpos, 1);
+
+#ifdef DEBUG
+ if (state->pos.row < 0 || state->pos.row >= state->rows
+ || state->pos.col < 0 || state->pos.col >= state->cols) {
+ fprintf(stderr, "Position out of bounds after Ctrl %02x: (%d,%d)\n",
+ control, state->pos.row, state->pos.col);
+ abort();
+ }
+#endif
+
+ return 1;
+}
+
+static int settermprop_bool(VTermState *state, VTermProp prop, int v)
+{
+ VTermValue val = { .boolean = v };
+ return vterm_state_set_termprop(state, prop, &val);
+}
+
+static int settermprop_int(VTermState *state, VTermProp prop, int v)
+{
+ VTermValue val = { .number = v };
+ return vterm_state_set_termprop(state, prop, &val);
+}
+
+static int settermprop_string(VTermState *state, VTermProp prop, VTermStringFragment frag)
+{
+ VTermValue val = { .string = frag };
+ return vterm_state_set_termprop(state, prop, &val);
+}
+
+static void savecursor(VTermState *state, int save)
+{
+ if (save) {
+ state->saved.pos = state->pos;
+ state->saved.mode.cursor_visible = state->mode.cursor_visible;
+ state->saved.mode.cursor_blink = state->mode.cursor_blink;
+ state->saved.mode.cursor_shape = state->mode.cursor_shape;
+
+ vterm_state_savepen(state, 1);
+ } else {
+ VTermPos oldpos = state->pos;
+
+ state->pos = state->saved.pos;
+
+ settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, state->saved.mode.cursor_visible);
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, state->saved.mode.cursor_blink);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, state->saved.mode.cursor_shape);
+
+ vterm_state_savepen(state, 0);
+
+ updatecursor(state, &oldpos, 1);
+ }
+}
+
+static int on_escape(const char *bytes, size_t len, void *user)
+{
+ VTermState *state = user;
+
+ // Easier to decode this from the first byte, even though the final byte terminates it
+ switch (bytes[0]) {
+ case ' ':
+ if (len != 2) {
+ return 0;
+ }
+
+ switch (bytes[1]) {
+ case 'F': // S7C1T
+ state->vt->mode.ctrl8bit = 0;
+ break;
+
+ case 'G': // S8C1T
+ state->vt->mode.ctrl8bit = 1;
+ break;
+
+ default:
+ return 0;
+ }
+ return 2;
+
+ case '#':
+ if (len != 2) {
+ return 0;
+ }
+
+ switch (bytes[1]) {
+ case '3': // DECDHL top
+ if (state->mode.leftrightmargin) {
+ break;
+ }
+ set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_TOP);
+ break;
+
+ case '4': // DECDHL bottom
+ if (state->mode.leftrightmargin) {
+ break;
+ }
+ set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_BOTTOM);
+ break;
+
+ case '5': // DECSWL
+ if (state->mode.leftrightmargin) {
+ break;
+ }
+ set_lineinfo(state, state->pos.row, NO_FORCE, DWL_OFF, DHL_OFF);
+ break;
+
+ case '6': // DECDWL
+ if (state->mode.leftrightmargin) {
+ break;
+ }
+ set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_OFF);
+ break;
+
+ case '8': // DECALN
+ {
+ VTermPos pos;
+ schar_T E = schar_from_ascii('E'); // E
+ for (pos.row = 0; pos.row < state->rows; pos.row++) {
+ for (pos.col = 0; pos.col < ROWWIDTH(state, pos.row); pos.col++) {
+ putglyph(state, E, 1, pos);
+ }
+ }
+ break;
+ }
+
+ default:
+ return 0;
+ }
+ return 2;
+
+ case '(':
+ case ')':
+ case '*':
+ case '+': // SCS
+ if (len != 2) {
+ return 0;
+ }
+
+ {
+ int setnum = bytes[0] - 0x28;
+ VTermEncoding *newenc = vterm_lookup_encoding(ENC_SINGLE_94, bytes[1]);
+
+ if (newenc) {
+ state->encoding[setnum].enc = newenc;
+
+ if (newenc->init) {
+ (*newenc->init)(newenc, state->encoding[setnum].data);
+ }
+ }
+ }
+
+ return 2;
+
+ case '7': // DECSC
+ savecursor(state, 1);
+ return 1;
+
+ case '8': // DECRC
+ savecursor(state, 0);
+ return 1;
+
+ case '<': // Ignored by VT100. Used in VT52 mode to switch up to VT100
+ return 1;
+
+ case '=': // DECKPAM
+ state->mode.keypad = 1;
+ return 1;
+
+ case '>': // DECKPNM
+ state->mode.keypad = 0;
+ return 1;
+
+ case 'c': // RIS - ECMA-48 8.3.105
+ {
+ VTermPos oldpos = state->pos;
+ vterm_state_reset(state, 1);
+ if (state->callbacks && state->callbacks->movecursor) {
+ (*state->callbacks->movecursor)(state->pos, oldpos, state->mode.cursor_visible,
+ state->cbdata);
+ }
+ return 1;
+ }
+
+ case 'n': // LS2 - ECMA-48 8.3.78
+ state->gl_set = 2;
+ return 1;
+
+ case 'o': // LS3 - ECMA-48 8.3.80
+ state->gl_set = 3;
+ return 1;
+
+ case '~': // LS1R - ECMA-48 8.3.77
+ state->gr_set = 1;
+ return 1;
+
+ case '}': // LS2R - ECMA-48 8.3.79
+ state->gr_set = 2;
+ return 1;
+
+ case '|': // LS3R - ECMA-48 8.3.81
+ state->gr_set = 3;
+ return 1;
+
+ default:
+ return 0;
+ }
+}
+
+static void set_mode(VTermState *state, int num, int val)
+{
+ switch (num) {
+ case 4: // IRM - ECMA-48 7.2.10
+ state->mode.insert = (unsigned)val;
+ break;
+
+ case 20: // LNM - ANSI X3.4-1977
+ state->mode.newline = (unsigned)val;
+ break;
+
+ default:
+ DEBUG_LOG("libvterm: Unknown mode %d\n", num);
+ return;
+ }
+}
+
+static void set_dec_mode(VTermState *state, int num, int val)
+{
+ switch (num) {
+ case 1:
+ state->mode.cursor = (unsigned)val;
+ break;
+
+ case 5: // DECSCNM - screen mode
+ settermprop_bool(state, VTERM_PROP_REVERSE, val);
+ break;
+
+ case 6: // DECOM - origin mode
+ {
+ VTermPos oldpos = state->pos;
+ state->mode.origin = (unsigned)val;
+ state->pos.row = state->mode.origin ? state->scrollregion_top : 0;
+ state->pos.col = state->mode.origin ? SCROLLREGION_LEFT(state) : 0;
+ updatecursor(state, &oldpos, 1);
+ }
+ break;
+
+ case 7:
+ state->mode.autowrap = (unsigned)val;
+ break;
+
+ case 12:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, val);
+ break;
+
+ case 25:
+ settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, val);
+ break;
+
+ case 69: // DECVSSM - vertical split screen mode
+ // DECLRMM - left/right margin mode
+ state->mode.leftrightmargin = (unsigned)val;
+ if (val) {
+ // Setting DECVSSM must clear doublewidth/doubleheight state of every line
+ for (int row = 0; row < state->rows; row++) {
+ set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+ }
+ }
+
+ break;
+
+ case 1000:
+ case 1002:
+ case 1003:
+ settermprop_int(state, VTERM_PROP_MOUSE,
+ !val ? VTERM_PROP_MOUSE_NONE
+ : (num == 1000) ? VTERM_PROP_MOUSE_CLICK
+ : (num == 1002) ? VTERM_PROP_MOUSE_DRAG
+ : VTERM_PROP_MOUSE_MOVE);
+ break;
+
+ case 1004:
+ settermprop_bool(state, VTERM_PROP_FOCUSREPORT, val);
+ state->mode.report_focus = (unsigned)val;
+ break;
+
+ case 1005:
+ state->mouse_protocol = val ? MOUSE_UTF8 : MOUSE_X10;
+ break;
+
+ case 1006:
+ state->mouse_protocol = val ? MOUSE_SGR : MOUSE_X10;
+ break;
+
+ case 1015:
+ state->mouse_protocol = val ? MOUSE_RXVT : MOUSE_X10;
+ break;
+
+ case 1047:
+ settermprop_bool(state, VTERM_PROP_ALTSCREEN, val);
+ break;
+
+ case 1048:
+ savecursor(state, val);
+ break;
+
+ case 1049:
+ settermprop_bool(state, VTERM_PROP_ALTSCREEN, val);
+ savecursor(state, val);
+ break;
+
+ case 2004:
+ state->mode.bracketpaste = (unsigned)val;
+ break;
+
+ case 2031:
+ settermprop_bool(state, VTERM_PROP_THEMEUPDATES, val);
+ break;
+
+ default:
+ DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num);
+ return;
+ }
+}
+
+static void request_dec_mode(VTermState *state, int num)
+{
+ int reply;
+
+ switch (num) {
+ case 1:
+ reply = state->mode.cursor;
+ break;
+
+ case 5:
+ reply = state->mode.screen;
+ break;
+
+ case 6:
+ reply = state->mode.origin;
+ break;
+
+ case 7:
+ reply = state->mode.autowrap;
+ break;
+
+ case 12:
+ reply = state->mode.cursor_blink;
+ break;
+
+ case 25:
+ reply = state->mode.cursor_visible;
+ break;
+
+ case 69:
+ reply = state->mode.leftrightmargin;
+ break;
+
+ case 1000:
+ reply = state->mouse_flags == MOUSE_WANT_CLICK;
+ break;
+
+ case 1002:
+ reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_DRAG);
+ break;
+
+ case 1003:
+ reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_MOVE);
+ break;
+
+ case 1004:
+ reply = state->mode.report_focus;
+ break;
+
+ case 1005:
+ reply = state->mouse_protocol == MOUSE_UTF8;
+ break;
+
+ case 1006:
+ reply = state->mouse_protocol == MOUSE_SGR;
+ break;
+
+ case 1015:
+ reply = state->mouse_protocol == MOUSE_RXVT;
+ break;
+
+ case 1047:
+ reply = state->mode.alt_screen;
+ break;
+
+ case 2004:
+ reply = state->mode.bracketpaste;
+ break;
+
+ case 2031:
+ reply = state->mode.theme_updates;
+ break;
+
+ default:
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0);
+ return;
+ }
+
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, reply ? 1 : 2);
+}
+
+static void request_version_string(VTermState *state)
+{
+ vterm_push_output_sprintf_str(state->vt, C1_DCS, true, ">|libvterm(%d.%d)",
+ VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR);
+}
+
+static void request_key_encoding_flags(VTermState *state)
+{
+ int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY;
+ struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen];
+
+ int reply = 0;
+
+ assert(stack->size > 0);
+ VTermKeyEncodingFlags flags = stack->items[stack->size - 1];
+
+ if (flags.disambiguate) {
+ reply |= KEY_ENCODING_DISAMBIGUATE;
+ }
+
+ if (flags.report_events) {
+ reply |= KEY_ENCODING_REPORT_EVENTS;
+ }
+
+ if (flags.report_alternate) {
+ reply |= KEY_ENCODING_REPORT_ALTERNATE;
+ }
+
+ if (flags.report_all_keys) {
+ reply |= KEY_ENCODING_REPORT_ALL_KEYS;
+ }
+
+ if (flags.report_associated) {
+ reply |= KEY_ENCODING_REPORT_ASSOCIATED;
+ }
+
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%du", reply);
+}
+
+static void set_key_encoding_flags(VTermState *state, int arg, int mode)
+{
+ // When mode is 3, bits set in arg reset the corresponding mode
+ bool set = mode != 3;
+
+ // When mode is 1, unset bits are reset
+ bool reset_unset = mode == 1;
+
+ struct VTermKeyEncodingFlags flags = { 0 };
+ if (arg & KEY_ENCODING_DISAMBIGUATE) {
+ flags.disambiguate = set;
+ } else if (reset_unset) {
+ flags.disambiguate = false;
+ }
+
+ if (arg & KEY_ENCODING_REPORT_EVENTS) {
+ flags.report_events = set;
+ } else if (reset_unset) {
+ flags.report_events = false;
+ }
+
+ if (arg & KEY_ENCODING_REPORT_ALTERNATE) {
+ flags.report_alternate = set;
+ } else if (reset_unset) {
+ flags.report_alternate = false;
+ }
+ if (arg & KEY_ENCODING_REPORT_ALL_KEYS) {
+ flags.report_all_keys = set;
+ } else if (reset_unset) {
+ flags.report_all_keys = false;
+ }
+
+ if (arg & KEY_ENCODING_REPORT_ASSOCIATED) {
+ flags.report_associated = set;
+ } else if (reset_unset) {
+ flags.report_associated = false;
+ }
+
+ int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY;
+ struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen];
+ assert(stack->size > 0);
+ stack->items[stack->size - 1] = flags;
+}
+
+static void push_key_encoding_flags(VTermState *state, int arg)
+{
+ int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY;
+ struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen];
+ assert(stack->size <= ARRAY_SIZE(stack->items));
+
+ if (stack->size == ARRAY_SIZE(stack->items)) {
+ // Evict oldest entry when stack is full
+ for (size_t i = 0; i < ARRAY_SIZE(stack->items) - 1; i++) {
+ stack->items[i] = stack->items[i + 1];
+ }
+ } else {
+ stack->size++;
+ }
+
+ set_key_encoding_flags(state, arg, 1);
+}
+
+static void pop_key_encoding_flags(VTermState *state, int arg)
+{
+ int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY;
+ struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen];
+ if (arg >= stack->size) {
+ stack->size = 1;
+
+ // If a pop request is received that empties the stack, all flags are reset.
+ memset(&stack->items[0], 0, sizeof(stack->items[0]));
+ } else if (arg > 0) {
+ stack->size -= arg;
+ }
+}
+
+static int on_csi(const char *leader, const long args[], int argcount, const char *intermed,
+ char command, void *user)
+{
+ VTermState *state = user;
+ int leader_byte = 0;
+ int intermed_byte = 0;
+ int cancel_phantom = 1;
+
+ if (leader && leader[0]) {
+ if (leader[1]) { // longer than 1 char
+ return 0;
+ }
+
+ switch (leader[0]) {
+ case '?':
+ case '>':
+ case '<':
+ case '=':
+ leader_byte = (int)leader[0];
+ break;
+ default:
+ return 0;
+ }
+ }
+
+ if (intermed && intermed[0]) {
+ if (intermed[1]) { // longer than 1 char
+ return 0;
+ }
+
+ switch (intermed[0]) {
+ case ' ':
+ case '!':
+ case '"':
+ case '$':
+ case '\'':
+ intermed_byte = (int)intermed[0];
+ break;
+ default:
+ return 0;
+ }
+ }
+
+ VTermPos oldpos = state->pos;
+
+ // Some temporaries for later code
+ int count, val;
+ int row, col;
+ VTermRect rect;
+ int selective;
+
+#define LBOUND(v, min) if ((v) < (min))(v) = (min)
+#define UBOUND(v, max) if ((v) > (max))(v) = (max)
+
+#define LEADER(l, b) ((l << 8) | b)
+#define INTERMED(i, b) ((i << 16) | b)
+
+ switch (intermed_byte << 16 | leader_byte << 8 | command) {
+ case 0x40: // ICH - ECMA-48 8.3.64
+ count = CSI_ARG_COUNT(args[0]);
+
+ if (!is_cursor_in_scrollregion(state)) {
+ break;
+ }
+
+ rect.start_row = state->pos.row;
+ rect.end_row = state->pos.row + 1;
+ rect.start_col = state->pos.col;
+ if (state->mode.leftrightmargin) {
+ rect.end_col = SCROLLREGION_RIGHT(state);
+ } else {
+ rect.end_col = THISROWWIDTH(state);
+ }
+
+ scroll(state, rect, 0, -count);
+
+ break;
+
+ case 0x41: // CUU - ECMA-48 8.3.22
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.row -= count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x42: // CUD - ECMA-48 8.3.19
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.row += count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x43: // CUF - ECMA-48 8.3.20
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.col += count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x44: // CUB - ECMA-48 8.3.18
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.col -= count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x45: // CNL - ECMA-48 8.3.12
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.col = 0;
+ state->pos.row += count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x46: // CPL - ECMA-48 8.3.13
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.col = 0;
+ state->pos.row -= count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x47: // CHA - ECMA-48 8.3.9
+ val = CSI_ARG_OR(args[0], 1);
+ state->pos.col = val - 1;
+ state->at_phantom = 0;
+ break;
+
+ case 0x48: // CUP - ECMA-48 8.3.21
+ row = CSI_ARG_OR(args[0], 1);
+ col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]);
+ // zero-based
+ state->pos.row = row - 1;
+ state->pos.col = col - 1;
+ if (state->mode.origin) {
+ state->pos.row += state->scrollregion_top;
+ state->pos.col += SCROLLREGION_LEFT(state);
+ }
+ state->at_phantom = 0;
+ break;
+
+ case 0x49: // CHT - ECMA-48 8.3.10
+ count = CSI_ARG_COUNT(args[0]);
+ tab(state, count, +1);
+ break;
+
+ case 0x4a: // ED - ECMA-48 8.3.39
+ case LEADER('?', 0x4a): // DECSED - Selective Erase in Display
+ selective = (leader_byte == '?');
+ switch (CSI_ARG(args[0])) {
+ case CSI_ARG_MISSING:
+ case 0:
+ rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1;
+ rect.start_col = state->pos.col; rect.end_col = state->cols;
+ if (rect.end_col > rect.start_col) {
+ erase(state, rect, selective);
+ }
+
+ rect.start_row = state->pos.row + 1; rect.end_row = state->rows;
+ rect.start_col = 0;
+ for (int row_ = rect.start_row; row_ < rect.end_row; row_++) {
+ set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF);
+ }
+ if (rect.end_row > rect.start_row) {
+ erase(state, rect, selective);
+ }
+ break;
+
+ case 1:
+ rect.start_row = 0; rect.end_row = state->pos.row;
+ rect.start_col = 0; rect.end_col = state->cols;
+ for (int row_ = rect.start_row; row_ < rect.end_row; row_++) {
+ set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF);
+ }
+ if (rect.end_col > rect.start_col) {
+ erase(state, rect, selective);
+ }
+
+ rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1;
+ rect.end_col = state->pos.col + 1;
+ if (rect.end_row > rect.start_row) {
+ erase(state, rect, selective);
+ }
+ break;
+
+ case 2:
+ rect.start_row = 0; rect.end_row = state->rows;
+ rect.start_col = 0; rect.end_col = state->cols;
+ for (int row_ = rect.start_row; row_ < rect.end_row; row_++) {
+ set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF);
+ }
+ erase(state, rect, selective);
+ break;
+
+ case 3:
+ if (state->callbacks && state->callbacks->sb_clear) {
+ if ((*state->callbacks->sb_clear)(state->cbdata)) {
+ return 1;
+ }
+ }
+ break;
+ }
+ break;
+
+ case 0x4b: // EL - ECMA-48 8.3.41
+ case LEADER('?', 0x4b): // DECSEL - Selective Erase in Line
+ selective = (leader_byte == '?');
+ rect.start_row = state->pos.row;
+ rect.end_row = state->pos.row + 1;
+
+ switch (CSI_ARG(args[0])) {
+ case CSI_ARG_MISSING:
+ case 0:
+ rect.start_col = state->pos.col; rect.end_col = THISROWWIDTH(state); break;
+ case 1:
+ rect.start_col = 0; rect.end_col = state->pos.col + 1; break;
+ case 2:
+ rect.start_col = 0; rect.end_col = THISROWWIDTH(state); break;
+ default:
+ return 0;
+ }
+
+ if (rect.end_col > rect.start_col) {
+ erase(state, rect, selective);
+ }
+
+ break;
+
+ case 0x4c: // IL - ECMA-48 8.3.67
+ count = CSI_ARG_COUNT(args[0]);
+
+ if (!is_cursor_in_scrollregion(state)) {
+ break;
+ }
+
+ rect.start_row = state->pos.row;
+ rect.end_row = SCROLLREGION_BOTTOM(state);
+ rect.start_col = SCROLLREGION_LEFT(state);
+ rect.end_col = SCROLLREGION_RIGHT(state);
+
+ scroll(state, rect, -count, 0);
+
+ break;
+
+ case 0x4d: // DL - ECMA-48 8.3.32
+ count = CSI_ARG_COUNT(args[0]);
+
+ if (!is_cursor_in_scrollregion(state)) {
+ break;
+ }
+
+ rect.start_row = state->pos.row;
+ rect.end_row = SCROLLREGION_BOTTOM(state);
+ rect.start_col = SCROLLREGION_LEFT(state);
+ rect.end_col = SCROLLREGION_RIGHT(state);
+
+ scroll(state, rect, count, 0);
+
+ break;
+
+ case 0x50: // DCH - ECMA-48 8.3.26
+ count = CSI_ARG_COUNT(args[0]);
+
+ if (!is_cursor_in_scrollregion(state)) {
+ break;
+ }
+
+ rect.start_row = state->pos.row;
+ rect.end_row = state->pos.row + 1;
+ rect.start_col = state->pos.col;
+ if (state->mode.leftrightmargin) {
+ rect.end_col = SCROLLREGION_RIGHT(state);
+ } else {
+ rect.end_col = THISROWWIDTH(state);
+ }
+
+ scroll(state, rect, 0, count);
+
+ break;
+
+ case 0x53: // SU - ECMA-48 8.3.147
+ count = CSI_ARG_COUNT(args[0]);
+
+ rect.start_row = state->scrollregion_top;
+ rect.end_row = SCROLLREGION_BOTTOM(state);
+ rect.start_col = SCROLLREGION_LEFT(state);
+ rect.end_col = SCROLLREGION_RIGHT(state);
+
+ scroll(state, rect, count, 0);
+
+ break;
+
+ case 0x54: // SD - ECMA-48 8.3.113
+ count = CSI_ARG_COUNT(args[0]);
+
+ rect.start_row = state->scrollregion_top;
+ rect.end_row = SCROLLREGION_BOTTOM(state);
+ rect.start_col = SCROLLREGION_LEFT(state);
+ rect.end_col = SCROLLREGION_RIGHT(state);
+
+ scroll(state, rect, -count, 0);
+
+ break;
+
+ case 0x58: // ECH - ECMA-48 8.3.38
+ count = CSI_ARG_COUNT(args[0]);
+
+ rect.start_row = state->pos.row;
+ rect.end_row = state->pos.row + 1;
+ rect.start_col = state->pos.col;
+ rect.end_col = state->pos.col + count;
+ UBOUND(rect.end_col, THISROWWIDTH(state));
+
+ erase(state, rect, 0);
+ break;
+
+ case 0x5a: // CBT - ECMA-48 8.3.7
+ count = CSI_ARG_COUNT(args[0]);
+ tab(state, count, -1);
+ break;
+
+ case 0x60: // HPA - ECMA-48 8.3.57
+ col = CSI_ARG_OR(args[0], 1);
+ state->pos.col = col - 1;
+ state->at_phantom = 0;
+ break;
+
+ case 0x61: // HPR - ECMA-48 8.3.59
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.col += count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x62: { // REP - ECMA-48 8.3.103
+ const int row_width = THISROWWIDTH(state);
+ count = CSI_ARG_COUNT(args[0]);
+ col = state->pos.col + count;
+ UBOUND(col, row_width);
+ schar_T sc = schar_from_buf(state->grapheme_buf, state->grapheme_len);
+ while (state->pos.col < col) {
+ putglyph(state, sc, state->combine_width, state->pos);
+ state->pos.col += state->combine_width;
+ }
+ if (state->pos.col + state->combine_width >= row_width) {
+ if (state->mode.autowrap) {
+ state->at_phantom = 1;
+ cancel_phantom = 0;
+ }
+ }
+ break;
+ }
+
+ case 0x63: // DA - ECMA-48 8.3.24
+ val = CSI_ARG_OR(args[0], 0);
+ if (val == 0) {
+ // DEC VT100 response
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?1;2c");
+ }
+ break;
+
+ case LEADER('>', 0x63): // DEC secondary Device Attributes
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, ">%d;%d;%dc", 0, 100, 0);
+ break;
+
+ case 0x64: // VPA - ECMA-48 8.3.158
+ row = CSI_ARG_OR(args[0], 1);
+ state->pos.row = row - 1;
+ if (state->mode.origin) {
+ state->pos.row += state->scrollregion_top;
+ }
+ state->at_phantom = 0;
+ break;
+
+ case 0x65: // VPR - ECMA-48 8.3.160
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.row += count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x66: // HVP - ECMA-48 8.3.63
+ row = CSI_ARG_OR(args[0], 1);
+ col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]);
+ // zero-based
+ state->pos.row = row - 1;
+ state->pos.col = col - 1;
+ if (state->mode.origin) {
+ state->pos.row += state->scrollregion_top;
+ state->pos.col += SCROLLREGION_LEFT(state);
+ }
+ state->at_phantom = 0;
+ break;
+
+ case 0x67: // TBC - ECMA-48 8.3.154
+ val = CSI_ARG_OR(args[0], 0);
+
+ switch (val) {
+ case 0:
+ clear_col_tabstop(state, state->pos.col);
+ break;
+ case 3:
+ case 5:
+ for (col = 0; col < state->cols; col++) {
+ clear_col_tabstop(state, col);
+ }
+ break;
+ case 1:
+ case 2:
+ case 4:
+ break;
+ // TODO(vterm): 1, 2 and 4 aren't meaningful yet without line tab stops
+ default:
+ return 0;
+ }
+ break;
+
+ case 0x68: // SM - ECMA-48 8.3.125
+ if (!CSI_ARG_IS_MISSING(args[0])) {
+ set_mode(state, CSI_ARG(args[0]), 1);
+ }
+ break;
+
+ case LEADER('?', 0x68): // DEC private mode set
+ for (int i = 0; i < argcount; i++) {
+ if (!CSI_ARG_IS_MISSING(args[i])) {
+ set_dec_mode(state, CSI_ARG(args[i]), 1);
+ }
+ }
+ break;
+
+ case 0x6a: // HPB - ECMA-48 8.3.58
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.col -= count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x6b: // VPB - ECMA-48 8.3.159
+ count = CSI_ARG_COUNT(args[0]);
+ state->pos.row -= count;
+ state->at_phantom = 0;
+ break;
+
+ case 0x6c: // RM - ECMA-48 8.3.106
+ if (!CSI_ARG_IS_MISSING(args[0])) {
+ set_mode(state, CSI_ARG(args[0]), 0);
+ }
+ break;
+
+ case LEADER('?', 0x6c): // DEC private mode reset
+ for (int i = 0; i < argcount; i++) {
+ if (!CSI_ARG_IS_MISSING(args[i])) {
+ set_dec_mode(state, CSI_ARG(args[i]), 0);
+ }
+ }
+ break;
+
+ case 0x6d: // SGR - ECMA-48 8.3.117
+ vterm_state_setpen(state, args, argcount);
+ break;
+
+ case LEADER('?', 0x6d): // DECSGR
+ // No actual DEC terminal recognised these, but some printers did. These are alternative ways to
+ // request subscript/superscript/off
+ for (int argi = 0; argi < argcount; argi++) {
+ long arg;
+ switch (arg = CSI_ARG(args[argi])) {
+ case 4: // Superscript on
+ arg = 73;
+ vterm_state_setpen(state, &arg, 1);
+ break;
+ case 5: // Subscript on
+ arg = 74;
+ vterm_state_setpen(state, &arg, 1);
+ break;
+ case 24: // Super+subscript off
+ arg = 75;
+ vterm_state_setpen(state, &arg, 1);
+ break;
+ }
+ }
+ break;
+
+ case 0x6e: // DSR - ECMA-48 8.3.35
+ case LEADER('?', 0x6e): // DECDSR
+ val = CSI_ARG_OR(args[0], 0);
+
+ {
+ char *qmark = (leader_byte == '?') ? "?" : "";
+ bool dark = false;
+
+ switch (val) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ // ignore - these are replies
+ break;
+ case 5:
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s0n", qmark);
+ break;
+ case 6: // CPR - cursor position report
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1,
+ state->pos.col + 1);
+ break;
+ case 996:
+ if (state->callbacks && state->callbacks->theme) {
+ if (state->callbacks->theme(&dark, state->cbdata)) {
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?997;%cn", dark ? '1' : '2');
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case INTERMED('!', 0x70): // DECSTR - DEC soft terminal reset
+ vterm_state_reset(state, 0);
+ break;
+
+ case LEADER('?', INTERMED('$', 0x70)):
+ request_dec_mode(state, CSI_ARG(args[0]));
+ break;
+
+ case LEADER('>', 0x71): // XTVERSION - xterm query version string
+ request_version_string(state);
+ break;
+
+ case INTERMED(' ', 0x71): // DECSCUSR - DEC set cursor shape
+ val = CSI_ARG_OR(args[0], 1);
+
+ switch (val) {
+ case 0:
+ case 1:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK);
+ break;
+ case 2:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK);
+ break;
+ case 3:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE);
+ break;
+ case 4:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE);
+ break;
+ case 5:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT);
+ break;
+ case 6:
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT);
+ break;
+ }
+
+ break;
+
+ case INTERMED('"', 0x71): // DECSCA - DEC select character protection attribute
+ val = CSI_ARG_OR(args[0], 0);
+
+ switch (val) {
+ case 0:
+ case 2:
+ state->protected_cell = 0;
+ break;
+ case 1:
+ state->protected_cell = 1;
+ break;
+ }
+
+ break;
+
+ case 0x72: // DECSTBM - DEC custom
+ state->scrollregion_top = CSI_ARG_OR(args[0], 1) - 1;
+ state->scrollregion_bottom = argcount < 2
+ || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]);
+ LBOUND(state->scrollregion_top, 0);
+ UBOUND(state->scrollregion_top, state->rows);
+ LBOUND(state->scrollregion_bottom, -1);
+ if (state->scrollregion_top == 0 && state->scrollregion_bottom == state->rows) {
+ state->scrollregion_bottom = -1;
+ } else {
+ UBOUND(state->scrollregion_bottom, state->rows);
+ }
+
+ if (SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) {
+ // Invalid
+ state->scrollregion_top = 0;
+ state->scrollregion_bottom = -1;
+ }
+
+ // Setting the scrolling region restores the cursor to the home position
+ state->pos.row = 0;
+ state->pos.col = 0;
+ if (state->mode.origin) {
+ state->pos.row += state->scrollregion_top;
+ state->pos.col += SCROLLREGION_LEFT(state);
+ }
+
+ break;
+
+ case 0x73: // DECSLRM - DEC custom
+ // Always allow setting these margins, just they won't take effect without DECVSSM
+ state->scrollregion_left = CSI_ARG_OR(args[0], 1) - 1;
+ state->scrollregion_right = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]);
+ LBOUND(state->scrollregion_left, 0);
+ UBOUND(state->scrollregion_left, state->cols);
+ LBOUND(state->scrollregion_right, -1);
+ if (state->scrollregion_left == 0 && state->scrollregion_right == state->cols) {
+ state->scrollregion_right = -1;
+ } else {
+ UBOUND(state->scrollregion_right, state->cols);
+ }
+
+ if (state->scrollregion_right > -1
+ && state->scrollregion_right <= state->scrollregion_left) {
+ // Invalid
+ state->scrollregion_left = 0;
+ state->scrollregion_right = -1;
+ }
+
+ // Setting the scrolling region restores the cursor to the home position
+ state->pos.row = 0;
+ state->pos.col = 0;
+ if (state->mode.origin) {
+ state->pos.row += state->scrollregion_top;
+ state->pos.col += SCROLLREGION_LEFT(state);
+ }
+
+ break;
+
+ case LEADER('?', 0x75): // Kitty query
+ request_key_encoding_flags(state);
+ break;
+
+ case LEADER('>', 0x75): // Kitty push flags
+ push_key_encoding_flags(state, CSI_ARG_OR(args[0], 0));
+ break;
+
+ case LEADER('<', 0x75): // Kitty pop flags
+ pop_key_encoding_flags(state, CSI_ARG_OR(args[0], 1));
+ break;
+
+ case LEADER('=', 0x75): // Kitty set flags
+ val = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]);
+ set_key_encoding_flags(state, CSI_ARG_OR(args[0], 0), val);
+ break;
+
+ case INTERMED('\'', 0x7D): // DECIC
+ count = CSI_ARG_COUNT(args[0]);
+
+ if (!is_cursor_in_scrollregion(state)) {
+ break;
+ }
+
+ rect.start_row = state->scrollregion_top;
+ rect.end_row = SCROLLREGION_BOTTOM(state);
+ rect.start_col = state->pos.col;
+ rect.end_col = SCROLLREGION_RIGHT(state);
+
+ scroll(state, rect, 0, -count);
+
+ break;
+
+ case INTERMED('\'', 0x7E): // DECDC
+ count = CSI_ARG_COUNT(args[0]);
+
+ if (!is_cursor_in_scrollregion(state)) {
+ break;
+ }
+
+ rect.start_row = state->scrollregion_top;
+ rect.end_row = SCROLLREGION_BOTTOM(state);
+ rect.start_col = state->pos.col;
+ rect.end_col = SCROLLREGION_RIGHT(state);
+
+ scroll(state, rect, 0, count);
+
+ break;
+
+ default:
+ if (state->fallbacks && state->fallbacks->csi) {
+ if ((*state->fallbacks->csi)(leader, args, argcount, intermed, command, state->fbdata)) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ if (state->mode.origin) {
+ LBOUND(state->pos.row, state->scrollregion_top);
+ UBOUND(state->pos.row, SCROLLREGION_BOTTOM(state) - 1);
+ LBOUND(state->pos.col, SCROLLREGION_LEFT(state));
+ UBOUND(state->pos.col, SCROLLREGION_RIGHT(state) - 1);
+ } else {
+ LBOUND(state->pos.row, 0);
+ UBOUND(state->pos.row, state->rows - 1);
+ LBOUND(state->pos.col, 0);
+ UBOUND(state->pos.col, THISROWWIDTH(state) - 1);
+ }
+
+ updatecursor(state, &oldpos, cancel_phantom);
+
+#ifdef DEBUG
+ if (state->pos.row < 0 || state->pos.row >= state->rows
+ || state->pos.col < 0 || state->pos.col >= state->cols) {
+ fprintf(stderr, "Position out of bounds after CSI %c: (%d,%d)\n",
+ command, state->pos.row, state->pos.col);
+ abort();
+ }
+
+ if (SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) {
+ fprintf(stderr, "Scroll region height out of bounds after CSI %c: %d <= %d\n",
+ command, SCROLLREGION_BOTTOM(state), state->scrollregion_top);
+ abort();
+ }
+
+ if (SCROLLREGION_RIGHT(state) <= SCROLLREGION_LEFT(state)) {
+ fprintf(stderr, "Scroll region width out of bounds after CSI %c: %d <= %d\n",
+ command, SCROLLREGION_RIGHT(state), SCROLLREGION_LEFT(state));
+ abort();
+ }
+#endif
+
+ return 1;
+}
+
+static uint8_t unbase64one(char c)
+{
+ if (c >= 'A' && c <= 'Z') {
+ return (uint8_t)c - 'A';
+ } else if (c >= 'a' && c <= 'z') {
+ return (uint8_t)c - 'a' + 26;
+ } else if (c >= '0' && c <= '9') {
+ return (uint8_t)c - '0' + 52;
+ } else if (c == '+') {
+ return 62;
+ } else if (c == '/') {
+ return 63;
+ }
+
+ return 0xFF;
+}
+
+static void osc_selection(VTermState *state, VTermStringFragment frag)
+{
+ if (frag.initial) {
+ state->tmp.selection.mask = 0;
+ state->tmp.selection.state = SELECTION_INITIAL;
+ }
+
+ while (!state->tmp.selection.state && frag.len) {
+ // Parse selection parameter
+ switch (frag.str[0]) {
+ case 'c':
+ state->tmp.selection.mask |= VTERM_SELECTION_CLIPBOARD;
+ break;
+ case 'p':
+ state->tmp.selection.mask |= VTERM_SELECTION_PRIMARY;
+ break;
+ case 'q':
+ state->tmp.selection.mask |= VTERM_SELECTION_SECONDARY;
+ break;
+ case 's':
+ state->tmp.selection.mask |= VTERM_SELECTION_SELECT;
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ state->tmp.selection.mask |= (VTERM_SELECTION_CUT0 << (frag.str[0] - '0'));
+ break;
+
+ case ';':
+ state->tmp.selection.state = SELECTION_SELECTED;
+ if (!state->tmp.selection.mask) {
+ state->tmp.selection.mask = VTERM_SELECTION_SELECT|VTERM_SELECTION_CUT0;
+ }
+ break;
+ }
+
+ frag.str++;
+ frag.len--;
+ }
+
+ if (!frag.len) {
+ // Clear selection if we're already finished but didn't do anything
+ if (frag.final && state->selection.callbacks->set) {
+ (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){
+ .str = NULL,
+ .len = 0,
+ .initial = state->tmp.selection.state != SELECTION_SET,
+ .final = true,
+ }, state->selection.user);
+ }
+ return;
+ }
+
+ if (state->tmp.selection.state == SELECTION_SELECTED) {
+ if (frag.str[0] == '?') {
+ state->tmp.selection.state = SELECTION_QUERY;
+ } else {
+ state->tmp.selection.state = SELECTION_SET_INITIAL;
+ state->tmp.selection.recvpartial = 0;
+ }
+ }
+
+ if (state->tmp.selection.state == SELECTION_QUERY) {
+ if (state->selection.callbacks->query) {
+ (*state->selection.callbacks->query)(state->tmp.selection.mask, state->selection.user);
+ }
+ return;
+ }
+
+ if (state->tmp.selection.state == SELECTION_INVALID) {
+ return;
+ }
+
+ if (state->selection.callbacks->set) {
+ size_t bufcur = 0;
+ char *buffer = state->selection.buffer;
+
+ uint32_t x = 0; // Current decoding value
+ int n = 0; // Number of sextets consumed
+
+ if (state->tmp.selection.recvpartial) {
+ n = state->tmp.selection.recvpartial >> 24;
+ x = state->tmp.selection.recvpartial & 0x03FFFF; // could be up to 18 bits of state in here
+
+ state->tmp.selection.recvpartial = 0;
+ }
+
+ while ((state->selection.buflen - bufcur) >= 3 && frag.len) {
+ if (frag.str[0] == '=') {
+ if (n == 2) {
+ buffer[0] = (char)(x >> 4 & 0xFF);
+ buffer += 1, bufcur += 1;
+ }
+ if (n == 3) {
+ buffer[0] = (char)(x >> 10 & 0xFF);
+ buffer[1] = (char)(x >> 2 & 0xFF);
+ buffer += 2, bufcur += 2;
+ }
+
+ while (frag.len && frag.str[0] == '=') {
+ frag.str++, frag.len--;
+ }
+
+ n = 0;
+ } else {
+ uint8_t b = unbase64one(frag.str[0]);
+ if (b == 0xFF) {
+ DEBUG_LOG("base64decode bad input %02X\n", (uint8_t)frag.str[0]);
+
+ state->tmp.selection.state = SELECTION_INVALID;
+ if (state->selection.callbacks->set) {
+ (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){
+ .str = NULL,
+ .len = 0,
+ .initial = true,
+ .final = true,
+ }, state->selection.user);
+ }
+ break;
+ }
+
+ x = (x << 6) | b;
+ n++;
+ frag.str++, frag.len--;
+
+ if (n == 4) {
+ buffer[0] = (char)(x >> 16 & 0xFF);
+ buffer[1] = (char)(x >> 8 & 0xFF);
+ buffer[2] = (char)(x >> 0 & 0xFF);
+
+ buffer += 3, bufcur += 3;
+ x = 0;
+ n = 0;
+ }
+ }
+
+ if (!frag.len || (state->selection.buflen - bufcur) < 3) {
+ if (bufcur) {
+ (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){
+ .str = state->selection.buffer,
+ .len = bufcur,
+ .initial = state->tmp.selection.state == SELECTION_SET_INITIAL,
+ .final = frag.final && !frag.len,
+ }, state->selection.user);
+ state->tmp.selection.state = SELECTION_SET;
+ }
+
+ buffer = state->selection.buffer;
+ bufcur = 0;
+ }
+ }
+
+ if (n) {
+ state->tmp.selection.recvpartial = (uint32_t)(n << 24) | x;
+ }
+ }
+}
+
+static int on_osc(int command, VTermStringFragment frag, void *user)
+{
+ VTermState *state = user;
+
+ switch (command) {
+ case 0:
+ settermprop_string(state, VTERM_PROP_ICONNAME, frag);
+ settermprop_string(state, VTERM_PROP_TITLE, frag);
+ return 1;
+
+ case 1:
+ settermprop_string(state, VTERM_PROP_ICONNAME, frag);
+ return 1;
+
+ case 2:
+ settermprop_string(state, VTERM_PROP_TITLE, frag);
+ return 1;
+
+ case 52:
+ if (state->selection.callbacks) {
+ osc_selection(state, frag);
+ }
+
+ return 1;
+
+ default:
+ if (state->fallbacks && state->fallbacks->osc) {
+ if ((*state->fallbacks->osc)(command, frag, state->fbdata)) {
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static void request_status_string(VTermState *state, VTermStringFragment frag)
+{
+ VTerm *vt = state->vt;
+
+ char *tmp = state->tmp.decrqss;
+
+ if (frag.initial) {
+ tmp[0] = tmp[1] = tmp[2] = tmp[3] = 0;
+ }
+
+ size_t i = 0;
+ while (i < sizeof(state->tmp.decrqss) - 1 && tmp[i]) {
+ i++;
+ }
+ while (i < sizeof(state->tmp.decrqss) - 1 && frag.len--) {
+ tmp[i++] = (frag.str++)[0];
+ }
+ tmp[i] = 0;
+
+ if (!frag.final) {
+ return;
+ }
+
+ switch (tmp[0] | tmp[1] << 8 | tmp[2] << 16) {
+ case 'm': {
+ // Query SGR
+ long args[20];
+ int argc = vterm_state_getpen(state, args, sizeof(args)/sizeof(args[0]));
+ size_t cur = 0;
+
+ cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur,
+ vt->mode.ctrl8bit ? "\x90" "1$r" : ESC_S "P" "1$r"); // DCS 1$r ...
+ if (cur >= vt->tmpbuffer_len) {
+ return;
+ }
+
+ for (int argi = 0; argi < argc; argi++) {
+ cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur,
+ argi == argc - 1 ? "%ld"
+ : CSI_ARG_HAS_MORE(args[argi]) ? "%ld:"
+ : "%ld;",
+ CSI_ARG(args[argi]));
+ if (cur >= vt->tmpbuffer_len) {
+ return;
+ }
+ }
+
+ cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur,
+ vt->mode.ctrl8bit ? "m" "\x9C" : "m" ESC_S "\\"); // ... m ST
+ if (cur >= vt->tmpbuffer_len) {
+ return;
+ }
+
+ vterm_push_output_bytes(vt, vt->tmpbuffer, cur);
+ return;
+ }
+
+ case 'r':
+ // Query DECSTBM
+ vterm_push_output_sprintf_str(vt, C1_DCS, true,
+ "1$r%d;%dr", state->scrollregion_top + 1,
+ SCROLLREGION_BOTTOM(state));
+ return;
+
+ case 's':
+ // Query DECSLRM
+ vterm_push_output_sprintf_str(vt, C1_DCS, true,
+ "1$r%d;%ds", SCROLLREGION_LEFT(state) + 1,
+ SCROLLREGION_RIGHT(state));
+ return;
+
+ case ' '|('q' << 8): {
+ // Query DECSCUSR
+ int reply = 0;
+ switch (state->mode.cursor_shape) {
+ case VTERM_PROP_CURSORSHAPE_BLOCK:
+ reply = 2; break;
+ case VTERM_PROP_CURSORSHAPE_UNDERLINE:
+ reply = 4; break;
+ case VTERM_PROP_CURSORSHAPE_BAR_LEFT:
+ reply = 6; break;
+ }
+ if (state->mode.cursor_blink) {
+ reply--;
+ }
+ vterm_push_output_sprintf_str(vt, C1_DCS, true,
+ "1$r%d q", reply);
+ return;
+ }
+
+ case '\"'|('q' << 8):
+ // Query DECSCA
+ vterm_push_output_sprintf_str(vt, C1_DCS, true,
+ "1$r%d\"q", state->protected_cell ? 1 : 2);
+ return;
+ }
+
+ vterm_push_output_sprintf_str(state->vt, C1_DCS, true, "0$r");
+}
+
+static int on_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user)
+{
+ VTermState *state = user;
+
+ if (commandlen == 2 && strneq(command, "$q", 2)) {
+ request_status_string(state, frag);
+ return 1;
+ } else if (state->fallbacks && state->fallbacks->dcs) {
+ if ((*state->fallbacks->dcs)(command, commandlen, frag, state->fbdata)) {
+ return 1;
+ }
+ }
+
+ DEBUG_LOG("libvterm: Unhandled DCS %.*s\n", (int)commandlen, command);
+ return 0;
+}
+
+static int on_apc(VTermStringFragment frag, void *user)
+{
+ VTermState *state = user;
+
+ if (state->fallbacks && state->fallbacks->apc) {
+ if ((*state->fallbacks->apc)(frag, state->fbdata)) {
+ return 1;
+ }
+ }
+
+ // No DEBUG_LOG because all APCs are unhandled
+ return 0;
+}
+
+static int on_pm(VTermStringFragment frag, void *user)
+{
+ VTermState *state = user;
+
+ if (state->fallbacks && state->fallbacks->pm) {
+ if ((*state->fallbacks->pm)(frag, state->fbdata)) {
+ return 1;
+ }
+ }
+
+ // No DEBUG_LOG because all PMs are unhandled
+ return 0;
+}
+
+static int on_sos(VTermStringFragment frag, void *user)
+{
+ VTermState *state = user;
+
+ if (state->fallbacks && state->fallbacks->sos) {
+ if ((*state->fallbacks->sos)(frag, state->fbdata)) {
+ return 1;
+ }
+ }
+
+ // No DEBUG_LOG because all SOSs are unhandled
+ return 0;
+}
+
+static int on_resize(int rows, int cols, void *user)
+{
+ VTermState *state = user;
+ VTermPos oldpos = state->pos;
+
+ if (cols != state->cols) {
+ uint8_t *newtabstops = vterm_allocator_malloc(state->vt, ((size_t)cols + 7) / 8);
+
+ // TODO(vterm): This can all be done much more efficiently bytewise
+ int col;
+ for (col = 0; col < state->cols && col < cols; col++) {
+ uint8_t mask = (uint8_t)(1 << (col & 7));
+ if (state->tabstops[col >> 3] & mask) {
+ newtabstops[col >> 3] |= mask;
+ } else {
+ newtabstops[col >> 3] &= ~mask;
+ }
+ }
+
+ for (; col < cols; col++) {
+ uint8_t mask = (uint8_t)(1 << (col & 7));
+ if (col % 8 == 0) {
+ newtabstops[col >> 3] |= mask;
+ } else {
+ newtabstops[col >> 3] &= ~mask;
+ }
+ }
+
+ vterm_allocator_free(state->vt, state->tabstops);
+ state->tabstops = newtabstops;
+ }
+
+ state->rows = rows;
+ state->cols = cols;
+
+ if (state->scrollregion_bottom > -1) {
+ UBOUND(state->scrollregion_bottom, state->rows);
+ }
+ if (state->scrollregion_right > -1) {
+ UBOUND(state->scrollregion_right, state->cols);
+ }
+
+ VTermStateFields fields = {
+ .pos = state->pos,
+ .lineinfos = {[0] = state->lineinfos[0], [1] = state->lineinfos[1] },
+ };
+
+ if (state->callbacks && state->callbacks->resize) {
+ (*state->callbacks->resize)(rows, cols, &fields, state->cbdata);
+ state->pos = fields.pos;
+
+ state->lineinfos[0] = fields.lineinfos[0];
+ state->lineinfos[1] = fields.lineinfos[1];
+ } else {
+ if (rows != state->rows) {
+ for (int bufidx = BUFIDX_PRIMARY; bufidx <= BUFIDX_ALTSCREEN; bufidx++) {
+ VTermLineInfo *oldlineinfo = state->lineinfos[bufidx];
+ if (!oldlineinfo) {
+ continue;
+ }
+
+ VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt,
+ (size_t)rows * sizeof(VTermLineInfo));
+
+ int row;
+ for (row = 0; row < state->rows && row < rows; row++) {
+ newlineinfo[row] = oldlineinfo[row];
+ }
+
+ for (; row < rows; row++) {
+ newlineinfo[row] = (VTermLineInfo){
+ .doublewidth = 0,
+ };
+ }
+
+ vterm_allocator_free(state->vt, state->lineinfos[bufidx]);
+ state->lineinfos[bufidx] = newlineinfo;
+ }
+ }
+ }
+
+ state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY];
+
+ if (state->at_phantom && state->pos.col < cols - 1) {
+ state->at_phantom = 0;
+ state->pos.col++;
+ }
+
+ if (state->pos.row < 0) {
+ state->pos.row = 0;
+ }
+ if (state->pos.row >= rows) {
+ state->pos.row = rows - 1;
+ }
+ if (state->pos.col < 0) {
+ state->pos.col = 0;
+ }
+ if (state->pos.col >= cols) {
+ state->pos.col = cols - 1;
+ }
+
+ updatecursor(state, &oldpos, 1);
+
+ return 1;
+}
+
+static const VTermParserCallbacks parser_callbacks = {
+ .text = on_text,
+ .control = on_control,
+ .escape = on_escape,
+ .csi = on_csi,
+ .osc = on_osc,
+ .dcs = on_dcs,
+ .apc = on_apc,
+ .pm = on_pm,
+ .sos = on_sos,
+ .resize = on_resize,
+};
+
+VTermState *vterm_obtain_state(VTerm *vt)
+{
+ if (vt->state) {
+ return vt->state;
+ }
+
+ VTermState *state = vterm_state_new(vt);
+ vt->state = state;
+
+ vterm_parser_set_callbacks(vt, &parser_callbacks, state);
+
+ return state;
+}
+
+void vterm_state_reset(VTermState *state, int hard)
+{
+ state->scrollregion_top = 0;
+ state->scrollregion_bottom = -1;
+ state->scrollregion_left = 0;
+ state->scrollregion_right = -1;
+
+ state->mode.keypad = 0;
+ state->mode.cursor = 0;
+ state->mode.autowrap = 1;
+ state->mode.insert = 0;
+ state->mode.newline = 0;
+ state->mode.alt_screen = 0;
+ state->mode.origin = 0;
+ state->mode.leftrightmargin = 0;
+ state->mode.bracketpaste = 0;
+ state->mode.report_focus = 0;
+
+ state->mouse_flags = 0;
+
+ state->vt->mode.ctrl8bit = 0;
+
+ for (int col = 0; col < state->cols; col++) {
+ if (col % 8 == 0) {
+ set_col_tabstop(state, col);
+ } else {
+ clear_col_tabstop(state, col);
+ }
+ }
+
+ for (int row = 0; row < state->rows; row++) {
+ set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+ }
+
+ if (state->callbacks && state->callbacks->initpen) {
+ (*state->callbacks->initpen)(state->cbdata);
+ }
+
+ vterm_state_resetpen(state);
+
+ VTermEncoding *default_enc = state->vt->mode.utf8
+ ? vterm_lookup_encoding(ENC_UTF8, 'u')
+ : vterm_lookup_encoding(ENC_SINGLE_94, 'B');
+
+ for (int i = 0; i < 4; i++) {
+ state->encoding[i].enc = default_enc;
+ if (default_enc->init) {
+ (*default_enc->init)(default_enc, state->encoding[i].data);
+ }
+ }
+
+ state->gl_set = 0;
+ state->gr_set = 1;
+ state->gsingle_set = 0;
+
+ state->protected_cell = 0;
+
+ // Initialise the props
+ settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, 1);
+ settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+ settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK);
+
+ if (hard) {
+ state->pos.row = 0;
+ state->pos.col = 0;
+ state->at_phantom = 0;
+
+ VTermRect rect = { 0, state->rows, 0, state->cols };
+ erase(state, rect, 0);
+ }
+}
+
+void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user)
+{
+ if (callbacks) {
+ state->callbacks = callbacks;
+ state->cbdata = user;
+
+ if (state->callbacks && state->callbacks->initpen) {
+ (*state->callbacks->initpen)(state->cbdata);
+ }
+ } else {
+ state->callbacks = NULL;
+ state->cbdata = NULL;
+ }
+}
+
+void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermStateFallbacks *fallbacks,
+ void *user)
+{
+ if (fallbacks) {
+ state->fallbacks = fallbacks;
+ state->fbdata = user;
+ } else {
+ state->fallbacks = NULL;
+ state->fbdata = NULL;
+ }
+}
+
+int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val)
+{
+ // Only store the new value of the property if usercode said it was happy. This is especially
+ // important for altscreen switching
+ if (state->callbacks && state->callbacks->settermprop) {
+ if (!(*state->callbacks->settermprop)(prop, val, state->cbdata)) {
+ return 0;
+ }
+ }
+
+ switch (prop) {
+ case VTERM_PROP_TITLE:
+ case VTERM_PROP_ICONNAME:
+ // we don't store these, just transparently pass through
+ return 1;
+ case VTERM_PROP_CURSORVISIBLE:
+ state->mode.cursor_visible = (unsigned)val->boolean;
+ return 1;
+ case VTERM_PROP_CURSORBLINK:
+ state->mode.cursor_blink = (unsigned)val->boolean;
+ return 1;
+ case VTERM_PROP_CURSORSHAPE:
+ state->mode.cursor_shape = (unsigned)val->number;
+ return 1;
+ case VTERM_PROP_REVERSE:
+ state->mode.screen = (unsigned)val->boolean;
+ return 1;
+ case VTERM_PROP_ALTSCREEN:
+ state->mode.alt_screen = (unsigned)val->boolean;
+ state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY];
+ if (state->mode.alt_screen) {
+ VTermRect rect = {
+ .start_row = 0,
+ .start_col = 0,
+ .end_row = state->rows,
+ .end_col = state->cols,
+ };
+ erase(state, rect, 0);
+ }
+ return 1;
+ case VTERM_PROP_MOUSE:
+ state->mouse_flags = 0;
+ if (val->number) {
+ state->mouse_flags |= MOUSE_WANT_CLICK;
+ }
+ if (val->number == VTERM_PROP_MOUSE_DRAG) {
+ state->mouse_flags |= MOUSE_WANT_DRAG;
+ }
+ if (val->number == VTERM_PROP_MOUSE_MOVE) {
+ state->mouse_flags |= MOUSE_WANT_MOVE;
+ }
+ return 1;
+ case VTERM_PROP_FOCUSREPORT:
+ state->mode.report_focus = (unsigned)val->boolean;
+ return 1;
+ case VTERM_PROP_THEMEUPDATES:
+ state->mode.theme_updates = (unsigned)val->boolean;
+ return 1;
+
+ case VTERM_N_PROPS:
+ return 0;
+ }
+
+ return 0;
+}
+
+void vterm_state_focus_in(VTermState *state)
+{
+ if (state->mode.report_focus) {
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "I");
+ }
+}
+
+void vterm_state_focus_out(VTermState *state)
+{
+ if (state->mode.report_focus) {
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "O");
+ }
+}
+
+const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row)
+{
+ return state->lineinfo + row;
+}
+
+void vterm_state_set_selection_callbacks(VTermState *state,
+ const VTermSelectionCallbacks *callbacks, void *user,
+ char *buffer, size_t buflen)
+{
+ if (buflen && !buffer) {
+ buffer = vterm_allocator_malloc(state->vt, buflen);
+ }
+
+ state->selection.callbacks = callbacks;
+ state->selection.user = user;
+ state->selection.buffer = buffer;
+ state->selection.buflen = buflen;
+}