1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net> 4 5 6import os 7import re 8import sys 9from typing import ( 10 Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, 11 Union 12) 13 14import kitty.fast_data_types as defines 15from kitty.conf.utils import ( 16 KeyAction, key_func, positive_float, positive_int, python_string, to_bool, 17 to_cmdline, to_color, uniq, unit_float 18) 19from kitty.constants import config_dir, is_macos 20from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE 21from kitty.fonts import FontFeature 22from kitty.key_names import ( 23 character_key_name_aliases, functional_key_name_aliases, 24 get_key_name_lookup 25) 26from kitty.rgb import Color, color_as_int 27from kitty.types import FloatEdges, MouseEvent, SingleKey 28from kitty.utils import expandvars, log_error 29 30KeyMap = Dict[SingleKey, KeyAction] 31MouseMap = Dict[MouseEvent, KeyAction] 32KeySequence = Tuple[SingleKey, ...] 33SubSequenceMap = Dict[KeySequence, KeyAction] 34SequenceMap = Dict[SingleKey, SubSequenceMap] 35MINIMUM_FONT_SIZE = 4 36default_tab_separator = ' ┇' 37mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', 38 '⌥': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'} 39character_key_name_aliases_with_ascii_lowercase: Dict[str, str] = character_key_name_aliases.copy() 40for x in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': 41 character_key_name_aliases_with_ascii_lowercase[x] = x.lower() 42sequence_sep = '>' 43func_with_args, args_funcs = key_func() 44FuncArgsType = Tuple[str, Sequence[Any]] 45 46 47class InvalidMods(ValueError): 48 pass 49 50 51# Actions {{{ 52@func_with_args( 53 'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window', 54 'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd', 55 'launch' 56 ) 57def shlex_parse(func: str, rest: str) -> FuncArgsType: 58 return func, to_cmdline(rest) 59 60 61@func_with_args('combine') 62def combine_parse(func: str, rest: str) -> FuncArgsType: 63 sep, rest = rest.split(maxsplit=1) 64 parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest) 65 args = tuple(map(parse_key_action, filter(None, parts))) 66 return func, args 67 68 69def parse_send_text_bytes(text: str) -> bytes: 70 return python_string(text).encode('utf-8') 71 72 73@func_with_args('send_text') 74def send_text_parse(func: str, rest: str) -> FuncArgsType: 75 args = rest.split(maxsplit=1) 76 mode = '' 77 data = b'' 78 if len(args) > 1: 79 mode = args[0] 80 try: 81 data = parse_send_text_bytes(args[1]) 82 except Exception: 83 log_error('Ignoring invalid send_text string: ' + args[1]) 84 return func, [mode, data] 85 86 87@func_with_args('run_kitten', 'run_simple_kitten', 'kitten') 88def kitten_parse(func: str, rest: str) -> FuncArgsType: 89 if func == 'kitten': 90 args = rest.split(maxsplit=1) 91 else: 92 args = rest.split(maxsplit=2)[1:] 93 func = 'kitten' 94 return func, args 95 96 97@func_with_args('goto_tab') 98def goto_tab_parse(func: str, rest: str) -> FuncArgsType: 99 args = (max(0, int(rest)), ) 100 return func, args 101 102 103@func_with_args('detach_window') 104def detach_window_parse(func: str, rest: str) -> FuncArgsType: 105 if rest not in ('new', 'new-tab', 'ask', 'tab-prev', 'tab-left', 'tab-right'): 106 log_error('Ignoring invalid detach_window argument: {}'.format(rest)) 107 rest = 'new' 108 return func, (rest,) 109 110 111@func_with_args('detach_tab') 112def detach_tab_parse(func: str, rest: str) -> FuncArgsType: 113 if rest not in ('new', 'ask'): 114 log_error('Ignoring invalid detach_tab argument: {}'.format(rest)) 115 rest = 'new' 116 return func, (rest,) 117 118 119@func_with_args('set_background_opacity', 'goto_layout', 'toggle_layout', 'kitty_shell') 120def simple_parse(func: str, rest: str) -> FuncArgsType: 121 return func, [rest] 122 123 124@func_with_args('set_font_size') 125def float_parse(func: str, rest: str) -> FuncArgsType: 126 return func, (float(rest),) 127 128 129@func_with_args('signal_child') 130def signal_child_parse(func: str, rest: str) -> FuncArgsType: 131 import signal 132 signals = [] 133 for q in rest.split(): 134 try: 135 signum = getattr(signal, q.upper()) 136 except AttributeError: 137 log_error(f'Unknown signal: {rest} ignoring') 138 else: 139 signals.append(signum) 140 return func, tuple(signals) 141 142 143@func_with_args('change_font_size') 144def parse_change_font_size(func: str, rest: str) -> Tuple[str, Tuple[bool, Optional[str], float]]: 145 vals = rest.strip().split(maxsplit=1) 146 if len(vals) != 2: 147 log_error('Invalid change_font_size specification: {}, treating it as default'.format(rest)) 148 return func, (True, None, 0) 149 c_all = vals[0].lower() == 'all' 150 sign: Optional[str] = None 151 amt = vals[1] 152 if amt[0] in '+-': 153 sign = amt[0] 154 amt = amt[1:] 155 return func, (c_all, sign, float(amt.strip())) 156 157 158@func_with_args('clear_terminal') 159def clear_terminal(func: str, rest: str) -> FuncArgsType: 160 vals = rest.strip().split(maxsplit=1) 161 if len(vals) != 2: 162 log_error('clear_terminal needs two arguments, using defaults') 163 args: List[Union[str, bool]] = ['reset', 'active'] 164 else: 165 args = [vals[0].lower(), vals[1].lower() == 'active'] 166 return func, args 167 168 169@func_with_args('copy_to_buffer') 170def copy_to_buffer(func: str, rest: str) -> FuncArgsType: 171 return func, [rest] 172 173 174@func_with_args('paste_from_buffer') 175def paste_from_buffer(func: str, rest: str) -> FuncArgsType: 176 return func, [rest] 177 178 179@func_with_args('neighboring_window') 180def neighboring_window(func: str, rest: str) -> FuncArgsType: 181 rest = rest.lower() 182 rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest) 183 if rest not in ('left', 'right', 'top', 'bottom'): 184 log_error('Invalid neighbor specification: {}'.format(rest)) 185 rest = 'right' 186 return func, [rest] 187 188 189@func_with_args('resize_window') 190def resize_window(func: str, rest: str) -> FuncArgsType: 191 vals = rest.strip().split(maxsplit=1) 192 if len(vals) > 2: 193 log_error('resize_window needs one or two arguments, using defaults') 194 args = ['wider', 1] 195 else: 196 quality = vals[0].lower() 197 if quality not in ('taller', 'shorter', 'wider', 'narrower'): 198 log_error('Invalid quality specification: {}'.format(quality)) 199 quality = 'wider' 200 increment = 1 201 if len(vals) == 2: 202 try: 203 increment = int(vals[1]) 204 except Exception: 205 log_error('Invalid increment specification: {}'.format(vals[1])) 206 args = [quality, increment] 207 return func, args 208 209 210@func_with_args('move_window') 211def move_window(func: str, rest: str) -> FuncArgsType: 212 rest = rest.lower() 213 rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest) 214 prest: Union[int, str] = rest 215 try: 216 prest = int(prest) 217 except Exception: 218 if prest not in ('left', 'right', 'top', 'bottom'): 219 log_error('Invalid move_window specification: {}'.format(rest)) 220 prest = 0 221 return func, [prest] 222 223 224@func_with_args('pipe') 225def pipe(func: str, rest: str) -> FuncArgsType: 226 import shlex 227 r = shlex.split(rest) 228 if len(r) < 3: 229 log_error('Too few arguments to pipe function') 230 r = ['none', 'none', 'true'] 231 return func, r 232 233 234@func_with_args('set_colors') 235def set_colors(func: str, rest: str) -> FuncArgsType: 236 import shlex 237 r = shlex.split(rest) 238 if len(r) < 1: 239 log_error('Too few arguments to set_colors function') 240 return func, r 241 242 243@func_with_args('remote_control') 244def remote_control(func: str, rest: str) -> FuncArgsType: 245 import shlex 246 r = shlex.split(rest) 247 if len(r) < 1: 248 log_error('Too few arguments to remote_control function') 249 return func, r 250 251 252@func_with_args('nth_window') 253def nth_window(func: str, rest: str) -> FuncArgsType: 254 try: 255 num = int(rest) 256 except Exception: 257 log_error('Invalid nth_window number: {}'.format(rest)) 258 num = 1 259 return func, [num] 260 261 262@func_with_args('disable_ligatures_in') 263def disable_ligatures_in(func: str, rest: str) -> FuncArgsType: 264 parts = rest.split(maxsplit=1) 265 if len(parts) == 1: 266 where, strategy = 'active', parts[0] 267 else: 268 where, strategy = parts 269 if where not in ('active', 'all', 'tab'): 270 raise ValueError('{} is not a valid set of windows to disable ligatures in'.format(where)) 271 if strategy not in ('never', 'always', 'cursor'): 272 raise ValueError('{} is not a valid disable ligatures strategy'.format(strategy)) 273 return func, [where, strategy] 274 275 276@func_with_args('layout_action') 277def layout_action(func: str, rest: str) -> FuncArgsType: 278 parts = rest.split(maxsplit=1) 279 if not parts: 280 raise ValueError('layout_action must have at least one argument') 281 return func, [parts[0], tuple(parts[1:])] 282 283 284def parse_marker_spec(ftype: str, parts: Sequence[str]) -> Tuple[str, Union[str, Tuple[Tuple[int, str], ...]], int]: 285 flags = re.UNICODE 286 if ftype in ('text', 'itext', 'regex', 'iregex'): 287 if ftype.startswith('i'): 288 flags |= re.IGNORECASE 289 if not parts or len(parts) % 2 != 0: 290 raise ValueError('No color specified in marker: {}'.format(' '.join(parts))) 291 ans = [] 292 for i in range(0, len(parts), 2): 293 try: 294 color = max(1, min(int(parts[i]), 3)) 295 except Exception: 296 raise ValueError('color {} in marker specification is not an integer'.format(parts[i])) 297 sspec = parts[i + 1] 298 if 'regex' not in ftype: 299 sspec = re.escape(sspec) 300 ans.append((color, sspec)) 301 ftype = 'regex' 302 spec: Union[str, Tuple[Tuple[int, str], ...]] = tuple(ans) 303 elif ftype == 'function': 304 spec = ' '.join(parts) 305 else: 306 raise ValueError('Unknown marker type: {}'.format(ftype)) 307 return ftype, spec, flags 308 309 310@func_with_args('toggle_marker') 311def toggle_marker(func: str, rest: str) -> FuncArgsType: 312 import shlex 313 parts = rest.split(maxsplit=1) 314 if len(parts) != 2: 315 raise ValueError('{} is not a valid marker specification'.format(rest)) 316 ftype, spec = parts 317 parts = shlex.split(spec) 318 return func, list(parse_marker_spec(ftype, parts)) 319 320 321@func_with_args('scroll_to_mark') 322def scroll_to_mark(func: str, rest: str) -> FuncArgsType: 323 parts = rest.split() 324 if not parts or not rest: 325 return func, [True, 0] 326 if len(parts) == 1: 327 q = parts[0].lower() 328 if q in ('prev', 'previous', 'next'): 329 return func, [q != 'next', 0] 330 try: 331 return func, [True, max(0, min(int(q), 3))] 332 except Exception: 333 raise ValueError('{} is not a valid scroll_to_mark destination'.format(rest)) 334 return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))] 335 336 337@func_with_args('mouse_selection') 338def mouse_selection(func: str, rest: str) -> FuncArgsType: 339 cmap = getattr(mouse_selection, 'code_map', None) 340 if cmap is None: 341 cmap = { 342 'normal': defines.MOUSE_SELECTION_NORMAL, 343 'extend': defines.MOUSE_SELECTION_EXTEND, 344 'move-end': defines.MOUSE_SELECTION_MOVE_END, 345 'rectangle': defines.MOUSE_SELECTION_RECTANGLE, 346 'word': defines.MOUSE_SELECTION_WORD, 347 'line': defines.MOUSE_SELECTION_LINE, 348 'line_from_point': defines.MOUSE_SELECTION_LINE_FROM_POINT, 349 } 350 setattr(mouse_selection, 'code_map', cmap) 351 return func, [cmap[rest]] 352 353 354@func_with_args('load_config_file') 355def load_config_file(func: str, rest: str) -> FuncArgsType: 356 import shlex 357 return func, shlex.split(rest) 358# }}} 359 360 361def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]: 362 363 def map_mod(m: str) -> str: 364 return mod_map.get(m, m) 365 366 mods = 0 367 for m in parts: 368 try: 369 mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper())) 370 except AttributeError: 371 if m.upper() != 'NONE': 372 log_error('Shortcut: {} has unknown modifier, ignoring'.format(sc)) 373 return None 374 375 return mods 376 377 378def to_modifiers(val: str) -> int: 379 return parse_mods(val.split('+'), val) or 0 380 381 382def parse_shortcut(sc: str) -> SingleKey: 383 if sc.endswith('+') and len(sc) > 1: 384 sc = sc[:-1] + 'plus' 385 parts = sc.split('+') 386 mods = 0 387 if len(parts) > 1: 388 mods = parse_mods(parts[:-1], sc) or 0 389 if not mods: 390 raise InvalidMods('Invalid shortcut') 391 q = parts[-1] 392 q = character_key_name_aliases_with_ascii_lowercase.get(q.upper(), q) 393 is_native = False 394 if q.startswith('0x'): 395 try: 396 key = int(q, 16) 397 except Exception: 398 key = 0 399 else: 400 is_native = True 401 else: 402 try: 403 key = ord(q) 404 except Exception: 405 uq = q.upper() 406 uq = functional_key_name_aliases.get(uq, uq) 407 x: Optional[int] = getattr(defines, f'GLFW_FKEY_{uq}', None) 408 if x is None: 409 lf = get_key_name_lookup() 410 key = lf(q, False) or 0 411 is_native = key > 0 412 else: 413 key = x 414 415 return SingleKey(mods, is_native, key or 0) 416 417 418def adjust_line_height(x: str) -> Union[int, float]: 419 if x.endswith('%'): 420 ans = float(x[:-1].strip()) / 100.0 421 if ans < 0: 422 log_error('Percentage adjustments of cell sizes must be positive numbers') 423 return 0 424 return ans 425 return int(x) 426 427 428def adjust_baseline(x: str) -> Union[int, float]: 429 if x.endswith('%'): 430 ans = float(x[:-1].strip()) / 100.0 431 if abs(ans) > 1: 432 log_error('Percentage adjustments of the baseline cannot exceed 100%') 433 return 0 434 return ans 435 return int(x) 436 437 438def to_font_size(x: str) -> float: 439 return max(MINIMUM_FONT_SIZE, float(x)) 440 441 442def disable_ligatures(x: str) -> int: 443 cmap = {'never': 0, 'cursor': 1, 'always': 2} 444 return cmap.get(x.lower(), 0) 445 446 447def box_drawing_scale(x: str) -> Tuple[float, float, float, float]: 448 ans = tuple(float(q.strip()) for q in x.split(',')) 449 if len(ans) != 4: 450 raise ValueError('Invalid box_drawing scale, must have four entries') 451 return ans[0], ans[1], ans[2], ans[3] 452 453 454def cursor_text_color(x: str) -> Optional[Color]: 455 if x.lower() == 'background': 456 return None 457 return to_color(x) 458 459 460cshapes = { 461 'block': CURSOR_BLOCK, 462 'beam': CURSOR_BEAM, 463 'underline': CURSOR_UNDERLINE 464} 465 466 467def to_cursor_shape(x: str) -> int: 468 try: 469 return cshapes[x.lower()] 470 except KeyError: 471 raise ValueError( 472 'Invalid cursor shape: {} allowed values are {}'.format( 473 x, ', '.join(cshapes) 474 ) 475 ) 476 477 478def scrollback_lines(x: str) -> int: 479 ans = int(x) 480 if ans < 0: 481 ans = 2 ** 32 - 1 482 return ans 483 484 485def scrollback_pager_history_size(x: str) -> int: 486 ans = int(max(0, float(x)) * 1024 * 1024) 487 return min(ans, 4096 * 1024 * 1024 - 1) 488 489 490def url_style(x: str) -> int: 491 return url_style_map.get(x, url_style_map['curly']) 492 493 494url_style_map = dict( 495 ((v, i) for i, v in enumerate('none single double curly'.split())) 496) 497 498 499def url_prefixes(x: str) -> Tuple[str, ...]: 500 return tuple(a.lower() for a in x.replace(',', ' ').split()) 501 502 503def copy_on_select(raw: str) -> str: 504 q = raw.lower() 505 # boolean values special cased for backwards compat 506 if q in ('y', 'yes', 'true', 'clipboard'): 507 return 'clipboard' 508 if q in ('n', 'no', 'false', ''): 509 return '' 510 return raw 511 512 513def window_size(val: str) -> Tuple[int, str]: 514 val = val.lower() 515 unit = 'cells' if val.endswith('c') else 'px' 516 return positive_int(val.rstrip('c')), unit 517 518 519def to_layout_names(raw: str) -> List[str]: 520 from kitty.layout.interface import all_layouts 521 parts = [x.strip().lower() for x in raw.split(',')] 522 ans: List[str] = [] 523 for p in parts: 524 if p in ('*', 'all'): 525 ans.extend(sorted(all_layouts)) 526 continue 527 name = p.partition(':')[0] 528 if name not in all_layouts: 529 raise ValueError('The window layout {} is unknown'.format(p)) 530 ans.append(p) 531 return uniq(ans) 532 533 534def window_border_width(x: Union[str, int, float]) -> Tuple[float, str]: 535 unit = 'pt' 536 if isinstance(x, str): 537 trailer = x[-2:] 538 if trailer in ('px', 'pt'): 539 unit = trailer 540 val = float(x[:-2]) 541 else: 542 val = float(x) 543 else: 544 val = float(x) 545 return max(0, val), unit 546 547 548def edge_width(x: str, converter: Callable[[str], float] = positive_float) -> FloatEdges: 549 parts = str(x).split() 550 num = len(parts) 551 if num == 1: 552 val = converter(parts[0]) 553 return FloatEdges(val, val, val, val) 554 if num == 2: 555 v = converter(parts[0]) 556 h = converter(parts[1]) 557 return FloatEdges(h, v, h, v) 558 if num == 3: 559 top, h, bottom = map(converter, parts) 560 return FloatEdges(h, top, h, bottom) 561 top, right, bottom, left = map(converter, parts) 562 return FloatEdges(left, top, right, bottom) 563 564 565def optional_edge_width(x: str) -> FloatEdges: 566 return edge_width(x, float) 567 568 569def hide_window_decorations(x: str) -> int: 570 if x == 'titlebar-only': 571 return 0b10 572 if to_bool(x): 573 return 0b01 574 return 0b00 575 576 577def resize_draw_strategy(x: str) -> int: 578 cmap = {'static': 0, 'scale': 1, 'blank': 2, 'size': 3} 579 return cmap.get(x.lower(), 0) 580 581 582def tab_separator(x: str) -> str: 583 for q in '\'"': 584 if x.startswith(q) and x.endswith(q): 585 x = x[1:-1] 586 if not x: 587 return '' 588 break 589 if not x.strip(): 590 x = ('\xa0' * len(x)) if x else default_tab_separator 591 return x 592 593 594def tab_bar_edge(x: str) -> int: 595 return {'top': 1, 'bottom': 3}.get(x.lower(), 3) 596 597 598def tab_font_style(x: str) -> Tuple[bool, bool]: 599 return { 600 'bold-italic': (True, True), 601 'bold': (True, False), 602 'italic': (False, True) 603 }.get(x.lower().replace('_', '-'), (False, False)) 604 605 606def tab_bar_min_tabs(x: str) -> int: 607 return max(1, positive_int(x)) 608 609 610def tab_fade(x: str) -> Tuple[float, ...]: 611 return tuple(map(unit_float, x.split())) 612 613 614def tab_activity_symbol(x: str) -> Optional[str]: 615 if x == 'none': 616 return None 617 return tab_title_template(x) or None 618 619 620def tab_title_template(x: str) -> str: 621 if x: 622 for q in '\'"': 623 if x.startswith(q) and x.endswith(q): 624 x = x[1:-1] 625 break 626 return x 627 628 629def active_tab_title_template(x: str) -> Optional[str]: 630 x = tab_title_template(x) 631 return None if x == 'none' else x 632 633 634def config_or_absolute_path(x: str) -> Optional[str]: 635 if x.lower() == 'none': 636 return None 637 x = os.path.expanduser(x) 638 x = os.path.expandvars(x) 639 if not os.path.isabs(x): 640 x = os.path.join(config_dir, x) 641 return x 642 643 644def allow_remote_control(x: str) -> str: 645 if x != 'socket-only': 646 x = 'y' if to_bool(x) else 'n' 647 return x 648 649 650def clipboard_control(x: str) -> Tuple[str, ...]: 651 return tuple(x.lower().split()) 652 653 654def allow_hyperlinks(x: str) -> int: 655 if x == 'ask': 656 return 0b11 657 return 1 if to_bool(x) else 0 658 659 660def macos_titlebar_color(x: str) -> int: 661 x = x.strip('"') 662 if x == 'system': 663 return 0 664 if x == 'background': 665 return 1 666 return (color_as_int(to_color(x)) << 8) | 2 667 668 669def macos_option_as_alt(x: str) -> int: 670 x = x.lower() 671 if x == 'both': 672 return 0b11 673 if x == 'left': 674 return 0b10 675 if x == 'right': 676 return 0b01 677 if to_bool(x): 678 return 0b11 679 return 0 680 681 682class TabBarMarginHeight(NamedTuple): 683 outer: float = 0 684 inner: float = 0 685 686 def __bool__(self) -> bool: 687 return (self.outer + self.inner) > 0 688 689 690def tab_bar_margin_height(x: str) -> TabBarMarginHeight: 691 parts = x.split(maxsplit=1) 692 if len(parts) != 2: 693 log_error(f'Invalid tab_bar_margin_height: {tab_bar_margin_height}, ignoring') 694 return TabBarMarginHeight() 695 ans = map(positive_float, parts) 696 return TabBarMarginHeight(next(ans), next(ans)) 697 698 699def clear_all_mouse_actions(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> bool: 700 ans = to_bool(val) 701 if ans and dict_with_parse_results is not None: 702 dict_with_parse_results['mouse_map'] = [None] 703 return ans 704 705 706def clear_all_shortcuts(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> bool: 707 ans = to_bool(val) 708 if ans and dict_with_parse_results is not None: 709 dict_with_parse_results['map'] = [None] 710 return ans 711 712 713def font_features(val: str) -> Iterable[Tuple[str, Tuple[FontFeature, ...]]]: 714 if val == 'none': 715 return 716 parts = val.split() 717 if len(parts) < 2: 718 log_error("Ignoring invalid font_features {}".format(val)) 719 return 720 if parts[0]: 721 features = [] 722 for feat in parts[1:]: 723 try: 724 parsed = defines.parse_font_feature(feat) 725 except ValueError: 726 log_error('Ignoring invalid font feature: {}'.format(feat)) 727 else: 728 features.append(FontFeature(feat, parsed)) 729 yield parts[0], tuple(features) 730 731 732def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]: 733 key, val = val.partition('=')[::2] 734 key, val = key.strip(), val.strip() 735 if key: 736 yield key, expandvars(val, current_val) 737 738 739def kitten_alias(val: str) -> Iterable[Tuple[str, List[str]]]: 740 parts = val.split(maxsplit=2) 741 if len(parts) >= 2: 742 name = parts.pop(0) 743 yield name, parts 744 745 746def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]: 747 parts = val.split() 748 749 def abort() -> Dict[Tuple[int, int], str]: 750 log_error(f'Symbol map: {val} is invalid, ignoring') 751 752 if len(parts) < 2: 753 return abort() 754 family = ' '.join(parts[1:]) 755 756 def to_chr(x: str) -> int: 757 if not x.startswith('U+'): 758 raise ValueError() 759 return int(x[2:], 16) 760 761 for x in parts[0].split(','): 762 a_, b_ = x.partition('-')[::2] 763 b_ = b_ or a_ 764 try: 765 a, b = map(to_chr, (a_, b_)) 766 except Exception: 767 return abort() 768 if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1: 769 return abort() 770 yield (a, b), family 771 772 773def parse_key_action(action: str, action_type: str = 'map') -> Optional[KeyAction]: 774 parts = action.strip().split(maxsplit=1) 775 func = parts[0] 776 if len(parts) == 1: 777 return KeyAction(func, ()) 778 rest = parts[1] 779 parser = args_funcs.get(func) 780 if parser is not None: 781 try: 782 func, args = parser(func, rest) 783 except Exception as err: 784 log_error(f'Ignoring invalid {action_type} action: {action} with err: {err}') 785 else: 786 return KeyAction(func, tuple(args)) 787 else: 788 log_error(f'Ignoring unknown {action_type} action: {action}') 789 return None 790 791 792class BaseDefinition: 793 action: KeyAction 794 795 def resolve_kitten_aliases(self, aliases: Dict[str, List[str]]) -> KeyAction: 796 if not self.action.args or not aliases: 797 return self.action 798 kitten = self.action.args[0] 799 rest = str(self.action.args[1] if len(self.action.args) > 1 else '') 800 changed = False 801 for key, expanded in aliases.items(): 802 if key == kitten: 803 changed = True 804 kitten = expanded[0] 805 if len(expanded) > 1: 806 rest = expanded[1] + ' ' + rest 807 return self.action._replace(args=(kitten, rest.rstrip())) if changed else self.action 808 809 810class MouseMapping(BaseDefinition): 811 812 def __init__(self, button: int, mods: int, repeat_count: int, grabbed: bool, action: KeyAction): 813 self.button = button 814 self.mods = mods 815 self.repeat_count = repeat_count 816 self.grabbed = grabbed 817 self.action = action 818 819 def __repr__(self) -> str: 820 return f'MouseMapping({self.button}, {self.mods}, {self.repeat_count}, {self.grabbed}, {self.action})' 821 822 def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, List[str]]) -> 'MouseMapping': 823 return MouseMapping(self.button, defines.resolve_key_mods(kitty_mod, self.mods), self.repeat_count, self.grabbed, self.resolve_kitten_aliases(aliases)) 824 825 @property 826 def trigger(self) -> MouseEvent: 827 return MouseEvent(self.button, self.mods, self.repeat_count, self.grabbed) 828 829 830class KeyDefinition(BaseDefinition): 831 832 def __init__(self, is_sequence: bool, action: KeyAction, mods: int, is_native: bool, key: int, rest: Tuple[SingleKey, ...] = ()): 833 self.is_sequence = is_sequence 834 self.action = action 835 self.trigger = SingleKey(mods, is_native, key) 836 self.rest = rest 837 838 def __repr__(self) -> str: 839 return f'KeyDefinition({self.is_sequence}, {self.action}, {self.trigger.mods}, {self.trigger.is_native}, {self.trigger.key}, {self.rest})' 840 841 def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, List[str]]) -> 'KeyDefinition': 842 def r(k: SingleKey) -> SingleKey: 843 mods = defines.resolve_key_mods(kitty_mod, k.mods) 844 return k._replace(mods=mods) 845 return KeyDefinition( 846 self.is_sequence, self.resolve_kitten_aliases(aliases), 847 defines.resolve_key_mods(kitty_mod, self.trigger.mods), 848 self.trigger.is_native, self.trigger.key, tuple(map(r, self.rest))) 849 850 851def parse_map(val: str) -> Iterable[KeyDefinition]: 852 parts = val.split(maxsplit=1) 853 if len(parts) != 2: 854 return 855 sc, action = parts 856 sc, action = sc.strip().strip(sequence_sep), action.strip() 857 if not sc or not action: 858 return 859 is_sequence = sequence_sep in sc 860 if is_sequence: 861 trigger: Optional[SingleKey] = None 862 restl: List[SingleKey] = [] 863 for part in sc.split(sequence_sep): 864 try: 865 mods, is_native, key = parse_shortcut(part) 866 except InvalidMods: 867 return 868 if key == 0: 869 if mods is not None: 870 log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) 871 return 872 if trigger is None: 873 trigger = SingleKey(mods, is_native, key) 874 else: 875 restl.append(SingleKey(mods, is_native, key)) 876 rest = tuple(restl) 877 else: 878 try: 879 mods, is_native, key = parse_shortcut(sc) 880 except InvalidMods: 881 return 882 if key == 0: 883 if mods is not None: 884 log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) 885 return 886 try: 887 paction = parse_key_action(action) 888 except Exception: 889 log_error('Invalid shortcut action: {}. Ignoring.'.format( 890 action)) 891 else: 892 if paction is not None: 893 if is_sequence: 894 if trigger is not None: 895 yield KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest) 896 else: 897 assert key is not None 898 yield KeyDefinition(False, paction, mods, is_native, key) 899 900 901def parse_mouse_map(val: str) -> Iterable[MouseMapping]: 902 parts = val.split(maxsplit=3) 903 if len(parts) != 4: 904 log_error(f'Ignoring invalid mouse action: {val}') 905 return 906 xbutton, event, modes, action = parts 907 kparts = xbutton.split('+') 908 if len(kparts) > 1: 909 mparts, obutton = kparts[:-1], kparts[-1].lower() 910 mods = parse_mods(mparts, obutton) 911 if mods is None: 912 return 913 else: 914 obutton = parts[0].lower() 915 mods = 0 916 try: 917 b = {'left': 'b1', 'middle': 'b3', 'right': 'b2'}.get(obutton, obutton)[1:] 918 button = getattr(defines, f'GLFW_MOUSE_BUTTON_{b}') 919 except Exception: 920 log_error(f'Mouse button: {xbutton} not recognized, ignoring') 921 return 922 try: 923 count = {'doubleclick': -3, 'click': -2, 'release': -1, 'press': 1, 'doublepress': 2, 'triplepress': 3}[event.lower()] 924 except KeyError: 925 log_error(f'Mouse event type: {event} not recognized, ignoring') 926 return 927 specified_modes = frozenset(modes.lower().split(',')) 928 if specified_modes - {'grabbed', 'ungrabbed'}: 929 log_error(f'Mouse modes: {modes} not recognized, ignoring') 930 return 931 try: 932 paction = parse_key_action(action, 'mouse_map') 933 except Exception: 934 log_error(f'Invalid mouse action: {action}. Ignoring.') 935 return 936 if paction is None: 937 return 938 for mode in sorted(specified_modes): 939 yield MouseMapping(button, mods, count, mode == 'grabbed', paction) 940 941 942def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None: 943 if not hasattr(deprecated_hide_window_decorations_aliases, key): 944 setattr(deprecated_hide_window_decorations_aliases, key, True) 945 log_error('The option {} is deprecated. Use hide_window_decorations instead.'.format(key)) 946 if to_bool(val): 947 if is_macos and key == 'macos_hide_titlebar' or (not is_macos and key == 'x11_hide_window_decorations'): 948 ans['hide_window_decorations'] = True 949 950 951def deprecated_macos_show_window_title_in_menubar_alias(key: str, val: str, ans: Dict[str, Any]) -> None: 952 if not hasattr(deprecated_macos_show_window_title_in_menubar_alias, key): 953 setattr(deprecated_macos_show_window_title_in_menubar_alias, 'key', True) 954 log_error('The option {} is deprecated. Use macos_show_window_title_in menubar instead.'.format(key)) 955 macos_show_window_title_in = ans.get('macos_show_window_title_in', 'all') 956 if to_bool(val): 957 if macos_show_window_title_in == 'none': 958 macos_show_window_title_in = 'menubar' 959 elif macos_show_window_title_in == 'window': 960 macos_show_window_title_in = 'all' 961 else: 962 if macos_show_window_title_in == 'all': 963 macos_show_window_title_in = 'window' 964 elif macos_show_window_title_in == 'menubar': 965 macos_show_window_title_in = 'none' 966 ans['macos_show_window_title_in'] = macos_show_window_title_in 967 968 969def deprecated_send_text(key: str, val: str, ans: Dict[str, Any]) -> None: 970 parts = val.split(' ') 971 972 def abort(msg: str) -> None: 973 log_error('Send text: {} is invalid ({}), ignoring'.format( 974 val, msg)) 975 976 if len(parts) < 3: 977 return abort('Incomplete') 978 mode, sc = parts[:2] 979 text = ' '.join(parts[2:]) 980 key_str = '{} send_text {} {}'.format(sc, mode, text) 981 for k in parse_map(key_str): 982 ans['map'].append(k) 983