kitty/gen/apc_parsers.py
Matsumoto Kotaro cc2d7a1789 graphics: add memory-only storage for graphics data
Add a new graphics protocol key, N=1, to request that transmitted
image/frame data is kept only in memory and not written to the graphics
disk cache file.

This is useful for transient high-frequency updates such as video-like
streams, where the latest frame is the only useful data and persisting
each frame to the disk cache causes unnecessary write traffic.

The implementation keeps the existing graphics cache abstraction intact:
memory-only entries can still be read back by animation, composition, and
frame coalescing paths. Only persistence to the disk cache file is skipped.

The default behavior is unchanged when N is omitted or set to zero.
2026-05-30 18:46:24 +09:00

359 lines
12 KiB
Python
Executable file

#!/usr/bin/env python
# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import subprocess
import sys
from collections import defaultdict
from typing import Any, DefaultDict, Union
if __name__ == '__main__' and not __package__:
import __main__
__main__.__package__ = 'gen'
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
KeymapType = dict[str, tuple[str, Union[frozenset[str], str]]]
def resolve_keys(keymap: KeymapType) -> DefaultDict[str, list[str]]:
ans: DefaultDict[str, list[str]] = defaultdict(list)
for ch, (attr, atype) in keymap.items():
if isinstance(atype, str) and atype in ('int', 'uint'):
q = atype
else:
q = 'flag'
ans[q].append(ch)
return ans
def enum(keymap: KeymapType) -> str:
lines = []
for ch, (attr, atype) in keymap.items():
lines.append(f"{attr}='{ch}'")
return '''
enum KEYS {{
{}
}};
'''.format(',\n'.join(lines))
def parse_key(keymap: KeymapType) -> str:
lines = []
for attr, atype in keymap.values():
vs = atype.upper() if isinstance(atype, str) and atype in ('uint', 'int') else 'FLAG'
lines.append(f'case {attr}: value_state = {vs}; break;')
return ' \n'.join(lines)
def parse_flag(keymap: KeymapType, type_map: dict[str, Any], command_class: str) -> str:
lines = []
for ch in type_map['flag']:
attr, allowed_values = keymap[ch]
q = ' && '.join(f"g.{attr} != '{x}'" for x in sorted(allowed_values))
lines.append(f'''
case {attr}: {{
g.{attr} = parser_buf[pos++];
if ({q}) {{
REPORT_ERROR("Malformed {command_class} control block, unknown flag value for {attr}: 0x%x", g.{attr});
return;
}};
}}
break;
''')
return ' \n'.join(lines)
def parse_number(keymap: KeymapType) -> tuple[str, str]:
int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int']
uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint']
return '; '.join(int_keys), '; '.join(uint_keys)
def cmd_for_report(report_name: str, keymap: KeymapType, type_map: dict[str, Any], payload_allowed: bool, payload_is_base64: bool) -> str:
def group(atype: str, conv: str) -> tuple[str, str]:
flag_fmt, flag_attrs = [], []
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
for ch in type_map[atype]:
flag_fmt.append(f's{cv}')
attr = keymap[ch][0]
flag_attrs.append(f'"{attr}", {conv}g.{attr}')
return ' '.join(flag_fmt), ', '.join(flag_attrs)
flag_fmt, flag_attrs = group('flag', '')
int_fmt, int_attrs = group('int', '(int)')
uint_fmt, uint_attrs = group('uint', '(unsigned int)')
fmt = f'{flag_fmt} {uint_fmt} {int_fmt}'
if payload_allowed:
ans = [f'REPORT_VA_COMMAND("K s {{{fmt} ss#}}", self->window_id, "{report_name}",\n']
else:
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:
if payload_is_base64:
ans.append('"", (char*)parser_buf, g.payload_sz')
else:
ans.append('"", (char*)parser_buf + payload_start, g.payload_sz')
ans.append(');')
return '\n'.join(ans)
def generate(
function_name: str,
callback_name: str,
report_name: str,
keymap: KeymapType,
command_class: str,
initial_key: str = 'a',
payload_allowed: bool = True,
payload_is_base64: bool = True,
start_parsing_at: int = 1,
field_sep: str = ',',
post_init: str = '',
) -> str:
type_map = resolve_keys(keymap)
keys_enum = enum(keymap)
handle_key = parse_key(keymap)
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, payload_is_base64)
post_init_line = f'\n {post_init}' if post_init else ''
extra_init = ''
if payload_allowed:
payload_after_value = "case ';': state = PAYLOAD; break;"
payload = ', PAYLOAD'
if payload_is_base64:
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)) {{
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 = sz;
pos = parser_buf_pos;
} 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 = {start_parsing_at};
{extra_init}
enum PARSER_STATES {{ KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE {payload} }};
enum PARSER_STATES state = KEY, value_state = FLAG;
{command_class} g = {{0}};{post_init_line}
unsigned int i, code;
uint64_t lcode; int64_t accumulator;
bool is_negative; (void)is_negative;
size_t sz;
{keys_enum}
enum KEYS key = '{initial_key}';
if (parser_buf[pos] == ';') state = AFTER_VALUE;
while (pos < parser_buf_pos) {{
switch(state) {{
case KEY:
key = parser_buf[pos++];
state = EQUAL;
switch(key) {{
{handle_key}
default:
REPORT_ERROR("Malformed {command_class} control block, invalid key character: 0x%x", key);
return;
}}
break;
case EQUAL:
if (parser_buf[pos++] != '=') {{
REPORT_ERROR("Malformed {command_class} control block, no = after key, found: 0x%x instead", parser_buf[pos-1]);
return;
}}
state = value_state;
break;
case FLAG:
switch(key) {{
{flag_keys}
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 {command_class} 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 {command_class} 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) {{
{int_keys};
default: break;
}}
state = AFTER_VALUE;
break;
#undef I
case UINT:
READ_UINT;
#define U(x) case x: g.x = code; break
switch(key) {{
{uint_keys};
default: break;
}}
state = AFTER_VALUE;
break;
#undef U
#undef READ_UINT
case AFTER_VALUE:
switch (parser_buf[pos++]) {{
default:
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 '{field_sep}':
state = KEY;
break;
{payload_after_value}
}}
break;
{payload_case}
}} // end switch
}} // end while
switch(state) {{
case EQUAL:
REPORT_ERROR("Malformed {command_class} control block, no = after key"); return;
case INT:
case UINT:
REPORT_ERROR("Malformed {command_class} control block, expecting an integer value"); return;
case FLAG:
REPORT_ERROR("Malformed {command_class} control block, expecting a flag value"); return;
default:
break;
}}
{report_cmd}
{callback};
}}
'''
def write_header(text: str, path: str) -> None:
with open(path, 'w') as f:
print(f'// This file is generated by {os.path.basename(__file__)} do not edit!', file=f, end='\n\n')
print('#pragma once', file=f)
print(text, file=f)
subprocess.check_call(['clang-format', '-i', path])
def parsers() -> None:
flag = frozenset
keymap: KeymapType = {
'a': ('action', flag('tTqpdfac')),
'd': ('delete_action', flag('aAiIcCfFnNpPqQrRxXyYzZ')),
't': ('transmission_type', flag('dfts')),
'o': ('compressed', flag('z')),
'f': ('format', 'uint'),
'm': ('more', 'uint'),
'i': ('id', 'uint'),
'I': ('image_number', 'uint'),
'p': ('placement_id', 'uint'),
'q': ('quiet', 'uint'),
'w': ('width', 'uint'),
'h': ('height', 'uint'),
'x': ('x_offset', 'uint'),
'y': ('y_offset', 'uint'),
'v': ('data_height', 'uint'),
's': ('data_width', 'uint'),
'S': ('data_sz', 'uint'),
'O': ('data_offset', 'uint'),
'c': ('num_cells', 'uint'),
'r': ('num_lines', 'uint'),
'X': ('cell_x_offset', 'uint'),
'Y': ('cell_y_offset', 'uint'),
'z': ('z_index', 'int'),
'C': ('cursor_movement', 'uint'),
'U': ('unicode_placement', 'uint'),
'P': ('parent_id', 'uint'),
'Q': ('parent_placement_id', 'uint'),
'N': ('no_disk_cache', 'uint'),
'H': ('offset_from_parent_x', 'int'),
'V': ('offset_from_parent_y', 'int'),
}
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'),
'n': ('subscale_n', 'uint'),
'd': ('subscale_d', 'uint'),
'v': ('vertical_align', 'uint'),
'h': ('horizontal_align', 'uint'),
}
text = generate(
'parse_multicell_code', 'screen_handle_multicell_command', 'multicell_command', keymap, 'MultiCellCommand',
payload_is_base64=False, start_parsing_at=0, field_sep=':')
write_header(text, 'kitty/parse-multicell-command.h')
keymap = {
't': ('type', flag('aAmMrRopPqeEk')),
'm': ('more', 'uint'),
'i': ('client_id', 'uint'),
'o': ('operation', 'uint'),
'x': ('cell_x', 'int'),
'y': ('cell_y', 'int'),
'X': ('pixel_x', 'int'),
'Y': ('pixel_y', 'int'),
}
text = generate(
'parse_dnd_code', 'screen_handle_dnd_command', 'dnd_command', keymap, 'DnDCommand',
payload_is_base64=False, start_parsing_at=0, field_sep=':')
write_header(text, 'kitty/parse-dnd-command.h')
def main(args: list[str]=sys.argv) -> None:
parsers()
if __name__ == '__main__':
import runpy
m = runpy.run_path(os.path.dirname(os.path.abspath(__file__)))
m['main']([sys.executable, 'apc-parsers'])