mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-02 12:44:01 +02:00
Start work on testing multicell commands
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
4
kitty/parse-graphics-command.h
generated
4
kitty/parse-graphics-command.h
generated
@@ -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 ',':
|
||||
|
||||
7
kitty/parse-multicell-command.h
generated
7
kitty/parse-multicell-command.h
generated
@@ -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
|
||||
|
||||
109
kitty/screen.c
109
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)
|
||||
|
||||
63
kitty_tests/multicell.py
Normal file
63
kitty_tests/multicell.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user