1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> 4 5import subprocess 6from collections import defaultdict 7from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union 8 9KeymapType = Dict[str, Tuple[str, Union[FrozenSet[str], str]]] 10 11 12def resolve_keys(keymap: KeymapType) -> DefaultDict[str, List[str]]: 13 ans: DefaultDict[str, List[str]] = defaultdict(list) 14 for ch, (attr, atype) in keymap.items(): 15 if isinstance(atype, str) and atype in ('int', 'uint'): 16 q = atype 17 else: 18 q = 'flag' 19 ans[q].append(ch) 20 return ans 21 22 23def enum(keymap: KeymapType) -> str: 24 lines = [] 25 for ch, (attr, atype) in keymap.items(): 26 lines.append(f"{attr}='{ch}'") 27 return ''' 28 enum KEYS {{ 29 {} 30 }}; 31 '''.format(',\n'.join(lines)) 32 33 34def parse_key(keymap: KeymapType) -> str: 35 lines = [] 36 for attr, atype in keymap.values(): 37 vs = atype.upper() if isinstance(atype, str) and atype in ('uint', 'int') else 'FLAG' 38 lines.append(f'case {attr}: value_state = {vs}; break;') 39 return ' \n'.join(lines) 40 41 42def parse_flag(keymap: KeymapType, type_map: Dict[str, Any], command_class: str) -> str: 43 lines = [] 44 for ch in type_map['flag']: 45 attr, allowed_values = keymap[ch] 46 q = ' && '.join(f"g.{attr} != '{x}'" for x in allowed_values) 47 lines.append(f''' 48 case {attr}: {{ 49 g.{attr} = screen->parser_buf[pos++] & 0xff; 50 if ({q}) {{ 51 REPORT_ERROR("Malformed {command_class} control block, unknown flag value for {attr}: 0x%x", g.{attr}); 52 return; 53 }}; 54 }} 55 break; 56 ''') 57 return ' \n'.join(lines) 58 59 60def parse_number(keymap: KeymapType) -> Tuple[str, str]: 61 int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int'] 62 uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint'] 63 return '; '.join(int_keys), '; '.join(uint_keys) 64 65 66def cmd_for_report(report_name: str, keymap: KeymapType, type_map: Dict[str, Any], payload_allowed: bool) -> str: 67 def group(atype: str, conv: str) -> Tuple[str, str]: 68 flag_fmt, flag_attrs = [], [] 69 cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype] 70 for ch in type_map[atype]: 71 flag_fmt.append('s' + cv) 72 attr = keymap[ch][0] 73 flag_attrs.append(f'"{attr}", {conv}g.{attr}') 74 return ' '.join(flag_fmt), ', '.join(flag_attrs) 75 76 flag_fmt, flag_attrs = group('flag', '') 77 int_fmt, int_attrs = group('int', '(int)') 78 uint_fmt, uint_attrs = group('uint', '(unsigned int)') 79 80 fmt = f'{flag_fmt} {uint_fmt} {int_fmt}' 81 if payload_allowed: 82 ans = [f'REPORT_VA_COMMAND("s {{{fmt} sI}} y#", "{report_name}",'] 83 else: 84 ans = [f'REPORT_VA_COMMAND("s {{{fmt}}}", "{report_name}",'] 85 ans.append(',\n '.join((flag_attrs, uint_attrs, int_attrs))) 86 if payload_allowed: 87 ans.append(', "payload_sz", g.payload_sz, payload, g.payload_sz') 88 ans.append(');') 89 return '\n'.join(ans) 90 91 92def generate( 93 function_name: str, 94 callback_name: str, 95 report_name: str, 96 keymap: KeymapType, 97 command_class: str, 98 initial_key: str = 'a', 99 payload_allowed: bool = True 100) -> str: 101 type_map = resolve_keys(keymap) 102 keys_enum = enum(keymap) 103 handle_key = parse_key(keymap) 104 flag_keys = parse_flag(keymap, type_map, command_class) 105 int_keys, uint_keys = parse_number(keymap) 106 report_cmd = cmd_for_report(report_name, keymap, type_map, payload_allowed) 107 if payload_allowed: 108 payload_after_value = "case ';': state = PAYLOAD; break;" 109 payload = ', PAYLOAD' 110 parr = 'static uint8_t payload[4096];' 111 payload_case = f''' 112 case PAYLOAD: {{ 113 sz = screen->parser_buf_pos - pos; 114 const char *err = base64_decode(screen->parser_buf + pos, sz, payload, sizeof(payload), &g.payload_sz); 115 if (err != NULL) {{ REPORT_ERROR("Failed to parse {command_class} command payload with error: %s", err); return; }} 116 pos = screen->parser_buf_pos; 117 }} 118 break; 119 ''' 120 callback = f'{callback_name}(screen, &g, payload)' 121 else: 122 payload_after_value = payload = parr = payload_case = '' 123 callback = f'{callback_name}(screen, &g)' 124 125 return f''' 126static inline void 127{function_name}(Screen *screen, PyObject UNUSED *dump_callback) {{ 128 unsigned int pos = 1; 129 enum PARSER_STATES {{ KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE {payload} }}; 130 enum PARSER_STATES state = KEY, value_state = FLAG; 131 static {command_class} g; 132 unsigned int i, code; 133 uint64_t lcode; 134 bool is_negative; 135 memset(&g, 0, sizeof(g)); 136 size_t sz; 137 {parr} 138 {keys_enum} 139 enum KEYS key = '{initial_key}'; 140 if (screen->parser_buf[pos] == ';') state = AFTER_VALUE; 141 142 while (pos < screen->parser_buf_pos) {{ 143 switch(state) {{ 144 case KEY: 145 key = screen->parser_buf[pos++]; 146 state = EQUAL; 147 switch(key) {{ 148 {handle_key} 149 default: 150 REPORT_ERROR("Malformed {command_class} control block, invalid key character: 0x%x", key); 151 return; 152 }} 153 break; 154 155 case EQUAL: 156 if (screen->parser_buf[pos++] != '=') {{ 157 REPORT_ERROR("Malformed {command_class} control block, no = after key, found: 0x%x instead", screen->parser_buf[pos-1]); 158 return; 159 }} 160 state = value_state; 161 break; 162 163 case FLAG: 164 switch(key) {{ 165 {flag_keys} 166 default: 167 break; 168 }} 169 state = AFTER_VALUE; 170 break; 171 172 case INT: 173#define READ_UINT \\ 174 for (i = pos; i < MIN(screen->parser_buf_pos, pos + 10); i++) {{ \\ 175 if (screen->parser_buf[i] < '0' || screen->parser_buf[i] > '9') break; \\ 176 }} \\ 177 if (i == pos) {{ REPORT_ERROR("Malformed {command_class} control block, expecting an integer value for key: %c", key & 0xFF); return; }} \\ 178 lcode = utoi(screen->parser_buf + pos, i - pos); pos = i; \\ 179 if (lcode > UINT32_MAX) {{ REPORT_ERROR("Malformed {command_class} control block, number is too large"); return; }} \\ 180 code = lcode; 181 182 is_negative = false; 183 if(screen->parser_buf[pos] == '-') {{ is_negative = true; pos++; }} 184#define I(x) case x: g.x = is_negative ? 0 - (int32_t)code : (int32_t)code; break 185 READ_UINT; 186 switch(key) {{ 187 {int_keys}; 188 default: break; 189 }} 190 state = AFTER_VALUE; 191 break; 192#undef I 193 case UINT: 194 READ_UINT; 195#define U(x) case x: g.x = code; break 196 switch(key) {{ 197 {uint_keys}; 198 default: break; 199 }} 200 state = AFTER_VALUE; 201 break; 202#undef U 203#undef READ_UINT 204 205 case AFTER_VALUE: 206 switch (screen->parser_buf[pos++]) {{ 207 default: 208 REPORT_ERROR("Malformed {command_class} control block, expecting a comma or semi-colon after a value, found: 0x%x", 209 screen->parser_buf[pos - 1]); 210 return; 211 case ',': 212 state = KEY; 213 break; 214 {payload_after_value} 215 }} 216 break; 217 218 {payload_case} 219 220 }} // end switch 221 }} // end while 222 223 switch(state) {{ 224 case EQUAL: 225 REPORT_ERROR("Malformed {command_class} control block, no = after key"); return; 226 case INT: 227 case UINT: 228 REPORT_ERROR("Malformed {command_class} control block, expecting an integer value"); return; 229 case FLAG: 230 REPORT_ERROR("Malformed {command_class} control block, expecting a flag value"); return; 231 default: 232 break; 233 }} 234 235 {report_cmd} 236 237 {callback}; 238}} 239 ''' 240 241 242def write_header(text: str, path: str) -> None: 243 with open(path, 'w') as f: 244 print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n') 245 print('#pragma once', file=f) 246 print(text, file=f) 247 subprocess.check_call(['clang-format', '-i', path]) 248 249 250def graphics_parser() -> None: 251 flag = frozenset 252 keymap: KeymapType = { 253 'a': ('action', flag('tTqpdfac')), 254 'd': ('delete_action', flag('aAiIcCfFnNpPqQxXyYzZ')), 255 't': ('transmission_type', flag('dfts')), 256 'o': ('compressed', flag('z')), 257 'f': ('format', 'uint'), 258 'm': ('more', 'uint'), 259 'i': ('id', 'uint'), 260 'I': ('image_number', 'uint'), 261 'p': ('placement_id', 'uint'), 262 'q': ('quiet', 'uint'), 263 'w': ('width', 'uint'), 264 'h': ('height', 'uint'), 265 'x': ('x_offset', 'uint'), 266 'y': ('y_offset', 'uint'), 267 'v': ('data_height', 'uint'), 268 's': ('data_width', 'uint'), 269 'S': ('data_sz', 'uint'), 270 'O': ('data_offset', 'uint'), 271 'c': ('num_cells', 'uint'), 272 'r': ('num_lines', 'uint'), 273 'X': ('cell_x_offset', 'uint'), 274 'Y': ('cell_y_offset', 'uint'), 275 'z': ('z_index', 'int'), 276 'C': ('cursor_movement', 'uint'), 277 } 278 text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand') 279 write_header(text, 'kitty/parse-graphics-command.h') 280 281 282graphics_parser() 283