From b94d2b27f49b95193eecbb8e6c2085a00f69d9b0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Jan 2021 18:07:17 +0530 Subject: [PATCH] Refactor single key config parsing to use a special type --- kitty/boss.py | 5 ++-- kitty/cli.py | 12 ++++---- kitty/config.py | 64 +++++++++++++++---------------------------- kitty/config_data.py | 34 +++++++++++++++++++++-- kitty/constants.py | 6 ++++ kitty/keys.py | 9 +++--- kitty/main.py | 6 ++-- kitty/options_stub.py | 3 +- kitty/typing.py | 2 +- kitty/typing.pyi | 17 +++++------- 10 files changed, 86 insertions(+), 72 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index 1b907ce65..c9703d06e 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -25,7 +25,8 @@ from .config import ( ) from .config_data import MINIMUM_FONT_SIZE from .constants import ( - appname, config_dir, is_macos, kitty_exe, supports_primary_selection + SingleKey, appname, config_dir, is_macos, kitty_exe, + supports_primary_selection ) from .fast_data_types import ( CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, @@ -140,7 +141,7 @@ class Boss: opts: Options, args: CLIOptions, cached_values: Dict[str, Any], - new_os_window_trigger: Optional[Tuple[int, bool, int]] + new_os_window_trigger: Optional[SingleKey] ): set_layout_options(opts) self.clipboard_buffers: Dict[str, str] = {} diff --git a/kitty/cli.py b/kitty/cli.py index 2cf68c555..0b6a6f498 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -14,9 +14,9 @@ from typing import ( from .cli_stub import CLIOptions from .conf.utils import resolve_config from .config import KeyAction -from .constants import appname, defconf, is_macos, is_wayland, str_version +from .constants import appname, defconf, is_macos, is_wayland, str_version, SingleKey from .options_stub import Options as OptionsStub -from .typing import BadLineType, KeySpec, SequenceMap, TypedDict +from .typing import BadLineType, SequenceMap, TypedDict class OptionDict(TypedDict): @@ -755,10 +755,10 @@ def parse_args( SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf' -ShortcutMap = Dict[Tuple[KeySpec, ...], KeyAction] +ShortcutMap = Dict[Tuple[SingleKey, ...], KeyAction] -def print_shortcut(key_sequence: Iterable[KeySpec], action: KeyAction) -> None: +def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction) -> None: if not getattr(print_shortcut, 'maps', None): from kitty.keys import defines v = vars(defines) @@ -786,7 +786,7 @@ def print_shortcut(key_sequence: Iterable[KeySpec], action: KeyAction) -> None: print('\t', ' > '.join(keys), action) -def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[KeySpec, ...]]) -> None: +def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[SingleKey, ...]]) -> None: if changes: print(title(text)) @@ -804,7 +804,7 @@ def compare_keymaps(final: ShortcutMap, initial: ShortcutMap) -> None: def flatten_sequence_map(m: SequenceMap) -> ShortcutMap: - ans: Dict[Tuple[KeySpec, ...], KeyAction] = {} + 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 diff --git a/kitty/config.py b/kitty/config.py index 81c07745f..ed0533a67 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -19,44 +19,17 @@ from .conf.utils import ( BadLine, init_config, key_func, load_config as _load_config, merge_dicts, parse_config_base, python_string, to_bool, to_cmdline ) -from .config_data import all_options, parse_mods, type_convert -from .constants import cache_dir, defconf, is_macos +from .config_data import InvalidMods, all_options, parse_shortcut, type_convert +from .constants import SingleKey, cache_dir, defconf, is_macos from .fonts import FontFeature -from .key_names import get_key_name_lookup, key_name_aliases from .options_stub import Options as OptionsStub from .typing import TypedDict from .utils import expandvars, log_error -KeySpec = Tuple[int, bool, int] -KeyMap = Dict[KeySpec, 'KeyAction'] -KeySequence = Tuple[KeySpec, ...] +KeyMap = Dict[SingleKey, 'KeyAction'] +KeySequence = Tuple[SingleKey, ...] SubSequenceMap = Dict[KeySequence, 'KeyAction'] -SequenceMap = Dict[KeySpec, SubSequenceMap] - - -class InvalidMods(ValueError): - pass - - -def parse_shortcut(sc: str) -> Tuple[int, bool, Optional[int]]: - parts = sc.split('+') - mods = 0 - if len(parts) > 1: - mods = parse_mods(parts[:-1], sc) or 0 - if not mods: - raise InvalidMods('Invalid shortcut') - q = parts[-1].upper() - key: Optional[int] = getattr(defines, 'GLFW_KEY_' + key_name_aliases.get(q, q), None) - is_native = False - if key is None: - q = parts[-1] - if q.startswith('0x'): - with suppress(Exception): - key = int(q, 16) - else: - key = get_key_name_lookup()(q, False) - is_native = key is not None - return mods, is_native, key +SequenceMap = Dict[SingleKey, SubSequenceMap] class KeyAction(NamedTuple): @@ -371,15 +344,22 @@ sequence_sep = '>' class KeyDefinition: - def __init__(self, is_sequence: bool, action: KeyAction, mods: int, is_native: bool, key: int, rest: Tuple[KeySpec, ...] = ()): + def __init__(self, is_sequence: bool, action: KeyAction, mods: int, is_native: bool, key: int, rest: Tuple[SingleKey, ...] = ()): self.is_sequence = is_sequence self.action = action - self.trigger = mods, is_native, key + self.trigger = SingleKey(mods, is_native, key) self.rest = rest def resolve(self, kitty_mod: int) -> None: - self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1], self.trigger[2] - self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), is_native, key) for mods, is_native, key in self.rest) + + def r(k: SingleKey) -> SingleKey: + mods = defines.resolve_key_mods(kitty_mod, k.mods) + key = k.key + is_native = k.is_native + return SingleKey(mods, is_native, key) + + self.trigger = r(self.trigger) + self.rest = tuple(map(r, self.rest)) def resolve_kitten_aliases(self, aliases: Dict[str, Sequence[str]]) -> None: if not self.action.args: @@ -407,28 +387,28 @@ def parse_key(val: str, key_definitions: List[KeyDefinition]) -> None: return is_sequence = sequence_sep in sc if is_sequence: - trigger: Optional[Tuple[int, bool, int]] = None - restl: List[Tuple[int, bool, int]] = [] + trigger: Optional[SingleKey] = None + restl: List[SingleKey] = [] for part in sc.split(sequence_sep): try: mods, is_native, key = parse_shortcut(part) except InvalidMods: return - if key is None: + if key is defines.GLFW_KEY_UNKNOWN: if mods is not None: log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) return if trigger is None: - trigger = mods, is_native, key + trigger = SingleKey(mods, is_native, key) else: - restl.append((mods, is_native, key)) + restl.append(SingleKey(mods, is_native, key)) rest = tuple(restl) else: try: mods, is_native, key = parse_shortcut(sc) except InvalidMods: return - if key is None: + if key is defines.GLFW_KEY_UNKNOWN: if mods is not None: log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) return diff --git a/kitty/config_data.py b/kitty/config_data.py index ce0dd55d8..834d59746 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -4,6 +4,7 @@ # Utils {{{ import os +from contextlib import suppress from gettext import gettext as _ from typing import ( Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, @@ -16,15 +17,21 @@ from .conf.utils import ( choices, positive_float, positive_int, to_bool, to_cmdline as tc, to_color, to_color_or_none, unit_float ) -from .constants import FloatEdges, config_dir, is_macos +from .constants import ( + FloatEdges, SingleKey, config_dir, is_macos +) from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE +from .key_names import get_key_name_lookup, key_name_aliases from .layout.interface import all_layouts from .rgb import Color, color_as_int, color_as_sharp, color_from_int from .utils import log_error + +class InvalidMods(ValueError): + pass + + MINIMUM_FONT_SIZE = 4 - - mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', '⌥': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'} @@ -53,6 +60,27 @@ def to_modifiers(val: str) -> int: return parse_mods(val.split('+'), val) or 0 +def parse_shortcut(sc: str) -> SingleKey: + parts = sc.split('+') + mods = 0 + if len(parts) > 1: + mods = parse_mods(parts[:-1], sc) or 0 + if not mods: + raise InvalidMods('Invalid shortcut') + q = parts[-1].upper() + key: Optional[int] = getattr(defines, 'GLFW_KEY_' + key_name_aliases.get(q, q), None) + is_native = False + if key is None: + q = parts[-1] + if q.startswith('0x'): + with suppress(Exception): + key = int(q, 16) + else: + key = get_key_name_lookup()(q, False) + is_native = key is not None + return SingleKey(mods, is_native, key or defines.GLFW_KEY_UNKNOWN) + + T = TypeVar('T') diff --git a/kitty/constants.py b/kitty/constants.py index 8ead0384a..c370f830e 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -60,6 +60,12 @@ class WindowGeometry(NamedTuple): spaces: Edges = Edges() +class SingleKey(NamedTuple): + mods: int = 0 + is_native: bool = False + key: int = -1 + + @lru_cache(maxsize=2) def kitty_exe() -> str: rpath = sys._xoptions.get('bundle_exe_dir') diff --git a/kitty/keys.py b/kitty/keys.py index 1533e7b46..e8b66bf69 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -6,7 +6,8 @@ import string from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union from . import fast_data_types as defines -from .config import KeyAction, KeyMap, KeySpec, SequenceMap, SubSequenceMap +from .constants import SingleKey +from .config import KeyAction, KeyMap, SequenceMap, SubSequenceMap from .key_encoding import KEY_MAP from .terminfo import key_as_bytes, modify_key_bytes from .typing import ScreenType, WindowType @@ -278,13 +279,13 @@ def interpret_key_event(key: int, native_key: int, mods: int, window: WindowType def get_shortcut(keymap: Union[KeyMap, SequenceMap], mods: int, key: int, native_key: int) -> Optional[Union[KeyAction, SubSequenceMap]]: mods &= 0b1111 - ans = keymap.get((mods, False, key)) + ans = keymap.get(SingleKey(mods, False, key)) if ans is None: - ans = keymap.get((mods, True, native_key)) + ans = keymap.get(SingleKey(mods, True, native_key)) return ans -def shortcut_matches(s: KeySpec, mods: int, key: int, native_key: int) -> bool: +def shortcut_matches(s: SingleKey, mods: int, key: int, native_key: int) -> bool: mods &= 0b1111 q = native_key if s[1] else key return bool(s[0] & 0b1111 == mods & 0b1111 and s[2] == q) diff --git a/kitty/main.py b/kitty/main.py index 68902e89a..2683f95dc 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -7,7 +7,7 @@ import os import shutil import sys from contextlib import contextmanager, suppress -from typing import Generator, List, Mapping, Optional, Sequence, Tuple +from typing import Generator, List, Mapping, Optional, Sequence from .borders import load_borders_program from .boss import Boss @@ -17,7 +17,7 @@ from .cli_stub import CLIOptions from .conf.utils import BadLine from .config import cached_values_for from .constants import ( - appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, + SingleKey, appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, is_wayland, kitty_exe, logo_data_file, running_in_kitty ) from .fast_data_types import ( @@ -103,7 +103,7 @@ def init_glfw(opts: OptionsStub, debug_keyboard: bool = False) -> str: return glfw_module -def get_new_os_window_trigger(opts: OptionsStub) -> Optional[Tuple[int, bool, int]]: +def get_new_os_window_trigger(opts: OptionsStub) -> Optional[SingleKey]: new_os_window_trigger = None if is_macos: new_os_window_shortcuts = [] diff --git a/kitty/options_stub.py b/kitty/options_stub.py index ffe7f974d..fcf43eb66 100644 --- a/kitty/options_stub.py +++ b/kitty/options_stub.py @@ -20,7 +20,8 @@ def generate_stub(): 'font_features': 'typing.Dict[str, typing.Tuple[str, ...]]' }, preamble_lines=( - 'from kitty.config import KeyAction, KeyMap, SequenceMap, KeySpec', + 'from kitty.constants import SingleKey', + 'from kitty.config import KeyAction, KeyMap, SequenceMap', ), extra_fields=( ('keymap', 'KeyMap'), diff --git a/kitty/typing.py b/kitty/typing.py index 3207ed740..0b3eb2e4b 100644 --- a/kitty/typing.py +++ b/kitty/typing.py @@ -6,7 +6,7 @@ from typing import Tuple BossType = ChildType = TabType = WindowType = ScreenType = None -BadLineType = KeySpec = SequenceMap = KeyActionType = None +BadLineType = SequenceMap = KeyActionType = None AddressFamily = PopenType = Socket = StartupCtx = None SessionTab = SessionType = LayoutType = SpecialWindowInstance = None MarkType = RemoteCommandType = CoreTextFont = FontConfigPattern = None diff --git a/kitty/typing.pyi b/kitty/typing.pyi index 7417e6f48..938fd6259 100644 --- a/kitty/typing.pyi +++ b/kitty/typing.pyi @@ -1,5 +1,8 @@ from asyncio import AbstractEventLoop as AbstractEventLoop # noqa from socket import AddressFamily as AddressFamily, socket as Socket # noqa +from subprocess import ( # noqa; noqa + CompletedProcess as CompletedProcess, Popen as PopenType +) from typing import ( # noqa Literal, Protocol as Protocol, TypedDict as TypedDict ) @@ -18,6 +21,10 @@ from kitty.conf.utils import KittensKeyAction as KittensKeyActionType # noqa from .boss import Boss as BossType # noqa from .child import Child as ChildType # noqa from .conf.utils import BadLine as BadLineType # noqa +from .config import ( # noqa; noqa + KeyAction as KeyActionType, KeyMap as KeyMap, + KittyCommonOpts as KittyCommonOpts, SequenceMap as SequenceMap +) from .fast_data_types import ( # noqa CoreTextFont as CoreTextFont, FontConfigPattern as FontConfigPattern, Screen as ScreenType, StartupCtx as StartupCtx @@ -32,16 +39,6 @@ from .tabs import ( # noqa from .utils import ScreenSize as ScreenSize # noqa from .window import Window as WindowType # noqa -from subprocess import ( # noqa; noqa - CompletedProcess as CompletedProcess, Popen as PopenType -) - - -from .config import ( # noqa; noqa - KeyAction as KeyActionType, KeyMap as KeyMap, KeySpec as KeySpec, - KittyCommonOpts as KittyCommonOpts, SequenceMap as SequenceMap -) - EdgeLiteral = Literal['left', 'top', 'right', 'bottom'] MatchType = Literal['mime', 'ext', 'protocol', 'file', 'path', 'url', 'fragment_matches'] GRT_a = Literal['t', 'T', 'q', 'p', 'd']