diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2025-02-05 23:09:29 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2025-02-05 23:09:29 +0000 |
| commit | d5f194ce780c95821a855aca3c19426576d28ae0 (patch) | |
| tree | d45f461b19f9118ad2bb1f440a7a08973ad18832 /src/nvim/vterm/state.c | |
| parent | c5d770d311841ea5230426cc4c868e8db27300a8 (diff) | |
| parent | 44740e561fc93afe3ebecfd3618bda2d2abeafb0 (diff) | |
| download | rneovim-rahm.tar.gz rneovim-rahm.tar.bz2 rneovim-rahm.zip | |
Diffstat (limited to 'src/nvim/vterm/state.c')
| -rw-r--r-- | src/nvim/vterm/state.c | 2467 |
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; |