diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 408c8b4aa2..0d007f2fea 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -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 group(atype: str, conv: str) -> tuple[str, str]: 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 group(atype: str, conv: str) -> tuple[str, str]: 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;" diff --git a/kitty/boss.py b/kitty/boss.py index 217d394bb2..14d8e53a3f 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -238,8 +238,13 @@ def __call__(self, window_id: int, what: str, *a: Any) -> None: 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)) # }}} diff --git a/kitty/client.py b/kitty/client.py index 541b7ac55a..4faf920470 100644 --- a/kitty/client.py +++ b/kitty/client.py @@ -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] diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 8401d16a50..aa101357e8 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -308,6 +308,7 @@ WINDOW_NORMAL: int = 0 WINDOW_FULLSCREEN: int WINDOW_MAXIMIZED: int WINDOW_MINIMIZED: int +TEXT_SIZE_CODE: int # }}} diff --git a/kitty/line.h b/kitty/line.h index d99f392509..60c135e568 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -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 diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index 622dcb3c40..092740f807 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -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); } diff --git a/kitty/parse-multicell-command.h b/kitty/parse-multicell-command.h index b9f9ef6621..1cd8876573 100644 --- a/kitty/parse-multicell-command.h +++ b/kitty/parse-multicell-command.h @@ -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); } diff --git a/kitty/vt-parser.c b/kitty/vt-parser.c index a1d06e78c8..5cac099e08 100644 --- a/kitty/vt-parser.c +++ b/kitty/vt-parser.c @@ -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; diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index aec8c9086d..38d994f51f 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -385,7 +385,7 @@ def test_csi_codes(self): 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 @@ def sgr(*params): 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 @@ def sgr(*params): 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 @@ def c(**k): ' 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 @@ def e(cmd, err): 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 @@ def e(cmd, err): 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 @@ def test_deccara(self): 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 @@ def test_deccara(self): 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):