Implement reporting of multicell commands

This commit is contained in:
Kovid Goyal
2024-12-10 07:09:03 +05:30
parent 1aec299ad8
commit 5c2c88858b
9 changed files with 59 additions and 34 deletions

View File

@@ -69,7 +69,7 @@ def parse_number(keymap: KeymapType) -> tuple[str, str]:
return '; '.join(int_keys), '; '.join(uint_keys)
def cmd_for_report(report_name: str, keymap: KeymapType, type_map: dict[str, Any], payload_allowed: bool) -> str:
def cmd_for_report(report_name: str, keymap: KeymapType, type_map: dict[str, Any], payload_allowed: bool, payload_is_base64: bool) -> str:
def group(atype: str, conv: str) -> tuple[str, str]:
flag_fmt, flag_attrs = [], []
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
@@ -85,7 +85,7 @@ def cmd_for_report(report_name: str, keymap: KeymapType, type_map: dict[str, Any
fmt = f'{flag_fmt} {uint_fmt} {int_fmt}'
if payload_allowed:
ans = [f'REPORT_VA_COMMAND("K s {{{fmt} sI}} y#", self->window_id, "{report_name}",\n']
ans = [f'REPORT_VA_COMMAND("K s {{{fmt} ss#}}", self->window_id, "{report_name}",\n']
else:
ans = [f'REPORT_VA_COMMAND("K s {{{fmt}}}", self->window_id, "{report_name}",\n']
if flag_attrs:
@@ -95,7 +95,10 @@ def cmd_for_report(report_name: str, keymap: KeymapType, type_map: dict[str, Any
if int_attrs:
ans.append(f'{int_attrs},\n')
if payload_allowed:
ans.append('"payload_sz", g.payload_sz, parser_buf, g.payload_sz')
if payload_is_base64:
ans.append('"", (char*)parser_buf, g.payload_sz')
else:
ans.append('"", (char*)parser_buf + payload_start, g.payload_sz')
ans.append(');')
return '\n'.join(ans)
@@ -117,7 +120,7 @@ def generate(
handle_key = parse_key(keymap)
flag_keys = parse_flag(keymap, type_map, command_class)
int_keys, uint_keys = parse_number(keymap)
report_cmd = cmd_for_report(report_name, keymap, type_map, payload_allowed)
report_cmd = cmd_for_report(report_name, keymap, type_map, payload_allowed, payload_is_base64)
extra_init = ''
if payload_allowed:
payload_after_value = "case ';': state = PAYLOAD; break;"

View File

@@ -268,8 +268,13 @@ class DumpCommands: # {{{
if self.draw_dump_buf:
safe_print('draw', ''.join(self.draw_dump_buf))
self.draw_dump_buf = []
a = tuple(str(x, 'utf-8', 'replace') if isinstance(x, memoryview) else x for x in a)
safe_print(what, *a)
def fmt(x: Any) -> Any:
if isinstance(x, (bytes, memoryview)):
return str(x, 'utf-8', 'replace')
if isinstance(x, dict):
return json.dumps(x)
return x
safe_print(what, *map(fmt, a))
# }}}

View File

@@ -7,10 +7,13 @@
# kitty --replay-commands file.txt
# will replay the commands and pause at the end waiting for user to press enter
import json
import sys
from contextlib import suppress
from typing import Any
from .fast_data_types import TEXT_SIZE_CODE
CSI = '\x1b['
OSC = '\x1b]'
@@ -74,8 +77,8 @@ def screen_designate_charset(which: int, to: int) -> None:
write(f'\x1b{w}{t}')
def select_graphic_rendition(*a: int) -> None:
write(f'{CSI}{";".join(map(str, a))}m')
def select_graphic_rendition(payload: str) -> None:
write(f'{CSI}{payload}m')
def deccara(*a: int) -> None:
@@ -267,11 +270,26 @@ def clipboard_control(payload: str) -> None:
write(f'{OSC}{payload}\x07')
def multicell_command(payload: str) -> None:
c = json.loads(payload)
text = c.pop('', '')
m = ''
if (w := c.get('width')) is not None and w > 0:
m += f'w={w}:'
if (s := c.get('scale')) is not None and s > 1:
m += f's={s}:'
if (n := c.get('subscale_n')) is not None and n > 0:
m += f'n={n}:'
if (d := c.get('subscale_d')) is not None and d > 0:
m += f'd={d}:'
write(f'{OSC}{TEXT_SIZE_CODE};{m.rstrip(":")};{text}\a')
def replay(raw: str) -> None:
specials = {
'draw', 'set_title', 'set_icon', 'set_dynamic_color', 'set_color_table_color',
'process_cwd_notification', 'clipboard_control', 'shell_prompt_marking'
}
specials = frozenset({
'draw', 'set_title', 'set_icon', 'set_dynamic_color', 'set_color_table_color', 'select_graphic_rendition',
'process_cwd_notification', 'clipboard_control', 'shell_prompt_marking', 'multicell_command',
})
for line in raw.splitlines():
if line.strip() and not line.startswith('#'):
cmd, rest = line.partition(' ')[::2]

View File

@@ -308,6 +308,7 @@ WINDOW_NORMAL: int = 0
WINDOW_FULLSCREEN: int
WINDOW_MAXIMIZED: int
WINDOW_MINIMIZED: int
TEXT_SIZE_CODE: int
# }}}

View File

@@ -11,11 +11,9 @@
// TODO: Test setting of ch_and_idx to make sure the right ch_is_idx bit is set
// TODO: Test handling of calt ligatures with scale see is_group_calt_ligature()
// TODO: Font Rendering of scale > 1 and width > 1
// TODO: Handle selection with multicell
// TODO: URL detection with multicell
// TODO: Cursor rendering over multicell
// TODO: Handle replay of dumped graphics_command and multicell_command
// TODO: Handle rewrap and restitch of multiline chars
// TODO: Handle rewrap when a character is too wide/tall to fit on resized screen
// TODO: Implement baseline align for box drawing

View File

@@ -359,7 +359,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
REPORT_VA_COMMAND(
"K s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI "
"sI sI sI sI si si si sI} y#",
"sI sI sI sI si si si ss#}",
self->window_id, "graphics_command",
"action", g.action, "delete_action", g.delete_action, "transmission_type",
@@ -384,7 +384,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf,
(int)g.offset_from_parent_x, "offset_from_parent_y",
(int)g.offset_from_parent_y,
"payload_sz", g.payload_sz, parser_buf, g.payload_sz);
"", (char *)parser_buf, g.payload_sz);
screen_handle_graphics_command(self->screen, &g, parser_buf);
}

View File

@@ -183,14 +183,14 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf,
}
REPORT_VA_COMMAND(
"K s { sI sI sI sI sI sI} y#", self->window_id, "multicell_command",
"K s { sI sI sI sI sI ss#}", self->window_id, "multicell_command",
"width", (unsigned int)g.width, "scale", (unsigned int)g.scale,
"subscale_n", (unsigned int)g.subscale_n, "subscale_d",
(unsigned int)g.subscale_d, "vertical_align",
(unsigned int)g.vertical_align,
"payload_sz", g.payload_sz, parser_buf, g.payload_sz);
"", (char *)parser_buf + payload_start, g.payload_sz);
screen_handle_multicell_command(self->screen, &g, parser_buf + payload_start);
}

