aboutsummaryrefslogtreecommitdiff
path: root/test/unit/vterm_spec.lua
diff options
context:
space:
mode:
authordundargoc <gocdundar@gmail.com>2024-09-25 16:07:37 +0200
committerdundargoc <33953936+dundargoc@users.noreply.github.com>2024-10-22 21:20:11 +0200
commit4c25e60767508a3f8bc0631d280407a08f24a67e (patch)
tree1de749c20df1b169557efd44373f4764f511bca1 /test/unit/vterm_spec.lua
parent07b4cb6ada2da108bbf8277bec63068234ceaa67 (diff)
downloadrneovim-4c25e60767508a3f8bc0631d280407a08f24a67e.tar.gz
rneovim-4c25e60767508a3f8bc0631d280407a08f24a67e.tar.bz2
rneovim-4c25e60767508a3f8bc0631d280407a08f24a67e.zip
test: port libvterm unit test to neovim test suite
These were imported from the v0.3.3 git tag https://github.com/neovim/libvterm/tree/v0.3.3 and not the latest commit. This is for compatibility reasons as the libvterm code was imported from v0.3.3.
Diffstat (limited to 'test/unit/vterm_spec.lua')
-rw-r--r--test/unit/vterm_spec.lua3591
1 files changed, 3591 insertions, 0 deletions
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)