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