diff --git a/kitty/boss.py b/kitty/boss.py index 7a6babd8c..beaec1972 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -91,7 +91,6 @@ from .fast_data_types import ( get_options, get_os_window_size, global_font_size, - is_modifier_key, last_focused_os_window_id, mark_os_window_for_close, os_window_focus_counters, @@ -105,7 +104,6 @@ from .fast_data_types import ( set_application_quit_request, set_background_image, set_boss, - set_ignore_os_keyboard_processing, set_options, set_os_window_chrome, set_os_window_size, @@ -117,11 +115,11 @@ from .fast_data_types import ( wrapped_kitten_names, ) from .key_encoding import get_name_to_functional_number_map -from .keys import get_shortcut +from .keys import Mappings from .layout.base import set_layout_options from .notify import notification_activated from .options.types import Options -from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition, KeyMap +from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition from .os_window_size import initial_window_size_func from .rgb import color_from_int from .session import Session, create_sessions, get_os_window_sizing_data @@ -297,7 +295,7 @@ class VisualSelect: set_os_window_title(self.os_window_id, '') boss = get_boss() redirect_mouse_handling(False) - boss.keyboard_mode_stack = [] + boss.mappings.clear_keyboard_modes() for wid in self.window_ids: w = boss.window_id_map.get(wid) if w is not None: @@ -375,27 +373,11 @@ class Boss: set_boss(self) self.args = args self.mouse_handler: Optional[Callable[[WindowSystemMouseEvent], None]] = None - self.keyboard_mode_stack: List[KeyboardMode] = [] - self.update_keymap(global_shortcuts) + self.mappings = Mappings(global_shortcuts) if is_macos: from .fast_data_types import cocoa_set_notification_activated_callback cocoa_set_notification_activated_callback(notification_activated) - def update_keymap(self, global_shortcuts:Optional[Dict[str, SingleKey]] = None) -> None: - if global_shortcuts is None: - if is_macos: - from .main import set_cocoa_global_shortcuts - global_shortcuts = set_cocoa_global_shortcuts(get_options()) - else: - global_shortcuts = {} - self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k)] for k, v in global_shortcuts.items()} - self.global_shortcuts = global_shortcuts - self.keyboard_modes = get_options().keyboard_modes.copy() - km = self.keyboard_modes[''].keymap - self.keyboard_modes[''].keymap = km = km.copy() - for sc in self.global_shortcuts.values(): - km.pop(sc, None) - def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None: si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session) focused_os_window = wid = 0 @@ -1341,117 +1323,16 @@ class Boss: End the current keyboard mode switching to the previous mode. ''') def pop_keyboard_mode(self) -> bool: - passthrough = True - if self.keyboard_mode_stack: - self.keyboard_mode_stack.pop() - if not self.keyboard_mode_stack: - set_ignore_os_keyboard_processing(False) - passthrough = False - return passthrough + return self.mappings.pop_keyboard_mode() @ac('misc', ''' Switch to the specified keyboard mode, pushing it onto the stack of keyboard modes. ''') def push_keyboard_mode(self, new_mode: str) -> None: - mode = self.keyboard_modes[new_mode] - self._push_keyboard_mode(mode) - - def _push_keyboard_mode(self, mode: KeyboardMode) -> None: - self.keyboard_mode_stack.append(mode) - set_ignore_os_keyboard_processing(True) + self.mappings.push_keyboard_mode(new_mode) def dispatch_possible_special_key(self, ev: KeyEvent) -> bool: - # Handles shortcuts, return True if the key was consumed - is_root_mode = not self.keyboard_mode_stack - mode = self.keyboard_modes[''] if is_root_mode else self.keyboard_mode_stack[-1] - key_action = get_shortcut(mode.keymap, ev) - if key_action is None: - if is_modifier_key(ev.key): - return False - if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev): - return True - if not is_root_mode: - if mode.sequence_keys is not None: - self.pop_keyboard_mode() - w = self.active_window - if w is not None: - w.send_key_sequence(*mode.sequence_keys) - return False - if mode.on_unknown in ('beep', 'ignore'): - if mode.on_unknown == 'beep' and get_options().enable_audio_bell: - ring_bell() - return True - if mode.on_unknown == 'passthrough': - return False - if not self.pop_keyboard_mode(): - if get_options().enable_audio_bell: - ring_bell() - return True - else: - final_actions = self.matching_key_actions(key_action) - if final_actions: - mode_pos = len(self.keyboard_mode_stack) - 1 - if final_actions[0].is_sequence: - if mode.sequence_keys is None: - sm = KeyboardMode('__sequence__') - sm.on_action = 'end' - sm.sequence_keys = [ev] - for fa in final_actions: - sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy()) - self._push_keyboard_mode(sm) - if self.args.debug_keyboard: - print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='', flush=True) - else: - mode.sequence_keys.append(ev) - if len(final_actions) == 1: - self.pop_keyboard_mode() - return self.combine(final_actions[0].definition) - if self.args.debug_keyboard: - print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='', flush=True) - mode.keymap.clear() - for fa in final_actions: - mode.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy()) - return True - final_action = final_actions[0] - consumed = self.combine(final_action.definition) - if consumed and not is_root_mode and mode.on_action == 'end': - if mode_pos < len(self.keyboard_mode_stack) and self.keyboard_mode_stack[mode_pos] is mode: - del self.keyboard_mode_stack[mode_pos] - if not self.keyboard_mode_stack: - set_ignore_os_keyboard_processing(False) - return consumed - return False - - def matching_key_actions(self, candidates: Iterable[KeyDefinition]) -> List[KeyDefinition]: - w = self.active_window - matches = [] - has_sequence_match = False - for x in candidates: - if x.options.when_focus_on: - try: - if w and w in self.match_windows(x.options.when_focus_on): - matches.append(x) - if x.is_sequence: - has_sequence_match = True - except Exception: - self.show_error(_('Invalid key mapping'), _( - 'The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on')) - return [] - else: - if x.is_sequence: - has_sequence_match = True - matches.append(x) - if has_sequence_match: - terminal_matches = [x for x in matches if not x.rest] - if terminal_matches: - matches = [terminal_matches[-1]] - else: - matches = [x for x in matches if x.is_sequence] - q = matches[-1].options.when_focus_on - matches = [x for x in matches if x.options.when_focus_on == q] - else: - matches = [matches[-1]] - return matches + return self.mappings.dispatch_possible_special_key(ev) def cancel_current_visual_select(self) -> None: if self.current_visual_select: @@ -1498,7 +1379,7 @@ class Boss: if ch in string.digits: km.keymap[SingleKey(mods=mods, key=fmap[f'KP_{ch}'])].append(ac) if len(self.current_visual_select.window_ids) > 1: - self._push_keyboard_mode(km) + self.mappings._push_keyboard_mode(km) redirect_mouse_handling(True) self.mouse_handler = self.visual_window_select_mouse_handler else: @@ -2633,7 +2514,7 @@ class Boss: if is_macos: from .fast_data_types import cocoa_clear_global_shortcuts cocoa_clear_global_shortcuts() - self.update_keymap() + self.mappings.update_keymap() if is_macos: from .fast_data_types import cocoa_recreate_global_menu cocoa_recreate_global_menu() diff --git a/kitty/keys.py b/kitty/keys.py index eba7c1b2f..04390be86 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -1,12 +1,31 @@ #!/usr/bin/env python # License: GPL v3 Copyright: 2016, Kovid Goyal -from typing import List, Optional +from gettext import gettext as _ +from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional -from .fast_data_types import GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_HYPER, GLFW_MOD_META, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent, SingleKey -from .options.utils import KeyDefinition, KeyMap +from .constants import is_macos +from .fast_data_types import ( + GLFW_MOD_ALT, + GLFW_MOD_CONTROL, + GLFW_MOD_HYPER, + GLFW_MOD_META, + GLFW_MOD_SHIFT, + GLFW_MOD_SUPER, + KeyEvent, + SingleKey, + get_boss, + get_options, + is_modifier_key, + ring_bell, + set_ignore_os_keyboard_processing, +) +from .options.utils import KeyboardMode, KeyDefinition, KeyMap from .typing import ScreenType +if TYPE_CHECKING: + from .window import Window + mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER @@ -37,3 +56,163 @@ def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool: if ev.shifted_key and mods & GLFW_MOD_SHIFT and (mods & ~GLFW_MOD_SHIFT) == smods and ev.shifted_key == s.key: return True return False + + +class Mappings: + + ' Manage all keyboard mappings ' + + def __init__(self, global_shortcuts:Optional[Dict[str, SingleKey]] = None) -> None: + self.keyboard_mode_stack: List[KeyboardMode] = [] + self.update_keymap(global_shortcuts) + + def update_keymap(self, global_shortcuts:Optional[Dict[str, SingleKey]] = None) -> None: + if global_shortcuts is None: + if is_macos: + from .main import set_cocoa_global_shortcuts + global_shortcuts = set_cocoa_global_shortcuts(get_options()) + else: + global_shortcuts = {} + self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k)] for k, v in global_shortcuts.items()} + self.global_shortcuts = global_shortcuts + self.keyboard_modes = get_options().keyboard_modes.copy() + km = self.keyboard_modes[''].keymap + self.keyboard_modes[''].keymap = km = km.copy() + for sc in self.global_shortcuts.values(): + km.pop(sc, None) + + def clear_keyboard_modes(self) -> None: + self.keyboard_mode_stack = [] + + def pop_keyboard_mode(self) -> bool: + passthrough = True + if self.keyboard_mode_stack: + self.keyboard_mode_stack.pop() + if not self.keyboard_mode_stack: + self.set_ignore_os_keyboard_processing(False) + passthrough = False + return passthrough + + def _push_keyboard_mode(self, mode: KeyboardMode) -> None: + self.keyboard_mode_stack.append(mode) + self.set_ignore_os_keyboard_processing(True) + + def push_keyboard_mode(self, new_mode: str) -> None: + mode = self.keyboard_modes[new_mode] + self._push_keyboard_mode(mode) + + def debug_print(self, *args: Any, end: str = '\n') -> None: + b = get_boss() + if b.args.debug_keyboard: + print(*args, end=end, flush=True) + + def matching_key_actions(self, candidates: Iterable[KeyDefinition]) -> List[KeyDefinition]: + w = self.get_active_window() + matches = [] + has_sequence_match = False + for x in candidates: + if x.options.when_focus_on: + try: + if w and w in self.match_windows(x.options.when_focus_on): + matches.append(x) + if x.is_sequence: + has_sequence_match = True + except Exception: + self.show_error(_('Invalid key mapping'), _( + 'The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on')) + return [] + else: + if x.is_sequence: + has_sequence_match = True + matches.append(x) + if has_sequence_match: + terminal_matches = [x for x in matches if not x.rest] + if terminal_matches: + matches = [terminal_matches[-1]] + else: + matches = [x for x in matches if x.is_sequence] + q = matches[-1].options.when_focus_on + matches = [x for x in matches if x.options.when_focus_on == q] + else: + matches = [matches[-1]] + return matches + + def dispatch_possible_special_key(self, ev: KeyEvent) -> bool: + # Handles shortcuts, return True if the key was consumed + is_root_mode = not self.keyboard_mode_stack + mode = self.keyboard_modes[''] if is_root_mode else self.keyboard_mode_stack[-1] + key_action = get_shortcut(mode.keymap, ev) + if key_action is None: + if is_modifier_key(ev.key): + return False + if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev): + return True + if not is_root_mode: + if mode.sequence_keys is not None: + self.pop_keyboard_mode() + w = self.get_active_window() + if w is not None: + w.send_key_sequence(*mode.sequence_keys) + return False + if mode.on_unknown in ('beep', 'ignore'): + if mode.on_unknown == 'beep': + self.ring_bell() + return True + if mode.on_unknown == 'passthrough': + return False + if not self.pop_keyboard_mode(): + self.ring_bell() + return True + else: + final_actions = self.matching_key_actions(key_action) + if final_actions: + mode_pos = len(self.keyboard_mode_stack) - 1 + if final_actions[0].is_sequence: + if mode.sequence_keys is None: + sm = KeyboardMode('__sequence__') + sm.on_action = 'end' + sm.sequence_keys = [ev] + for fa in final_actions: + sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy()) + self._push_keyboard_mode(sm) + self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='') + else: + mode.sequence_keys.append(ev) + if len(final_actions) == 1: + self.pop_keyboard_mode() + return self.combine(final_actions[0].definition) + self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='') + mode.keymap.clear() + for fa in final_actions: + mode.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy()) + return True + final_action = final_actions[0] + consumed = self.combine(final_action.definition) + if consumed and not is_root_mode and mode.on_action == 'end': + if mode_pos < len(self.keyboard_mode_stack) and self.keyboard_mode_stack[mode_pos] is mode: + del self.keyboard_mode_stack[mode_pos] + if not self.keyboard_mode_stack: + self.set_ignore_os_keyboard_processing(False) + return consumed + return False + + # System integration {{{ + def get_active_window(self) -> Optional['Window']: + return get_boss().active_window + + def match_windows(self, expr: str) -> Iterator['Window']: + return get_boss().match_windows(expr) + + def show_error(self, title: str, msg: str) -> None: + return get_boss().show_error(title, msg) + + def ring_bell(self) -> None: + if get_options().enable_audio_bell: + ring_bell() + + def combine(self, action_definition: str) -> bool: + return get_boss().combine(action_definition) + + def set_ignore_os_keyboard_processing(self, on: bool) -> None: + set_ignore_os_keyboard_processing(on) + # }}}