Start work on handling multicell escape code

This commit is contained in:
Kovid Goyal 2024-11-14 11:33:56 +05:30
parent b5dff921c4
commit 540e030f96
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 323 additions and 51 deletions

1
.gitattributes vendored
View file

@ -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

View file

@ -85,12 +85,17 @@ 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}", ']
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__':

View file

@ -4,6 +4,7 @@
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>

View file

@ -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 *);

View file

@ -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);
}

179
kitty/parse-multicell-command.h generated Normal file
View file

@ -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);
}

View file

@ -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 {{{

View file

@ -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);

View file

@ -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