diff --git a/kitty/boss.py b/kitty/boss.py index 2e5f6d97e..5bfdaf5c4 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -19,11 +19,7 @@ from .child import cached_process_data, cwd_of_process, default_env from .cli import create_opts, parse_args from .cli_stub import CLIOptions from .conf.utils import BadLine, to_cmdline -from .config import ( - KeyAction, SubSequenceMap, common_opts_as_dict, - prepare_config_file_for_editing -) -from .options_types import MINIMUM_FONT_SIZE +from .config import common_opts_as_dict, prepare_config_file_for_editing from .constants import ( appname, config_dir, is_macos, kitty_exe, supports_primary_selection ) @@ -42,6 +38,7 @@ from .keys import get_shortcut, shortcut_matches from .layout.base import set_layout_options from .notify import notification_activated from .options_stub import Options +from .options_types import MINIMUM_FONT_SIZE, KeyAction, SubSequenceMap from .os_window_size import initial_window_size_func from .rgb import Color, color_from_int from .session import Session, create_sessions, get_os_window_sizing_data diff --git a/kitty/cli.py b/kitty/cli.py index 65b255949..ed57859cf 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -13,7 +13,7 @@ from typing import ( from .cli_stub import CLIOptions from .conf.utils import resolve_config -from .config import KeyAction, MouseMap +from .options_types import KeyAction, MouseMap from .constants import appname, defconf, is_macos, is_wayland, str_version from .options_stub import Options as OptionsStub from .types import MouseEvent, SingleKey diff --git a/kitty/config.py b/kitty/config.py index 3c55fb69a..1d05dfb3f 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -9,40 +9,26 @@ from contextlib import contextmanager, suppress from functools import partial from typing import ( Any, Callable, Dict, FrozenSet, Generator, Iterable, List, NamedTuple, - Optional, Sequence, Set, Tuple, Type, Union + Optional, Sequence, Tuple, Type, Union ) from . import fast_data_types as defines from .conf.definition import as_conf_file, config_lines from .conf.utils import ( - BadLine, init_config, key_func, load_config as _load_config, merge_dicts, + BadLine, init_config, load_config as _load_config, merge_dicts, parse_config_base, python_string, to_bool, to_cmdline ) from .config_data import all_options from .constants import cache_dir, defconf, is_macos from .options_stub import Options as OptionsStub from .options_types import ( - InvalidMods, env, font_features, parse_mods, parse_shortcut, symbol_map + FuncArgsType, KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, + env, font_features, func_with_args, parse_key, parse_key_action, + parse_mouse_action, symbol_map ) -from .types import MouseEvent, SingleKey from .typing import TypedDict from .utils import log_error -KeyMap = Dict[SingleKey, 'KeyAction'] -MouseMap = Dict[MouseEvent, 'KeyAction'] -KeySequence = Tuple[SingleKey, ...] -SubSequenceMap = Dict[KeySequence, 'KeyAction'] -SequenceMap = Dict[SingleKey, SubSequenceMap] - - -class KeyAction(NamedTuple): - func: str - args: Sequence[str] = () - - -func_with_args, args_funcs = key_func() -FuncArgsType = Tuple[str, Sequence[Any]] - @func_with_args( 'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window', @@ -341,176 +327,6 @@ def mouse_selection(func: str, rest: str) -> FuncArgsType: return func, [cmap[rest]] -def parse_key_action(action: str) -> Optional[KeyAction]: - parts = action.strip().split(maxsplit=1) - func = parts[0] - if len(parts) == 1: - return KeyAction(func, ()) - rest = parts[1] - parser = args_funcs.get(func) - if parser is not None: - try: - func, args = parser(func, rest) - except Exception as err: - log_error('Ignoring invalid key action: {} with err: {}'.format(action, err)) - else: - return KeyAction(func, args) - return None - - -all_key_actions: Set[str] = set() -sequence_sep = '>' - - -class BaseDefinition: - action: KeyAction - - def resolve_kitten_aliases(self, aliases: Dict[str, Sequence[str]]) -> None: - if not self.action.args: - return - kitten = self.action.args[0] - rest = self.action.args[1] if len(self.action.args) > 1 else '' - changed = False - for key, expanded in aliases.items(): - if key == kitten: - changed = True - kitten = expanded[0] - if len(expanded) > 1: - rest = expanded[1] + ' ' + rest - if changed: - self.action = self.action._replace(args=[kitten, rest.rstrip()]) - - -class MouseMapping(BaseDefinition): - - def __init__(self, button: int, mods: int, repeat_count: int, grabbed: bool, action: KeyAction): - self.button = button - self.mods = mods - self.repeat_count = repeat_count - self.grabbed = grabbed - self.action = action - - def resolve(self, kitty_mod: int) -> None: - self.mods = defines.resolve_key_mods(kitty_mod, self.mods) - - @property - def trigger(self) -> MouseEvent: - return MouseEvent(self.button, self.mods, self.repeat_count, self.grabbed) - - -class KeyDefinition(BaseDefinition): - - 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 = SingleKey(mods, is_native, key) - self.rest = rest - - def resolve(self, kitty_mod: int) -> None: - - 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 parse_key(val: str, key_definitions: List[KeyDefinition]) -> None: - parts = val.split(maxsplit=1) - if len(parts) != 2: - return - sc, action = parts - sc, action = sc.strip().strip(sequence_sep), action.strip() - if not sc or not action: - return - is_sequence = sequence_sep in sc - if is_sequence: - 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 == 0: - if mods is not None: - log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) - return - if trigger is None: - trigger = SingleKey(mods, is_native, key) - else: - restl.append(SingleKey(mods, is_native, key)) - rest = tuple(restl) - else: - try: - mods, is_native, key = parse_shortcut(sc) - except InvalidMods: - return - if key == 0: - if mods is not None: - log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) - return - try: - paction = parse_key_action(action) - except Exception: - log_error('Invalid shortcut action: {}. Ignoring.'.format( - action)) - else: - if paction is not None: - all_key_actions.add(paction.func) - if is_sequence: - if trigger is not None: - key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest)) - else: - assert key is not None - key_definitions.append(KeyDefinition(False, paction, mods, is_native, key)) - - -def parse_mouse_action(val: str, mouse_mappings: List[MouseMapping]) -> None: - parts = val.split(maxsplit=3) - if len(parts) != 4: - log_error(f'Ignoring invalid mouse action: {val}') - return - xbutton, event, modes, action = parts - kparts = xbutton.split('+') - if len(kparts) > 1: - mparts, obutton = kparts[:-1], kparts[-1].lower() - mods = parse_mods(mparts, obutton) - if mods is None: - return - else: - obutton = parts[0].lower() - mods = 0 - try: - b = {'left': 'b1', 'middle': 'b3', 'right': 'b2'}.get(obutton, obutton)[1:] - button = getattr(defines, f'GLFW_MOUSE_BUTTON_{b}') - except Exception: - log_error(f'Mouse button: {xbutton} not recognized, ignoring') - return - try: - count = {'doubleclick': -3, 'click': -2, 'release': -1, 'press': 1, 'doublepress': 2, 'triplepress': 3}[event.lower()] - except KeyError: - log_error(f'Mouse event type: {event} not recognized, ignoring') - return - specified_modes = frozenset(modes.lower().split(',')) - if specified_modes - {'grabbed', 'ungrabbed'}: - log_error(f'Mouse modes: {modes} not recognized, ignoring') - return - try: - paction = parse_key_action(action) - except Exception: - log_error(f'Invalid mouse action: {action}. Ignoring.') - return - if paction is None: - log_error(f'Ignoring unknown mouse action: {action}') - return - for mode in specified_modes: - mouse_mappings.append(MouseMapping(button, mods, count, mode == 'grabbed', paction)) - - def parse_send_text_bytes(text: str) -> bytes: return python_string(text).encode('utf-8') @@ -663,10 +479,6 @@ def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, xc = init_config(config_lines(all_options), parse_defaults) Options: Type[OptionsStub] = xc[0] defaults: OptionsStub = xc[1] -actions = frozenset(all_key_actions) | frozenset( - 'run_simple_kitten combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'. - split() -) no_op_actions = frozenset({'noop', 'no-op', 'no_op'}) diff --git a/kitty/keys.py b/kitty/keys.py index 3b92324a7..66c146985 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -4,15 +4,14 @@ from typing import Optional, Union -from .config import KeyAction, KeyMap, SequenceMap, SubSequenceMap 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, KeyEvent ) +from .options_types import KeyAction, KeyMap, SequenceMap, SubSequenceMap from .types import SingleKey from .typing import ScreenType - mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER lock_mask = GLFW_MOD_NUM_LOCK | GLFW_MOD_CAPS_LOCK diff --git a/kitty/open_actions.py b/kitty/open_actions.py index 279f28072..c3da9411c 100644 --- a/kitty/open_actions.py +++ b/kitty/open_actions.py @@ -12,9 +12,9 @@ from typing import ( from urllib.parse import ParseResult, unquote, urlparse from .conf.utils import to_cmdline_implementation -from .config import KeyAction, parse_key_action from .constants import config_dir from .guess_mime_type import guess_type +from .options_types import KeyAction, parse_key_action from .types import run_once from .typing import MatchType from .utils import expandvars, log_error diff --git a/kitty/options_stub.py b/kitty/options_stub.py index 886cb5922..c2b4e4bad 100644 --- a/kitty/options_stub.py +++ b/kitty/options_stub.py @@ -17,7 +17,7 @@ def generate_stub(): all_options, preamble_lines=( 'from kitty.types import SingleKey', - 'from kitty.config import KeyAction, KeyMap, SequenceMap, MouseMap', + 'from kitty.options_types import KeyAction, KeyMap, SequenceMap, MouseMap', 'from kitty.fonts import FontFeature', ), extra_fields=( diff --git a/kitty/options_types.py b/kitty/options_types.py index e62de5f85..9275c8556 100644 --- a/kitty/options_types.py +++ b/kitty/options_types.py @@ -6,14 +6,15 @@ import os import sys from typing import ( - Callable, Dict, FrozenSet, Iterable, List, Optional, Tuple, Union + Any, Callable, Dict, FrozenSet, Iterable, List, NamedTuple, Optional, + Sequence, Tuple, Union ) import kitty.fast_data_types as defines from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from .conf.utils import ( - positive_float, positive_int, to_bool, to_color, uniq, unit_float + key_func, positive_float, positive_int, to_bool, to_color, uniq, unit_float ) from .constants import config_dir from .fonts import FontFeature @@ -23,9 +24,14 @@ from .key_names import ( ) from .layout.interface import all_layouts from .rgb import Color, color_as_int -from .types import FloatEdges, SingleKey +from .types import FloatEdges, MouseEvent, SingleKey from .utils import expandvars, log_error +KeyMap = Dict[SingleKey, 'KeyAction'] +MouseMap = Dict[MouseEvent, 'KeyAction'] +KeySequence = Tuple[SingleKey, ...] +SubSequenceMap = Dict[KeySequence, 'KeyAction'] +SequenceMap = Dict[SingleKey, SubSequenceMap] MINIMUM_FONT_SIZE = 4 default_tab_separator = ' ┇' mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', @@ -33,6 +39,14 @@ mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', character_key_name_aliases_with_ascii_lowercase: Dict[str, str] = character_key_name_aliases.copy() for x in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': character_key_name_aliases_with_ascii_lowercase[x] = x.lower() +sequence_sep = '>' +func_with_args, args_funcs = key_func() +FuncArgsType = Tuple[str, Sequence[Any]] + + +class KeyAction(NamedTuple): + func: str + args: Sequence[str] = () class InvalidMods(ValueError): @@ -400,3 +414,168 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]: if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1: return abort() yield (a, b), family + + +def parse_key_action(action: str) -> Optional[KeyAction]: + parts = action.strip().split(maxsplit=1) + func = parts[0] + if len(parts) == 1: + return KeyAction(func, ()) + rest = parts[1] + parser = args_funcs.get(func) + if parser is not None: + try: + func, args = parser(func, rest) + except Exception as err: + log_error('Ignoring invalid key action: {} with err: {}'.format(action, err)) + else: + return KeyAction(func, args) + return None + + +class BaseDefinition: + action: KeyAction + + def resolve_kitten_aliases(self, aliases: Dict[str, Sequence[str]]) -> None: + if not self.action.args: + return + kitten = self.action.args[0] + rest = self.action.args[1] if len(self.action.args) > 1 else '' + changed = False + for key, expanded in aliases.items(): + if key == kitten: + changed = True + kitten = expanded[0] + if len(expanded) > 1: + rest = expanded[1] + ' ' + rest + if changed: + self.action = self.action._replace(args=[kitten, rest.rstrip()]) + + +class MouseMapping(BaseDefinition): + + def __init__(self, button: int, mods: int, repeat_count: int, grabbed: bool, action: KeyAction): + self.button = button + self.mods = mods + self.repeat_count = repeat_count + self.grabbed = grabbed + self.action = action + + def resolve(self, kitty_mod: int) -> None: + self.mods = defines.resolve_key_mods(kitty_mod, self.mods) + + @property + def trigger(self) -> MouseEvent: + return MouseEvent(self.button, self.mods, self.repeat_count, self.grabbed) + + +class KeyDefinition(BaseDefinition): + + 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 = SingleKey(mods, is_native, key) + self.rest = rest + + def resolve(self, kitty_mod: int) -> None: + + 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 parse_key(val: str, key_definitions: List[KeyDefinition]) -> None: + parts = val.split(maxsplit=1) + if len(parts) != 2: + return + sc, action = parts + sc, action = sc.strip().strip(sequence_sep), action.strip() + if not sc or not action: + return + is_sequence = sequence_sep in sc + if is_sequence: + 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 == 0: + if mods is not None: + log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) + return + if trigger is None: + trigger = SingleKey(mods, is_native, key) + else: + restl.append(SingleKey(mods, is_native, key)) + rest = tuple(restl) + else: + try: + mods, is_native, key = parse_shortcut(sc) + except InvalidMods: + return + if key == 0: + if mods is not None: + log_error('Shortcut: {} has unknown key, ignoring'.format(sc)) + return + try: + paction = parse_key_action(action) + except Exception: + log_error('Invalid shortcut action: {}. Ignoring.'.format( + action)) + else: + if paction is not None: + if is_sequence: + if trigger is not None: + key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest)) + else: + assert key is not None + key_definitions.append(KeyDefinition(False, paction, mods, is_native, key)) + + +def parse_mouse_action(val: str, mouse_mappings: List[MouseMapping]) -> None: + parts = val.split(maxsplit=3) + if len(parts) != 4: + log_error(f'Ignoring invalid mouse action: {val}') + return + xbutton, event, modes, action = parts + kparts = xbutton.split('+') + if len(kparts) > 1: + mparts, obutton = kparts[:-1], kparts[-1].lower() + mods = parse_mods(mparts, obutton) + if mods is None: + return + else: + obutton = parts[0].lower() + mods = 0 + try: + b = {'left': 'b1', 'middle': 'b3', 'right': 'b2'}.get(obutton, obutton)[1:] + button = getattr(defines, f'GLFW_MOUSE_BUTTON_{b}') + except Exception: + log_error(f'Mouse button: {xbutton} not recognized, ignoring') + return + try: + count = {'doubleclick': -3, 'click': -2, 'release': -1, 'press': 1, 'doublepress': 2, 'triplepress': 3}[event.lower()] + except KeyError: + log_error(f'Mouse event type: {event} not recognized, ignoring') + return + specified_modes = frozenset(modes.lower().split(',')) + if specified_modes - {'grabbed', 'ungrabbed'}: + log_error(f'Mouse modes: {modes} not recognized, ignoring') + return + try: + paction = parse_key_action(action) + except Exception: + log_error(f'Invalid mouse action: {action}. Ignoring.') + return + if paction is None: + log_error(f'Ignoring unknown mouse action: {action}') + return + for mode in specified_modes: + mouse_mappings.append(MouseMapping(button, mods, count, mode == 'grabbed', paction)) diff --git a/kitty/typing.pyi b/kitty/typing.pyi index 5d88a7fdb..d880cbec3 100644 --- a/kitty/typing.pyi +++ b/kitty/typing.pyi @@ -1,11 +1,7 @@ from asyncio import AbstractEventLoop as AbstractEventLoop from socket import AddressFamily as AddressFamily, socket as Socket -from subprocess import ( - CompletedProcess as CompletedProcess, Popen as PopenType -) -from typing import ( - Literal, Protocol as Protocol, TypedDict as TypedDict -) +from subprocess import CompletedProcess as CompletedProcess, Popen as PopenType +from typing import Literal, Protocol as Protocol, TypedDict as TypedDict from kittens.hints.main import Mark as MarkType from kittens.tui.handler import Handler as HandlerType @@ -21,16 +17,16 @@ from kitty.conf.utils import KittensKeyAction as KittensKeyActionType from .boss import Boss as BossType from .child import Child as ChildType from .conf.utils import BadLine as BadLineType -from .config import ( - KeyAction as KeyActionType, KeyMap as KeyMap, - KittyCommonOpts as KittyCommonOpts, SequenceMap as SequenceMap -) +from .config import KittyCommonOpts from .fast_data_types import ( CoreTextFont as CoreTextFont, FontConfigPattern as FontConfigPattern, Screen as ScreenType, StartupCtx as StartupCtx ) from .key_encoding import KeyEvent as KeyEventType from .layout.base import Layout as LayoutType +from .options_types import ( + KeyAction as KeyActionType, KeyMap as KeyMap, SequenceMap as SequenceMap +) from .rc.base import RemoteCommand as RemoteCommandType from .session import Session as SessionType, Tab as SessionTab from .tabs import (