kitty/kitty_tests/parser.py
Kovid Goyal e4d8aac3d5
Make CSI parsing a bit more spec compliant
Now the full list of secondary characters (intermediate bytes) is used from ECMA 48
Also the codes that accept only one parameter now report errors when
multiple parameters are passed.
2021-01-21 06:41:27 +05:30

446 lines
19 KiB
Python

#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import time
from base64 import standard_b64encode
from binascii import hexlify
from functools import partial
from kitty.fast_data_types import CURSOR_BLOCK, parse_bytes, parse_bytes_dump
from kitty.notify import (
NotificationCommand, handle_notification_cmd, notification_activated,
reset_registry
)
from . import BaseTest
class CmdDump(list):
def __call__(self, *a):
self.append(a)
class TestParser(BaseTest):
def parse_bytes_dump(self, s, x, *cmds):
cd = CmdDump()
if isinstance(x, str):
x = x.encode('utf-8')
cmds = tuple(('draw', x) if isinstance(x, str) else x for x in cmds)
parse_bytes_dump(cd, s, x)
current = ''
q = []
for args in cd:
if args[0] == 'draw':
if args[1] is not None:
current += args[1]
else:
if current:
q.append(('draw', current))
current = ''
q.append(args)
if current:
q.append(('draw', current))
self.ae(tuple(q), cmds)
def test_simple_parsing(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('12', '12')
self.ae(str(s.line(0)), '12')
self.ae(s.cursor.x, 2)
pb('3456', '3456')
self.ae(str(s.line(0)), '12345')
self.ae(str(s.line(1)), '6')
pb(b'\n123\n\r45', ('screen_linefeed',), '123', ('screen_linefeed',), ('screen_carriage_return',), '45')
self.ae(str(s.line(1)), '6')
self.ae(str(s.line(2)), ' 123')
self.ae(str(s.line(3)), '45')
parse_bytes(s, b'\rabcde')
self.ae(str(s.line(3)), 'abcde')
pb('\rßxyz1', ('screen_carriage_return',), 'ßxyz1')
self.ae(str(s.line(3)), 'ßxyz1')
pb('ニチ ', 'ニチ ')
self.ae(str(s.line(4)), 'ニチ ')
def test_esc_codes(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('12\033Da', '12', ('screen_index',), 'a')
self.ae(str(s.line(0)), '12')
self.ae(str(s.line(1)), ' a')
pb('\033x', ('Unknown char after ESC: 0x%x' % ord('x'),))
pb('\033c123', ('screen_reset', ), '123')
self.ae(str(s.line(0)), '123')
def test_charsets(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb(b'\xc3')
pb(b'\xa1', ('draw', b'\xc3\xa1'.decode('utf-8')))
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033)0\x0e/_', ('screen_designate_charset', 1, ord('0')), ('screen_change_charset', 1), '/_')
self.ae(str(s.line(0)), '/\xa0')
self.assertTrue(s.callbacks.iutf8)
pb('\033%@_', ('screen_use_latin1', 1), '_')
self.assertFalse(s.callbacks.iutf8)
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033(0/_', ('screen_designate_charset', 0, ord('0')), '/_')
self.ae(str(s.line(0)), '/\xa0')
def test_csi_codes(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('abcde', 'abcde')
s.cursor_back(5)
pb('x\033[2@y', 'x', ('screen_insert_characters', 2), 'y')
self.ae(str(s.line(0)), 'xy bc')
pb('x\033[2;7@y', 'x', ('CSI code 0x40 has 2 > 1 parameters',), 'y')
pb('x\033[@y', 'x', ('screen_insert_characters', 1), 'y')
pb('x\033[345@y', 'x', ('screen_insert_characters', 345), 'y')
pb('x\033[345;@y', 'x', ('screen_insert_characters', 345), 'y')
pb('\033[H', ('screen_cursor_position', 1, 1))
self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 0)
pb('\033[4H', ('screen_cursor_position', 4, 1))
pb('\033[4;0H', ('screen_cursor_position', 4, 0))
pb('\033[3;2H', ('screen_cursor_position', 3, 2))
pb('\033[3;2;H', ('screen_cursor_position', 3, 2))
pb('\033[00000000003;0000000000000002H', ('screen_cursor_position', 3, 2))
self.ae(s.cursor.x, 1), self.ae(s.cursor.y, 2)
pb('\033[J', ('screen_erase_in_display', 0, 0))
pb('\033[?J', ('screen_erase_in_display', 0, 1))
pb('\033[?2J', ('screen_erase_in_display', 2, 1))
pb('\033[h')
pb('\033[20;4h', ('screen_set_mode', 20, 0), ('screen_set_mode', 4, 0))
pb('\033[?1000;1004h', ('screen_set_mode', 1000, 1), ('screen_set_mode', 1004, 1))
pb('\033[20;4;20l', ('screen_reset_mode', 20, 0), ('screen_reset_mode', 4, 0), ('screen_reset_mode', 20, 0))
pb('\033[=c', ('report_device_attributes', 0, 61))
s.reset()
def sgr(params):
return (('select_graphic_rendition', '{} '.format(x)) for x in params.split())
pb('\033[1;2;3;4;7;9;34;44m', *sgr('1 2 3 4 7 9 34 44'))
for attr in 'bold italic reverse strikethrough dim'.split():
self.assertTrue(getattr(s.cursor, attr))
self.ae(s.cursor.decoration, 1)
self.ae(s.cursor.fg, 4 << 8 | 1)
self.ae(s.cursor.bg, 4 << 8 | 1)
pb('\033[38;5;1;48;5;7m', ('select_graphic_rendition', '38 5 1 '), ('select_graphic_rendition', '48 5 7 '))
self.ae(s.cursor.fg, 1 << 8 | 1)
self.ae(s.cursor.bg, 7 << 8 | 1)
pb('\033[38;2;1;2;3;48;2;7;8;9m', ('select_graphic_rendition', '38 2 1 2 3 '), ('select_graphic_rendition', '48 2 7 8 9 '))
self.ae(s.cursor.fg, 1 << 24 | 2 << 16 | 3 << 8 | 2)
self.ae(s.cursor.bg, 7 << 24 | 8 << 16 | 9 << 8 | 2)
pb('\033[0;2m', *sgr('0 2'))
pb('\033[;2m', *sgr('0 2'))
pb('\033[m', *sgr('0 '))
pb('\033[1;;2m', *sgr('1 0 2'))
pb('\033[38;5;1m', ('select_graphic_rendition', '38 5 1 '))
pb('\033[58;2;1;2;3m', ('select_graphic_rendition', '58 2 1 2 3 '))
pb('\033[38;2;1;2;3m', ('select_graphic_rendition', '38 2 1 2 3 '))
pb('\033[1001:2:1:2:3m', ('select_graphic_rendition', '1001 2 1 2 3 '))
pb('\033[38:2:1:2:3;48:5:9;58;5;7m', (
'select_graphic_rendition', '38 2 1 2 3 '), ('select_graphic_rendition', '48 5 9 '), ('select_graphic_rendition', '58 5 7 '))
c = s.callbacks
pb('\033[5n', ('report_device_status', 5, 0))
self.ae(c.wtcbuf, b'\033[0n')
c.clear()
pb('\033[6n', ('report_device_status', 6, 0))
self.ae(c.wtcbuf, b'\033[1;1R')
pb('12345', '12345')
c.clear()
pb('\033[6n', ('report_device_status', 6, 0))
self.ae(c.wtcbuf, b'\033[2;1R')
c.clear()
s.cursor_key_mode = True
pb('\033[?1$p', ('report_mode_status', 1, 1))
self.ae(c.wtcbuf, b'\033[?1;1$y')
pb('\033[?1l', ('screen_reset_mode', 1, 1))
self.assertFalse(s.cursor_key_mode)
c.clear()
pb('\033[?1$p', ('report_mode_status', 1, 1))
self.ae(c.wtcbuf, b'\033[?1;2$y')
pb('\033[2;4r', ('screen_set_margins', 2, 4))
c.clear()
pb('\033[14t', ('screen_report_size', 14))
self.ae(c.wtcbuf, b'\033[4;100;50t')
self.ae(s.margin_top, 1), self.ae(s.margin_bottom, 3)
pb('\033[r', ('screen_set_margins', 0, 0))
self.ae(s.margin_top, 0), self.ae(s.margin_bottom, 4)
pb('\033[1 q', ('screen_set_cursor', 1, ord(' ')))
self.assertTrue(s.cursor.blink)
self.ae(s.cursor.shape, CURSOR_BLOCK)
s.reset()
pb('\033[3 @', ('Shift left escape code not implemented',))
pb('\033[3 A', ('Shift right escape code not implemented',))
pb('\033[3;4 S', ('Select presentation directions escape code not implemented',))
def test_csi_code_rep(self):
s = self.create_screen(8)
pb = partial(self.parse_bytes_dump, s)
pb('\033[1b', ('screen_repeat_character', 1))
self.ae(str(s.line(0)), '')
pb('x\033[7b', 'x', ('screen_repeat_character', 7))
self.ae(str(s.line(0)), 'xxxxxxxx')
pb('\033[1;3H', ('screen_cursor_position', 1, 3))
pb('\033[byz\033[b', ('screen_repeat_character', 1), 'yz', ('screen_repeat_character', 1))
# repeat 'x' at 3, then 'yz' at 4-5, then repeat 'z' at 6
self.ae(str(s.line(0)), 'xxxyzzxx')
s.reset()
pb(' \033[3b', ' ', ('screen_repeat_character', 3))
self.ae(str(s.line(0)), ' ')
s.reset()
pb('\t\033[b', ('screen_tab',), ('screen_repeat_character', 1))
self.ae(str(s.line(0)), '\t')
s.reset()
b']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'
def test_osc_codes(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
c = s.callbacks
pb('a\033]2;x\\ryz\x9cbcde', 'a', ('set_title', 'x\\ryz'), 'bcde')
self.ae(str(s.line(0)), 'abcde')
self.ae(c.titlebuf, 'x\\ryz')
c.clear()
pb('\033]\x07', ('set_title', ''), ('set_icon', ''))
self.ae(c.titlebuf, ''), self.ae(c.iconbuf, '')
pb('\033]ab\x07', ('set_title', 'ab'), ('set_icon', 'ab'))
self.ae(c.titlebuf, 'ab'), self.ae(c.iconbuf, 'ab')
c.clear()
pb('\033]2;;;;\x07', ('set_title', ';;;'))
self.ae(c.titlebuf, ';;;')
c.clear()
pb('\033]2;\x07', ('set_title', ''))
self.ae(c.titlebuf, '')
pb('\033]110\x07', ('set_dynamic_color', 110, ''))
self.ae(c.colorbuf, '')
c.clear()
pb('\033]9;\x07', ('desktop_notify', 9, ''))
pb('\033]9;test it\x07', ('desktop_notify', 9, 'test it'))
pb('\033]99;moo=foo;test it\x07', ('desktop_notify', 99, 'moo=foo;test it'))
self.ae(c.notifications, [(9, ''), (9, 'test it'), (99, 'moo=foo;test it')])
c.clear()
pb('\033]8;;\x07', ('set_active_hyperlink', None, None))
pb('\033]8moo\x07', ('Ignoring malformed OSC 8 code',))
pb('\033]8;moo\x07', ('Ignoring malformed OSC 8 code',))
pb('\033]8;id=xyz;\x07', ('set_active_hyperlink', 'xyz', None))
pb('\033]8;moo:x=z:id=xyz:id=abc;http://yay;.com\x07', ('set_active_hyperlink', 'xyz', 'http://yay;.com'))
def test_desktop_notify(self):
reset_registry()
notifications = []
activations = []
prev_cmd = NotificationCommand()
def reset():
nonlocal prev_cmd
reset_registry()
del notifications[:]
del activations[:]
prev_cmd = NotificationCommand()
def notify(title, body, identifier):
notifications.append((title, body, identifier))
def h(raw_data, osc_code=99, window_id=1):
nonlocal prev_cmd
x = handle_notification_cmd(osc_code, raw_data, window_id, prev_cmd, notify)
if x is not None and osc_code == 99:
prev_cmd = x
def activated(identifier, window_id, focus, report):
activations.append((identifier, window_id, focus, report))
h('test it', osc_code=9)
self.ae(notifications, [('test it', '', 'i0')])
notification_activated(notifications[-1][-1], activated)
self.ae(activations, [('0', 1, True, False)])
reset()
h('d=0:i=x;title')
h('d=1:i=x:p=body;body')
self.ae(notifications, [('title', 'body', 'i0')])
notification_activated(notifications[-1][-1], activated)
self.ae(activations, [('x', 1, True, False)])
reset()
h('i=x:p=body:a=-focus;body')
self.ae(notifications, [('body', '', 'i0')])
notification_activated(notifications[-1][-1], activated)
self.ae(activations, [])
reset()
h('i=x:e=1;' + standard_b64encode(b'title').decode('ascii'))
self.ae(notifications, [('title', '', 'i0')])
notification_activated(notifications[-1][-1], activated)
self.ae(activations, [('x', 1, True, False)])
reset()
h('d=0:i=x:a=-report;title')
h('d=1:i=x:a=report;body')
self.ae(notifications, [('titlebody', '', 'i0')])
notification_activated(notifications[-1][-1], activated)
self.ae(activations, [('x', 1, True, True)])
reset()
h(';title')
self.ae(notifications, [('title', '', 'i0')])
notification_activated(notifications[-1][-1], activated)
self.ae(activations, [('0', 1, True, False)])
reset()
def test_dcs_codes(self):
s = self.create_screen()
c = s.callbacks
pb = partial(self.parse_bytes_dump, s)
q = hexlify(b'kind').decode('ascii')
pb('a\033P+q{}\x9cbcde'.format(q), 'a', ('screen_request_capabilities', 43, q), 'bcde')
self.ae(str(s.line(0)), 'abcde')
self.ae(c.wtcbuf, '1+r{}={}'.format(q, '1b5b313b3242').encode('ascii'))
c.clear()
pb('\033P$q q\033\\', ('screen_request_capabilities', ord('$'), ' q'))
self.ae(c.wtcbuf, b'\033P1$r1 q\033\\')
c.clear()
pb('\033P$qm\033\\', ('screen_request_capabilities', ord('$'), 'm'))
self.ae(c.wtcbuf, b'\033P1$rm\033\\')
for sgr in '0;34;102;1;2;3;4 0;38:5:200;58:2:10:11:12'.split():
expected = set(sgr.split(';')) - {'0'}
c.clear()
parse_bytes(s, '\033[{}m\033P$qm\033\\'.format(sgr).encode('ascii'))
r = c.wtcbuf.decode('ascii').partition('r')[2].partition('m')[0]
self.ae(expected, set(r.split(';')))
c.clear()
pb('\033P$qr\033\\', ('screen_request_capabilities', ord('$'), 'r'))
self.ae(c.wtcbuf, '\033P1$r{};{}r\033\\'.format(s.margin_top + 1, s.margin_bottom + 1).encode('ascii'))
def test_sc81t(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033 G', ('screen_set_8bit_controls', 1))
c = s.callbacks
pb('\033P$qm\033\\', ('screen_request_capabilities', ord('$'), 'm'))
self.ae(c.wtcbuf, b'\x901$rm\x9c')
c.clear()
pb('\033[0c', ('report_device_attributes', 0, 0))
self.ae(c.wtcbuf, b'\x9b?62;c')
def test_pending(self):
s = self.create_screen()
timeout = 0.1
s.set_pending_timeout(timeout)
pb = partial(self.parse_bytes_dump, s)
pb('\033P=1s\033\\', ('screen_start_pending_mode',))
pb('a')
self.ae(str(s.line(0)), '')
pb('\033P=2s\033\\', ('screen_stop_pending_mode',), ('draw', 'a'))
self.ae(str(s.line(0)), 'a')
pb('\033P=1s\033\\', ('screen_start_pending_mode',))
pb('b')
self.ae(str(s.line(0)), 'a')
time.sleep(timeout)
pb('c', ('draw', 'bc'))
self.ae(str(s.line(0)), 'abc')
pb('\033P=1s\033\\d', ('screen_start_pending_mode',))
pb('\033P=2s\033\\', ('screen_stop_pending_mode',), ('draw', 'd'))
pb('\033P=1s\033\\e', ('screen_start_pending_mode',))
pb('\033P'), pb('='), pb('2s')
pb('\033\\', ('screen_stop_pending_mode',), ('draw', 'e'))
pb('\033P=1sxyz;.;\033\\''\033P=2skjf".,><?_+)98\033\\', ('screen_start_pending_mode',), ('screen_stop_pending_mode',))
pb('\033P=1s\033\\f\033P=1s\033\\', ('screen_start_pending_mode',), ('screen_start_pending_mode',))
def test_oth_codes(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
for prefix in '\033_', '\u009f':
for suffix in '\u009c', '\033\\':
pb('a{}+\\++{}bcde'.format(prefix, suffix), ('draw', 'a'), ('Unrecognized APC code: 0x2b',), ('draw', 'bcde'))
for prefix in '\033^', '\u009e':
for suffix in '\u009c', '\033\\':
pb('a{}+\\++{}bcde'.format(prefix, suffix), ('draw', 'a'), ('Unrecognized PM code: 0x2b',), ('draw', 'bcde'))
def test_graphics_command(self):
from base64 import standard_b64encode
def enc(x):
return standard_b64encode(x.encode('utf-8') if isinstance(x, str) else x).decode('ascii')
def c(**k):
for p, v in tuple(k.items()):
if isinstance(v, str) and p != 'payload':
k[p] = v.encode('ascii')
for f in 'action delete_action transmission_type compressed'.split():
k.setdefault(f, b'\0')
for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width'
' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet').split():
k.setdefault(f, 0)
p = k.pop('payload', '').encode('utf-8')
k['payload_sz'] = len(p)
return ('graphics_command', k, p)
def t(cmd, **kw):
pb('\033_G{};{}\033\\'.format(cmd, enc(kw.get('payload', ''))), c(**kw))
def e(cmd, err):
pb('\033_G{}\033\\'.format(cmd), (err,))
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
uint32_max = 2**32 - 1
t('i=%d' % uint32_max, id=uint32_max)
t('i=3,p=4', id=3, placement_id=4)
e('i=%d' % (uint32_max + 1), 'Malformed GraphicsCommand control block, number is too large')
pb('\033_Gi=12\033\\', c(id=12))
t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9, payload_sz=1)
t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9, payload_sz=7)
t('a=t,t=d,s=100,z=9,q=2', action='t', transmission_type='d', data_width=100, z_index=9, quiet=2)
e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c')
e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57')
e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31')
e('a=t,,w=2', 'Malformed GraphicsCommand control block, invalid key character: 0x2c')
e('s', 'Malformed GraphicsCommand control block, no = after key')
e('s=', 'Malformed GraphicsCommand control block, expecting an integer value')
e('s==', 'Malformed GraphicsCommand control block, expecting an integer value for key: s')
e('s=1=', 'Malformed GraphicsCommand control block, expecting a comma or semi-colon after a value, found: 0x3d')
def test_deccara(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033[$r', ('deccara', '0 0 0 0 0 '))
pb('\033[;;;;4:3;38:5:10;48:2:1:2:3;1$r',
('deccara', '0 0 0 0 4 3 '), ('deccara', '0 0 0 0 38 5 10 '), ('deccara', '0 0 0 0 48 2 1 2 3 '), ('deccara', '0 0 0 0 1 '))
for y in range(s.lines):
line = s.line(y)
for x in range(s.columns):
c = line.cursor_from(x)
self.ae(c.bold, True)
self.ae(c.italic, False)
self.ae(c.decoration, 3)
self.ae(c.fg, (10 << 8) | 1)
self.ae(c.bg, (1 << 24 | 2 << 16 | 3 << 8 | 2))
self.ae(s.line(0).cursor_from(0).bold, True)
pb('\033[1;2;2;3;22;39$r', ('deccara', '1 2 2 3 22 '), ('deccara', '1 2 2 3 39 '))
self.ae(s.line(0).cursor_from(0).bold, True)
line = s.line(0)
for x in range(1, s.columns):
c = line.cursor_from(x)
self.ae(c.bold, False)
self.ae(c.fg, 0)
line = s.line(1)
for x in range(0, 3):
c = line.cursor_from(x)
self.ae(c.bold, False)
self.ae(line.cursor_from(3).bold, True)
pb('\033[2*x\033[3;2;4;3;34$r\033[*x', ('screen_decsace', 2), ('deccara', '3 2 4 3 34 '), ('screen_decsace', 0))
for y in range(2, 4):
line = s.line(y)
for x in range(s.columns):
self.ae(line.cursor_from(x).fg, (10 << 8 | 1) if x < 1 or x > 2 else (4 << 8) | 1)