#include "vterm_internal.h" #include #include #include "../../runtime/printf.h" #define strneq(a, b, n) (strncmp(a, b, n) == 0) #if defined(DEBUG) && DEBUG > 1 #define DEBUG_GLYPH_COMBINE #endif /* Some convenient wrappers to make callback functions easier */ static void putglyph(VTermState *state, const uint32_t chars[], int width, VTermPos pos) { VTermGlyphInfo info = { .chars = chars, .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 (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; vterm_state_newpen(state); state->bold_is_highbright = 0; return state; } INTERNAL void vterm_state_free(VTermState *state) { vterm_allocator_free(state->vt, state->tabstops); vterm_allocator_free(state->vt, state->lineinfo); vterm_allocator_free(state->vt, state->combine_chars); 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, height * sizeof(state->lineinfo[0])); else memmove(state->lineinfo + rect.start_row - downward, state->lineinfo + rect.start_row, height * sizeof(state->lineinfo[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 grow_combine_buffer(VTermState *state) { size_t new_size = state->combine_chars_size * 2; uint32_t *new_chars = vterm_allocator_malloc(state->vt, new_size * sizeof(new_chars[0])); memcpy(new_chars, state->combine_chars, state->combine_chars_size * sizeof(new_chars[0])); vterm_allocator_free(state->vt, state->combine_chars); state->combine_chars = new_chars; state->combine_chars_size = new_size; } static void set_col_tabstop(VTermState *state, int col) { unsigned char mask = 1 << (col & 7); state->tabstops[col >> 3] |= mask; } static void clear_col_tabstop(VTermState *state, int col) { unsigned char mask = 1 << (col & 7); state->tabstops[col >> 3] &= ~mask; } static int is_col_tabstop(VTermState *state, int col) { unsigned char mask = 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; // We'll have at most len codepoints uint32_t codepoints[len]; 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 : len, 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 eaten; if (state->gsingle_set && npoints) state->gsingle_set = 0; int i = 0; /* This is a combining char. that needs to be merged with the previous * glyph output */ if (vterm_unicode_is_combining(codepoints[i])) { /* 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) { /* Find where we need to append these combining chars */ int saved_i = 0; while (state->combine_chars[saved_i]) saved_i++; /* Add extra ones */ while (i < npoints && vterm_unicode_is_combining(codepoints[i])) { if (saved_i >= state->combine_chars_size) grow_combine_buffer(state); state->combine_chars[saved_i++] = codepoints[i++]; } if (saved_i >= state->combine_chars_size) grow_combine_buffer(state); state->combine_chars[saved_i] = 0; /* Now render it */ putglyph(state, state->combine_chars, state->combine_width, state->combine_pos); } else { DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n"); } } for (; i < npoints; i++) { // Try to find combining characters following this int glyph_starts = i; int glyph_ends; for (glyph_ends = i + 1; glyph_ends < npoints; glyph_ends++) if (!vterm_unicode_is_combining(codepoints[glyph_ends])) break; int width = 0; uint32_t chars[glyph_ends - glyph_starts + 1]; for (; i < glyph_ends; i++) { chars[i - glyph_starts] = codepoints[i]; int this_width = vterm_unicode_width(codepoints[i]); width += this_width; } chars[glyph_ends - glyph_starts] = 0; i--; if (state->at_phantom || state->pos.col + width > THISROWWIDTH(state)) { linefeed(state); state->pos.col = 0; state->at_phantom = 0; } if (state->mode.insert) { /* TODO: 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); } putglyph(state, chars, width, state->pos); if (i == npoints - 1) { /* End of the buffer. Save the chars in case we have to combine with * more on the next call */ int save_i; for (save_i = 0; chars[save_i]; save_i++) { if (save_i >= state->combine_chars_size) grow_combine_buffer(state); state->combine_chars[save_i] = chars[save_i]; } if (save_i >= state->combine_chars_size) grow_combine_buffer(state); state->combine_chars[save_i] = 0; state->combine_width = width; state->combine_pos = state->pos; } if (state->pos.col + width >= THISROWWIDTH(state)) { if (state->mode.autowrap) state->at_phantom = 1; } else { state->pos.col += width; } } updatecursor(state, &oldpos, 0); return eaten; } static int on_control(unsigned char 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); 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, const char *str, size_t len) { char strvalue[len + 1]; strncpy(strvalue, str, len); strvalue[len] = 0; VTermValue val = {.string = strvalue}; 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; uint32_t E[] = {'E', 0}; 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 = val; break; case 20: // LNM - ANSI X3.4-1977 state->mode.newline = 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 = 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 = 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 = 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 = 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: state->mode.report_focus = 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 = 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; 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 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 '>': leader_byte = 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 '\'': intermed_byte = 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; } 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); while (state->pos.col < col) { putglyph(state, state->combine_chars, 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: 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 if (!CSI_ARG_IS_MISSING(args[0])) set_dec_mode(state, CSI_ARG(args[0]), 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 if (!CSI_ARG_IS_MISSING(args[0])) set_dec_mode(state, CSI_ARG(args[0]), 0); break; case 0x6d: // SGR - ECMA-48 8.3.117 vterm_state_setpen(state, args, argcount); break; case 0x6e: // DSR - ECMA-48 8.3.35 case LEADER('?', 0x6e): // DECDSR val = CSI_ARG_OR(args[0], 0); { char *qmark = (leader_byte == '?') ? "?" : ""; 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; } } break; case LEADER('!', 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 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 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); return 1; } static int on_osc(const char *command, size_t cmdlen, void *user) { VTermState *state = user; if (cmdlen < 2) return 0; if (strneq(command, "0;", 2)) { settermprop_string(state, VTERM_PROP_ICONNAME, command + 2, cmdlen - 2); settermprop_string(state, VTERM_PROP_TITLE, command + 2, cmdlen - 2); return 1; } else if (strneq(command, "1;", 2)) { settermprop_string(state, VTERM_PROP_ICONNAME, command + 2, cmdlen - 2); return 1; } else if (strneq(command, "2;", 2)) { settermprop_string(state, VTERM_PROP_TITLE, command + 2, cmdlen - 2); return 1; } else if (state->fallbacks && state->fallbacks->osc) if ((*state->fallbacks->osc)(command, cmdlen, state->fbdata)) return 1; return 0; } static void request_status_string(VTermState *state, const char *command, size_t cmdlen) { VTerm *vt = state->vt; if (cmdlen == 1) switch (command[0]) { case 'm': // Query SGR { long args[20]; int argc = vterm_state_getpen(state, args, sizeof(args) / sizeof(args[0])); size_t cur = 0; cur += 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 += 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 += 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_dcs(vt, "1$r%d;%dr", state->scrollregion_top + 1, SCROLLREGION_BOTTOM(state)); return; case 's': // Query DECSLRM vterm_push_output_sprintf_dcs(vt, "1$r%d;%ds", SCROLLREGION_LEFT(state) + 1, SCROLLREGION_RIGHT(state)); return; } if (cmdlen == 2) { if (strneq(command, " q", 2)) { int reply; 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_dcs(vt, "1$r%d q", reply); return; } else if (strneq(command, "\"q", 2)) { vterm_push_output_sprintf_dcs(vt, "1$r%d\"q", state->protected_cell ? 1 : 2); return; } } vterm_push_output_sprintf_dcs(state->vt, "0$r%.s", (int)cmdlen, command); } static int on_dcs(const char *command, size_t cmdlen, void *user) { VTermState *state = user; if (cmdlen >= 2 && strneq(command, "$q", 2)) { request_status_string(state, command + 2, cmdlen - 2); return 1; } else if (state->fallbacks && state->fallbacks->dcs) if ((*state->fallbacks->dcs)(command, cmdlen, state->fbdata)) return 1; return 0; } static int on_resize(int rows, int cols, void *user) { VTermState *state = user; VTermPos oldpos = state->pos; if (cols != state->cols) { unsigned char *newtabstops = vterm_allocator_malloc(state->vt, (cols + 7) / 8); /* TODO: This can all be done much more efficiently bytewise */ int col; for (col = 0; col < state->cols && col < cols; col++) { unsigned char mask = 1 << (col & 7); if (state->tabstops[col >> 3] & mask) newtabstops[col >> 3] |= mask; else newtabstops[col >> 3] &= ~mask; } for (; col < cols; col++) { unsigned char mask = 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; } if (rows != state->rows) { VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt, rows * sizeof(VTermLineInfo)); int row; for (row = 0; row < state->rows && row < rows; row++) { newlineinfo[row] = state->lineinfo[row]; } for (; row < rows; row++) { newlineinfo[row] = (VTermLineInfo){ .doublewidth = 0, }; } vterm_allocator_free(state->vt, state->lineinfo); state->lineinfo = newlineinfo; } 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); VTermPos delta = {0, 0}; if (state->callbacks && state->callbacks->resize) (*state->callbacks->resize)(rows, cols, &delta, state->cbdata); if (state->at_phantom && state->pos.col < cols - 1) { state->at_phantom = 0; state->pos.col++; } state->pos.row += delta.row; state->pos.col += delta.col; if (state->pos.row >= rows) state->pos.row = rows - 1; 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, .resize = on_resize, }; VTermState *vterm_obtain_state(VTerm *vt) { if (vt->state) return vt->state; VTermState *state = vterm_state_new(vt); vt->state = state; state->combine_chars_size = 16; state->combine_chars = vterm_allocator_malloc(state->vt, state->combine_chars_size * sizeof(state->combine_chars[0])); state->tabstops = vterm_allocator_malloc(state->vt, (state->cols + 7) / 8); state->lineinfo = vterm_allocator_malloc(state->vt, state->rows * sizeof(VTermLineInfo)); 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); 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->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_get_cursorpos(const VTermState *state, VTermPos *cursorpos) { *cursorpos = state->pos; } 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_get_cbdata(VTermState *state) { return state->cbdata; } void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermParserCallbacks *fallbacks, void *user) { if (fallbacks) { state->fallbacks = fallbacks; state->fbdata = user; } else { state->fallbacks = NULL; state->fbdata = NULL; } } void *vterm_state_get_unrecognised_fbdata(VTermState *state) { return state->fbdata; } 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 = val->boolean; return 1; case VTERM_PROP_CURSORBLINK: state->mode.cursor_blink = val->boolean; return 1; case VTERM_PROP_CURSORSHAPE: state->mode.cursor_shape = val->number; return 1; case VTERM_PROP_REVERSE: state->mode.screen = val->boolean; return 1; case VTERM_PROP_ALTSCREEN: state->mode.alt_screen = val->boolean; 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_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; }