diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 045c578d0..3ddf5fb19 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -110,6 +110,7 @@ def generate( payload_allowed: bool = True, payload_is_base64: bool = True, start_parsing_at: int = 1, + field_sep: str = ',', ) -> str: type_map = resolve_keys(keymap) keys_enum = enum(keymap) @@ -140,6 +141,7 @@ def generate( sz = parser_buf_pos - pos; payload_start = pos; g.payload_sz = MAX(BUF_EXTRA, sz); + pos = parser_buf_pos; } break; ''' extra_init = 'size_t payload_start = 0;' @@ -234,10 +236,10 @@ static inline void case AFTER_VALUE: switch (parser_buf[pos++]) {{ default: - REPORT_ERROR("Malformed {command_class} control block, expecting a comma or semi-colon after a value, found: 0x%x", + REPORT_ERROR("Malformed {command_class} control block, expecting a {field_sep} or semi-colon after a value, found: 0x%x", parser_buf[pos - 1]); return; - case ',': + case '{field_sep}': state = KEY; break; {payload_after_value} @@ -318,7 +320,7 @@ def parsers() -> None: } text = generate( 'parse_multicell_code', 'screen_handle_multicell_command', 'multicell_command', keymap, 'MultiCellCommand', - payload_is_base64=False, start_parsing_at=0) + payload_is_base64=False, start_parsing_at=0, field_sep=':') write_header(text, 'kitty/parse-multicell-command.h') diff --git a/kitty/data-types.c b/kitty/data-types.c index 301b561b8..c452ecb0c 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -783,6 +783,7 @@ PyInit_fast_data_types(void) { PyModule_AddIntMacro(m, ESC_APC); PyModule_AddIntMacro(m, ESC_DCS); PyModule_AddIntMacro(m, ESC_PM); + PyModule_AddIntMacro(m, TEXT_SIZE_CODE); #ifdef __APPLE__ // Apple says its SHM_NAME_MAX but SHM_NAME_MAX is not actually declared in typical CrApple style. // This value is based on experimentation and from qsharedmemory.cpp in Qt diff --git a/kitty/line.h b/kitty/line.h index ea6525a96..04d98f6e9 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -17,6 +17,8 @@ // 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 +// TODO: Handle rewrap of multiline chars +// TODO: Handle rewrap when a character is too wide/tall to fit on resized screen typedef union CellAttrs { struct { diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index c63af726f..622dcb3c4 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -310,8 +310,8 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, case AFTER_VALUE: switch (parser_buf[pos++]) { default: - REPORT_ERROR("Malformed GraphicsCommand control block, expecting a " - "comma or semi-colon after a value, found: 0x%x", + REPORT_ERROR("Malformed GraphicsCommand control block, expecting a , " + "or semi-colon after a value, found: 0x%x", parser_buf[pos - 1]); return; case ',': diff --git a/kitty/parse-multicell-command.h b/kitty/parse-multicell-command.h index 5514b6306..bb4aacf57 100644 --- a/kitty/parse-multicell-command.h +++ b/kitty/parse-multicell-command.h @@ -128,11 +128,11 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, 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", + REPORT_ERROR("Malformed MultiCellCommand control block, expecting a : " + "or semi-colon after a value, found: 0x%x", parser_buf[pos - 1]); return; - case ',': + case ':': state = KEY; break; case ';': @@ -145,6 +145,7 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, sz = parser_buf_pos - pos; payload_start = pos; g.payload_sz = MAX(BUF_EXTRA, sz); + pos = parser_buf_pos; } break; } // end switch diff --git a/kitty/screen.c b/kitty/screen.c index c31e0a3f5..ea96e88bb 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1057,20 +1057,23 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_ #undef init_line } +#define PREPARE_FOR_DRAW_TEXT \ + const bool force_underline = OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && self->active_hyperlink_id != 0; \ + CellAttrs attrs = cursor_to_attrs(self->cursor); \ + if (force_underline) attrs.decoration = OPT(url_style); \ + text_loop_state s={ \ + .cc=(CPUCell){.hyperlink_id=self->active_hyperlink_id}, \ + .g=(GPUCell){ \ + .attrs=attrs, \ + .fg=self->cursor->fg & COL_MASK, .bg=self->cursor->bg & COL_MASK, \ + .decoration_fg=force_underline ? ((OPT(url_color) & COL_MASK) << 8) | 2 : self->cursor->decoration_fg & COL_MASK, \ + } \ + }; + static void draw_text(Screen *self, const uint32_t *chars, size_t num_chars) { + PREPARE_FOR_DRAW_TEXT; self->is_dirty = true; - const bool force_underline = OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && self->active_hyperlink_id != 0; - CellAttrs attrs = cursor_to_attrs(self->cursor); - if (force_underline) attrs.decoration = OPT(url_style); - text_loop_state s={ - .cc=(CPUCell){.hyperlink_id=self->active_hyperlink_id}, - .g=(GPUCell){ - .attrs=attrs, - .fg=self->cursor->fg & COL_MASK, .bg=self->cursor->bg & COL_MASK, - .decoration_fg=force_underline ? ((OPT(url_color) & COL_MASK) << 8) | 2 : self->cursor->decoration_fg & COL_MASK, - } - }; draw_text_loop(self, chars, num_chars, &s); } @@ -1118,11 +1121,12 @@ decode_utf8_safe_string(const uint8_t *src, size_t sz, uint32_t *dest) { void screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload) { + screen_on_input(self); 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; + index_type width = cmd->width; if (!width) { self->lc->chars[self->lc->count] = 0; width = wcswidth_string(self->lc->chars); @@ -1134,6 +1138,45 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const }; self->lc->chars[self->lc->count++] = mcd.val; width = mcd.width * mcd.scale; + index_type height = mcd.scale; + index_type max_height = self->margin_bottom - self->margin_top + 1; + if (width > self->columns || height > max_height) return; + PREPARE_FOR_DRAW_TEXT; + if (self->columns < self->cursor->x + width) { + if (self->modes.mDECAWM) { + continue_to_next_line(self); + } else { + self->cursor->x = self->columns - width; + CPUCell *cp = linebuf_cpu_cell_at(self->linebuf, self->cursor->x, self->cursor->y); + if (cp->is_multicell) replace_multicell_char_under_cursor_with_spaces(self); + } + } + if (height > 1) { + index_type available_height = self->margin_bottom - self->cursor->y + 1; + if (height > available_height) { + index_type extra_lines = height - available_height; + screen_scroll(self, extra_lines); + self->cursor->y -= extra_lines; + } + } + if (self->modes.mIRM) { + for (index_type y = self->cursor->y; y < self->cursor->y + height; y++) { + if (self->modes.mIRM) insert_characters(self, self->cursor->x, width, y, true); + } + } + CPUCell c = s.cc; + c.ch_is_idx = true; c.ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); + c.is_multicell = true; + for (index_type y = self->cursor->y; y < self->cursor->y + height; y++) { + linebuf_init_cells(self->linebuf, y, &s.cp, &s.gp); + linebuf_mark_line_dirty(self->linebuf, y); + c.x = 0; c.y = y - self->cursor->y; + for (index_type x = self->cursor->x; x < self->cursor->x + width; x++, c.x++) { + s.cp[x] = c; s.gp[x] = s.g; + } + } + self->cursor->x += width; + self->is_dirty = true; } // }}} @@ -5068,6 +5111,47 @@ test_parse_written_data(Screen *screen, PyObject *args) { Py_RETURN_NONE; } +static PyObject* +multicell_data_as_dict(MultiCellData mcd) { + if (!mcd.msb) { PyErr_SetString(PyExc_RuntimeError, "mcd does not have its msb set"); return NULL; } + return Py_BuildValue("{sI sI sI sO sI}", "scale", (unsigned int)mcd.scale, "width", (unsigned int)mcd.width, "subscale", (unsigned int)mcd.subscale, "explicitly_set", mcd.explicitly_set ? Py_True : Py_False, "vertical_align", mcd.vertical_align); +} + +static PyObject* +cpu_cell_as_dict(CPUCell *c, TextCache *tc, ListOfChars *lc, HYPERLINK_POOL_HANDLE h) { + text_in_cell(c, tc, lc); + RAII_PyObject(mcd, lc->is_multicell ? multicell_data_as_dict((MultiCellData){.val=lc->chars[lc->count]}) : Py_NewRef(Py_None)); + if ((lc->is_multicell && !lc->is_topleft) || (lc->count == 1 && lc->chars[0] == 0)) lc->count = 0; + RAII_PyObject(text, PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lc->chars, lc->count)); + const char *url = c->hyperlink_id ? get_hyperlink_for_id(h, c->hyperlink_id, false) : NULL; + RAII_PyObject(hyperlink, url ? PyUnicode_FromString(url) : Py_NewRef(Py_None)); + return Py_BuildValue("{sO sO sI sI sO sO}", + "text", text, "hyperlink", hyperlink, "x", (unsigned int)c->x, "y", (unsigned int)c->y, + "mcd", mcd, "next_char_was_wrapped", c->next_char_was_wrapped ? Py_True : Py_False + ); +} + +static PyObject* +cpu_cells(Screen *self, PyObject *args) { + int y, x = -1; + if (!PyArg_ParseTuple(args, "i|i", &y, &x)) return NULL; + if (y < 0 || y >= (int)self->lines) { PyErr_SetString(PyExc_IndexError, "y out of bounds"); return NULL; } + if (x > -1) { + if (x >= (int)self->columns) { PyErr_SetString(PyExc_IndexError, "x out of bounds"); return NULL; } + return cpu_cell_as_dict(linebuf_cpu_cell_at(self->linebuf, x, y), self->text_cache, self->lc, self->hyperlink_pool); + } + index_type start_x = 0, x_limit = self->columns; + RAII_PyObject(ans, PyTuple_New(x_limit - start_x)); + if (ans) { + for (index_type x = start_x; x < x_limit; x++) { + PyObject *d = cpu_cell_as_dict(linebuf_cpu_cell_at(self->linebuf, x, y), self->text_cache, self->lc, self->hyperlink_pool); + if (!d) return NULL; + PyTuple_SET_ITEM(ans, x, d); + } + } + return Py_NewRef(ans); +} + static PyMethodDef methods[] = { METHODB(test_create_write_buffer, METH_NOARGS), METHODB(test_commit_write_buffer, METH_VARARGS), @@ -5075,6 +5159,7 @@ static PyMethodDef methods[] = { MND(line_edge_colors, METH_NOARGS) MND(line, METH_O) MND(dump_lines_with_attrs, METH_VARARGS) + MND(cpu_cells, METH_VARARGS) MND(cursor_at_prompt, METH_NOARGS) MND(visual_line, METH_VARARGS) MND(current_url_text, METH_NOARGS) diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py new file mode 100644 index 000000000..073c1b70b --- /dev/null +++ b/kitty_tests/multicell.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2024, Kovid Goyal + + +from kitty.fast_data_types import TEXT_SIZE_CODE, Screen + +from . import BaseTest, parse_bytes + + +class TestMulticell(BaseTest): + + def test_multicell(self): + test_multicell(self) + + +def multicell(screen: Screen, text: str, width: int = 0, scale: int = 1, subscale: int = 0) -> None: + cmd = f'\x1b]{TEXT_SIZE_CODE};w={width}:s={scale}:f={subscale};{text}\a' + parse_bytes(screen, cmd.encode()) + + +def test_multicell(self: TestMulticell) -> None: + + def ac(x_, y_, **assertions): # assert cell + cell = s.cpu_cells(y_, x_) + msg = f'Assertion failed for cell at ({x_}, {y_})\n{cell}\n' + if 'is_multicell' in assertions: + if assertions['is_multicell']: + assert cell['mcd'] is not None, msg + else: + assert cell['mcd'] is None, msg + def ae(key): + if key not in assertions: + return + if key in cell: + val = cell[key] + else: + mcd = cell['mcd'] + if mcd is None: + raise AssertionError(f'{msg}Unexpectedly not a multicell') + val = mcd[key] + assert assertions[key] == val, f'{msg}{assertions[key]!r} != {val!r}' + + ae('x') + ae('y') + ae('width') + ae('scale') + ae('subscale') + ae('vertical_align') + ae('text') + ae('explicitly_set') + if 'cursor' in assertions: + self.ae(assertions['cursor'], (s.cursor.x, s.cursor.y), msg) + + s = self.create_screen(cols=5, lines=5) + ac(0, 0, is_multicell=False) + multicell(s, 'a') + ac(0, 0, is_multicell=True, width=1, scale=1, subscale=0, x=0, y=0, text='a', explicitly_set=True, cursor=(1, 0)) + ac(0, 1, is_multicell=False), ac(1, 0, is_multicell=False), ac(1, 1, is_multicell=False) + s.draw('莊') + ac(0, 0, is_multicell=True, width=1, scale=1, subscale=0, x=0, y=0, text='a', explicitly_set=True) + ac(1, 0, is_multicell=True, width=2, scale=1, subscale=0, x=0, y=0, text='莊', explicitly_set=False, cursor=(3, 0)) + ac(2, 0, is_multicell=True, width=2, scale=1, subscale=0, x=1, y=0, text='', explicitly_set=False) + ac(1, 0, is_multicell=False), ac(1, 1, is_multicell=False)