View File

@@ -61,8 +61,8 @@ static void
_report_params(PyObject *dump_callback, id_type window_id, const char *name, int *params, unsigned int count, bool is_group, Region *r) {
static char buf[MAX_CSI_PARAMS*3] = {0};
unsigned int i, p=0;
if (r) p += snprintf(buf + p, sizeof(buf) - 2, "%u %u %u %u ", r->top, r->left, r->bottom, r->right);
const char *fmt = is_group ? "%i:" : "%i ";
if (r) p += snprintf(buf + p, sizeof(buf) - 2, "%u;%u;%u;%u;", r->top, r->left, r->bottom, r->right);
const char *fmt = is_group ? "%i:" : "%i;";
for(i = 0; i < count && p < arraysz(buf)-20; i++) {
int n = snprintf(buf + p, arraysz(buf) - p, fmt, params[i]);
if (n < 0) break;

View File

@@ -385,7 +385,7 @@ class TestParser(BaseTest):
def sgr(*params):
return (('select_graphic_rendition', f'{x}') for x in params)
pb('\033[1;2;3;4;7;9;34;44m', *sgr('1 2 3 4 7 9 34 44'))
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), attr)
self.ae(s.cursor.decoration, 1)
@@ -397,10 +397,10 @@ class TestParser(BaseTest):
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[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[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'))
@@ -408,7 +408,7 @@ class TestParser(BaseTest):
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'))
s.reset()
pb('\033[1;2;3;4:5;7;9;34;44m', *sgr('1 2 3', '4:5', '7 9 34 44'))
pb('\033[1;2;3;4:5;7;9;34;44m', *sgr('1;2;3', '4:5', '7;9;34;44'))
for attr in 'bold italic reverse strikethrough dim'.split():
self.assertTrue(getattr(s.cursor, attr), attr)
self.ae(s.cursor.decoration, 5)
@@ -584,9 +584,9 @@ class TestParser(BaseTest):
' parent_id parent_placement_id offset_from_parent_x offset_from_parent_y'
).split():
k.setdefault(f, 0)
p = k.pop('payload', '').encode('utf-8')
k['payload_sz'] = len(p)
return ('graphics_command', k, p)
p = k.pop('payload', '')
k[''] = p
return ('graphics_command', k)
def t(cmd, **kw):
pb('\033_G{};{}\033\\'.format(cmd, enc(kw.get('payload', ''))), c(**kw))
@@ -601,8 +601,8 @@ class TestParser(BaseTest):
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', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9)
t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9)
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')
@@ -616,9 +616,9 @@ class TestParser(BaseTest):
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[$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'))
('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):
@@ -629,7 +629,7 @@ class TestParser(BaseTest):
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 39'))
pb('\033[1;2;2;3;22;39$r', ('deccara', '1;2;2;3;22;39'))
self.ae(s.line(0).cursor_from(0).bold, True)
line = s.line(0)
for x in range(1, s.columns):
@@ -641,7 +641,7 @@ class TestParser(BaseTest):
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))
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):