diff --git a/.gitattributes b/.gitattributes index 62122153c..59cfcb5c2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,7 @@ kitty/srgb_gamma.* linguist-generated=true kitty/gl-wrapper.* linguist-generated=true kitty/glfw-wrapper.* linguist-generated=true kitty/parse-graphics-command.h linguist-generated=true +kitty/parse-multicell-command.h linguist-generated=true kitty/options/types.py linguist-generated=true kitty/options/parse.py linguist-generated=true kitty/options/to-c-generated.h linguist-generated=true diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 8dfb5ce75..045c578d0 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -85,12 +85,17 @@ 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}", '] + ans = [f'REPORT_VA_COMMAND("K s {{{fmt} sI}} y#", self->window_id, "{report_name}",\n'] else: - ans = [f'REPORT_VA_COMMAND("K s {{{fmt}}}", self->window_id, "{report_name}", '] - ans.append(',\n '.join((flag_attrs, uint_attrs, int_attrs))) + ans = [f'REPORT_VA_COMMAND("K s {{{fmt}}}", self->window_id, "{report_name}",\n'] + if flag_attrs: + ans.append(f'{flag_attrs},\n') + if uint_attrs: + ans.append(f'{uint_attrs},\n') + if int_attrs: + ans.append(f'{int_attrs},\n') if payload_allowed: - ans.append(', "payload_sz", g.payload_sz, parser_buf, g.payload_sz') + ans.append('"payload_sz", g.payload_sz, parser_buf, g.payload_sz') ans.append(');') return '\n'.join(ans) @@ -102,7 +107,9 @@ def generate( keymap: KeymapType, command_class: str, initial_key: str = 'a', - payload_allowed: bool = True + payload_allowed: bool = True, + payload_is_base64: bool = True, + start_parsing_at: int = 1, ) -> str: type_map = resolve_keys(keymap) keys_enum = enum(keymap) @@ -110,38 +117,51 @@ def generate( 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) + extra_init = '' if payload_allowed: payload_after_value = "case ';': state = PAYLOAD; break;" payload = ', PAYLOAD' - payload_case = f''' - case PAYLOAD: {{ - sz = parser_buf_pos - pos; - g.payload_sz = MAX(BUF_EXTRA, sz); - if (!base64_decode8(parser_buf + pos, sz, parser_buf, &g.payload_sz)) {{ + if payload_is_base64: + payload_case = f''' + case PAYLOAD: {{ + sz = parser_buf_pos - pos; g.payload_sz = MAX(BUF_EXTRA, sz); - REPORT_ERROR("Failed to parse {command_class} command payload with error: \ -invalid base64 data in chunk of size: %zu with output buffer size: %zu", sz, g.payload_sz); return; }} - pos = parser_buf_pos; - }} - break; - ''' - callback = f'{callback_name}(self->screen, &g, parser_buf)' + if (!base64_decode8(parser_buf + pos, sz, parser_buf, &g.payload_sz)) {{ + g.payload_sz = MAX(BUF_EXTRA, sz); + REPORT_ERROR("Failed to parse {command_class} command payload with error: \ + invalid base64 data in chunk of size: %zu with output buffer size: %zu", sz, g.payload_sz); return; }} + pos = parser_buf_pos; + }} break; + ''' + callback = f'{callback_name}(self->screen, &g, parser_buf)' + else: + payload_case = ''' + case PAYLOAD: { + sz = parser_buf_pos - pos; + payload_start = pos; + g.payload_sz = MAX(BUF_EXTRA, sz); + } break; + ''' + extra_init = 'size_t payload_start = 0;' + callback = f'{callback_name}(self->screen, &g, parser_buf + payload_start)' + else: payload_after_value = payload = payload_case = '' callback = f'{callback_name}(self->screen, &g)' return f''' #include "base64.h" + static inline void {function_name}(PS *self, uint8_t *parser_buf, const size_t parser_buf_pos) {{ - unsigned int pos = 1; + unsigned int pos = {start_parsing_at}; + {extra_init} enum PARSER_STATES {{ KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE {payload} }}; enum PARSER_STATES state = KEY, value_state = FLAG; - static {command_class} g; + {command_class} g = {{0}}; unsigned int i, code; uint64_t lcode; int64_t accumulator; - bool is_negative; - memset(&g, 0, sizeof(g)); + bool is_negative; (void)is_negative; size_t sz; {keys_enum} enum KEYS key = '{initial_key}'; @@ -256,7 +276,7 @@ def write_header(text: str, path: str) -> None: subprocess.check_call(['clang-format', '-i', path]) -def graphics_parser() -> None: +def parsers() -> None: flag = frozenset keymap: KeymapType = { 'a': ('action', flag('tTqpdfac')), @@ -291,10 +311,19 @@ def graphics_parser() -> None: } text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand') write_header(text, 'kitty/parse-graphics-command.h') + keymap = { + 'w': ('width', 'uint'), + 's': ('scale', 'uint'), + 'f': ('subscale', 'uint'), + } + text = generate( + 'parse_multicell_code', 'screen_handle_multicell_command', 'multicell_command', keymap, 'MultiCellCommand', + payload_is_base64=False, start_parsing_at=0) + write_header(text, 'kitty/parse-multicell-command.h') def main(args: list[str]=sys.argv) -> None: - graphics_parser() + parsers() if __name__ == '__main__': diff --git a/kitty/base64.h b/kitty/base64.h index 626a0b1b7..331443763 100644 --- a/kitty/base64.h +++ b/kitty/base64.h @@ -4,6 +4,7 @@ * Distributed under terms of the GPL3 license. */ +#pragma once #include #include #include diff --git a/kitty/line.h b/kitty/line.h index 7aee2d75a..ea6525a96 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -16,6 +16,7 @@ // TODO: URL detection with multicell // TODO: Cursor rendering over multicell // TODO: Test the escape codes to delete and insert characters and lines with multicell +// TODO: Handle replay of dumped graphics_command and multicell_command typedef union CellAttrs { struct { @@ -54,7 +55,8 @@ typedef union MultiCellData { char_type width: 3; char_type subscale: 2; char_type vertical_align: 2; - char_type : 21; + char_type explicitly_set: 1; + char_type : 20; char_type msb : 1; }; char_type val; @@ -67,10 +69,10 @@ typedef union CPUCell { char_type ch_is_idx: 1; char_type hyperlink_id: sizeof(hyperlink_id_type) * 8; char_type x : 8; - char_type y : 3; + char_type y : 4; char_type is_multicell : 1; char_type next_char_was_wrapped : 1; - char_type : 3; + char_type : 2; }; struct { char_type ch_and_idx: sizeof(char_type) * 8; @@ -104,6 +106,11 @@ typedef struct { TextCache *text_cache; } Line; +typedef struct MultiCellCommand { + unsigned int width, scale, subscale; + size_t payload_sz; +} MultiCellCommand; + Line* alloc_line(TextCache *text_cache); void apply_sgr_to_cells(GPUCell *first_cell, unsigned int cell_count, int *params, unsigned int count, bool is_group); const char* cell_as_sgr(const GPUCell *, const GPUCell *); diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index 9feca8510..c63af726f 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -3,17 +3,19 @@ #pragma once #include "base64.h" + static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, const size_t parser_buf_pos) { unsigned int pos = 1; + enum PARSER_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD }; enum PARSER_STATES state = KEY, value_state = FLAG; - static GraphicsCommand g; + GraphicsCommand g = {0}; unsigned int i, code; uint64_t lcode; int64_t accumulator; bool is_negative; - memset(&g, 0, sizeof(g)); + (void)is_negative; size_t sz; enum KEYS { @@ -327,8 +329,8 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, if (!base64_decode8(parser_buf + pos, sz, parser_buf, &g.payload_sz)) { g.payload_sz = MAX(BUF_EXTRA, sz); REPORT_ERROR("Failed to parse GraphicsCommand command payload with " - "error: invalid base64 data in chunk of size: %zu with " - "output buffer size: %zu", + "error: invalid base64 data in chunk of size: %zu " + "with output buffer size: %zu", sz, g.payload_sz); return; } @@ -336,7 +338,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, } break; } // end switch - } // end while + } // end while switch (state) { case EQUAL: @@ -358,26 +360,31 @@ 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#", - self->window_id, "graphics_command", "action", g.action, "delete_action", - g.delete_action, "transmission_type", g.transmission_type, "compressed", - g.compressed, "format", (unsigned int)g.format, "more", - (unsigned int)g.more, "id", (unsigned int)g.id, "image_number", - (unsigned int)g.image_number, "placement_id", - (unsigned int)g.placement_id, "quiet", (unsigned int)g.quiet, "width", - (unsigned int)g.width, "height", (unsigned int)g.height, "x_offset", - (unsigned int)g.x_offset, "y_offset", (unsigned int)g.y_offset, - "data_height", (unsigned int)g.data_height, "data_width", - (unsigned int)g.data_width, "data_sz", (unsigned int)g.data_sz, - "data_offset", (unsigned int)g.data_offset, "num_cells", - (unsigned int)g.num_cells, "num_lines", (unsigned int)g.num_lines, - "cell_x_offset", (unsigned int)g.cell_x_offset, "cell_y_offset", - (unsigned int)g.cell_y_offset, "cursor_movement", + self->window_id, "graphics_command", + + "action", g.action, "delete_action", g.delete_action, "transmission_type", + g.transmission_type, "compressed", g.compressed, + + "format", (unsigned int)g.format, "more", (unsigned int)g.more, "id", + (unsigned int)g.id, "image_number", (unsigned int)g.image_number, + "placement_id", (unsigned int)g.placement_id, "quiet", + (unsigned int)g.quiet, "width", (unsigned int)g.width, "height", + (unsigned int)g.height, "x_offset", (unsigned int)g.x_offset, "y_offset", + (unsigned int)g.y_offset, "data_height", (unsigned int)g.data_height, + "data_width", (unsigned int)g.data_width, "data_sz", + (unsigned int)g.data_sz, "data_offset", (unsigned int)g.data_offset, + "num_cells", (unsigned int)g.num_cells, "num_lines", + (unsigned int)g.num_lines, "cell_x_offset", (unsigned int)g.cell_x_offset, + "cell_y_offset", (unsigned int)g.cell_y_offset, "cursor_movement", (unsigned int)g.cursor_movement, "unicode_placement", (unsigned int)g.unicode_placement, "parent_id", (unsigned int)g.parent_id, - "parent_placement_id", (unsigned int)g.parent_placement_id, "z_index", - (int)g.z_index, "offset_from_parent_x", (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); + "parent_placement_id", (unsigned int)g.parent_placement_id, + + "z_index", (int)g.z_index, "offset_from_parent_x", + (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); screen_handle_graphics_command(self->screen, &g, parser_buf); } diff --git a/kitty/parse-multicell-command.h b/kitty/parse-multicell-command.h new file mode 100644 index 000000000..5514b6306 --- /dev/null +++ b/kitty/parse-multicell-command.h @@ -0,0 +1,179 @@ +// This file is generated by apc_parsers.py do not edit! + +#pragma once + +#include "base64.h" + +static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, + const size_t parser_buf_pos) { + unsigned int pos = 0; + size_t payload_start = 0; + enum PARSER_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD }; + enum PARSER_STATES state = KEY, value_state = FLAG; + MultiCellCommand g = {0}; + unsigned int i, code; + uint64_t lcode; + int64_t accumulator; + bool is_negative; + (void)is_negative; + size_t sz; + + enum KEYS { width = 'w', scale = 's', subscale = 'f' }; + + enum KEYS key = 'a'; + if (parser_buf[pos] == ';') + state = AFTER_VALUE; + + while (pos < parser_buf_pos) { + switch (state) { + case KEY: + key = parser_buf[pos++]; + state = EQUAL; + switch (key) { + case width: + value_state = UINT; + break; + case scale: + value_state = UINT; + break; + case subscale: + value_state = UINT; + break; + default: + REPORT_ERROR("Malformed MultiCellCommand control block, invalid key " + "character: 0x%x", + key); + return; + } + break; + + case EQUAL: + if (parser_buf[pos++] != '=') { + REPORT_ERROR("Malformed MultiCellCommand control block, no = after " + "key, found: 0x%x instead", + parser_buf[pos - 1]); + return; + } + state = value_state; + break; + + case FLAG: + switch (key) { + + default: + break; + } + state = AFTER_VALUE; + break; + + case INT: +#define READ_UINT \ + for (i = pos, accumulator = 0; i < MIN(parser_buf_pos, pos + 10); i++) { \ + int64_t n = parser_buf[i] - '0'; \ + if (n < 0 || n > 9) \ + break; \ + accumulator += n * digit_multipliers[i - pos]; \ + } \ + if (i == pos) { \ + REPORT_ERROR("Malformed MultiCellCommand control block, expecting an " \ + "integer value for key: %c", \ + key & 0xFF); \ + return; \ + } \ + lcode = accumulator / digit_multipliers[i - pos - 1]; \ + pos = i; \ + if (lcode > UINT32_MAX) { \ + REPORT_ERROR( \ + "Malformed MultiCellCommand control block, number is too large"); \ + return; \ + } \ + code = lcode; + + is_negative = false; + if (parser_buf[pos] == '-') { + is_negative = true; + pos++; + } +#define I(x) \ + case x: \ + g.x = is_negative ? 0 - (int32_t)code : (int32_t)code; \ + break + READ_UINT; + switch (key) { + ; + default: + break; + } + state = AFTER_VALUE; + break; +#undef I + case UINT: + READ_UINT; +#define U(x) \ + case x: \ + g.x = code; \ + break + switch (key) { + U(width); + U(scale); + U(subscale); + default: + break; + } + state = AFTER_VALUE; + break; +#undef U +#undef READ_UINT + + case AFTER_VALUE: + switch (parser_buf[pos++]) { + default: + REPORT_ERROR("Malformed MultiCellCommand control block, expecting a " + "comma or semi-colon after a value, found: 0x%x", + parser_buf[pos - 1]); + return; + case ',': + state = KEY; + break; + case ';': + state = PAYLOAD; + break; + } + break; + + case PAYLOAD: { + sz = parser_buf_pos - pos; + payload_start = pos; + g.payload_sz = MAX(BUF_EXTRA, sz); + } break; + + } // end switch + } // end while + + switch (state) { + case EQUAL: + REPORT_ERROR("Malformed MultiCellCommand control block, no = after key"); + return; + case INT: + case UINT: + REPORT_ERROR( + "Malformed MultiCellCommand control block, expecting an integer value"); + return; + case FLAG: + REPORT_ERROR( + "Malformed MultiCellCommand control block, expecting a flag value"); + return; + default: + break; + } + + REPORT_VA_COMMAND("K s { sI sI sI sI} y#", self->window_id, + "multicell_command", + + "width", (unsigned int)g.width, "scale", + (unsigned int)g.scale, "subscale", (unsigned int)g.subscale, + + "payload_sz", g.payload_sz, parser_buf, g.payload_sz); + + screen_handle_multicell_command(self->screen, &g, parser_buf + payload_start); +} diff --git a/kitty/screen.c b/kitty/screen.c index 6a7522456..c31e0a3f5 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -879,7 +879,7 @@ draw_combining_char(Screen *self, text_loop_state *s, char_type ch) { self->lc->chars[self->lc->count++] = mcd.val; } else { ensure_space_for_chars(self->lc, self->lc->count + 1); - MultiCellData mcd = {.width=2, .scale=1}; + MultiCellData mcd = {.width=2, .scale=1, .msb=1}; self->lc->chars[self->lc->count++] = mcd.val; cpu_cell->is_multicell = true; } @@ -1040,7 +1040,7 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_ CPUCell *fc = s->cp + self->cursor->x; zero_cells(s, fc, s->gp + self->cursor->x); if (char_width == 2) { - MultiCellData mcd = {.width = 2, .scale = 1}; + MultiCellData mcd = {.width = 2, .scale = 1, .msb = 1}; RAII_ListOfChars(lc); lc.chars[lc.count++] = ch; lc.chars[lc.count++] = mcd.val; @@ -1094,6 +1094,48 @@ screen_align(Screen *self) { linebuf_clear(self->linebuf, 'E'); } +static size_t +decode_utf8_safe_string(const uint8_t *src, size_t sz, uint32_t *dest) { + // dest must be an array of size at least sz + uint32_t codep = 0; + UTF8State state = 0, prev = UTF8_ACCEPT; + size_t i, d; + for (i = 0, d = 0; i < sz; i++) { + switch(decode_utf8(&state, &codep, src[i])) { + case UTF8_ACCEPT: + // Ignore C0 and C1 chars + if (codep >= ' ' && !(codep >= DEL && codep <= 159)) dest[d++] = codep; + break; + case UTF8_REJECT: + state = UTF8_ACCEPT; + if (prev != UTF8_ACCEPT && i > 0) i--; + break; + } + prev = state; + } + return d; +} + +void +screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload) { + if (!cmd->payload_sz) return; + ensure_space_for_chars(self->lc, cmd->payload_sz + 1); + self->lc->count = decode_utf8_safe_string(payload, cmd->payload_sz, self->lc->chars); + if (!self->lc->count) return; + unsigned width = cmd->width; + if (!width) { + self->lc->chars[self->lc->count] = 0; + width = wcswidth_string(self->lc->chars); + } + if (!width) return; + MultiCellData mcd = { + .width=MIN(width, 15), .scale=MAX(1, MIN(cmd->scale, 15)), .subscale=MIN(cmd->subscale, 3), + .explicitly_set=1, .msb=1 + }; + self->lc->chars[self->lc->count++] = mcd.val; + width = mcd.width * mcd.scale; +} + // }}} // Graphics {{{ diff --git a/kitty/screen.h b/kitty/screen.h index e93eba7b0..af58b8a81 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -265,6 +265,7 @@ void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index void set_active_hyperlink(Screen*, char*, char*); hyperlink_id_type screen_mark_hyperlink(Screen*, index_type, index_type); void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload); +void screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload); bool screen_open_url(Screen*); bool screen_set_last_visited_prompt(Screen*, index_type); bool screen_select_cmd_output(Screen*, index_type); diff --git a/kitty/vt-parser.c b/kitty/vt-parser.c index df789d1b0..7c06d85b4 100644 --- a/kitty/vt-parser.c +++ b/kitty/vt-parser.c @@ -377,6 +377,8 @@ find_st_terminator(PS *self, size_t *end_pos) { // OSC {{{ +#include "parse-multicell-command.h" + static bool is_osc_52(PS *self) { return memcmp(self->buf + self->read.consumed, "52;", 3) == 0; @@ -537,6 +539,9 @@ dispatch_osc(PS *self, uint8_t *buf, size_t limit, bool is_extended_osc) { if (is_extended_osc && code == 52) code = -52; DISPATCH_OSC_WITH_CODE(clipboard_control); END_DISPATCH + case 66: + parse_multicell_code(self, buf + i, limit - 1); + break; case 133: #ifdef DUMP_COMMANDS START_DISPATCH