1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net> 4 5import os 6from functools import partial 7from pprint import pformat 8from typing import Callable, Dict, Generator, Iterable, Set, Tuple 9 10from kittens.tui.operations import colored, styled 11 12from .cli import version 13from .conf.utils import KeyAction 14from .constants import is_macos, is_wayland 15from .options.types import Options as KittyOpts, defaults 16from .options.utils import MouseMap 17from .rgb import Color, color_as_sharp 18from .types import MouseEvent, SingleKey 19from .typing import SequenceMap 20 21ShortcutMap = Dict[Tuple[SingleKey, ...], KeyAction] 22 23 24def green(x: str) -> str: 25 return colored(x, 'green') 26 27 28def title(x: str) -> str: 29 return colored(x, 'blue', intense=True) 30 31 32def mod_to_names(mods: int) -> Generator[str, None, None]: 33 from .fast_data_types import ( 34 GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER, 35 GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER 36 ) 37 modmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER, 38 'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META, 'num_lock': GLFW_MOD_NUM_LOCK, 'caps_lock': GLFW_MOD_CAPS_LOCK} 39 for name, val in modmap.items(): 40 if mods & val: 41 yield name 42 43 44def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction, print: Callable) -> None: 45 from .fast_data_types import glfw_get_key_name 46 keys = [] 47 for key_spec in key_sequence: 48 names = [] 49 mods, is_native, key = key_spec 50 names = list(mod_to_names(mods)) 51 if key: 52 kname = glfw_get_key_name(0, key) if is_native else glfw_get_key_name(key, 0) 53 names.append(kname or f'{key}') 54 keys.append('+'.join(names)) 55 56 print('\t' + ' > '.join(keys), action) 57 58 59def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[SingleKey, ...]], print: Callable) -> None: 60 if changes: 61 print(title(text)) 62 63 for k in sorted(changes): 64 print_shortcut(k, defns[k], print) 65 66 67def compare_keymaps(final: ShortcutMap, initial: ShortcutMap, print: Callable) -> None: 68 added = set(final) - set(initial) 69 removed = set(initial) - set(final) 70 changed = {k for k in set(final) & set(initial) if final[k] != initial[k]} 71 print_shortcut_changes(final, 'Added shortcuts:', added, print) 72 print_shortcut_changes(initial, 'Removed shortcuts:', removed, print) 73 print_shortcut_changes(final, 'Changed shortcuts:', changed, print) 74 75 76def flatten_sequence_map(m: SequenceMap) -> ShortcutMap: 77 ans: Dict[Tuple[SingleKey, ...], KeyAction] = {} 78 for key_spec, rest_map in m.items(): 79 for r, action in rest_map.items(): 80 ans[(key_spec,) + (r)] = action 81 return ans 82 83 84def compare_mousemaps(final: MouseMap, initial: MouseMap, print: Callable) -> None: 85 added = set(final) - set(initial) 86 removed = set(initial) - set(final) 87 changed = {k for k in set(final) & set(initial) if final[k] != initial[k]} 88 89 def print_mouse_action(trigger: MouseEvent, action: KeyAction) -> None: 90 names = list(mod_to_names(trigger.mods)) + [f'b{trigger.button+1}'] 91 when = {-1: 'repeat', 1: 'press', 2: 'doublepress', 3: 'triplepress'}.get(trigger.repeat_count, trigger.repeat_count) 92 grabbed = 'grabbed' if trigger.grabbed else 'ungrabbed' 93 print('\t', '+'.join(names), when, grabbed, action) 94 95 def print_changes(defns: MouseMap, changes: Set[MouseEvent], text: str) -> None: 96 if changes: 97 print(title(text)) 98 for k in sorted(changes): 99 print_mouse_action(k, defns[k]) 100 101 print_changes(final, added, 'Added mouse actions:') 102 print_changes(initial, removed, 'Removed mouse actions:') 103 print_changes(final, changed, 'Changed mouse actions:') 104 105 106def compare_opts(opts: KittyOpts, print: Callable) -> None: 107 from .config import load_config 108 print() 109 print('Config options different from defaults:') 110 default_opts = load_config() 111 ignored = ('keymap', 'sequence_map', 'mousemap', 'map', 'mouse_map') 112 changed_opts = [ 113 f for f in sorted(defaults._fields) 114 if f not in ignored and getattr(opts, f) != getattr(defaults, f) 115 ] 116 field_len = max(map(len, changed_opts)) if changed_opts else 20 117 fmt = '{{:{:d}s}}'.format(field_len) 118 colors = [] 119 for f in changed_opts: 120 val = getattr(opts, f) 121 if isinstance(val, dict): 122 print(f'{title(f)}:') 123 if f == 'symbol_map': 124 for k in sorted(val): 125 print(f'\tU+{k[0]:04x} - U+{k[1]:04x} → {val[k]}') 126 else: 127 print(pformat(val)) 128 else: 129 val = getattr(opts, f) 130 if isinstance(val, Color): 131 colors.append(fmt.format(f) + ' ' + color_as_sharp(val) + ' ' + styled(' ', bg=val)) 132 else: 133 print(fmt.format(f), str(getattr(opts, f))) 134 135 compare_mousemaps(opts.mousemap, default_opts.mousemap, print) 136 final_, initial_ = opts.keymap, default_opts.keymap 137 final: ShortcutMap = {(k,): v for k, v in final_.items()} 138 initial: ShortcutMap = {(k,): v for k, v in initial_.items()} 139 final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map)) 140 final.update(final_s) 141 initial.update(initial_s) 142 compare_keymaps(final, initial, print) 143 if colors: 144 print(f'{title("Colors")}:', end='\n\t') 145 print('\n\t'.join(sorted(colors))) 146 147 148def debug_config(opts: KittyOpts) -> str: 149 from io import StringIO 150 out = StringIO() 151 p = partial(print, file=out) 152 p(version(add_rev=True)) 153 p(' '.join(os.uname())) 154 if is_macos: 155 import subprocess 156 p(' '.join(subprocess.check_output(['sw_vers']).decode('utf-8').splitlines()).strip()) 157 if os.path.exists('/etc/issue'): 158 with open('/etc/issue', encoding='utf-8', errors='replace') as f: 159 p(f.read().strip()) 160 if os.path.exists('/etc/lsb-release'): 161 with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f: 162 p(f.read().strip()) 163 if not is_macos: 164 p('Running under:' + green('Wayland' if is_wayland() else 'X11')) 165 if opts.config_paths: 166 p(green('Loaded config files:')) 167 p(' ', '\n '.join(opts.config_paths)) 168 if opts.config_overrides: 169 p(green('Loaded config overrides:')) 170 p(' ', '\n '.join(opts.config_overrides)) 171 compare_opts(opts, p) 172 return out.getvalue() 173