#!/usr/bin/env python # License: GPLv3 Copyright: 2021, Kovid Goyal import os import re import socket import sys import termios import time from functools import partial from pprint import pformat from typing import ( IO, Callable, Dict, Generator, Iterable, Iterator, Optional, Set, Tuple ) from kittens.tui.operations import colored, styled from .cli import version from .conf.utils import KeyAction from .constants import ( extensions_dir, is_macos, is_wayland, kitty_base_dir, kitty_exe, shell_path ) from .fast_data_types import num_users, Color from .options.types import Options as KittyOpts, defaults from .options.utils import MouseMap from .rgb import color_as_sharp from .types import MouseEvent, SingleKey from .typing import SequenceMap ShortcutMap = Dict[Tuple[SingleKey, ...], KeyAction] def green(x: str) -> str: return colored(x, 'green') def yellow(x: str) -> str: return colored(x, 'yellow') def title(x: str) -> str: return colored(x, 'blue', intense=True) def mod_to_names(mods: int) -> Generator[str, None, None]: from .fast_data_types import ( GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER, GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER ) modmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER, 'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META, 'num_lock': GLFW_MOD_NUM_LOCK, 'caps_lock': GLFW_MOD_CAPS_LOCK} for name, val in modmap.items(): if mods & val: yield name def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction, print: Callable[..., None]) -> None: from .fast_data_types import glfw_get_key_name keys = [] for key_spec in key_sequence: names = [] mods, is_native, key = key_spec names = list(mod_to_names(mods)) if key: kname = glfw_get_key_name(0, key) if is_native else glfw_get_key_name(key, 0) names.append(kname or f'{key}') keys.append('+'.join(names)) print('\t' + ' > '.join(keys), action) def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[SingleKey, ...]], print: Callable[..., None]) -> None: if changes: print(title(text)) for k in sorted(changes): print_shortcut(k, defns[k], print) def compare_keymaps(final: ShortcutMap, initial: ShortcutMap, print: Callable[..., None]) -> None: added = set(final) - set(initial) removed = set(initial) - set(final) changed = {k for k in set(final) & set(initial) if final[k] != initial[k]} print_shortcut_changes(final, 'Added shortcuts:', added, print) print_shortcut_changes(initial, 'Removed shortcuts:', removed, print) print_shortcut_changes(final, 'Changed shortcuts:', changed, print) def flatten_sequence_map(m: SequenceMap) -> ShortcutMap: ans: Dict[Tuple[SingleKey, ...], KeyAction] = {} for key_spec, rest_map in m.items(): for r, action in rest_map.items(): ans[(key_spec,) + (r)] = action return ans def compare_mousemaps(final: MouseMap, initial: MouseMap, print: Callable[..., None]) -> None: added = set(final) - set(initial) removed = set(initial) - set(final) changed = {k for k in set(final) & set(initial) if final[k] != initial[k]} def print_mouse_action(trigger: MouseEvent, action: KeyAction) -> None: names = list(mod_to_names(trigger.mods)) + [f'b{trigger.button+1}'] when = {-1: 'repeat', 1: 'press', 2: 'doublepress', 3: 'triplepress'}.get(trigger.repeat_count, trigger.repeat_count) grabbed = 'grabbed' if trigger.grabbed else 'ungrabbed' print('\t', '+'.join(names), when, grabbed, action) def print_changes(defns: MouseMap, changes: Set[MouseEvent], text: str) -> None: if changes: print(title(text)) for k in sorted(changes): print_mouse_action(k, defns[k]) print_changes(final, added, 'Added mouse actions:') print_changes(initial, removed, 'Removed mouse actions:') print_changes(final, changed, 'Changed mouse actions:') def compare_opts(opts: KittyOpts, print: Callable[..., None]) -> None: from .config import load_config print() print('Config options different from defaults:') default_opts = load_config() ignored = ('keymap', 'sequence_map', 'mousemap', 'map', 'mouse_map') changed_opts = [ f for f in sorted(defaults._fields) if f not in ignored and getattr(opts, f) != getattr(defaults, f) ] field_len = max(map(len, changed_opts)) if changed_opts else 20 fmt = f'{{:{field_len:d}s}}' colors = [] for f in changed_opts: val = getattr(opts, f) if isinstance(val, dict): print(f'{title(f)}:') if f == 'symbol_map': for k in sorted(val): print(f'\tU+{k[0]:04x} - U+{k[1]:04x} → {val[k]}') else: print(pformat(val)) else: val = getattr(opts, f) if isinstance(val, Color): colors.append(fmt.format(f) + ' ' + color_as_sharp(val) + ' ' + styled(' ', bg=val)) else: print(fmt.format(f), str(getattr(opts, f))) compare_mousemaps(opts.mousemap, default_opts.mousemap, print) final_, initial_ = opts.keymap, default_opts.keymap final: ShortcutMap = {(k,): v for k, v in final_.items()} initial: ShortcutMap = {(k,): v for k, v in initial_.items()} final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map)) final.update(final_s) initial.update(initial_s) compare_keymaps(final, initial, print) if colors: print(f'{title("Colors")}:', end='\n\t') print('\n\t'.join(sorted(colors))) class IssueData: def __init__(self) -> None: self.uname = os.uname() self.s, self.n, self.r, self.v, self.m = self.uname self.hostname = self.o = socket.gethostname() _time = time.localtime() self.formatted_time = self.d = time.strftime('%a %b %d %Y', _time) self.formatted_date = self.t = time.strftime('%H:%M:%S', _time) try: self.tty_name = format_tty_name(os.ttyname(sys.stdin.fileno())) except OSError: self.tty_name = '(none)' self.l = self.tty_name # noqa self.baud_rate = termios.tcgetattr(sys.stdin.fileno())[5] self.b = str(self.baud_rate) try: self.num_users = num_users() except RuntimeError: self.num_users = -1 self.u = str(self.num_users) self.U = self.u + ' user' + ('' if self.num_users == 1 else 's') def translate_issue_char(self, char: str) -> str: try: return str(getattr(self, char)) if len(char) == 1 else char except AttributeError: return char def parse_issue_file(self, issue_file: IO[str]) -> Iterator[str]: last_char: Optional[str] = None while True: this_char = issue_file.read(1) if not this_char: break if last_char == '\\': yield self.translate_issue_char(this_char) elif last_char is not None: yield last_char # `\\\a` should not match the last two slashes, # so make it look like it was `\?\a` where `?` # is some character other than `\`. last_char = None if last_char == '\\' else this_char if last_char is not None: yield last_char def format_tty_name(raw: str) -> str: return re.sub(r'^/dev/([^/]+)/([^/]+)$', r'\1\2', raw) def debug_config(opts: KittyOpts) -> str: from io import StringIO out = StringIO() p = partial(print, file=out) p(version(add_rev=True)) p(' '.join(os.uname())) if is_macos: import subprocess p(' '.join(subprocess.check_output(['sw_vers']).decode('utf-8').splitlines()).strip()) if os.path.exists('/etc/issue'): with open('/etc/issue', encoding='utf-8', errors='replace') as f: p(end=''.join(IssueData().parse_issue_file(f))) if os.path.exists('/etc/lsb-release'): with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f: p(f.read().strip()) if not is_macos: p('Running under:' + green('Wayland' if is_wayland() else 'X11')) p(green('Frozen:'), 'True' if getattr(sys, 'frozen', False) else 'False') p(green('Paths:')) p(yellow(' kitty:'), os.path.realpath(kitty_exe())) p(yellow(' base dir:'), kitty_base_dir) p(yellow(' extensions dir:'), extensions_dir) p(yellow(' system shell:'), shell_path) if opts.config_paths: p(green('Loaded config files:')) p(' ', '\n '.join(opts.config_paths)) if opts.config_overrides: p(green('Loaded config overrides:')) p(' ', '\n '.join(opts.config_overrides)) compare_opts(opts, p) return out.getvalue()