diff --git a/CMakeLists.txt b/CMakeLists.txt index f6c5427c7d..764d670973 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ set(DEPS_IGNORE_SHA FALSE) #------------------------------------------------------------------------------- set(FUNCS_DATA ${PROJECT_BINARY_DIR}/funcs_data.mpack) set(TOUCHES_DIR ${PROJECT_BINARY_DIR}/touches) +set(VTERM_TEST_FILE ${PROJECT_BINARY_DIR}/test/vterm_test_output) file(GLOB DOCFILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/runtime/doc/*.txt) diff --git a/cmake.config/config.h.in b/cmake.config/config.h.in index fd712032c3..833218abf9 100644 --- a/cmake.config/config.h.in +++ b/cmake.config/config.h.in @@ -52,3 +52,5 @@ #cmakedefine HAVE_BUILTIN_ADD_OVERFLOW #cmakedefine HAVE_WIMPLICIT_FALLTHROUGH_FLAG #cmakedefine HAVE_BITSCANFORWARD64 + +#define VTERM_TEST_FILE "@VTERM_TEST_FILE@" diff --git a/src/vterm/vterm.c b/src/vterm/vterm.c index 870a61566e..e8c87929e2 100644 --- a/src/vterm/vterm.c +++ b/src/vterm/vterm.c @@ -1,8 +1,9 @@ #include "vterm_internal.h" +#include "auto/config.h" +#include #include #include -#include #include /***************** @@ -429,3 +430,509 @@ void vterm_check_version(int major, int minor) // Happy } + +// For unit tests. +#ifndef NDEBUG + +int parser_text(const char bytes[], size_t len, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "text "); + int i; + for(i = 0; i < len; i++) { + unsigned char b = bytes[i]; + if(b < 0x20 || b == 0x7f || (b >= 0x80 && b < 0xa0)) { + break; + } + fprintf(f, i ? ",%x" : "%x", b); + } + fprintf(f, "\n"); + fclose(f); + + return i; +} + +static void printchars(const char *s, size_t len, FILE *f) +{ + while(len--) { + fprintf(f, "%c", (s++)[0]); + } +} + +int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "csi %02x", command); + + if(leader && leader[0]) { + fprintf(f, " L="); + for(int i = 0; leader[i]; i++) { + fprintf(f, "%02x", leader[i]); + } + } + + for(int i = 0; i < argcount; i++) { + char sep = i ? ',' : ' '; + + if(args[i] == CSI_ARG_MISSING) { + fprintf(f, "%c*", sep); + } else { + fprintf(f, "%c%ld%s", sep, CSI_ARG(args[i]), CSI_ARG_HAS_MORE(args[i]) ? "+" : ""); + } + } + + if(intermed && intermed[0]) { + fprintf(f, " I="); + for(int i = 0; intermed[i]; i++) { + fprintf(f, "%02x", intermed[i]); + } + } + + fprintf(f, "\n"); + + fclose(f); + + return 1; +} + +int parser_osc(int command, VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "osc "); + + if(frag.initial) { + if(command == -1) { + fprintf(f, "["); + } else { + fprintf(f, "[%d;", command); + } + } + + printchars(frag.str, frag.len, f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "dcs "); + + if(frag.initial) { + fprintf(f, "["); + for(int i = 0; i < commandlen; i++) { + fprintf(f, "%c", command[i]); + } + } + + printchars(frag.str, frag.len,f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_apc(VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "apc "); + + if(frag.initial) { + fprintf(f, "["); + } + + printchars(frag.str, frag.len, f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_pm(VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "pm "); + + if(frag.initial) { + fprintf(f, "["); + } + + printchars(frag.str, frag.len,f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_sos(VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "sos "); + + if(frag.initial) { + fprintf(f, "["); + } + + printchars(frag.str, frag.len,f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "selection-set mask=%04X ", mask); + if(frag.initial) { + fprintf(f, "["); +} + printchars(frag.str, frag.len, f); + if(frag.final) { + fprintf(f, "]"); +} + fprintf(f,"\n"); + + fclose(f); + return 1; +} + +int selection_query(VTermSelectionMask mask, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"selection-query mask=%04X\n", mask); + + fclose(f); + return 1; +} + +bool want_state_putglyph; +int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user) +{ + if(!want_state_putglyph) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "putglyph "); + for(int i = 0; i < VTERM_MAX_CHARS_PER_CELL && info->chars[i]; i++) { + fprintf(f, i ? ",%x" : "%x", info->chars[i]); + } + fprintf(f, " %d %d,%d", info->width, pos.row, pos.col); + if(info->protected_cell) { + fprintf(f, " prot"); + } + if(info->dwl) { + fprintf(f, " dwl"); + } + if(info->dhl) { + fprintf(f, " dhl-%s", info->dhl == 1 ? "top" : info->dhl == 2 ? "bottom" : "?" ); + } + fprintf(f, "\n"); + + fclose(f); + + return 1; +} + +bool want_state_movecursor; +VTermPos state_pos; +int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + state_pos = pos; + + if(want_state_movecursor) { + fprintf(f,"movecursor %d,%d\n", pos.row, pos.col); + } + + fclose(f); + return 1; +} + +bool want_state_scrollrect; +int state_scrollrect(VTermRect rect, int downward, int rightward, void *user) +{ + if(!want_state_scrollrect) { + return 0; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + + fprintf(f,"scrollrect %d..%d,%d..%d => %+d,%+d\n", + rect.start_row, rect.end_row, rect.start_col, rect.end_col, + downward, rightward); + + fclose(f); + return 1; +} + +bool want_state_moverect; +int state_moverect(VTermRect dest, VTermRect src, void *user) +{ + if(!want_state_moverect) { + return 0; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"moverect %d..%d,%d..%d -> %d..%d,%d..%d\n", + src.start_row, src.end_row, src.start_col, src.end_col, + dest.start_row, dest.end_row, dest.start_col, dest.end_col); + + fclose(f); + return 1; +} + +void print_color(const VTermColor *col) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + if (VTERM_COLOR_IS_RGB(col)) { + fprintf(f,"rgb(%d,%d,%d", col->rgb.red, col->rgb.green, col->rgb.blue); + } + else if (VTERM_COLOR_IS_INDEXED(col)) { + fprintf(f,"idx(%d", col->indexed.idx); + } + else { + fprintf(f,"invalid(%d", col->type); + } + if (VTERM_COLOR_IS_DEFAULT_FG(col)) { + fprintf(f,",is_default_fg"); + } + if (VTERM_COLOR_IS_DEFAULT_BG(col)) { + fprintf(f,",is_default_bg"); + } + fprintf(f,")"); + fclose(f); +} + +bool want_state_settermprop; +int state_settermprop(VTermProp prop, VTermValue *val, void *user) +{ + if(!want_state_settermprop) { + return 1; + } + + int errcode = 0; + FILE *f = fopen(VTERM_TEST_FILE, "a"); + + VTermValueType type = vterm_get_prop_type(prop); + switch(type) { + case VTERM_VALUETYPE_BOOL: + fprintf(f,"settermprop %d %s\n", prop, val->boolean ? "true" : "false"); + errcode = 1; + goto end; + case VTERM_VALUETYPE_INT: + fprintf(f,"settermprop %d %d\n", prop, val->number); + errcode = 1; + goto end; + case VTERM_VALUETYPE_STRING: + fprintf(f,"settermprop %d %s\"%.*s\"%s\n", prop, + val->string.initial ? "[" : "", (int)val->string.len, val->string.str, val->string.final ? "]" : ""); + errcode=0; + goto end; + case VTERM_VALUETYPE_COLOR: + fprintf(f,"settermprop %d ", prop); + print_color(&val->color); + fprintf(f,"\n"); + errcode=1; + goto end; + case VTERM_N_VALUETYPES: + goto end; + } + +end: + fclose(f); + return errcode; +} + +bool want_state_erase; +int state_erase(VTermRect rect, int selective, void *user) +{ + if(!want_state_erase) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + + fprintf(f,"erase %d..%d,%d..%d%s\n", + rect.start_row, rect.end_row, rect.start_col, rect.end_col, + selective ? " selective" : ""); + + fclose(f); + return 1; +} + +struct { + int bold; + int underline; + int italic; + int blink; + int reverse; + int conceal; + int strike; + int font; + int small; + int baseline; + VTermColor foreground; + VTermColor background; +} state_pen; + +int state_setpenattr(VTermAttr attr, VTermValue *val, void *user) +{ + switch(attr) { + case VTERM_ATTR_BOLD: + state_pen.bold = val->boolean; + break; + case VTERM_ATTR_UNDERLINE: + state_pen.underline = val->number; + break; + case VTERM_ATTR_ITALIC: + state_pen.italic = val->boolean; + break; + case VTERM_ATTR_BLINK: + state_pen.blink = val->boolean; + break; + case VTERM_ATTR_REVERSE: + state_pen.reverse = val->boolean; + break; + case VTERM_ATTR_CONCEAL: + state_pen.conceal = val->boolean; + break; + case VTERM_ATTR_STRIKE: + state_pen.strike = val->boolean; + break; + case VTERM_ATTR_FONT: + state_pen.font = val->number; + break; + case VTERM_ATTR_SMALL: + state_pen.small = val->boolean; + break; + case VTERM_ATTR_BASELINE: + state_pen.baseline = val->number; + break; + case VTERM_ATTR_FOREGROUND: + state_pen.foreground = val->color; + break; + case VTERM_ATTR_BACKGROUND: + state_pen.background = val->color; + break; + + case VTERM_N_ATTRS: + return 0; + default: + break; + } + + return 1; +} + +bool want_state_scrollback; +int state_sb_clear(void *user) { + if(!want_state_scrollback) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"sb_clear\n"); + fclose(f); + + return 0; +} + +bool want_screen_scrollback; +int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user) +{ + if(!want_screen_scrollback) { + return 1; + } + + int eol = cols; + while(eol && !cells[eol-1].chars[0]) { + eol--; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "sb_pushline %d =", cols); + for(int c = 0; c < eol; c++) { + fprintf(f, " %02X", cells[c].chars[0]); + } + fprintf(f, "\n"); + + fclose(f); + + return 1; +} + +int screen_sb_popline(int cols, VTermScreenCell *cells, void *user) +{ + if(!want_screen_scrollback) { + return 0; + } + + // All lines of scrollback contain "ABCDE" + for(int col = 0; col < cols; col++) { + if(col < 5) { + cells[col].chars[0] = 'A' + col; + } else { + cells[col].chars[0] = 0; + } + + cells[col].width = 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"sb_popline %d\n", cols); + fclose(f); + return 1; +} + +int screen_sb_clear(void *user) +{ + if(!want_screen_scrollback) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "sb_clear\n"); + fclose(f); + return 0; +} + +void term_output(const char *s, size_t len, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "output "); + for(int i = 0; i < len; i++) { + fprintf(f, "%x%s", (unsigned char)s[i], i < len-1 ? "," : "\n"); + } + fclose(f); +} + +#endif diff --git a/src/vterm/vterm.h b/src/vterm/vterm.h index 929418c63a..df6878f744 100644 --- a/src/vterm/vterm.h +++ b/src/vterm/vterm.h @@ -9,6 +9,7 @@ extern "C" { #include #include +#include "nvim/macros_defs.h" #include "vterm_keycodes.h" #define VTERM_VERSION_MAJOR 0 @@ -21,7 +22,7 @@ extern "C" { /* Any cell can contain at most one basic printing character and 5 combining * characters. This number could be changed but will be ABI-incompatible if * you do */ -#define VTERM_MAX_CHARS_PER_CELL 6 +enum{ VTERM_MAX_CHARS_PER_CELL=6}; typedef struct VTerm VTerm; typedef struct VTermState VTermState; @@ -634,6 +635,40 @@ void vterm_copy_cells(VTermRect dest, void (*copycell)(VTermPos dest, VTermPos src, void *user), void *user); +#ifndef NDEBUG +int parser_text(const char bytes[], size_t len, void *user); +int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user); +int parser_osc(int command, VTermStringFragment frag, void *user); +int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user); +int parser_apc(VTermStringFragment frag, void *user); +int parser_pm(VTermStringFragment frag, void *user); +int parser_sos(VTermStringFragment frag, void *user); +int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user); +int selection_query(VTermSelectionMask mask, void *user); +int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user); +int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user); +int state_scrollrect(VTermRect rect, int downward, int rightward, void *user); +int state_moverect(VTermRect dest, VTermRect src, void *user); +int state_settermprop(VTermProp prop, VTermValue *val, void *user); +int state_erase(VTermRect rect, int selective, void *user); +int state_setpenattr(VTermAttr attr, VTermValue *val, void *user); +int state_sb_clear(void *user); +void print_color(const VTermColor *col); +int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user); +int screen_sb_popline(int cols, VTermScreenCell *cells, void *user); +int screen_sb_clear(void *user); +void term_output(const char *s, size_t len, void *user); +EXTERN VTermPos state_pos; +EXTERN bool want_state_putglyph INIT (=false); +EXTERN bool want_state_movecursor INIT(= false); +EXTERN bool want_state_erase INIT(= false); +EXTERN bool want_state_scrollrect INIT(= false); +EXTERN bool want_state_moverect INIT(= false); +EXTERN bool want_state_settermprop INIT(= false); +EXTERN bool want_state_scrollback INIT(= false); +EXTERN bool want_screen_scrollback INIT(= false); +#endif + #ifdef __cplusplus } #endif diff --git a/test/cmakeconfig/paths.lua.in b/test/cmakeconfig/paths.lua.in index ce0eb870e0..78c66a4eac 100644 --- a/test/cmakeconfig/paths.lua.in +++ b/test/cmakeconfig/paths.lua.in @@ -5,6 +5,7 @@ for p in ("${TEST_INCLUDE_DIRS}" .. ";"):gmatch("[^;]+") do table.insert(M.include_paths, p) end +M.vterm_test_file = "${VTERM_TEST_FILE}" M.test_build_dir = "${CMAKE_BINARY_DIR}" M.test_source_path = "${CMAKE_SOURCE_DIR}" M.test_lua_prg = "${LUA_PRG}" diff --git a/test/unit/testutil.lua b/test/unit/testutil.lua index a6db7beab1..4720d4d730 100644 --- a/test/unit/testutil.lua +++ b/test/unit/testutil.lua @@ -151,6 +151,13 @@ local function filter_complex_blocks(body) or string.find(line, 'mach_vm_range_recipe') ) then + -- HACK: remove bitfields from specific structs as luajit can't seem to handle them. + if line:find('struct VTermState') then + line = string.gsub(line, 'state : 8;', 'state;') + end + if line:find('VTermStringFragment') then + line = string.gsub(line, 'size_t.*len : 30;', 'size_t len;') + end result[#result + 1] = line end end diff --git a/test/unit/vterm_spec.lua b/test/unit/vterm_spec.lua new file mode 100644 index 0000000000..f0cf450e6c --- /dev/null +++ b/test/unit/vterm_spec.lua @@ -0,0 +1,3591 @@ +local t = require('test.unit.testutil') +local itp = t.gen_itp(it) +local bit = require('bit') + +--- @class vterm +--- @field ENC_UTF8 integer +--- @field VTERM_ATTR_BLINK integer +--- @field VTERM_ATTR_BOLD integer +--- @field VTERM_ATTR_FONT integer +--- @field VTERM_ATTR_ITALIC integer +--- @field VTERM_ATTR_REVERSE integer +--- @field VTERM_ATTR_UNDERLINE integer +--- @field VTERM_BASELINE_RAISE integer +--- @field VTERM_KEY_ENTER integer +--- @field VTERM_KEY_FUNCTION_0 integer +--- @field VTERM_KEY_KP_0 integer +--- @field VTERM_KEY_NONE integer +--- @field VTERM_KEY_TAB integer +--- @field VTERM_KEY_UP integer +--- @field VTERM_MAX_CHARS_PER_CELL integer +--- @field VTERM_MOD_ALT integer +--- @field VTERM_MOD_CTRL integer +--- @field VTERM_MOD_SHIFT integer +--- @field parser_apc function +--- @field parser_csi function +--- @field parser_dcs function +--- @field parser_osc function +--- @field parser_pm function +--- @field parser_sos function +--- @field parser_text function +--- @field print_color function +--- @field screen_sb_clear function +--- @field screen_sb_popline function +--- @field screen_sb_pushline function +--- @field selection_query function +--- @field selection_set function +--- @field state_erase function +--- @field state_movecursor function +--- @field state_moverect function +--- @field state_pos function +--- @field state_putglyph function +--- @field state_sb_clear function +--- @field state_scrollrect function +--- @field state_setpenattr function +--- @field state_settermprop function +--- @field term_output function +--- @field vterm_input_write function +--- @field vterm_keyboard_end_paste function +--- @field vterm_keyboard_key function +--- @field vterm_keyboard_start_paste function +--- @field vterm_keyboard_unichar function +--- @field vterm_lookup_encoding fun(any, any):any +--- @field vterm_mouse_button function +--- @field vterm_mouse_move function +--- @field vterm_new fun(any, any):any +--- @field vterm_obtain_screen fun(any):any +--- @field vterm_obtain_state fun(any): any +--- @field vterm_output_set_callback function +--- @field vterm_parser_set_callbacks fun(any, any, any):any +--- @field vterm_screen_convert_color_to_rgb function +--- @field vterm_screen_enable_altscreen function +--- @field vterm_screen_enable_reflow function +--- @field vterm_screen_get_attrs_extent function +--- @field vterm_screen_get_cell function +--- @field vterm_screen_get_chars fun(any, any, any, any):any +--- @field vterm_screen_get_text fun(any, any, any, any):any +--- @field vterm_screen_is_eol fun(any, any):any +--- @field vterm_screen_reset function +--- @field vterm_screen_set_callbacks function +--- @field vterm_set_size function +--- @field vterm_set_utf8 fun(any, any, any):any +--- @field vterm_state_focus_in function +--- @field vterm_state_focus_out function +--- @field vterm_state_get_cursorpos fun(any, any) +--- @field vterm_state_get_lineinfo fun(any, any):any +--- @field vterm_state_get_penattr function +--- @field vterm_state_reset function +--- @field vterm_state_set_bold_highbright function +--- @field vterm_state_set_callbacks function +--- @field vterm_state_set_selection_callbacks function +--- @field vterm_state_set_unrecognised_fallbacks function +local vterm = t.cimport('./src/vterm/vterm.h', './src/vterm/vterm_internal.h') + +--- @return string +local function read_rm() + local f = assert(io.open(t.paths.vterm_test_file, 'rb')) + local text = f:read('*a') + f:close() + vim.fs.rm(t.paths.vterm_test_file, { force = true }) + return text +end + +local function append(str) + local f = assert(io.open(t.paths.vterm_test_file, 'a')) + f:write(str) + f:close() + return 1 +end + +local function parser_control(control) + return append(string.format('control %02x\n', control)) +end + +local function parser_escape(bytes) + return append(string.format('escape %s\n', t.ffi.string(bytes))) +end + +local function wantparser(vt) + assert(vt) + + local parser_cbs = t.ffi.new('VTermParserCallbacks') + parser_cbs['text'] = vterm.parser_text + parser_cbs['control'] = parser_control + parser_cbs['escape'] = parser_escape + parser_cbs['csi'] = vterm.parser_csi + parser_cbs['osc'] = vterm.parser_osc + parser_cbs['dcs'] = vterm.parser_dcs + parser_cbs['apc'] = vterm.parser_apc + parser_cbs['pm'] = vterm.parser_pm + parser_cbs['sos'] = vterm.parser_sos + + vterm.vterm_parser_set_callbacks(vt, parser_cbs, nil) +end + +--- @return any +local function init() + local vt = vterm.vterm_new(25, 80) + vterm.vterm_output_set_callback(vt, vterm.term_output, nil) + vterm.vterm_set_utf8(vt, true) + return vt +end + +local function state_setlineinfo() + return 1 +end + +--- @return any +local function wantstate(vt, opts) + opts = opts or {} + assert(vt) + local state = vterm.vterm_obtain_state(vt) + + local state_cbs = t.ffi.new('VTermStateCallbacks') + state_cbs['putglyph'] = vterm.state_putglyph + state_cbs['movecursor'] = vterm.state_movecursor + state_cbs['scrollrect'] = vterm.state_scrollrect + state_cbs['moverect'] = vterm.state_moverect + state_cbs['erase'] = vterm.state_erase + state_cbs['setpenattr'] = vterm.state_setpenattr + state_cbs['settermprop'] = vterm.state_settermprop + state_cbs['setlineinfo'] = state_setlineinfo + state_cbs['sb_clear'] = vterm.state_sb_clear + + local selection_cbs = t.ffi.new('VTermSelectionCallbacks') + selection_cbs['set'] = vterm.selection_set + selection_cbs['query'] = vterm.selection_query + + vterm.vterm_state_set_callbacks(state, state_cbs, nil) + + -- In some tests we want to check the behaviour of overflowing the buffer, so make it nicely small + vterm.vterm_state_set_selection_callbacks(state, selection_cbs, nil, nil, 16) + vterm.vterm_state_set_bold_highbright(state, 1) + vterm.vterm_state_reset(state, 1) + + local fallbacks = t.ffi.new('VTermStateFallbacks') + fallbacks['control'] = parser_control + fallbacks['csi'] = vterm.parser_csi + fallbacks['osc'] = vterm.parser_osc + fallbacks['dcs'] = vterm.parser_dcs + fallbacks['apc'] = vterm.parser_apc + fallbacks['pm'] = vterm.parser_pm + fallbacks['sos'] = vterm.parser_sos + + vterm.want_state_scrollback = opts.b or false + vterm.want_state_erase = opts.e or false + vterm.vterm_state_set_unrecognised_fallbacks(state, opts.f and fallbacks or nil, nil) + vterm.want_state_putglyph = opts.g or false + vterm.want_state_moverect = opts.m or false + vterm.want_state_settermprop = opts.p or false + vterm.want_state_scrollrect = opts.s or false + + return state +end + +--- @return any +local function wantscreen(vt, opts) + opts = opts or {} + local screen = vterm.vterm_obtain_screen(vt) + local screen_cbs = t.ffi.new('VTermScreenCallbacks') + + -- TODO(dundargoc): fix + -- screen_cbs['damage'] = vterm.screen_damage + screen_cbs['moverect'] = vterm.state_moverect + screen_cbs['movecursor'] = vterm.state_movecursor + screen_cbs['settermprop'] = vterm.state_settermprop + screen_cbs['sb_pushline'] = vterm.screen_sb_pushline + screen_cbs['sb_popline'] = vterm.screen_sb_popline + screen_cbs['sb_clear'] = vterm.screen_sb_clear + + vterm.vterm_screen_set_callbacks(screen, screen_cbs, nil) + + if opts.a then + vterm.vterm_screen_enable_altscreen(screen, 1) + end + vterm.want_screen_scrollback = opts.b or false + vterm.want_state_movecursor = opts.c or false + -- TODO(dundargoc): fix + -- vterm.want_screen_damage = opts.d or opts.D or false + -- vterm.want_screen_cells = opts.D or false + vterm.want_state_moverect = opts.m or false + vterm.want_state_settermprop = opts.p or false + if opts.r then + vterm.vterm_screen_enable_reflow(screen, true) + end + + return screen +end + +local function reset(state, screen) + if state then + vterm.vterm_state_reset(state, 1) + vterm.vterm_state_get_cursorpos(state, vterm.state_pos) + end + if screen then + vterm.vterm_screen_reset(screen, 1) + end +end + +local function push(input, vt) + vterm.vterm_input_write(vt, input, string.len(input)) +end + +local function expect(expected) + local actual = read_rm() + t.eq(expected .. '\n', actual) +end + +local function expect_output(expected_preformat) + local actual = read_rm() + local expected = 'output ' + + for c in string.gmatch(expected_preformat, '.') do + if expected ~= 'output ' then + expected = expected .. ',' + end + expected = string.format('%s%x', expected, string.byte(c)) + end + + t.eq(expected .. '\n', actual) +end + +local function cursor(row, col, state) + local pos = t.ffi.new('VTermPos') --- @type {row: integer, col: integer} + vterm.vterm_state_get_cursorpos(state, pos) + t.eq(row, pos.row) + t.eq(col, pos.col) +end + +local function lineinfo(row, expected, state) + local info = vterm.vterm_state_get_lineinfo(state, row) + local dwl = info.doublewidth == 1 + local dhl = info.doubleheight == 1 + local cont = info.continuation == 1 + + t.eq(dwl, expected.dwl or false) + t.eq(dhl, expected.dhl or false) + t.eq(cont, expected.cont or false) +end + +local function pen(attribute, expected, state) + local is_bool = { bold = true, italic = true, blink = true, reverse = true } + local vterm_attribute = { + bold = vterm.VTERM_ATTR_BOLD, + underline = vterm.VTERM_ATTR_UNDERLINE, + italic = vterm.VTERM_ATTR_ITALIC, + blink = vterm.VTERM_ATTR_BLINK, + reverse = vterm.VTERM_ATTR_REVERSE, + font = vterm.VTERM_ATTR_FONT, + } + + local val = t.ffi.new('VTermValue') --- @type {boolean: integer} + vterm.vterm_state_get_penattr(state, vterm_attribute[attribute], val) + local actual = val.boolean --- @type integer|boolean + if is_bool[attribute] then + actual = val.boolean == 1 + end + t.eq(expected, actual) +end + +local function resize(rows, cols, vt) + vterm.vterm_set_size(vt, rows, cols) +end + +local function screen_chars(start_row, start_col, end_row, end_col, expected, screen) + local rect = t.ffi.new('VTermRect') + rect['start_row'] = start_row + rect['start_col'] = start_col + rect['end_row'] = end_row + rect['end_col'] = end_col + + local len = vterm.vterm_screen_get_chars(screen, nil, 0, rect) + + local chars = t.ffi.new('uint32_t[?]', len) + vterm.vterm_screen_get_chars(screen, chars, len, rect) + + local actual = '' + for i = 0, tonumber(len) - 1 do + actual = actual .. string.char(chars[i]) + end + + t.eq(expected, actual) +end + +local function screen_text(start_row, start_col, end_row, end_col, expected, screen) + local rect = t.ffi.new('VTermRect') + rect['start_row'] = start_row + rect['start_col'] = start_col + rect['end_row'] = end_row + rect['end_col'] = end_col + + local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) + + local text = t.ffi.new('unsigned char[?]', len) + vterm.vterm_screen_get_text(screen, text, len, rect) + + local actual = '' + for i = 0, tonumber(len) - 1 do + actual = string.format('%s%02x,', actual, text[i]) + end + actual = actual:sub(1, -2) + + t.eq(expected, actual) +end + +--- @param row integer +local function screen_row(row, expected, screen, end_col) + local rect = t.ffi.new('VTermRect') + rect['start_row'] = row + rect['start_col'] = 0 + rect['end_row'] = row + 1 + rect['end_col'] = end_col or 80 + + local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) + + local text = t.ffi.new('unsigned char[?]', len) + vterm.vterm_screen_get_text(screen, text, len, rect) + + t.eq(expected, t.ffi.string(text)) +end + +local function screen_cell(row, col, expected, screen) + local pos = t.ffi.new('VTermPos') + pos['row'] = row + pos['col'] = col + + local cell = t.ffi.new('VTermScreenCell') + vterm.vterm_screen_get_cell(screen, pos, cell) + + local actual = '{' + for i = 0, vterm.VTERM_MAX_CHARS_PER_CELL - 1 do + if cell['chars'][i] ~= 0 then + if i > 0 then + actual = actual .. ',' + end + actual = string.format('%s%02x', actual, cell['chars'][i]) + end + end + actual = string.format('%s} width=%d attrs={', actual, cell['width']) + actual = actual .. (cell['attrs'].bold ~= 0 and 'B' or '') + actual = actual + .. (cell['attrs'].underline ~= 0 and string.format('U%d', cell['attrs'].underline) or '') + actual = actual .. (cell['attrs'].italic ~= 0 and 'I' or '') + actual = actual .. (cell['attrs'].blink ~= 0 and 'K' or '') + actual = actual .. (cell['attrs'].reverse ~= 0 and 'R' or '') + actual = actual .. (cell['attrs'].font ~= 0 and string.format('F%d', cell['attrs'].font) or '') + actual = actual .. (cell['attrs'].small ~= 0 and 'S' or '') + if cell['attrs'].baseline ~= 0 then + actual = actual .. (cell['attrs'].baseline == vterm.VTERM_BASELINE_RAISE and '^' or '_') + end + actual = actual .. '} ' + + actual = actual .. (cell['attrs'].dwl ~= 0 and 'dwl ' or '') + if cell['attrs'].dhl ~= 0 then + actual = actual .. string.format('dhl-%s ', cell['attrs'].dhl == 2 and 'bottom' or 'top') + end + + actual = string.format('%sfg=', actual) + vterm.vterm_screen_convert_color_to_rgb(screen, cell['fg']) + vterm.print_color(cell['fg']) + + actual = actual .. read_rm() + actual = actual .. ' bg=' + + vterm.vterm_screen_convert_color_to_rgb(screen, cell['bg']) + vterm.print_color(cell['bg']) + + actual = actual .. read_rm() + + t.eq(expected, actual) +end + +local function screen_eol(row, col, expected, screen) + local pos = t.ffi.new('VTermPos') + pos['row'] = row + pos['col'] = col + + local is_eol = vterm.vterm_screen_is_eol(screen, pos) + t.eq(expected, is_eol) +end + +local function screen_attrs_extent(row, col, expected, screen) + local pos = t.ffi.new('VTermPos') + pos['row'] = row + pos['col'] = col + + local rect = t.ffi.new('VTermRect') + rect['start_col'] = 0 + rect['end_col'] = -1 + vterm.vterm_screen_get_attrs_extent(screen, rect, pos, 1) + + local actual = string.format( + '%d,%d-%d,%d', + rect['start_row'], + rect['start_col'], + rect['end_row'], + rect['end_col'] + ) + + t.eq(expected, actual) +end + +local function wantencoding() + local encoding = t.ffi.new('VTermEncodingInstance') + encoding['enc'] = vterm.vterm_lookup_encoding(vterm.ENC_UTF8, string.byte('u')) + if encoding.enc.init then + encoding.enc.init(encoding.enc, encoding['data']) + end + return encoding +end + +local function encin(input, encoding) + local len = string.len(input) + + local cp = t.ffi.new('uint32_t[?]', len) + local cpi = t.ffi.new('int[1]') + local pos = t.ffi.new('size_t[1]', 0) + + encoding.enc.decode(encoding.enc, encoding.data, cp, cpi, len, input, pos, len) + + local f = assert(io.open(t.paths.vterm_test_file, 'w')) + if tonumber(cpi[0]) > 0 then + f:write('encout ') + for i = 0, cpi[0] - 1 do + if i == 0 then + f:write(string.format('%x', cp[i])) + else + f:write(string.format(',%x', cp[i])) + end + end + f:write('\n') + end + f:close() +end + +local function strpe_modifiers(input_mod) + local mod = t.ffi.new('VTermModifier') ---@type any + if input_mod.C then + mod = bit.bor(mod, vterm.VTERM_MOD_CTRL) + end + if input_mod.S then + mod = bit.bor(mod, vterm.VTERM_MOD_SHIFT) + end + if input_mod.A then + mod = bit.bor(mod, vterm.VTERM_MOD_ALT) + end + return mod +end + +local function strp_key(input_key) + if input_key == 'up' then + return vterm.VTERM_KEY_UP + end + + if input_key == 'tab' then + return vterm.VTERM_KEY_TAB + end + + if input_key == 'enter' then + return vterm.VTERM_KEY_ENTER + end + + if input_key == 'f1' then + return vterm.VTERM_KEY_FUNCTION_0 + 1 + end + + if input_key == 'kp0' then + return vterm.VTERM_KEY_KP_0 + end + + return vterm.VTERM_KEY_NONE +end + +local function mousemove(row, col, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + vterm.vterm_mouse_move(vt, row, col, mod) +end + +local function mousebtn(press, button, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + local flag = press == 'd' or press == 'D' + vterm.vterm_mouse_button(vt, button, flag, mod) +end + +local function inchar(c, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + vterm.vterm_keyboard_unichar(vt, c, mod) +end + +local function inkey(input_key, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + local key = strp_key(input_key) + vterm.vterm_keyboard_key(vt, key, mod) +end + +before_each(function() + vim.fs.rm(t.paths.vterm_test_file, { force = true }) +end) + +describe('vterm', function() + itp('02parser', function() + local vt = init() + vterm.vterm_set_utf8(vt, false) + wantparser(vt) + + -- Basic text + push('hello', vt) + expect('text 68,65,6c,6c,6f') + + -- C0 + push('\x03', vt) + expect('control 03') + push('\x1f', vt) + expect('control 1f') + + -- C1 8bit + push('\x83', vt) + expect('control 83') + push('\x99', vt) + expect('control 99') + + -- C1 7bit + push('\x1b\x43', vt) + expect('control 83') + push('\x1b\x59', vt) + expect('control 99') + + -- High bytes + push('\xa0\xcc\xfe', vt) + expect('text a0,cc,fe') + + -- Mixed + push('1\n2', vt) + expect('text 31\ncontrol 0a\ntext 32') + + -- Escape + push('\x1b=', vt) + expect('escape =') + + -- Escape 2-byte + push('\x1b(X', vt) + expect('escape (X') + + -- Split write Escape + push('\x1b(', vt) + push('Y', vt) + expect('escape (Y') + + -- Escape cancels Escape, starts another + push('\x1b(\x1b)Z', vt) + expect('escape )Z') + + -- CAN cancels Escape, returns to normal mode + push('\x1b(\x18AB', vt) + expect('text 41,42') + + -- C0 in Escape interrupts and continues + push('\x1b(\nX', vt) + expect('control 0a\nescape (X') + + -- CSI 0 args + push('\x1b[a', vt) + expect('csi 61 *') + + -- CSI 1 arg + push('\x1b[9b', vt) + expect('csi 62 9') + + -- CSI 2 args + push('\x1b[3;4c', vt) + expect('csi 63 3,4') + + -- CSI 1 arg 1 sub + push('\x1b[1:2c', vt) + expect('csi 63 1+,2') + + -- CSI many digits + push('\x1b[678d', vt) + expect('csi 64 678') + + -- CSI leading zero + push('\x1b[007e', vt) + expect('csi 65 7') + + -- CSI qmark + push('\x1b[?2;7f', vt) + expect('csi 66 L=3f 2,7') + + -- CSI greater + push('\x1b[>c', vt) + expect('csi 63 L=3e *') + + -- CSI SP + push('\x1b[12 q', vt) + expect('csi 71 12 I=20') + + -- Mixed CSI + push('A\x1b[8mB', vt) + expect('text 41\ncsi 6d 8\ntext 42') + + -- Split write + push('\x1b', vt) + push('[a', vt) + expect('csi 61 *') + push('foo\x1b[', vt) + expect('text 66,6f,6f') + push('4b', vt) + expect('csi 62 4') + push('\x1b[12;', vt) + push('3c', vt) + expect('csi 63 12,3') + + -- Escape cancels CSI, starts Escape + push('\x1b[123\x1b9', vt) + expect('escape 9') + + -- CAN cancels CSI, returns to normal mode + push('\x1b[12\x18AB', vt) + expect('text 41,42') + + -- C0 in Escape interrupts and continues + push('\x1b(\nX', vt) + expect('control 0a\nescape (X') + + -- OSC BEL + push('\x1b]1;Hello\x07', vt) + expect('osc [1;Hello]') + + -- OSC ST (7bit) + push('\x1b]1;Hello\x1b\\', vt) + expect('osc [1;Hello]') + + -- OSC ST (8bit) + push('\x9d1;Hello\x9c', vt) + expect('osc [1;Hello]') + + -- OSC in parts + push('\x1b]52;abc', vt) + expect('osc [52;abc') + push('def', vt) + expect('osc def') + push('ghi\x1b\\', vt) + expect('osc ghi]') + + -- OSC BEL without semicolon + push('\x1b]1234\x07', vt) + expect('osc [1234;]') + + -- OSC ST without semicolon + push('\x1b]1234\x1b\\', vt) + expect('osc [1234;]') + + -- Escape cancels OSC, starts Escape + push('\x1b]Something\x1b9', vt) + expect('escape 9') + + -- CAN cancels OSC, returns to normal mode + push('\x1b]12\x18AB', vt) + expect('text 41,42') + + -- C0 in OSC interrupts and continues + push('\x1b]2;\nBye\x07', vt) + expect('osc [2;\ncontrol 0a\nosc Bye]') + + -- DCS BEL + push('\x1bPHello\x07', vt) + expect('dcs [Hello]') + + -- DCS ST (7bit) + push('\x1bPHello\x1b\\', vt) + expect('dcs [Hello]') + + -- DCS ST (8bit) + push('\x90Hello\x9c', vt) + expect('dcs [Hello]') + + -- Split write of 7bit ST + push('\x1bPABC\x1b', vt) + expect('dcs [ABC') + push('\\', vt) + expect('dcs ]') + + -- Escape cancels DCS, starts Escape + push('\x1bPSomething\x1b9', vt) + expect('escape 9') + + -- CAN cancels DCS, returns to normal mode + push('\x1bP12\x18AB', vt) + expect('text 41,42') + + -- C0 in OSC interrupts and continues + push('\x1bPBy\ne\x07', vt) + expect('dcs [By\ncontrol 0a\ndcs e]') + + -- APC BEL + push('\x1b_Hello\x07', vt) + expect('apc [Hello]') + + -- APC ST (7bit) + push('\x1b_Hello\x1b\\', vt) + expect('apc [Hello]') + + -- APC ST (8bit) + push('\x9fHello\x9c', vt) + expect('apc [Hello]') + + -- PM BEL + push('\x1b^Hello\x07', vt) + expect('pm [Hello]') + + -- PM ST (7bit) + push('\x1b^Hello\x1b\\', vt) + expect('pm [Hello]') + + -- PM ST (8bit) + push('\x9eHello\x9c', vt) + expect('pm [Hello]') + + -- SOS BEL + push('\x1bXHello\x07', vt) + expect('sos [Hello]') + + -- SOS ST (7bit) + push('\x1bXHello\x1b\\', vt) + expect('sos [Hello]') + + -- SOS ST (8bit) + push('\x98Hello\x9c', vt) + expect('sos [Hello]') + + push('\x1bXABC\x01DEF\x1b\\', vt) + expect('sos [ABC\x01DEF]') + push('\x1bXABC\x99DEF\x1b\\', vt) + expect('sos [ABC\x99DEF]') + + -- NUL ignored + push('\x00', vt) + + -- NUL ignored within CSI + push('\x1b[12\x003m', vt) + expect('csi 6d 123') + + -- DEL ignored + push('\x7f', vt) + + -- DEL ignored within CSI + push('\x1b[12\x7f3m', vt) + expect('csi 6d 123') + + -- DEL inside text" + push('AB\x7fC', vt) + expect('text 41,42\ntext 43') + end) + + itp('03encoding_utf8', function() + local encoding = wantencoding() + + -- Low + encin('123', encoding) + expect('encout 31,32,33') + + -- We want to prove the UTF-8 parser correctly handles all the sequences. + -- Easy way to do this is to check it does low/high boundary cases, as that + -- leaves only two for each sequence length + -- + -- These ranges are therefore: + -- + -- Two bytes: + -- U+0080 = 000 10000000 => 00010 000000 + -- => 11000010 10000000 = C2 80 + -- U+07FF = 111 11111111 => 11111 111111 + -- => 11011111 10111111 = DF BF + -- + -- Three bytes: + -- U+0800 = 00001000 00000000 => 0000 100000 000000 + -- => 11100000 10100000 10000000 = E0 A0 80 + -- U+FFFD = 11111111 11111101 => 1111 111111 111101 + -- => 11101111 10111111 10111101 = EF BF BD + -- (We avoid U+FFFE and U+FFFF as they're invalid codepoints) + -- + -- Four bytes: + -- U+10000 = 00001 00000000 00000000 => 000 010000 000000 000000 + -- => 11110000 10010000 10000000 10000000 = F0 90 80 80 + -- U+1FFFFF = 11111 11111111 11111111 => 111 111111 111111 111111 + -- => 11110111 10111111 10111111 10111111 = F7 BF BF BF + + -- 2 byte + encin('\xC2\x80\xDF\xBF', encoding) + expect('encout 80,7ff') + + -- 3 byte + encin('\xE0\xA0\x80\xEF\xBF\xBD', encoding) + expect('encout 800,fffd') + + -- 4 byte + encin('\xF0\x90\x80\x80\xF7\xBF\xBF\xBF', encoding) + expect('encout 10000,1fffff') + + -- Next up, we check some invalid sequences + -- + Early termination (back to low bytes too soon) + -- + Early restart (another sequence introduction before the previous one was finished) + + -- Early termination + encin('\xC2!', encoding) + expect('encout fffd,21') + + encin('\xE0!\xE0\xA0!', encoding) + expect('encout fffd,21,fffd,21') + + encin('\xF0!\xF0\x90!\xF0\x90\x80!', encoding) + expect('encout fffd,21,fffd,21,fffd,21') + + -- Early restart + encin('\xC2\xC2\x90', encoding) + expect('encout fffd,90') + + encin('\xE0\xC2\x90\xE0\xA0\xC2\x90', encoding) + expect('encout fffd,90,fffd,90') + + encin('\xF0\xC2\x90\xF0\x90\xC2\x90\xF0\x90\x80\xC2\x90', encoding) + expect('encout fffd,90,fffd,90,fffd,90') + + -- Test the overlong sequences by giving an overlong encoding of U+0000 and + -- an encoding of the highest codepoint still too short + -- + -- Two bytes: + -- U+0000 = C0 80 + -- U+007F = 000 01111111 => 00001 111111 => + -- => 11000001 10111111 => C1 BF + -- + -- Three bytes: + -- U+0000 = E0 80 80 + -- U+07FF = 00000111 11111111 => 0000 011111 111111 + -- => 11100000 10011111 10111111 = E0 9F BF + -- + -- Four bytes: + -- U+0000 = F0 80 80 80 + -- U+FFFF = 11111111 11111111 => 000 001111 111111 111111 + -- => 11110000 10001111 10111111 10111111 = F0 8F BF BF + + -- Overlong + encin('\xC0\x80\xC1\xBF', encoding) + expect('encout fffd,fffd') + + encin('\xE0\x80\x80\xE0\x9F\xBF', encoding) + expect('encout fffd,fffd') + + encin('\xF0\x80\x80\x80\xF0\x8F\xBF\xBF', encoding) + expect('encout fffd,fffd') + + -- UTF-16 surrogates U+D800 and U+DFFF + -- UTF-16 Surrogates + encin('\xED\xA0\x80\xED\xBF\xBF', encoding) + expect('encout fffd,fffd') + + -- Split write + encin('\xC2', encoding) + encin('\xA0', encoding) + expect('encout a0') + + encin('\xE0', encoding) + encin('\xA0\x80', encoding) + expect('encout 800') + encin('\xE0\xA0', encoding) + encin('\x80', encoding) + expect('encout 800') + + encin('\xF0', encoding) + encin('\x90\x80\x80', encoding) + expect('encout 10000') + encin('\xF0\x90', encoding) + encin('\x80\x80', encoding) + expect('encout 10000') + encin('\xF0\x90\x80', encoding) + encin('\x80', encoding) + expect('encout 10000') + end) + + itp('10state_putglyph', function() + local vt = init() + local state = wantstate(vt, { g = true }) + + -- Low + reset(state, nil) + push('ABC', vt) + expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,2') + + -- UTF-8 1 char + -- U+00C1 = 0xC3 0x81 name: LATIN CAPITAL LETTER A WITH ACUTE + -- U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE + reset(state, nil) + push('\xC3\x81\xC3\xA9', vt) + expect('putglyph c1 1 0,0\nputglyph e9 1 0,1') + + -- UTF-8 split writes + reset(state, nil) + push('\xC3', vt) + push('\x81', vt) + expect('putglyph c1 1 0,0') + + -- UTF-8 wide char + -- U+FF10 = EF BC 90 name: FULLWIDTH DIGIT ZERO + reset(state, nil) + push('\xEF\xBC\x90 ', vt) + expect('putglyph ff10 2 0,0\nputglyph 20 1 0,2') + + -- UTF-8 emoji wide char + -- U+1F600 = F0 9F 98 80 name: GRINNING FACE + reset(state, nil) + push('\xF0\x9F\x98\x80 ', vt) + expect('putglyph 1f600 2 0,0\nputglyph 20 1 0,2') + + -- UTF-8 combining chars + -- U+0301 = CC 81 name: COMBINING ACUTE + reset(state, nil) + push('e\xCC\x81Z', vt) + expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1') + + -- Combining across buffers + reset(state, nil) + push('e', vt) + expect('putglyph 65 1 0,0') + push('\xCC\x81Z', vt) + expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1') + + -- Spare combining chars get truncated + reset(state, nil) + push('e' .. string.rep('\xCC\x81', 10), vt) + expect('putglyph 65,301,301,301,301,301 1 0,0') -- and nothing more + + reset(state, nil) + push('e', vt) + expect('putglyph 65 1 0,0') + push('\xCC\x81', vt) + expect('putglyph 65,301 1 0,0') + push('\xCC\x82', vt) + expect('putglyph 65,301,302 1 0,0') + + -- DECSCA protected + reset(state, nil) + push('A\x1b[1"qB\x1b[2"qC', vt) + expect('putglyph 41 1 0,0\nputglyph 42 1 0,1 prot\nputglyph 43 1 0,2') + end) + + itp('11state_movecursor', function() + local vt = init() + local state = wantstate(vt) + + -- Implicit + push('ABC', vt) + cursor(0, 3, state) + + -- Backspace + push('\b', vt) + cursor(0, 2, state) + -- Horizontal Tab + push('\t', vt) + cursor(0, 8, state) + -- Carriage Return + push('\r', vt) + cursor(0, 0, state) + -- Linefeed + push('\n', vt) + cursor(1, 0, state) + + -- Backspace bounded by lefthand edge + push('\x1b[4;2H', vt) + cursor(3, 1, state) + push('\b', vt) + cursor(3, 0, state) + push('\b', vt) + cursor(3, 0, state) + + -- Backspace cancels phantom + push('\x1b[4;80H', vt) + cursor(3, 79, state) + push('X', vt) + cursor(3, 79, state) + push('\b', vt) + cursor(3, 78, state) + + -- HT bounded by righthand edge + push('\x1b[1;78H', vt) + cursor(0, 77, state) + push('\t', vt) + cursor(0, 79, state) + push('\t', vt) + cursor(0, 79, state) + + reset(state, nil) + + -- Index + push('ABC\x1bD', vt) + cursor(1, 3, state) + -- Reverse Index + push('\x1bM', vt) + cursor(0, 3, state) + -- Newline + push('\x1bE', vt) + cursor(1, 0, state) + + reset(state, nil) + + -- Cursor Forward + push('\x1b[B', vt) + cursor(1, 0, state) + push('\x1b[3B', vt) + cursor(4, 0, state) + push('\x1b[0B', vt) + cursor(5, 0, state) + + -- Cursor Down + push('\x1b[C', vt) + cursor(5, 1, state) + push('\x1b[3C', vt) + cursor(5, 4, state) + push('\x1b[0C', vt) + cursor(5, 5, state) + + -- Cursor Up + push('\x1b[A', vt) + cursor(4, 5, state) + push('\x1b[3A', vt) + cursor(1, 5, state) + push('\x1b[0A', vt) + cursor(0, 5, state) + + -- Cursor Backward + push('\x1b[D', vt) + cursor(0, 4, state) + push('\x1b[3D', vt) + cursor(0, 1, state) + push('\x1b[0D', vt) + cursor(0, 0, state) + + -- Cursor Next Line + push(' ', vt) + cursor(0, 3, state) + push('\x1b[E', vt) + cursor(1, 0, state) + push(' ', vt) + cursor(1, 3, state) + push('\x1b[2E', vt) + cursor(3, 0, state) + push('\x1b[0E', vt) + cursor(4, 0, state) + + -- Cursor Previous Line + push(' ', vt) + cursor(4, 3, state) + push('\x1b[F', vt) + cursor(3, 0, state) + push(' ', vt) + cursor(3, 3, state) + push('\x1b[2F', vt) + cursor(1, 0, state) + push('\x1b[0F', vt) + cursor(0, 0, state) + + -- Cursor Horizonal Absolute + push('\n', vt) + cursor(1, 0, state) + push('\x1b[20G', vt) + cursor(1, 19, state) + push('\x1b[G', vt) + cursor(1, 0, state) + + -- Cursor Position + push('\x1b[10;5H', vt) + cursor(9, 4, state) + push('\x1b[8H', vt) + cursor(7, 0, state) + push('\x1b[H', vt) + cursor(0, 0, state) + + -- Cursor Position cancels phantom + push('\x1b[10;78H', vt) + cursor(9, 77, state) + push('ABC', vt) + cursor(9, 79, state) + push('\x1b[10;80H', vt) + push('C', vt) + cursor(9, 79, state) + push('X', vt) + cursor(10, 1, state) + + reset(state, nil) + + -- Bounds Checking + push('\x1b[A', vt) + cursor(0, 0, state) + push('\x1b[D', vt) + cursor(0, 0, state) + push('\x1b[25;80H', vt) + cursor(24, 79, state) + push('\x1b[B', vt) + cursor(24, 79, state) + push('\x1b[C', vt) + cursor(24, 79, state) + push('\x1b[E', vt) + cursor(24, 0, state) + push('\x1b[H', vt) + cursor(0, 0, state) + push('\x1b[F', vt) + cursor(0, 0, state) + push('\x1b[999G', vt) + cursor(0, 79, state) + push('\x1b[99;99H', vt) + cursor(24, 79, state) + + reset(state, nil) + + -- Horizontal Position Absolute + push('\x1b[5`', vt) + cursor(0, 4, state) + + -- Horizontal Position Relative + push('\x1b[3a', vt) + cursor(0, 7, state) + + -- Horizontal Position Backward + push('\x1b[3j', vt) + cursor(0, 4, state) + + -- Horizontal and Vertical Position + push('\x1b[3;3f', vt) + cursor(2, 2, state) + + -- Vertical Position Absolute + push('\x1b[5d', vt) + cursor(4, 2, state) + + -- Vertical Position Relative + push('\x1b[2e', vt) + cursor(6, 2, state) + + -- Vertical Position Backward + push('\x1b[2k', vt) + cursor(4, 2, state) + + reset(state, nil) + + -- Horizontal Tab + push('\t', vt) + cursor(0, 8, state) + push(' ', vt) + cursor(0, 11, state) + push('\t', vt) + cursor(0, 16, state) + push(' ', vt) + cursor(0, 23, state) + push('\t', vt) + cursor(0, 24, state) + push(' ', vt) + cursor(0, 32, state) + push('\t', vt) + cursor(0, 40, state) + + -- Cursor Horizontal Tab + push('\x1b[I', vt) + cursor(0, 48, state) + push('\x1b[2I', vt) + cursor(0, 64, state) + + -- Cursor Backward Tab + push('\x1b[Z', vt) + cursor(0, 56, state) + push('\x1b[2Z', vt) + cursor(0, 40, state) + end) + + itp('12state_scroll', function() + local vt = init() + local state = wantstate(vt, { s = true }) + + -- Linefeed + push(string.rep('\n', 24), vt) + cursor(24, 0, state) + push('\n', vt) + expect('scrollrect 0..25,0..80 => +1,+0') + cursor(24, 0, state) + + reset(state, nil) + + -- Index + push('\x1b[25H', vt) + push('\x1bD', vt) + expect('scrollrect 0..25,0..80 => +1,+0') + + reset(state, nil) + + -- Reverse Index + push('\x1bM', vt) + expect('scrollrect 0..25,0..80 => -1,+0') + + reset(state, nil) + + -- Linefeed in DECSTBM + push('\x1b[1;10r', vt) + cursor(0, 0, state) + push(string.rep('\n', 9), vt) + cursor(9, 0, state) + push('\n', vt) + expect('scrollrect 0..10,0..80 => +1,+0') + cursor(9, 0, state) + + -- Linefeed outside DECSTBM + push('\x1b[20H', vt) + cursor(19, 0, state) + push('\n', vt) + cursor(20, 0, state) + + -- Index in DECSTBM + push('\x1b[9;10r', vt) + push('\x1b[10H', vt) + push('\x1bM', vt) + cursor(8, 0, state) + push('\x1bM', vt) + expect('scrollrect 8..10,0..80 => -1,+0') + + -- Reverse Index in DECSTBM + push('\x1b[25H', vt) + cursor(24, 0, state) + push('\n', vt) + -- no scrollrect + cursor(24, 0, state) + + -- Linefeed in DECSTBM+DECSLRM + push('\x1b[?69h', vt) + push('\x1b[3;10r\x1b[10;40s', vt) + push('\x1b[10;10H\n', vt) + expect('scrollrect 2..10,9..40 => +1,+0') + + -- IND/RI in DECSTBM+DECSLRM + push('\x1bD', vt) + expect('scrollrect 2..10,9..40 => +1,+0') + push('\x1b[3;10H\x1bM', vt) + expect('scrollrect 2..10,9..40 => -1,+0') + + -- DECRQSS on DECSTBM + push('\x1bP$qr\x1b\\', vt) + expect_output('\x1bP1$r3;10r\x1b\\') + + -- DECRQSS on DECSLRM + push('\x1bP$qs\x1b\\', vt) + expect_output('\x1bP1$r10;40s\x1b\\') + + -- Setting invalid DECSLRM with !DECVSSM is still rejected + push('\x1b[?69l\x1b[;0s\x1b[?69h', vt) + + reset(state, nil) + + -- Scroll Down + push('\x1b[S', vt) + expect('scrollrect 0..25,0..80 => +1,+0') + cursor(0, 0, state) + push('\x1b[2S', vt) + expect('scrollrect 0..25,0..80 => +2,+0') + cursor(0, 0, state) + push('\x1b[100S', vt) + expect('scrollrect 0..25,0..80 => +25,+0') + + -- Scroll Up + push('\x1b[T', vt) + expect('scrollrect 0..25,0..80 => -1,+0') + cursor(0, 0, state) + push('\x1b[2T', vt) + expect('scrollrect 0..25,0..80 => -2,+0') + cursor(0, 0, state) + push('\x1b[100T', vt) + expect('scrollrect 0..25,0..80 => -25,+0') + + -- SD/SU in DECSTBM + push('\x1b[5;20r', vt) + push('\x1b[S', vt) + expect('scrollrect 4..20,0..80 => +1,+0') + push('\x1b[T', vt) + expect('scrollrect 4..20,0..80 => -1,+0') + + reset(state, nil) + + -- SD/SU in DECSTBM+DECSLRM + push('\x1b[?69h', vt) + push('\x1b[3;10r\x1b[10;40s', vt) + cursor(0, 0, state) + push('\x1b[3;10H', vt) + cursor(2, 9, state) + push('\x1b[S', vt) + expect('scrollrect 2..10,9..40 => +1,+0') + push('\x1b[?69l', vt) + push('\x1b[S', vt) + expect('scrollrect 2..10,0..80 => +1,+0') + + -- Invalid boundaries + reset(state, nil) + + push('\x1b[100;105r\x1bD', vt) + push('\x1b[5;2r\x1bD', vt) + + reset(state, nil) + state = wantstate(vt, { m = true, e = true }) + + -- Scroll Down move+erase emulation + push('\x1b[S', vt) + expect('moverect 1..25,0..80 -> 0..24,0..80\nerase 24..25,0..80') + cursor(0, 0, state) + push('\x1b[2S', vt) + expect('moverect 2..25,0..80 -> 0..23,0..80\nerase 23..25,0..80') + cursor(0, 0, state) + + -- Scroll Up move+erase emulation + push('\x1b[T', vt) + expect('moverect 0..24,0..80 -> 1..25,0..80\nerase 0..1,0..80') + cursor(0, 0, state) + push('\x1b[2T', vt) + expect('moverect 0..23,0..80 -> 2..25,0..80\nerase 0..2,0..80') + cursor(0, 0, state) + + -- DECSTBM resets cursor position + push('\x1b[5;5H', vt) + cursor(4, 4, state) + push('\x1b[r', vt) + cursor(0, 0, state) + end) + + itp('13state_edit', function() + local vt = init() + local state = wantstate(vt, { s = true, e = true, b = true }) + + -- ICH + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ACD', vt) + push('\x1b[2D', vt) + cursor(0, 1, state) + push('\x1b[@', vt) + expect('scrollrect 0..1,1..80 => +0,-1') + cursor(0, 1, state) + push('B', vt) + cursor(0, 2, state) + push('\x1b[3@', vt) + expect('scrollrect 0..1,2..80 => +0,-3') + + -- ICH with DECSLRM + push('\x1b[?69h', vt) + push('\x1b[;50s', vt) + push('\x1b[20G\x1b[@', vt) + expect('scrollrect 0..1,19..50 => +0,-1') + + -- ICH outside DECSLRM + push('\x1b[70G\x1b[@', vt) + -- nothing happens + + -- DCH + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ABBC', vt) + push('\x1b[3D', vt) + cursor(0, 1, state) + push('\x1b[P', vt) + expect('scrollrect 0..1,1..80 => +0,+1') + cursor(0, 1, state) + push('\x1b[3P', vt) + expect('scrollrect 0..1,1..80 => +0,+3') + cursor(0, 1, state) + + -- DCH with DECSLRM + push('\x1b[?69h', vt) + push('\x1b[;50s', vt) + push('\x1b[20G\x1b[P', vt) + expect('scrollrect 0..1,19..50 => +0,+1') + + -- DCH outside DECSLRM + push('\x1b[70G\x1b[P', vt) + -- nothing happens + + -- ECH + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ABC', vt) + push('\x1b[2D', vt) + cursor(0, 1, state) + push('\x1b[X', vt) + expect('erase 0..1,1..2') + cursor(0, 1, state) + push('\x1b[3X', vt) + expect('erase 0..1,1..4') + cursor(0, 1, state) + -- ECH more columns than there are should be bounded + push('\x1b[100X', vt) + expect('erase 0..1,1..80') + + -- IL + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('A\r\nC', vt) + cursor(1, 1, state) + push('\x1b[L', vt) + expect('scrollrect 1..25,0..80 => -1,+0') + -- TODO(libvterm): ECMA-48 says we should move to line home, but neither xterm nor xfce4-terminal do this + cursor(1, 1, state) + push('\rB', vt) + cursor(1, 1, state) + push('\x1b[3L', vt) + expect('scrollrect 1..25,0..80 => -3,+0') + + -- IL with DECSTBM + push('\x1b[5;15r', vt) + push('\x1b[5H\x1b[L', vt) + expect('scrollrect 4..15,0..80 => -1,+0') + + -- IL outside DECSTBM + push('\x1b[20H\x1b[L', vt) + -- nothing happens + + -- IL with DECSTBM+DECSLRM + push('\x1b[?69h', vt) + push('\x1b[10;50s', vt) + push('\x1b[5;10H\x1b[L', vt) + expect('scrollrect 4..15,9..50 => -1,+0') + + -- DL + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('A\r\nB\r\nB\r\nC', vt) + cursor(3, 1, state) + push('\x1b[2H', vt) + cursor(1, 0, state) + push('\x1b[M', vt) + expect('scrollrect 1..25,0..80 => +1,+0') + cursor(1, 0, state) + push('\x1b[3M', vt) + expect('scrollrect 1..25,0..80 => +3,+0') + cursor(1, 0, state) + + -- DL with DECSTBM + push('\x1b[5;15r', vt) + push('\x1b[5H\x1b[M', vt) + expect('scrollrect 4..15,0..80 => +1,+0') + + -- DL outside DECSTBM + push('\x1b[20H\x1b[M', vt) + -- nothing happens + + -- DL with DECSTBM+DECSLRM + push('\x1b[?69h', vt) + push('\x1b[10;50s', vt) + push('\x1b[5;10H\x1b[M', vt) + expect('scrollrect 4..15,9..50 => +1,+0') + + -- DECIC + reset(state, nil) + expect('erase 0..25,0..80') + push("\x1b[20G\x1b[5'}", vt) + expect('scrollrect 0..25,19..80 => +0,-5') + + -- DECIC with DECSTBM+DECSLRM + push('\x1b[?69h', vt) + push('\x1b[4;20r\x1b[20;60s', vt) + push("\x1b[4;20H\x1b[3'}", vt) + expect('scrollrect 3..20,19..60 => +0,-3') + + -- DECIC outside DECSLRM + push("\x1b[70G\x1b['}", vt) + -- nothing happens + + -- DECDC + reset(state, nil) + expect('erase 0..25,0..80') + push("\x1b[20G\x1b[5'~", vt) + expect('scrollrect 0..25,19..80 => +0,+5') + + -- DECDC with DECSTBM+DECSLRM + push('\x1b[?69h', vt) + push('\x1b[4;20r\x1b[20;60s', vt) + push("\x1b[4;20H\x1b[3'~", vt) + expect('scrollrect 3..20,19..60 => +0,+3') + + -- DECDC outside DECSLRM + push("\x1b[70G\x1b['~", vt) + -- nothing happens + + -- EL 0 + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ABCDE', vt) + push('\x1b[3D', vt) + cursor(0, 2, state) + push('\x1b[0K', vt) + expect('erase 0..1,2..80') + cursor(0, 2, state) + + -- EL 1 + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ABCDE', vt) + push('\x1b[3D', vt) + cursor(0, 2, state) + push('\x1b[1K', vt) + expect('erase 0..1,0..3') + cursor(0, 2, state) + + -- EL 2 + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ABCDE', vt) + push('\x1b[3D', vt) + cursor(0, 2, state) + push('\x1b[2K', vt) + expect('erase 0..1,0..80') + cursor(0, 2, state) + + -- SEL + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('\x1b[11G', vt) + cursor(0, 10, state) + push('\x1b[?0K', vt) + expect('erase 0..1,10..80 selective') + cursor(0, 10, state) + push('\x1b[?1K', vt) + expect('erase 0..1,0..11 selective') + cursor(0, 10, state) + push('\x1b[?2K', vt) + expect('erase 0..1,0..80 selective') + cursor(0, 10, state) + + -- ED 0 + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('\x1b[2;2H', vt) + cursor(1, 1, state) + push('\x1b[0J', vt) + expect('erase 1..2,1..80\nerase 2..25,0..80') + cursor(1, 1, state) + + -- ED 1 + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('\x1b[2;2H', vt) + cursor(1, 1, state) + push('\x1b[1J', vt) + expect('erase 0..1,0..80\nerase 1..2,0..2') + cursor(1, 1, state) + + -- ED 2 + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('\x1b[2;2H', vt) + cursor(1, 1, state) + push('\x1b[2J', vt) + expect('erase 0..25,0..80') + cursor(1, 1, state) + + -- ED 3 + push('\x1b[3J', vt) + expect('sb_clear') + + -- SED + reset(state, nil) + expect('erase 0..25,0..80') + push('\x1b[5;5H', vt) + cursor(4, 4, state) + push('\x1b[?0J', vt) + expect('erase 4..5,4..80 selective\nerase 5..25,0..80 selective') + cursor(4, 4, state) + push('\x1b[?1J', vt) + expect('erase 0..4,0..80 selective\nerase 4..5,0..5 selective') + cursor(4, 4, state) + push('\x1b[?2J', vt) + expect('erase 0..25,0..80 selective') + cursor(4, 4, state) + + -- DECRQSS on DECSCA + push('\x1b[2"q', vt) + push('\x1bP$q"q\x1b\\', vt) + expect_output('\x1bP1$r2"q\x1b\\') + + state = wantstate(vt, { m = true, e = true, b = true }) + expect('erase 0..25,0..80') -- TODO(dundargoc): strange, this should not be needed according to the original code + + -- ICH move+erase emuation + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ACD', vt) + push('\x1b[2D', vt) + cursor(0, 1, state) + push('\x1b[@', vt) + expect('moverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2') + cursor(0, 1, state) + push('B', vt) + cursor(0, 2, state) + push('\x1b[3@', vt) + expect('moverect 0..1,2..77 -> 0..1,5..80\nerase 0..1,2..5') + + -- DCH move+erase emulation + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('ABBC', vt) + push('\x1b[3D', vt) + cursor(0, 1, state) + push('\x1b[P', vt) + expect('moverect 0..1,2..80 -> 0..1,1..79\nerase 0..1,79..80') + cursor(0, 1, state) + push('\x1b[3P', vt) + expect('moverect 0..1,4..80 -> 0..1,1..77\nerase 0..1,77..80') + cursor(0, 1, state) + end) + + itp('14state_encoding', function() + local vt = init() + vterm.vterm_set_utf8(vt, false) + local state = wantstate(vt, { g = true }) + + -- Default + reset(state, nil) + push('#', vt) + expect('putglyph 23 1 0,0') + + -- Designate G0=UK + reset(state, nil) + push('\x1b(A', vt) + push('#', vt) + expect('putglyph a3 1 0,0') + + -- Designate G0=DEC drawing + reset(state, nil) + push('\x1b(0', vt) + push('a', vt) + expect('putglyph 2592 1 0,0') + + -- Designate G1 + LS1 + reset(state, nil) + push('\x1b)0', vt) + push('a', vt) + expect('putglyph 61 1 0,0') + push('\x0e', vt) + push('a', vt) + expect('putglyph 2592 1 0,1') + -- LS0 + push('\x0f', vt) + push('a', vt) + expect('putglyph 61 1 0,2') + + -- Designate G2 + LS2 + push('\x1b*0', vt) + push('a', vt) + expect('putglyph 61 1 0,3') + push('\x1bn', vt) + push('a', vt) + expect('putglyph 2592 1 0,4') + push('\x0f', vt) + push('a', vt) + expect('putglyph 61 1 0,5') + + -- Designate G3 + LS3 + push('\x1b+0', vt) + push('a', vt) + expect('putglyph 61 1 0,6') + push('\x1bo', vt) + push('a', vt) + expect('putglyph 2592 1 0,7') + push('\x0f', vt) + push('a', vt) + expect('putglyph 61 1 0,8') + + -- SS2 + push('a\x8eaa', vt) + expect('putglyph 61 1 0,9\nputglyph 2592 1 0,10\nputglyph 61 1 0,11') + + -- SS3 + push('a\x8faa', vt) + expect('putglyph 61 1 0,12\nputglyph 2592 1 0,13\nputglyph 61 1 0,14') + + -- LS1R + reset(state, nil) + push('\x1b~', vt) + push('\xe1', vt) + expect('putglyph 61 1 0,0') + push('\x1b)0', vt) + push('\xe1', vt) + expect('putglyph 2592 1 0,1') + + -- LS2R + reset(state, nil) + push('\x1b}', vt) + push('\xe1', vt) + expect('putglyph 61 1 0,0') + push('\x1b*0', vt) + push('\xe1', vt) + expect('putglyph 2592 1 0,1') + + -- LS3R + reset(state, nil) + push('\x1b|', vt) + push('\xe1', vt) + expect('putglyph 61 1 0,0') + push('\x1b+0', vt) + push('\xe1', vt) + expect('putglyph 2592 1 0,1') + + vterm.vterm_set_utf8(vt, true) + -- U+0108 == c4 88 + reset(state, nil) + push('\x1b(B', vt) + push('AB\xc4\x88D', vt) + expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 108 1 0,2\nputglyph 44 1 0,3') + end) + + itp('15state_mode', function() + local vt = init() + local state = wantstate(vt, { g = true, m = true, e = true }) + + -- Insert/Replace Mode + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('AC\x1b[DB', vt) + expect('putglyph 41 1 0,0\nputglyph 43 1 0,1\nputglyph 42 1 0,1') + push('\x1b[4h', vt) + push('\x1b[G', vt) + push('AC\x1b[DB', vt) + expect( + 'moverect 0..1,0..79 -> 0..1,1..80\nerase 0..1,0..1\nputglyph 41 1 0,0\nmoverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2\nputglyph 43 1 0,1\nmoverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2\nputglyph 42 1 0,1' + ) + + -- Insert mode only happens once for UTF-8 combining + push('e', vt) + expect('moverect 0..1,2..79 -> 0..1,3..80\nerase 0..1,2..3\nputglyph 65 1 0,2') + push('\xCC\x81', vt) + expect('putglyph 65,301 1 0,2') + + -- Newline/Linefeed mode + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('\x1b[5G\n', vt) + cursor(1, 4, state) + push('\x1b[20h', vt) + push('\x1b[5G\n', vt) + cursor(2, 0, state) + + -- DEC origin mode + reset(state, nil) + expect('erase 0..25,0..80') + cursor(0, 0, state) + push('\x1b[5;15r', vt) + push('\x1b[H', vt) + cursor(0, 0, state) + push('\x1b[3;3H', vt) + cursor(2, 2, state) + push('\x1b[?6h', vt) + push('\x1b[H', vt) + cursor(4, 0, state) + push('\x1b[3;3H', vt) + cursor(6, 2, state) + + -- DECRQM on DECOM + push('\x1b[?6h', vt) + push('\x1b[?6$p', vt) + expect_output('\x1b[?6;1$y') + push('\x1b[?6l', vt) + push('\x1b[?6$p', vt) + expect_output('\x1b[?6;2$y') + + -- Origin mode with DECSLRM + push('\x1b[?6h', vt) + push('\x1b[?69h', vt) + push('\x1b[20;60s', vt) + push('\x1b[H', vt) + cursor(4, 19, state) + + push('\x1b[?69l', vt) + + -- Origin mode bounds cursor to scrolling region + push('\x1b[H', vt) + push('\x1b[10A', vt) + cursor(4, 0, state) + push('\x1b[20B', vt) + cursor(14, 0, state) + + -- Origin mode without scroll region + push('\x1b[?6l', vt) + push('\x1b[r\x1b[?6h', vt) + cursor(0, 0, state) + end) + + itp('16state_resize', function() + local vt = init() + local state = wantstate(vt, { g = true }) + + -- Placement + reset(state, nil) + push('AB\x1b[79GCDE', vt) + expect( + 'putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,78\nputglyph 44 1 0,79\nputglyph 45 1 1,0' + ) + + -- Resize + reset(state, nil) + resize(27, 85, vt) + push('AB\x1b[79GCDE', vt) + expect( + 'putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,78\nputglyph 44 1 0,79\nputglyph 45 1 0,80' + ) + cursor(0, 81, state) + + -- Resize without reset + resize(28, 90, vt) + cursor(0, 81, state) + push('FGHI', vt) + expect('putglyph 46 1 0,81\nputglyph 47 1 0,82\nputglyph 48 1 0,83\nputglyph 49 1 0,84') + cursor(0, 85, state) + + -- Resize shrink moves cursor + resize(25, 80, vt) + cursor(0, 79, state) + + -- Resize grow doesn't cancel phantom + reset(state, nil) + push('\x1b[79GAB', vt) + expect('putglyph 41 1 0,78\nputglyph 42 1 0,79') + cursor(0, 79, state) + resize(30, 100, vt) + cursor(0, 80, state) + push('C', vt) + expect('putglyph 43 1 0,80') + cursor(0, 81, state) + end) + + itp('17state_mouse', function() + local vt = init() + local state = wantstate(vt, { p = true }) + + -- DECRQM on with mouse off + push('\x1b[?1000$p', vt) + expect_output('\x1b[?1000;2$y') + push('\x1b[?1002$p', vt) + expect_output('\x1b[?1002;2$y') + push('\x1b[?1003$p', vt) + expect_output('\x1b[?1003;2$y') + + -- Mouse in simple button report mode + reset(state, nil) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + push('\x1b[?1000h', vt) + expect('settermprop 8 1') + + -- Press 1 + mousemove(0, 0, vt) + mousebtn('d', 1, vt) + expect_output('\x1b[M\x20\x21\x21') + + -- Release 1 + mousebtn('u', 1, vt) + expect_output('\x1b[M\x23\x21\x21') + + -- Ctrl-Press 1 + mousebtn('d', 1, vt, { C = true }) + expect_output('\x1b[M\x30\x21\x21') + mousebtn('u', 1, vt, { C = true }) + expect_output('\x1b[M\x33\x21\x21') + + -- Button 2 + mousebtn('d', 2, vt) + expect_output('\x1b[M\x21\x21\x21') + mousebtn('u', 2, vt) + expect_output('\x1b[M\x23\x21\x21') + + -- Position + mousemove(10, 20, vt) + mousebtn('d', 1, vt) + expect_output('\x1b[M\x20\x35\x2b') + + mousebtn('u', 1, vt) + expect_output('\x1b[M\x23\x35\x2b') + mousemove(10, 21, vt) + -- no output + + -- Wheel events + mousebtn('d', 4, vt) + expect_output('\x1b[M\x60\x36\x2b') + mousebtn('d', 4, vt) + expect_output('\x1b[M\x60\x36\x2b') + mousebtn('d', 5, vt) + expect_output('\x1b[M\x61\x36\x2b') + mousebtn('d', 6, vt) + expect_output('\x1b[M\x62\x36\x2b') + mousebtn('d', 7, vt) + expect_output('\x1b[M\x63\x36\x2b') + + -- DECRQM on mouse button mode + push('\x1b[?1000$p', vt) + expect_output('\x1b[?1000;1$y') + push('\x1b[?1002$p', vt) + expect_output('\x1b[?1002;2$y') + push('\x1b[?1003$p', vt) + expect_output('\x1b[?1003;2$y') + + -- Drag events + reset(state, nil) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + push('\x1b[?1002h', vt) + expect('settermprop 8 2') + + mousemove(5, 5, vt) + mousebtn('d', 1, vt) + expect_output('\x1b[M\x20\x26\x26') + mousemove(5, 6, vt) + expect_output('\x1b[M\x40\x27\x26') + mousemove(6, 6, vt) + expect_output('\x1b[M\x40\x27\x27') + mousemove(6, 6, vt) + -- no output + mousebtn('u', 1, vt) + expect_output('\x1b[M\x23\x27\x27') + mousemove(6, 7, vt) + -- no output + + -- DECRQM on mouse drag mode + push('\x1b[?1000$p', vt) + expect_output('\x1b[?1000;2$y') + push('\x1b[?1002$p', vt) + expect_output('\x1b[?1002;1$y') + push('\x1b[?1003$p', vt) + expect_output('\x1b[?1003;2$y') + + -- Non-drag motion events + push('\x1b[?1003h', vt) + expect('settermprop 8 3') + + mousemove(6, 8, vt) + expect_output('\x1b[M\x43\x29\x27') + + -- DECRQM on mouse motion mode + push('\x1b[?1000$p', vt) + expect_output('\x1b[?1000;2$y') + push('\x1b[?1002$p', vt) + expect_output('\x1b[?1002;2$y') + push('\x1b[?1003$p', vt) + expect_output('\x1b[?1003;1$y') + + -- Bounds checking + mousemove(300, 300, vt) + expect_output('\x1b[M\x43\xff\xff') + mousebtn('d', 1, vt) + expect_output('\x1b[M\x20\xff\xff') + mousebtn('u', 1, vt) + expect_output('\x1b[M\x23\xff\xff') + + -- DECRQM on standard encoding mode + push('\x1b[?1005$p', vt) + expect_output('\x1b[?1005;2$y') + push('\x1b[?1006$p', vt) + expect_output('\x1b[?1006;2$y') + push('\x1b[?1015$p', vt) + expect_output('\x1b[?1015;2$y') + + -- UTF-8 extended encoding mode + -- 300 + 32 + 1 = 333 = U+014d = \xc5\x8d + push('\x1b[?1005h', vt) + mousebtn('d', 1, vt) + expect_output('\x1b[M\x20\xc5\x8d\xc5\x8d') + mousebtn('u', 1, vt) + expect_output('\x1b[M\x23\xc5\x8d\xc5\x8d') + + -- DECRQM on UTF-8 extended encoding mode + push('\x1b[?1005$p', vt) + expect_output('\x1b[?1005;1$y') + push('\x1b[?1006$p', vt) + expect_output('\x1b[?1006;2$y') + push('\x1b[?1015$p', vt) + expect_output('\x1b[?1015;2$y') + + -- SGR extended encoding mode + push('\x1b[?1006h', vt) + mousebtn('d', 1, vt) + expect_output('\x1b[<0;301;301M') + mousebtn('u', 1, vt) + expect_output('\x1b[<0;301;301m') + + -- DECRQM on SGR extended encoding mode + push('\x1b[?1005$p', vt) + expect_output('\x1b[?1005;2$y') + push('\x1b[?1006$p', vt) + expect_output('\x1b[?1006;1$y') + push('\x1b[?1015$p', vt) + expect_output('\x1b[?1015;2$y') + + -- rxvt extended encoding mode + push('\x1b[?1015h', vt) + mousebtn('d', 1, vt) + expect_output('\x1b[0;301;301M') + mousebtn('u', 1, vt) + expect_output('\x1b[3;301;301M') + + -- DECRQM on rxvt extended encoding mode + push('\x1b[?1005$p', vt) + expect_output('\x1b[?1005;2$y') + push('\x1b[?1006$p', vt) + expect_output('\x1b[?1006;2$y') + push('\x1b[?1015$p', vt) + expect_output('\x1b[?1015;1$y') + + -- Mouse disabled reports nothing + reset(state, nil) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + mousemove(0, 0, vt) + mousebtn('d', 1, vt) + mousebtn('u', 1, vt) + + -- DECSM can set multiple modes at once + push('\x1b[?1002;1006h', vt) + expect('settermprop 8 2') + mousebtn('d', 1, vt) + expect_output('\x1b[<0;1;1M') + end) + + itp('18state_termprops', function() + local vt = init() + local state = wantstate(vt, { p = true }) + + reset(state, nil) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + + -- Cursor visibility + push('\x1b[?25h', vt) + expect('settermprop 1 true') + push('\x1b[?25$p', vt) + expect_output('\x1b[?25;1$y') + push('\x1b[?25l', vt) + expect('settermprop 1 false') + push('\x1b[?25$p', vt) + expect_output('\x1b[?25;2$y') + + -- Cursor blink + push('\x1b[?12h', vt) + expect('settermprop 2 true') + push('\x1b[?12$p', vt) + expect_output('\x1b[?12;1$y') + push('\x1b[?12l', vt) + expect('settermprop 2 false') + push('\x1b[?12$p', vt) + expect_output('\x1b[?12;2$y') + + -- Cursor shape + push('\x1b[3 q', vt) + expect('settermprop 2 true\nsettermprop 7 2') + + -- Title + push('\x1b]2;Here is my title\a', vt) + expect('settermprop 4 ["Here is my title"]') + + -- Title split write + push('\x1b]2;Here is', vt) + expect('settermprop 4 ["Here is"') + push(' another title\a', vt) + expect('settermprop 4 " another title"]') + end) + + itp('20state_wrapping', function() + local vt = init() + local state = wantstate(vt, { g = true, m = true }) + + -- 79th Column + push('\x1b[75G', vt) + push(string.rep('A', 5), vt) + expect( + 'putglyph 41 1 0,74\nputglyph 41 1 0,75\nputglyph 41 1 0,76\nputglyph 41 1 0,77\nputglyph 41 1 0,78' + ) + cursor(0, 79, state) + + -- 80th Column Phantom + push('A', vt) + expect('putglyph 41 1 0,79') + cursor(0, 79, state) + + -- Line Wraparound + push('B', vt) + expect('putglyph 42 1 1,0') + cursor(1, 1, state) + + -- Line Wraparound during combined write + push('\x1b[78G', vt) + push('BBBCC', vt) + expect( + 'putglyph 42 1 1,77\nputglyph 42 1 1,78\nputglyph 42 1 1,79\nputglyph 43 1 2,0\nputglyph 43 1 2,1' + ) + cursor(2, 2, state) + + -- DEC Auto Wrap Mode + reset(state, nil) + push('\x1b[?7l', vt) + push('\x1b[75G', vt) + push(string.rep('D', 6), vt) + expect( + 'putglyph 44 1 0,74\nputglyph 44 1 0,75\nputglyph 44 1 0,76\nputglyph 44 1 0,77\nputglyph 44 1 0,78\nputglyph 44 1 0,79' + ) + cursor(0, 79, state) + push('D', vt) + expect('putglyph 44 1 0,79') + cursor(0, 79, state) + push('\x1b[?7h', vt) + + -- 80th column causes linefeed on wraparound + push('\x1b[25;78HABC', vt) + expect('putglyph 41 1 24,77\nputglyph 42 1 24,78\nputglyph 43 1 24,79') + cursor(24, 79, state) + push('D', vt) + expect('moverect 1..25,0..80 -> 0..24,0..80\nputglyph 44 1 24,0') + + -- 80th column phantom linefeed phantom cancelled by explicit cursor move + push('\x1b[25;78HABC', vt) + expect('putglyph 41 1 24,77\nputglyph 42 1 24,78\nputglyph 43 1 24,79') + cursor(24, 79, state) + push('\x1b[25;1HD', vt) + expect('putglyph 44 1 24,0') + end) + + itp('21state_tabstops', function() + local vt = init() + local state = wantstate(vt, { g = true }) + + -- Initial + reset(state, nil) + push('\tX', vt) + expect('putglyph 58 1 0,8') + push('\tX', vt) + expect('putglyph 58 1 0,16') + cursor(0, 17, state) + + -- HTS + push('\x1b[5G\x1bH', vt) + push('\x1b[G\tX', vt) + expect('putglyph 58 1 0,4') + cursor(0, 5, state) + + -- TBC 0 + push('\x1b[9G\x1b[g', vt) + push('\x1b[G\tX\tX', vt) + expect('putglyph 58 1 0,4\nputglyph 58 1 0,16') + cursor(0, 17, state) + + -- TBC 3 + push('\x1b[3g\x1b[50G\x1bH\x1b[G', vt) + cursor(0, 0, state) + push('\tX', vt) + expect('putglyph 58 1 0,49') + cursor(0, 50, state) + + -- Tabstops after resize + reset(state, nil) + resize(30, 100, vt) + -- Should be 100/8 = 12 tabstops + push('\tX', vt) + expect('putglyph 58 1 0,8') + push('\tX', vt) + expect('putglyph 58 1 0,16') + push('\tX', vt) + expect('putglyph 58 1 0,24') + push('\tX', vt) + expect('putglyph 58 1 0,32') + push('\tX', vt) + expect('putglyph 58 1 0,40') + push('\tX', vt) + expect('putglyph 58 1 0,48') + push('\tX', vt) + expect('putglyph 58 1 0,56') + push('\tX', vt) + expect('putglyph 58 1 0,64') + push('\tX', vt) + expect('putglyph 58 1 0,72') + push('\tX', vt) + expect('putglyph 58 1 0,80') + push('\tX', vt) + expect('putglyph 58 1 0,88') + push('\tX', vt) + expect('putglyph 58 1 0,96') + cursor(0, 97, state) + end) + + itp('22state_save', function() + local vt = init() + local state = wantstate(vt, { p = true }) + + reset(state, nil) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + + -- Set up state + push('\x1b[2;2H', vt) + cursor(1, 1, state) + push('\x1b[1m', vt) + pen('bold', true, state) + + -- Save + push('\x1b[?1048h', vt) + + -- Change state + push('\x1b[5;5H', vt) + cursor(4, 4, state) + push('\x1b[4 q', vt) + expect('settermprop 2 false\nsettermprop 7 2') + push('\x1b[22;4m', vt) + pen('bold', false, state) + pen('underline', 1, state) + + -- Restore + push('\x1b[?1048l', vt) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + cursor(1, 1, state) + pen('bold', true, state) + pen('underline', 0, state) + + -- Save/restore using DECSC/DECRC + push('\x1b[2;2H\x1b7', vt) + cursor(1, 1, state) + + push('\x1b[5;5H', vt) + cursor(4, 4, state) + push('\x1b8', vt) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + cursor(1, 1, state) + + -- Save twice, restore twice happens on both edge transitions + push('\x1b[2;10H\x1b[?1048h\x1b[6;10H\x1b[?1048h', vt) + push('\x1b[H', vt) + cursor(0, 0, state) + push('\x1b[?1048l', vt) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + cursor(5, 9, state) + push('\x1b[H', vt) + cursor(0, 0, state) + push('\x1b[?1048l', vt) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + cursor(5, 9, state) + end) + + itp('25state_input', function() + local vt = init() + local state = wantstate(vt) + + -- Unmodified ASCII + inchar(41, vt) + expect('output 29') + inchar(61, vt) + expect('output 3d') + + -- Ctrl modifier on ASCII letters + inchar(41, vt, { C = true }) + expect('output 1b,5b,34,31,3b,35,75') + inchar(61, vt, { C = true }) + expect('output 1b,5b,36,31,3b,35,75') + + -- Alt modifier on ASCII letters + inchar(41, vt, { A = true }) + expect('output 1b,29') + inchar(61, vt, { A = true }) + expect('output 1b,3d') + + -- Ctrl-Alt modifier on ASCII letters + inchar(41, vt, { C = true, A = true }) + expect('output 1b,5b,34,31,3b,37,75') + inchar(61, vt, { C = true, A = true }) + expect('output 1b,5b,36,31,3b,37,75') + + -- Special handling of Ctrl-I + inchar(49, vt) + expect('output 31') + inchar(69, vt) + expect('output 45') + inchar(49, vt, { C = true }) + expect('output 1b,5b,34,39,3b,35,75') + inchar(69, vt, { C = true }) + expect('output 1b,5b,36,39,3b,35,75') + inchar(49, vt, { A = true }) + expect('output 1b,31') + inchar(69, vt, { A = true }) + expect('output 1b,45') + inchar(49, vt, { A = true, C = true }) + expect('output 1b,5b,34,39,3b,37,75') + inchar(69, vt, { A = true, C = true }) + expect('output 1b,5b,36,39,3b,37,75') + + -- Special handling of Space + inchar(20, vt) + expect('output 14') + inchar(20, vt, { S = true }) + expect('output 14') + inchar(20, vt, { C = true }) + expect('output 1b,5b,32,30,3b,35,75') + inchar(20, vt, { C = true, S = true }) + expect('output 1b,5b,32,30,3b,35,75') + inchar(20, vt, { A = true }) + expect('output 1b,14') + inchar(20, vt, { S = true, A = true }) + expect('output 1b,14') + inchar(20, vt, { C = true, A = true }) + expect('output 1b,5b,32,30,3b,37,75') + inchar(20, vt, { S = true, C = true, A = true }) + expect('output 1b,5b,32,30,3b,37,75') + + -- Cursor keys in reset (cursor) mode + inkey('up', vt) + expect_output('\x1b[A') + inkey('up', vt, { S = true }) + expect_output('\x1b[1;2A') + inkey('up', vt, { C = true }) + expect_output('\x1b[1;5A') + inkey('up', vt, { S = true, C = true }) + expect_output('\x1b[1;6A') + inkey('up', vt, { A = true }) + expect_output('\x1b[1;3A') + inkey('up', vt, { S = true, A = true }) + expect_output('\x1b[1;4A') + inkey('up', vt, { C = true, A = true }) + expect_output('\x1b[1;7A') + inkey('up', vt, { S = true, C = true, A = true }) + expect_output('\x1b[1;8A') + + -- Cursor keys in application mode + push('\x1b[?1h', vt) + -- Plain "Up" should be SS3 A now + inkey('up', vt) + expect_output('\x1bOA') + -- Modified keys should still use CSI + inkey('up', vt, { S = true }) + expect_output('\x1b[1;2A') + inkey('up', vt, { C = true }) + expect_output('\x1b[1;5A') + + -- Shift-Tab should be different + inkey('tab', vt) + expect_output('\x09') + inkey('tab', vt, { S = true }) + expect_output('\x1b[Z') + inkey('tab', vt, { C = true }) + expect_output('\x1b[9;5u') + inkey('tab', vt, { A = true }) + expect_output('\x1b\x09') + inkey('tab', vt, { C = true, A = true }) + expect_output('\x1b[9;7u') + + -- Enter in linefeed mode + inkey('enter', vt) + expect_output('\x0d') + + -- Enter in newline mode + push('\x1b[20h', vt) + inkey('enter', vt) + expect_output('\x0d\x0a') + + -- Unmodified F1 is SS3 P + inkey('f1', vt) + expect_output('\x1bOP') + + -- Modified F1 is CSI P + inkey('f1', vt, { S = true }) + expect_output('\x1b[1;2P') + inkey('f1', vt, { A = true }) + expect_output('\x1b[1;3P') + inkey('f1', vt, { C = true }) + expect_output('\x1b[1;5P') + + -- Keypad in DECKPNM + inkey('kp0', vt) + expect_output('0') + + -- Keypad in DECKPAM + push('\x1b=', vt) + inkey('kp0', vt) + expect_output('\x1bOp') + + -- Bracketed paste mode off + vterm.vterm_keyboard_start_paste(vt) + vterm.vterm_keyboard_end_paste(vt) + + -- Bracketed paste mode on + push('\x1b[?2004h', vt) + vterm.vterm_keyboard_start_paste(vt) + expect_output('\x1b[200~') + vterm.vterm_keyboard_end_paste(vt) + expect_output('\x1b[201~') + + -- Focus reporting disabled + vterm.vterm_state_focus_in(state) + vterm.vterm_state_focus_out(state) + + -- Focus reporting enabled + state = wantstate(vt, { p = true }) + push('\x1b[?1004h', vt) + expect('settermprop 9 true') + vterm.vterm_state_focus_in(state) + expect_output('\x1b[I') + vterm.vterm_state_focus_out(state) + expect_output('\x1b[O') + end) + + itp('26state_query', function() + local vt = init() + local state = wantstate(vt) + + -- DA + reset(state, nil) + push('\x1b[c', vt) + expect_output('\x1b[?1;2c') + + -- XTVERSION + reset(state, nil) + push('\x1b[>q', vt) + expect_output('\x1bP>|libvterm(0.3)\x1b\\') + + -- DSR + reset(state, nil) + push('\x1b[5n', vt) + expect_output('\x1b[0n') + + -- CPR + push('\x1b[6n', vt) + expect_output('\x1b[1;1R') + push('\x1b[10;10H\x1b[6n', vt) + expect_output('\x1b[10;10R') + + -- DECCPR + push('\x1b[?6n', vt) + expect_output('\x1b[?10;10R') + + -- DECRQSS on DECSCUSR + push('\x1b[3 q', vt) + push('\x1bP$q q\x1b\\', vt) + expect_output('\x1bP1$r3 q\x1b\\') + + -- DECRQSS on SGR + push('\x1b[1;5;7m', vt) + push('\x1bP$qm\x1b\\', vt) + expect_output('\x1bP1$r1;5;7m\x1b\\') + + -- DECRQSS on SGR ANSI colours + push('\x1b[0;31;42m', vt) + push('\x1bP$qm\x1b\\', vt) + expect_output('\x1bP1$r31;42m\x1b\\') + + -- DECRQSS on SGR ANSI hi-bright colours + push('\x1b[0;93;104m', vt) + push('\x1bP$qm\x1b\\', vt) + expect_output('\x1bP1$r93;104m\x1b\\') + + -- DECRQSS on SGR 256-palette colours + push('\x1b[0;38:5:56;48:5:78m', vt) + push('\x1bP$qm\x1b\\', vt) + expect_output('\x1bP1$r38:5:56;48:5:78m\x1b\\') + + -- DECRQSS on SGR RGB8 colours + push('\x1b[0;38:2:24:68:112;48:2:13:57:101m', vt) + push('\x1bP$qm\x1b\\', vt) + expect_output('\x1bP1$r38:2:24:68:112;48:2:13:57:101m\x1b\\') + + -- S8C1T on DSR + push('\x1b G', vt) + push('\x1b[5n', vt) + expect_output('\x9b0n') + push('\x1b F', vt) + end) + + itp('27state_reset', function() + local vt = init() + local state = wantstate(vt) + + reset(state, nil) + + -- RIS homes cursor + push('\x1b[5;5H', vt) + cursor(4, 4, state) + state = wantstate(vt, { m = true }) + push('\x1bc', vt) + cursor(0, 0, state) + wantstate(vt) + + -- RIS cancels scrolling region + push('\x1b[5;10r', vt) + wantstate(vt, { s = true }) + push('\x1bc\x1b[25H\n', vt) + expect('scrollrect 0..25,0..80 => +1,+0') + wantstate(vt) + + -- RIS erases screen + push('ABCDE', vt) + state = wantstate(vt, { e = true }) + push('\x1bc', vt) + expect('erase 0..25,0..80') + wantstate(vt) + + -- RIS clears tabstops + push('\x1b[5G\x1bH\x1b[G\t', vt) + cursor(0, 4, state) + push('\x1bc\t', vt) + cursor(0, 8, state) + end) + + itp('28state_dbl_wh', function() + local vt = init() + local state = wantstate(vt, { g = true }) + + -- Single Width, Single Height + reset(state, nil) + push('\x1b#5', vt) + push('Hello', vt) + expect( + 'putglyph 48 1 0,0\nputglyph 65 1 0,1\nputglyph 6c 1 0,2\nputglyph 6c 1 0,3\nputglyph 6f 1 0,4' + ) + + -- Double Width, Single Height + reset(state, nil) + push('\x1b#6', vt) + push('Hello', vt) + expect( + 'putglyph 48 1 0,0 dwl\nputglyph 65 1 0,1 dwl\nputglyph 6c 1 0,2 dwl\nputglyph 6c 1 0,3 dwl\nputglyph 6f 1 0,4 dwl' + ) + cursor(0, 5, state) + push('\x1b[40GAB', vt) + expect('putglyph 41 1 0,39 dwl\nputglyph 42 1 1,0') + cursor(1, 1, state) + + -- Double Height + reset(state, nil) + push('\x1b#3', vt) + push('Hello', vt) + expect( + 'putglyph 48 1 0,0 dwl dhl-top\nputglyph 65 1 0,1 dwl dhl-top\nputglyph 6c 1 0,2 dwl dhl-top\nputglyph 6c 1 0,3 dwl dhl-top\nputglyph 6f 1 0,4 dwl dhl-top' + ) + cursor(0, 5, state) + push('\r\n\x1b#4', vt) + push('Hello', vt) + expect( + 'putglyph 48 1 1,0 dwl dhl-bottom\nputglyph 65 1 1,1 dwl dhl-bottom\nputglyph 6c 1 1,2 dwl dhl-bottom\nputglyph 6c 1 1,3 dwl dhl-bottom\nputglyph 6f 1 1,4 dwl dhl-bottom' + ) + cursor(1, 5, state) + + -- Double Width scrolling + reset(state, nil) + push('\x1b[20H\x1b#6ABC', vt) + expect('putglyph 41 1 19,0 dwl\nputglyph 42 1 19,1 dwl\nputglyph 43 1 19,2 dwl') + push('\x1b[25H\n', vt) + push('\x1b[19;4HDE', vt) + expect('putglyph 44 1 18,3 dwl\nputglyph 45 1 18,4 dwl') + push('\x1b[H\x1bM', vt) + push('\x1b[20;6HFG', vt) + expect('putglyph 46 1 19,5 dwl\nputglyph 47 1 19,6 dwl') + end) + + itp('29state_fallback', function() + local vt = init() + local state = wantstate(vt, { f = true }) + reset(state, nil) + + -- Unrecognised control + push('\x03', vt) + expect('control 03') + + -- Unrecognised CSI + push('\x1b[?15;2z', vt) + expect('csi 7a L=3f 15,2') + + -- Unrecognised OSC + push('\x1b]27;Something\x1b\\', vt) + expect('osc [27;Something]') + + -- Unrecognised DCS + push('\x1bPz123\x1b\\', vt) + expect('dcs [z123]') + + -- Unrecognised APC + push('\x1b_z123\x1b\\', vt) + expect('apc [z123]') + + -- Unrecognised PM + push('\x1b^z123\x1b\\', vt) + expect('pm [z123]') + + -- Unrecognised SOS + push('\x1bXz123\x1b\\', vt) + expect('sos [z123]') + end) + + itp('30state_pen', function() + local vt = init() + local state = wantstate(vt) + + -- Reset + push('\x1b[m', vt) + pen('bold', false, state) + pen('underline', 0, state) + pen('italic', false, state) + pen('blink', false, state) + pen('reverse', false, state) + pen('font', 0, state) + -- TODO(dundargoc): fix + -- ?pen foreground = rgb(240,240,240,is_default_fg) + -- ?pen background = rgb(0,0,0,is_default_bg) + + -- Bold + push('\x1b[1m', vt) + pen('bold', true, state) + push('\x1b[22m', vt) + pen('bold', false, state) + push('\x1b[1m\x1b[m', vt) + pen('bold', false, state) + + -- Underline + push('\x1b[4m', vt) + pen('underline', 1, state) + push('\x1b[21m', vt) + pen('underline', 2, state) + push('\x1b[24m', vt) + pen('underline', 0, state) + push('\x1b[4m\x1b[4:0m', vt) + pen('underline', 0, state) + push('\x1b[4:1m', vt) + pen('underline', 1, state) + push('\x1b[4:2m', vt) + pen('underline', 2, state) + push('\x1b[4:3m', vt) + pen('underline', 3, state) + push('\x1b[4m\x1b[m', vt) + pen('underline', 0, state) + + -- Italic + push('\x1b[3m', vt) + pen('italic', true, state) + push('\x1b[23m', vt) + pen('italic', false, state) + push('\x1b[3m\x1b[m', vt) + pen('italic', false, state) + + -- Blink + push('\x1b[5m', vt) + pen('blink', true, state) + push('\x1b[25m', vt) + pen('blink', false, state) + push('\x1b[5m\x1b[m', vt) + pen('blink', false, state) + + -- Reverse + push('\x1b[7m', vt) + pen('reverse', true, state) + push('\x1b[27m', vt) + pen('reverse', false, state) + push('\x1b[7m\x1b[m', vt) + pen('reverse', false, state) + + -- Font Selection + push('\x1b[11m', vt) + pen('font', 1, state) + push('\x1b[19m', vt) + pen('font', 9, state) + push('\x1b[10m', vt) + pen('font', 0, state) + push('\x1b[11m\x1b[m', vt) + pen('font', 0, state) + + -- TODO(dundargoc): fix + -- -- Foreground + -- push "\x1b[31m" + -- ?pen foreground = idx(1) + -- push "\x1b[32m" + -- ?pen foreground = idx(2) + -- push "\x1b[34m" + -- ?pen foreground = idx(4) + -- push "\x1b[91m" + -- ?pen foreground = idx(9) + -- push "\x1b[38:2:10:20:30m" + -- ?pen foreground = rgb(10,20,30) + -- push "\x1b[38:5:1m" + -- ?pen foreground = idx(1) + -- push "\x1b[39m" + -- ?pen foreground = rgb(240,240,240,is_default_fg) + -- + -- -- Background + -- push "\x1b[41m" + -- ?pen background = idx(1) + -- push "\x1b[42m" + -- ?pen background = idx(2) + -- push "\x1b[44m" + -- ?pen background = idx(4) + -- push "\x1b[101m" + -- ?pen background = idx(9) + -- push "\x1b[48:2:10:20:30m" + -- ?pen background = rgb(10,20,30) + -- push "\x1b[48:5:1m" + -- ?pen background = idx(1) + -- push "\x1b[49m" + -- ?pen background = rgb(0,0,0,is_default_bg) + -- + -- -- Bold+ANSI colour == highbright + -- push "\x1b[m\x1b[1;37m" + -- ?pen bold = on + -- ?pen foreground = idx(15) + -- push "\x1b[m\x1b[37;1m" + -- ?pen bold = on + -- ?pen foreground = idx(15) + -- + -- -- Super/Subscript + -- push "\x1b[73m" + -- ?pen small = on + -- ?pen baseline = raise + -- push "\x1b[74m" + -- ?pen small = on + -- ?pen baseline = lower + -- push "\x1b[75m" + -- ?pen small = off + -- ?pen baseline = normal + -- + -- -- DECSTR resets pen attributes + -- push "\x1b[1;4m" + -- ?pen bold = on + -- ?pen underline = 1 + -- push "\x1b[!p" + -- ?pen bold = off + -- ?pen underline = 0 + end) + + itp('31state_rep', function() + local vt = init() + local state = wantstate(vt, { g = true }) + + -- REP no argument + reset(state, nil) + push('a\x1b[b', vt) + expect('putglyph 61 1 0,0\nputglyph 61 1 0,1') + + -- REP zero (zero should be interpreted as one) + reset(state, nil) + push('a\x1b[0b', vt) + expect('putglyph 61 1 0,0\nputglyph 61 1 0,1') + + -- REP lowercase a times two + reset(state, nil) + push('a\x1b[2b', vt) + expect('putglyph 61 1 0,0\nputglyph 61 1 0,1\nputglyph 61 1 0,2') + + -- REP with UTF-8 1 char + -- U+00E9 = C3 A9 name: LATIN SMALL LETTER E WITH ACUTE + reset(state, nil) + push('\xC3\xA9\x1b[b', vt) + expect('putglyph e9 1 0,0\nputglyph e9 1 0,1') + + -- REP with UTF-8 wide char + -- U+00E9 = C3 A9 name: LATIN SMALL LETTER E WITH ACUTE + reset(state, nil) + push('\xEF\xBC\x90\x1b[b', vt) + expect('putglyph ff10 2 0,0\nputglyph ff10 2 0,2') + + -- REP with UTF-8 combining character + reset(state, nil) + push('e\xCC\x81\x1b[b', vt) + expect('putglyph 65,301 1 0,0\nputglyph 65,301 1 0,1') + + -- REP till end of line + reset(state, nil) + push('a\x1b[1000bb', vt) + expect( + 'putglyph 61 1 0,0\nputglyph 61 1 0,1\nputglyph 61 1 0,2\nputglyph 61 1 0,3\nputglyph 61 1 0,4\nputglyph 61 1 0,5\nputglyph 61 1 0,6\nputglyph 61 1 0,7\nputglyph 61 1 0,8\nputglyph 61 1 0,9\nputglyph 61 1 0,10\nputglyph 61 1 0,11\nputglyph 61 1 0,12\nputglyph 61 1 0,13\nputglyph 61 1 0,14\nputglyph 61 1 0,15\nputglyph 61 1 0,16\nputglyph 61 1 0,17\nputglyph 61 1 0,18\nputglyph 61 1 0,19\nputglyph 61 1 0,20\nputglyph 61 1 0,21\nputglyph 61 1 0,22\nputglyph 61 1 0,23\nputglyph 61 1 0,24\nputglyph 61 1 0,25\nputglyph 61 1 0,26\nputglyph 61 1 0,27\nputglyph 61 1 0,28\nputglyph 61 1 0,29\nputglyph 61 1 0,30\nputglyph 61 1 0,31\nputglyph 61 1 0,32\nputglyph 61 1 0,33\nputglyph 61 1 0,34\nputglyph 61 1 0,35\nputglyph 61 1 0,36\nputglyph 61 1 0,37\nputglyph 61 1 0,38\nputglyph 61 1 0,39\nputglyph 61 1 0,40\nputglyph 61 1 0,41\nputglyph 61 1 0,42\nputglyph 61 1 0,43\nputglyph 61 1 0,44\nputglyph 61 1 0,45\nputglyph 61 1 0,46\nputglyph 61 1 0,47\nputglyph 61 1 0,48\nputglyph 61 1 0,49\nputglyph 61 1 0,50\nputglyph 61 1 0,51\nputglyph 61 1 0,52\nputglyph 61 1 0,53\nputglyph 61 1 0,54\nputglyph 61 1 0,55\nputglyph 61 1 0,56\nputglyph 61 1 0,57\nputglyph 61 1 0,58\nputglyph 61 1 0,59\nputglyph 61 1 0,60\nputglyph 61 1 0,61\nputglyph 61 1 0,62\nputglyph 61 1 0,63\nputglyph 61 1 0,64\nputglyph 61 1 0,65\nputglyph 61 1 0,66\nputglyph 61 1 0,67\nputglyph 61 1 0,68\nputglyph 61 1 0,69\nputglyph 61 1 0,70\nputglyph 61 1 0,71\nputglyph 61 1 0,72\nputglyph 61 1 0,73\nputglyph 61 1 0,74\nputglyph 61 1 0,75\nputglyph 61 1 0,76\nputglyph 61 1 0,77\nputglyph 61 1 0,78\nputglyph 61 1 0,79\nputglyph 62 1 1,0' + ) + end) + + itp('32state_flow', function() + local vt = init() + local state = wantstate(vt) + + -- Many of these test cases inspired by + -- https://blueprints.launchpad.net/libvterm/+spec/reflow-cases + + -- Spillover text marks continuation on second line + reset(state, nil) + push(string.rep('A', 100), vt) + push('\r\n', vt) + lineinfo(0, {}, state) + lineinfo(1, { cont = true }, state) + + -- CRLF in column 80 does not mark continuation + reset(state, nil) + push(string.rep('B', 80), vt) + push('\r\n', vt) + push(string.rep('B', 20), vt) + push('\r\n', vt) + lineinfo(0, {}, state) + lineinfo(1, {}, state) + + -- EL cancels continuation of following line + reset(state, nil) + push(string.rep('D', 100), vt) + lineinfo(1, { cont = true }, state) + push('\x1bM\x1b[79G\x1b[K', vt) + lineinfo(1, {}, state) + end) + + itp('40state_selection', function() + local vt = init() + wantstate(vt) + + -- Set clipboard; final chunk len 4 + push('\x1b]52;c;SGVsbG8s\x1b\\', vt) + expect('selection-set mask=0001 [Hello,]') + + -- Set clipboard; final chunk len 3 + push('\x1b]52;c;SGVsbG8sIHc=\x1b\\', vt) + expect('selection-set mask=0001 [Hello, w]') + + -- Set clipboard; final chunk len 2 + push('\x1b]52;c;SGVsbG8sIHdvcmxkCg==\x1b\\', vt) + expect('selection-set mask=0001 [Hello, world\n]') + + -- Set clipboard; split between chunks + push('\x1b]52;c;SGVs', vt) + expect('selection-set mask=0001 [Hel') + push('bG8s\x1b\\', vt) + expect('selection-set mask=0001 lo,]') + + -- Set clipboard; split within chunk + push('\x1b]52;c;SGVsbG', vt) + expect('selection-set mask=0001 [Hel') + push('8s\x1b\\', vt) + expect('selection-set mask=0001 lo,]') + + -- Set clipboard; empty first chunk + push('\x1b]52;c;', vt) + push('SGVsbG8s\x1b\\', vt) + expect('selection-set mask=0001 [Hello,]') + + -- Set clipboard; empty final chunk + push('\x1b]52;c;SGVsbG8s', vt) + expect('selection-set mask=0001 [Hello,') + push('\x1b\\', vt) + expect('selection-set mask=0001 ]') + + -- Set clipboard; longer than buffer + push('\x1b]52;c;' .. string.rep('LS0t', 10) .. '\x1b\\', vt) + expect('selection-set mask=0001 [---------------\nselection-set mask=0001 ---------------]') + + -- Clear clipboard + push('\x1b]52;c;\x1b\\', vt) + expect('selection-set mask=0001 []') + + -- Set invalid data clears and ignores + push('\x1b]52;c;SGVs*SGVsbG8s\x1b\\', vt) + expect('selection-set mask=0001 []') + + -- Query clipboard + push('\x1b]52;c;?\x1b\\', vt) + expect('selection-query mask=0001') + + -- TODO(dundargoc): fix + -- -- Send clipboard; final chunk len 4 + -- SELECTION 1 ["Hello,"] + -- output "\x1b]52;c;" + -- output "SGVsbG8s" + -- output "\x1b\\" + -- + -- -- Send clipboard; final chunk len 3 + -- SELECTION 1 ["Hello, w"] + -- output "\x1b]52;c;" + -- output "SGVsbG8s" + -- output "IHc=\x1b\\" + -- + -- -- Send clipboard; final chunk len 2 + -- SELECTION 1 ["Hello, world\n"] + -- output "\x1b]52;c;" + -- output "SGVsbG8sIHdvcmxk" + -- output "Cg==\x1b\\" + -- + -- -- Send clipboard; split between chunks + -- SELECTION 1 ["Hel" + -- output "\x1b]52;c;" + -- output "SGVs" + -- SELECTION 1 "lo,"] + -- output "bG8s" + -- output "\x1b\\" + -- + -- -- Send clipboard; split within chunk + -- SELECTION 1 ["Hello" + -- output "\x1b]52;c;" + -- output "SGVs" + -- SELECTION 1 ","] + -- output "bG8s" + -- output "\x1b\\" + end) + + itp('60screen_ascii', function() + local vt = init() + local screen = wantscreen(vt, { a = true, c = true }) + + -- Get + reset(nil, screen) + push('ABC', vt) + expect('movecursor 0,3') + screen_chars(0, 0, 1, 3, 'ABC', screen) + screen_chars(0, 0, 1, 80, 'ABC', screen) + screen_text(0, 0, 1, 3, '41,42,43', screen) + screen_text(0, 0, 1, 80, '41,42,43', screen) + screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(0, 1, '{42} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(0, 2, '{43} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_row(0, 'ABC', screen) + screen_eol(0, 0, 0, screen) + screen_eol(0, 2, 0, screen) + screen_eol(0, 3, 1, screen) + push('\x1b[H', vt) + expect('movecursor 0,0') + screen_row(0, 'ABC', screen) + screen_text(0, 0, 1, 80, '41,42,43', screen) + push('E', vt) + expect('movecursor 0,1') + screen_row(0, 'EBC', screen) + screen_text(0, 0, 1, 80, '45,42,43', screen) + + screen = wantscreen(vt, { a = true }) + + -- Erase + reset(nil, screen) + push('ABCDE\x1b[H\x1b[K', vt) + -- TODO(dundargoc): fix + -- screen_row(0, '', screen) + screen_text(0, 0, 1, 80, '', screen) + + -- Copycell + reset(nil, screen) + push('ABC\x1b[H\x1b[@', vt) + push('1', vt) + screen_row(0, '1ABC', screen) + + reset(nil, screen) + push('ABC\x1b[H\x1b[P', vt) + screen_chars(0, 0, 1, 1, 'B', screen) + screen_chars(0, 1, 1, 2, 'C', screen) + screen_chars(0, 0, 1, 80, 'BC', screen) + + -- Space padding + reset(nil, screen) + push('Hello\x1b[CWorld', vt) + screen_row(0, 'Hello World', screen) + screen_text(0, 0, 1, 80, '48,65,6c,6c,6f,20,57,6f,72,6c,64', screen) + + -- Linefeed padding + reset(nil, screen) + push('Hello\r\nWorld', vt) + screen_chars(0, 0, 2, 80, 'Hello\nWorld', screen) + screen_text(0, 0, 2, 80, '48,65,6c,6c,6f,0a,57,6f,72,6c,64', screen) + + -- Altscreen + reset(nil, screen) + push('P', vt) + screen_row(0, 'P', screen) + -- TODO(dundargoc): fix + -- push('\x1b[?1049h', vt) + -- screen_row(0, '', screen) + -- push('\x1b[2K\x1b[HA', vt) + -- screen_row(0, 'A', screen) + -- push('\x1b[?1049l', vt) + -- screen_row(0, 'P', screen) + end) + + itp('61screen_unicode', function() + local vt = init() + local screen = wantscreen(vt) + + -- Single width UTF-8 + -- U+00C1 = C3 81 name: LATIN CAPITAL LETTER A WITH ACUTE + -- U+00E9 = C3 A9 name: LATIN SMALL LETTER E WITH ACUTE + reset(nil, screen) + push('\xC3\x81\xC3\xA9', vt) + screen_row(0, 'Áé', screen) + screen_text(0, 0, 1, 80, 'c3,81,c3,a9', screen) + screen_cell(0, 0, '{c1} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Wide char + -- U+FF10 = EF BC 90 name: FULLWIDTH DIGIT ZERO + reset(nil, screen) + push('0123\x1b[H', vt) + push('\xEF\xBC\x90', vt) + screen_row(0, '023', screen) + screen_text(0, 0, 1, 80, 'ef,bc,90,32,33', screen) + screen_cell(0, 0, '{ff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Combining char + -- U+0301 = CC 81 name: COMBINING ACUTE + reset(nil, screen) + push('0123\x1b[H', vt) + push('e\xCC\x81', vt) + screen_row(0, 'é123', screen) + screen_text(0, 0, 1, 80, '65,cc,81,31,32,33', screen) + screen_cell(0, 0, '{65,301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- 10 combining accents should not crash + reset(nil, screen) + push('e\xCC\x81\xCC\x82\xCC\x83\xCC\x84\xCC\x85\xCC\x86\xCC\x87\xCC\x88\xCC\x89\xCC\x8A', vt) + screen_cell( + 0, + 0, + '{65,301,302,303,304,305} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', + screen + ) + + -- 40 combining accents in two split writes of 20 should not crash + reset(nil, screen) + push( + 'e\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81', + vt + ) + push( + '\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81', + vt + ) + screen_cell( + 0, + 0, + '{65,301,301,301,301,301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', + screen + ) + + -- Outputing CJK doublewidth in 80th column should wraparound to next line and not crash" + reset(nil, screen) + push('\x1b[80G\xEF\xBC\x90', vt) + screen_cell(0, 79, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(1, 0, '{ff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + end) + + pending('62screen_damage', function() end) + + itp('63screen_resize', function() + local vt = init() + local state = wantstate(vt) + local screen = wantscreen(vt) + + -- Resize wider preserves cells + reset(state, screen) + resize(25, 80, vt) + push('AB\r\nCD', vt) + screen_chars(0, 0, 1, 80, 'AB', screen) + screen_chars(1, 0, 2, 80, 'CD', screen) + resize(25, 100, vt) + screen_chars(0, 0, 1, 100, 'AB', screen) + screen_chars(1, 0, 2, 100, 'CD', screen) + + -- Resize wider allows print in new area + reset(state, screen) + resize(25, 80, vt) + push('AB\x1b[79GCD', vt) + screen_chars(0, 0, 1, 2, 'AB', screen) + screen_chars(0, 78, 1, 80, 'CD', screen) + resize(25, 100, vt) + screen_chars(0, 0, 1, 2, 'AB', screen) + screen_chars(0, 78, 1, 80, 'CD', screen) + push('E', vt) + screen_chars(0, 78, 1, 81, 'CDE', screen) + + -- Resize shorter with blanks just truncates + reset(state, screen) + resize(25, 80, vt) + push('Top\x1b[10HLine 10', vt) + screen_row(0, 'Top', screen) + screen_row(9, 'Line 10', screen) + cursor(9, 7, state) + resize(20, 80, vt) + screen_row(0, 'Top', screen) + screen_row(9, 'Line 10', screen) + cursor(9, 7, state) + + -- Resize shorter with content must scroll + reset(state, screen) + resize(25, 80, vt) + push('Top\x1b[25HLine 25\x1b[15H', vt) + screen_row(0, 'Top', screen) + screen_row(24, 'Line 25', screen) + cursor(14, 0, state) + screen = wantscreen(vt, { b = true }) + resize(20, 80, vt) + expect( + 'sb_pushline 80 = 54 6F 70\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =' + ) + -- TODO(dundargoc): fix or remove + -- screen_row( 0 , "",screen) + screen_row(19, 'Line 25', screen) + cursor(9, 0, state) + + -- Resize shorter does not lose line with cursor + -- See also https://github.com/neovim/libvterm/commit/1b745d29d45623aa8d22a7b9288c7b0e331c7088 + reset(state, screen) + wantscreen(vt) + resize(25, 80, vt) + screen = wantscreen(vt, { b = true }) + push('\x1b[24HLine 24\r\nLine 25\r\n', vt) + expect('sb_pushline 80 =') + screen_row(23, 'Line 25', screen) + cursor(24, 0, state) + resize(24, 80, vt) + expect('sb_pushline 80 =') + screen_row(22, 'Line 25', screen) + cursor(23, 0, state) + + -- Resize shorter does not send the cursor to a negative row + -- See also https://github.com/vim/vim/pull/6141 + reset(state, screen) + wantscreen(vt) + resize(25, 80, vt) + screen = wantscreen(vt, { b = true }) + push('\x1b[24HLine 24\r\nLine 25\x1b[H', vt) + cursor(0, 0, state) + resize(20, 80, vt) + expect( + 'sb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =' + ) + cursor(0, 0, state) + + -- Resize taller attempts to pop scrollback + reset(state, screen) + screen = wantscreen(vt) + resize(25, 80, vt) + push('Line 1\x1b[25HBottom\x1b[15H', vt) + screen_row(0, 'Line 1', screen) + screen_row(24, 'Bottom', screen) + cursor(14, 0, state) + screen = wantscreen(vt, { b = true }) + resize(30, 80, vt) + expect('sb_popline 80\nsb_popline 80\nsb_popline 80\nsb_popline 80\nsb_popline 80') + screen_row(0, 'ABCDE', screen) + screen_row(5, 'Line 1', screen) + screen_row(29, 'Bottom', screen) + cursor(19, 0, state) + screen = wantscreen(vt) + + -- Resize can operate on altscreen + reset(state, screen) + screen = wantscreen(vt, { a = true }) + resize(25, 80, vt) + push('Main screen\x1b[?1049h\x1b[HAlt screen', vt) + resize(30, 80, vt) + screen_row(0, 'Alt screen', screen) + push('\x1b[?1049l', vt) + screen_row(0, 'Main screen', screen) + end) + + itp('64screen_pen', function() + local vt = init() + local screen = wantscreen(vt) + + reset(nil, screen) + + -- Plain + push('A', vt) + screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Bold + push('\x1b[1mB', vt) + screen_cell(0, 1, '{42} width=1 attrs={B} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Italic + push('\x1b[3mC', vt) + screen_cell(0, 2, '{43} width=1 attrs={BI} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Underline + push('\x1b[4mD', vt) + screen_cell(0, 3, '{44} width=1 attrs={BU1I} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Reset + push('\x1b[mE', vt) + screen_cell(0, 4, '{45} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Font + push('\x1b[11mF\x1b[m', vt) + screen_cell(0, 5, '{46} width=1 attrs={F1} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Foreground + push('\x1b[31mG\x1b[m', vt) + screen_cell(0, 6, '{47} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)', screen) + + -- Background + push('\x1b[42mH\x1b[m', vt) + screen_cell(0, 7, '{48} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,224,0)', screen) + + -- Super/subscript + push('x\x1b[74m0\x1b[73m2\x1b[m', vt) + screen_cell(0, 8, '{78} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(0, 9, '{30} width=1 attrs={S_} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(0, 10, '{32} width=1 attrs={S^} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- EL sets only colours to end of line, not other attrs + push('\x1b[H\x1b[7;33;44m\x1b[K', vt) + screen_cell(0, 0, '{} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) + screen_cell(0, 79, '{} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) + + -- DECSCNM xors reverse for entire screen + push('R\x1b[?5h', vt) + screen_cell(0, 0, '{52} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) + screen_cell(1, 0, '{} width=1 attrs={R} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + push('\x1b[?5$p', vt) + expect_output('\x1b[?5;1$y') + push('\x1b[?5l', vt) + screen_cell(0, 0, '{52} width=1 attrs={R} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) + screen_cell(1, 0, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + -- TODO(dundargoc): fix + -- push('\x1b[?5$p') + -- expect_output('\x1b[?5;2$y') + + -- Set default colours + reset(nil, screen) + push('ABC\x1b[31mDEF\x1b[m', vt) + screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(0, 3, '{44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)', screen) + -- TODO(dundargoc): fix + -- SETDEFAULTCOL rgb(252,253,254) + -- ?screen_cell 0,0 = {41} width=1 attrs={} fg=rgb(252,253,254) bg=rgb(0,0,0) + -- ?screen_cell 0,3 = {44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0) + -- SETDEFAULTCOL rgb(250,250,250) rgb(10,20,30) + -- ?screen_cell 0,0 = {41} width=1 attrs={} fg=rgb(250,250,250) bg=rgb(10,20,30) + -- ?screen_cell 0,3 = {44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(10,20,30) + end) + + itp('65screen_protect', function() + local vt = init() + local screen = wantscreen(vt) + + -- Selective erase + reset(nil, screen) + push('A\x1b[1"qB\x1b["qC', vt) + screen_row(0, 'ABC', screen) + push('\x1b[G\x1b[?J', vt) + screen_row(0, ' B', screen) + + -- Non-selective erase + reset(nil, screen) + push('A\x1b[1"qB\x1b["qC', vt) + screen_row(0, 'ABC', screen) + -- TODO(dundargoc): fix + -- push('\x1b[G\x1b[J', vt) + -- screen_row(0, '', screen) + end) + + itp('66screen_extent', function() + local vt = init() + local screen = wantscreen(vt) + + -- Bold extent + reset(nil, screen) + push('AB\x1b[1mCD\x1b[mE', vt) + screen_attrs_extent(0, 0, '0,0-1,1', screen) + screen_attrs_extent(0, 1, '0,0-1,1', screen) + screen_attrs_extent(0, 2, '0,2-1,3', screen) + screen_attrs_extent(0, 3, '0,2-1,3', screen) + screen_attrs_extent(0, 4, '0,4-1,79', screen) + end) + + itp('67screen_dbl_wh', function() + local vt = init() + local screen = wantscreen(vt) + + reset(nil, screen) + + -- Single Width, Single Height + reset(nil, screen) + push('\x1b#5', vt) + push('abcde', vt) + screen_cell(0, 0, '{61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Double Width, Single Height + reset(nil, screen) + push('\x1b#6', vt) + push('abcde', vt) + screen_cell(0, 0, '{61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- Double Height + reset(nil, screen) + push('\x1b#3', vt) + push('abcde', vt) + push('\r\n\x1b#4', vt) + push('abcde', vt) + screen_cell(0, 0, '{61} width=1 attrs={} dwl dhl-top fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell( + 1, + 0, + '{61} width=1 attrs={} dwl dhl-bottom fg=rgb(240,240,240) bg=rgb(0,0,0)', + screen + ) + + -- Late change + reset(nil, screen) + push('abcde', vt) + screen_cell(0, 0, '{61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + push('\x1b#6', vt) + screen_cell(0, 0, '{61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + + -- DWL doesn't spill over on scroll + reset(nil, screen) + push('\x1b[25H\x1b#6Final\r\n', vt) + screen_cell(23, 0, '{46} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + screen_cell(24, 0, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) + end) + + itp('68screen_termprops', function() + local vt = init() + local screen = wantscreen(vt, { p = true }) + + reset(nil, screen) + expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') + + -- Cursor visibility + push('\x1b[?25h', vt) + expect('settermprop 1 true') + push('\x1b[?25l', vt) + expect('settermprop 1 false') + + -- Title + push('\x1b]2;Here is my title\a', vt) + expect('settermprop 4 ["Here is my title"]') + end) + + itp('69screen_pushline', function() + local vt = init() + -- Run these tests on a much smaller default screen, so debug output is nowhere near as noisy + resize(5, 10, vt) + local state = wantstate(vt) + local screen = wantscreen(vt, { r = true }) + reset(state, screen) + + -- Resize wider reflows wide lines + reset(state, screen) + push(string.rep('A', 12), vt) + screen_row(0, 'AAAAAAAAAA', screen, vt.cols) + screen_row(1, 'AA', screen, vt.cols) + lineinfo(1, { cont = true }, state) + cursor(1, 2, state) + resize(5, 15, vt) + screen_row(0, 'AAAAAAAAAAAA', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row(1, '', screen, vt.cols) + lineinfo(1, {}, state) + cursor(0, 12, state) + resize(5, 20, vt) + screen_row(0, 'AAAAAAAAAAAA', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row( 1 ,'',screen, vt.cols) + lineinfo(1, {}, state) + cursor(0, 12, state) + + -- Resize narrower can create continuation lines + reset(state, screen) + resize(5, 10, vt) + push('ABCDEFGHI', vt) + screen_row(0, 'ABCDEFGHI', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row( 1 , "",screen, vt.cols) + lineinfo(1, {}, state) + cursor(0, 9, state) + resize(5, 8, vt) + -- TODO(dundargoc): fix + -- screen_row( 0 , "ABCDEFGH",screen,vt.cols) + screen_row(1, 'I', screen, vt.cols) + lineinfo(1, { cont = true }, state) + cursor(1, 1, state) + resize(5, 6, vt) + screen_row(0, 'ABCDEF', screen, vt.cols) + screen_row(1, 'GHI', screen, vt.cols) + lineinfo(1, { cont = true }, state) + cursor(1, 3, state) + + -- Shell wrapped prompt behaviour + reset(state, screen) + resize(5, 10, vt) + push('PROMPT GOES HERE\r\n> \r\n\r\nPROMPT GOES HERE\r\n> ', vt) + screen_row(0, '> ', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row( 1 , "",screen,vt.cols) + screen_row(2, 'PROMPT GOE', screen, vt.cols) + screen_row(3, 'S HERE', screen, vt.cols) + lineinfo(3, { cont = true }, state) + screen_row(4, '> ', screen, vt.cols) + cursor(4, 2, state) + resize(5, 11, vt) + screen_row(0, '> ', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row( 1 , "",screen,vt.cols) + screen_row(2, 'PROMPT GOES', screen, vt.cols) + screen_row(3, ' HERE', screen, vt.cols) + lineinfo(3, { cont = true }, state) + screen_row(4, '> ', screen, vt.cols) + cursor(4, 2, state) + resize(5, 12, vt) + screen_row(0, '> ', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row( 1 , "",screen,vt.cols) + screen_row(2, 'PROMPT GOES ', screen, vt.cols) + screen_row(3, 'HERE', screen, vt.cols) + lineinfo(3, { cont = true }, state) + screen_row(4, '> ', screen, vt.cols) + cursor(4, 2, state) + resize(5, 16, vt) + screen_row(0, '> ', screen, vt.cols) + -- TODO(dundargoc): fix + -- screen_row( 1 , "",screen,vt.cols) + -- screen_row( 2 , "PROMPT GOES HERE",screen,vt.cols) + lineinfo(3, {}, state) + screen_row(3, '> ', screen, vt.cols) + cursor(3, 2, state) + + -- Cursor goes missing + -- For more context: https://github.com/neovim/neovim/pull/21124 + reset(state, screen) + resize(5, 5, vt) + resize(3, 1, vt) + push('\x1b[2;1Habc\r\n\x1b[H', vt) + resize(1, 1, vt) + cursor(0, 0, state) + end) + + pending('90vttest_01-movement-1', function() end) + pending('90vttest_01-movement-2', function() end) + + itp('90vttest_01-movement-3', function() + -- Test of cursor-control characters inside ESC sequences + local vt = init() + local state = wantstate(vt) + local screen = wantscreen(vt) + + reset(state, screen) + + push('A B C D E F G H I', vt) + push('\x0d\x0a', vt) + push('A\x1b[2\bCB\x1b[2\bCC\x1b[2\bCD\x1b[2\bCE\x1b[2\bCF\x1b[2\bCG\x1b[2\bCH\x1b[2\bCI', vt) + push('\x0d\x0a', vt) + push( + 'A \x1b[\x0d2CB\x1b[\x0d4CC\x1b[\x0d6CD\x1b[\x0d8CE\x1b[\x0d10CF\x1b[\x0d12CG\x1b[\x0d14CH\x1b[\x0d16CI', + vt + ) + push('\x0d\x0a', vt) + push( + 'A \x1b[1\x0bAB \x1b[1\x0bAC \x1b[1\x0bAD \x1b[1\x0bAE \x1b[1\x0bAF \x1b[1\x0bAG \x1b[1\x0bAH \x1b[1\x0bAI \x1b[1\x0bA', + vt + ) + + -- Output + + for i = 0, 2 do + screen_row(i, 'A B C D E F G H I', screen) + end + screen_row(3, 'A B C D E F G H I ', screen) + + cursor(3, 18, state) + end) + + itp('90vttest_01-movement-4', function() + -- Test of leading zeroes in ESC sequences + local vt = init() + local screen = wantscreen(vt) + + reset(nil, screen) + + push('\x1b[00000000004;000000001HT', vt) + push('\x1b[00000000004;000000002Hh', vt) + push('\x1b[00000000004;000000003Hi', vt) + push('\x1b[00000000004;000000004Hs', vt) + push('\x1b[00000000004;000000005H ', vt) + push('\x1b[00000000004;000000006Hi', vt) + push('\x1b[00000000004;000000007Hs', vt) + push('\x1b[00000000004;000000008H ', vt) + push('\x1b[00000000004;000000009Ha', vt) + push('\x1b[00000000004;0000000010H ', vt) + push('\x1b[00000000004;0000000011Hc', vt) + push('\x1b[00000000004;0000000012Ho', vt) + push('\x1b[00000000004;0000000013Hr', vt) + push('\x1b[00000000004;0000000014Hr', vt) + push('\x1b[00000000004;0000000015He', vt) + push('\x1b[00000000004;0000000016Hc', vt) + push('\x1b[00000000004;0000000017Ht', vt) + push('\x1b[00000000004;0000000018H ', vt) + push('\x1b[00000000004;0000000019Hs', vt) + push('\x1b[00000000004;0000000020He', vt) + push('\x1b[00000000004;0000000021Hn', vt) + push('\x1b[00000000004;0000000022Ht', vt) + push('\x1b[00000000004;0000000023He', vt) + push('\x1b[00000000004;0000000024Hn', vt) + push('\x1b[00000000004;0000000025Hc', vt) + push('\x1b[00000000004;0000000026He', vt) + + -- Output + + screen_row(3, 'This is a correct sentence', screen) + end) + + pending('90vttest_02-screen-1', function() end) + pending('90vttest_02-screen-2', function() end) + + itp('90vttest_02-screen-3', function() + -- Origin mode + local vt = init() + local screen = wantscreen(vt) + + reset(nil, screen) + + push('\x1b[?6h', vt) + push('\x1b[23;24r', vt) + push('\n', vt) + push('Bottom', vt) + push('\x1b[1;1H', vt) + push('Above', vt) + + -- Output + screen_row(22, 'Above', screen) + screen_row(23, 'Bottom', screen) + end) + + itp('90vttest_02-screen-4', function() + -- Origin mode (2) + local vt = init() + local screen = wantscreen(vt) + + reset(nil, screen) + + push('\x1b[?6l', vt) + push('\x1b[23;24r', vt) + push('\x1b[24;1H', vt) + push('Bottom', vt) + push('\x1b[1;1H', vt) + push('Top', vt) + + -- Output + screen_row(23, 'Bottom', screen) + screen_row(0, 'Top', screen) + end) + + itp('Mouse reporting should not break by idempotent DECSM 1002', function() + -- Regression test for https://bugs.launchpad.net/libvterm/+bug/1640917 + -- Related: https://github.com/neovim/neovim/issues/5583 + local vt = init() + wantstate(vt, {}) + + push('\x1b[?1002h', vt) + mousemove(0, 0, vt) + mousebtn('d', 1, vt) + expect_output('\x1b[M\x20\x21\x21') + mousemove(1, 0, vt) + expect_output('\x1b[M\x40\x21\x22') + push('\x1b[?1002h', vt) + mousemove(2, 0, vt) + expect_output('\x1b[M\x40\x21\x23') + end) +end)