Skip to content

Commit

Permalink
Start work on testing multicell commands
Browse files Browse the repository at this point in the history
  • Loading branch information
kovidgoyal committed Nov 15, 2024
1 parent 7cdf0bc commit 86eabd7
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 20 deletions.
8 changes: 5 additions & 3 deletions gen/apc_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;'
Expand Down Expand Up @@ -234,10 +236,10 @@ def generate(
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}
Expand Down Expand Up @@ -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')


Expand Down
1 change: 1 addition & 0 deletions kitty/data-types.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions kitty/line.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions kitty/parse-graphics-command.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions kitty/parse-multicell-command.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 97 additions & 12 deletions kitty/screen.c
Original file line number Diff line number Diff line change
Expand Up @@ -1053,20 +1053,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);
}

Expand Down Expand Up @@ -1114,11 +1117,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);
Expand All @@ -1130,6 +1134,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;
}

// }}}
Expand Down Expand Up @@ -5047,13 +5090,55 @@ 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),
METHODB(test_parse_written_data, METH_VARARGS),
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)
Expand Down
63 changes: 63 additions & 0 deletions kitty_tests/multicell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>


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)

0 comments on commit 86eabd7

Please sign in to comment.