From 88ee80b327b9430483c57d3c6fb086f3b15b8874 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 Mar 2026 09:30:14 +0530 Subject: [PATCH] Cleanup previous PR Actually respect the fallback order when finding matching shortcuts --- docs/changelog.rst | 3 ++ kitty/conf/generate.py | 4 +- kitty/conf/types.py | 2 +- kitty/keys.py | 41 ++++++++++++------ kitty/options/types.py | 96 +++++++++++++++++++++--------------------- kitty/options/utils.py | 36 ++++++++++------ kitty_tests/keys.py | 36 ++++++++++------ tools/config/utils.go | 2 +- 8 files changed, 130 insertions(+), 90 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e79c3aa12..cd5626652 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -168,6 +168,8 @@ Detailed list of changes 0.47.0 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- For builtin key mappings automatically fallback to matching ASCII key when the pressed key has no matches and is a non-English character (:pull:`9671`) + - :doc:`Remote control `: Expose :code:`session_name` in the output of ``kitten @ ls`` for each window (:iss:`9732`) - Fix thickness of diagonal lines in box drawing characters not the same as horizontal/vertical lines (:iss:`9719`) @@ -184,6 +186,7 @@ Detailed list of changes - The :opt:`show_hyperlink_targets` option now allows specifying a keyboard modifier so that target URLs are only shown on hover when the modifier is pressed (:pull:`9741`) + 0.46.2 [2026-03-21] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index 0f72f385c..6e4593003 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -173,6 +173,8 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: fmod = f'{loc}.options.utils' imports.add((fmod, ftype)) return ftype + if loc == 'kitty': + imports.add(('kitty.options.utils', 'KeyFallbackType')) for aname, action in defn.actions.items(): option_names.add(aname) @@ -620,7 +622,7 @@ def gen_go_code(defn: Definition) -> str: a('KeyboardShortcuts: []*config.KeyAction{') for sc in keyboard_shortcuts: options, leftover = parse_options_for_map(sc.parseable_text) - allow_fallback = options.allow_fallback + allow_fallback = ','.join(x.value for x in options.allow_fallback) key_spec, action = leftover.split(None, 1) aname, _, aargs = action.partition(' ') aname = serialize_as_go_string(aname) diff --git a/kitty/conf/types.py b/kitty/conf/types.py index b61238d2f..979499e30 100644 --- a/kitty/conf/types.py +++ b/kitty/conf/types.py @@ -445,7 +445,7 @@ class ShortcutMapping(Mapping): from kitty.options.utils import parse_options_for_map _, remainder = parse_options_for_map(raw_definition) parts = remainder.split(maxsplit=1) - self.key = parts[0] if parts else '' + self.key = parts[0] self.action_def = parts[1] if len(parts) > 1 else '' @property diff --git a/kitty/keys.py b/kitty/keys.py index 0d121ca31..99d9ba5eb 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -23,14 +23,14 @@ from .fast_data_types import ( set_ignore_os_keyboard_processing, ) from .options.types import Options -from .options.utils import KeyboardMode, KeyDefinition, KeyMap, KeyMapOptions +from .options.utils import KeyboardMode, KeyDefinition, KeyFallbackType, KeyMap, KeyMapOptions from .typing_compat 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 -_global_shortcut_options = KeyMapOptions(allow_fallback='shifted,ascii') +_global_shortcut_options = KeyMapOptions(allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)) def keyboard_mode_name(screen: ScreenType) -> str: @@ -43,24 +43,37 @@ def keyboard_mode_name(screen: ScreenType) -> str: def get_shortcut(keymap: KeyMap, ev: KeyEvent) -> list[KeyDefinition] | None: mods = ev.mods & mod_mask ans = keymap.get(SingleKey(mods, False, ev.key)) - if ans is None and ev.shifted_key and mods & GLFW_MOD_SHIFT: - candidate = keymap.get(SingleKey(mods & (~GLFW_MOD_SHIFT), False, ev.shifted_key)) - if candidate: - filtered = [d for d in candidate if 'shifted' in d.options.allow_fallback] - if filtered: - ans = filtered - if ans is None and ev.alternate_key and 127 < ev.key < 0xE000: - candidate = keymap.get(SingleKey(mods, False, ev.alternate_key)) - if candidate: - filtered = [d for d in candidate if 'ascii' in d.options.allow_fallback] - if filtered: - ans = filtered + if ans is None: + priority_map: dict[int, int] = {} + items: list[KeyDefinition] = [] + + def add(q: list[KeyDefinition] | None, ft: KeyFallbackType) -> None: + if q: + for d in q: + prio = -1 + for i, x in enumerate(d.options.allow_fallback): + if x is ft: + prio = i + break + key = id(d) + if -1 < prio < priority_map.get(key, 100000): + if key not in priority_map: + items.append(d) + priority_map[key] = prio + if ev.shifted_key and mods & GLFW_MOD_SHIFT: + add(keymap.get(SingleKey(mods & (~GLFW_MOD_SHIFT), False, ev.shifted_key)), KeyFallbackType.shifted) + if ev.alternate_key and 127 < ev.key < 0xE000: + add(keymap.get(SingleKey(mods, False, ev.alternate_key)), KeyFallbackType.alternate) + if items: + ans = sorted(items, key=lambda x: priority_map[id(x)]) + if ans is None: ans = keymap.get(SingleKey(mods, True, ev.native_key)) return ans def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool: + ' used only for testing ' mods = ev.mods & mod_mask smods = s.mods & mod_mask if s.is_native: diff --git a/kitty/options/types.py b/kitty/options/types.py index 0bfe619f0..eb27b0845 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -11,8 +11,8 @@ import kitty.fast_data_types from kitty.fonts import FontSpec import kitty.fonts from kitty.options.utils import ( - AliasMap, KeyDefinition, KeyMapOptions, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping, - NotifyOnCmdFinish, TabBarMarginHeight + AliasMap, KeyDefinition, KeyFallbackType, KeyMapOptions, KeyboardModeMap, MouseHideWait, MouseMap, + MouseMapping, NotifyOnCmdFinish, TabBarMarginHeight ) import kitty.options.utils from kitty.types import FloatEdges @@ -848,23 +848,23 @@ defaults.watcher = {} defaults.map = [ # copy_to_clipboard - KeyDefinition(trigger=SingleKey(mods=256, key=99), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='copy_to_clipboard'), + KeyDefinition(trigger=SingleKey(mods=256, key=99), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='copy_to_clipboard'), # paste_from_clipboard - KeyDefinition(trigger=SingleKey(mods=256, key=118), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='paste_from_clipboard'), + KeyDefinition(trigger=SingleKey(mods=256, key=118), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='paste_from_clipboard'), # paste_from_selection - KeyDefinition(trigger=SingleKey(mods=256, key=115), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='paste_from_selection'), + KeyDefinition(trigger=SingleKey(mods=256, key=115), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='paste_from_selection'), # paste_from_selection KeyDefinition(trigger=SingleKey(mods=1, key=57348), definition='paste_from_selection'), # pass_selection_to_program - KeyDefinition(trigger=SingleKey(mods=256, key=111), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='pass_selection_to_program'), + KeyDefinition(trigger=SingleKey(mods=256, key=111), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='pass_selection_to_program'), # scroll_line_up KeyDefinition(trigger=SingleKey(mods=256, key=57352), definition='scroll_line_up'), # scroll_line_up - KeyDefinition(trigger=SingleKey(mods=256, key=107), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='scroll_line_up'), + KeyDefinition(trigger=SingleKey(mods=256, key=107), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='scroll_line_up'), # scroll_line_down KeyDefinition(trigger=SingleKey(mods=256, key=57353), definition='scroll_line_down'), # scroll_line_down - KeyDefinition(trigger=SingleKey(mods=256, key=106), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='scroll_line_down'), + KeyDefinition(trigger=SingleKey(mods=256, key=106), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='scroll_line_down'), # scroll_page_up KeyDefinition(trigger=SingleKey(mods=256, key=57354), definition='scroll_page_up'), # scroll_page_down @@ -874,33 +874,33 @@ defaults.map = [ # scroll_end KeyDefinition(trigger=SingleKey(mods=256, key=57357), definition='scroll_end'), # scroll_to_previous_prompt - KeyDefinition(trigger=SingleKey(mods=256, key=122), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='scroll_to_prompt -1'), + KeyDefinition(trigger=SingleKey(mods=256, key=122), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='scroll_to_prompt -1'), # scroll_to_next_prompt - KeyDefinition(trigger=SingleKey(mods=256, key=120), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='scroll_to_prompt 1'), + KeyDefinition(trigger=SingleKey(mods=256, key=120), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='scroll_to_prompt 1'), # show_scrollback - KeyDefinition(trigger=SingleKey(mods=256, key=104), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='show_scrollback'), + KeyDefinition(trigger=SingleKey(mods=256, key=104), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='show_scrollback'), # show_last_command_output - KeyDefinition(trigger=SingleKey(mods=256, key=103), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='show_last_command_output'), + KeyDefinition(trigger=SingleKey(mods=256, key=103), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='show_last_command_output'), # search_scrollback KeyDefinition(trigger=SingleKey(mods=256, key=47), definition='search_scrollback'), # new_window KeyDefinition(trigger=SingleKey(mods=256, key=57345), definition='new_window'), # new_os_window - KeyDefinition(trigger=SingleKey(mods=256, key=110), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='new_os_window'), + KeyDefinition(trigger=SingleKey(mods=256, key=110), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='new_os_window'), # close_window - KeyDefinition(trigger=SingleKey(mods=256, key=119), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='close_window'), + KeyDefinition(trigger=SingleKey(mods=256, key=119), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='close_window'), # next_window KeyDefinition(trigger=SingleKey(mods=256, key=93), definition='next_window'), # previous_window KeyDefinition(trigger=SingleKey(mods=256, key=91), definition='previous_window'), # move_window_forward - KeyDefinition(trigger=SingleKey(mods=256, key=102), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='move_window_forward'), + KeyDefinition(trigger=SingleKey(mods=256, key=102), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='move_window_forward'), # move_window_backward - KeyDefinition(trigger=SingleKey(mods=256, key=98), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='move_window_backward'), + KeyDefinition(trigger=SingleKey(mods=256, key=98), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='move_window_backward'), # move_window_to_top KeyDefinition(trigger=SingleKey(mods=256, key=96), definition='move_window_to_top'), # start_resizing_window - KeyDefinition(trigger=SingleKey(mods=256, key=114), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='start_resizing_window'), + KeyDefinition(trigger=SingleKey(mods=256, key=114), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='start_resizing_window'), # first_window KeyDefinition(trigger=SingleKey(mods=256, key=49), definition='first_window'), # second_window @@ -934,17 +934,17 @@ defaults.map = [ # previous_tab KeyDefinition(trigger=SingleKey(mods=5, key=57346), definition='previous_tab'), # new_tab - KeyDefinition(trigger=SingleKey(mods=256, key=116), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='new_tab'), + KeyDefinition(trigger=SingleKey(mods=256, key=116), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='new_tab'), # close_tab - KeyDefinition(trigger=SingleKey(mods=256, key=113), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='close_tab'), + KeyDefinition(trigger=SingleKey(mods=256, key=113), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='close_tab'), # move_tab_forward KeyDefinition(trigger=SingleKey(mods=256, key=46), definition='move_tab_forward'), # move_tab_backward KeyDefinition(trigger=SingleKey(mods=256, key=44), definition='move_tab_backward'), # set_tab_title - KeyDefinition(trigger=SingleKey(mods=258, key=116), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='set_tab_title'), + KeyDefinition(trigger=SingleKey(mods=258, key=116), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='set_tab_title'), # next_layout - KeyDefinition(trigger=SingleKey(mods=256, key=108), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='next_layout'), + KeyDefinition(trigger=SingleKey(mods=256, key=108), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='next_layout'), # increase_font_size KeyDefinition(trigger=SingleKey(mods=256, key=61), definition='change_font_size all +2.0'), # increase_font_size @@ -958,25 +958,25 @@ defaults.map = [ # reset_font_size KeyDefinition(trigger=SingleKey(mods=256, key=57347), definition='change_font_size all 0'), # open_url - KeyDefinition(trigger=SingleKey(mods=256, key=101), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='open_url_with_hints'), + KeyDefinition(trigger=SingleKey(mods=256, key=101), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='open_url_with_hints'), # insert_selected_path - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=102),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type path --program -'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=102),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type path --program -'), # open_selected_path - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(mods=1, key=102),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type path'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(mods=1, key=102),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type path'), # insert_chosen_file - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=99),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten choose-files'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=99),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten choose-files'), # insert_chosen_directory - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=100),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten choose-files --mode=dir'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=100),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten choose-files --mode=dir'), # insert_selected_line - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=108),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type line --program -'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=108),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type line --program -'), # insert_selected_word - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=119),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type word --program -'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=119),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type word --program -'), # insert_selected_hash - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=104),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type hash --program -'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=104),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type hash --program -'), # goto_file_line - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=110),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type linenum'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=110),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type linenum'), # open_selected_hyperlink - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=121),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten hints --type hyperlink'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=121),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten hints --type hyperlink'), # show_kitty_doc KeyDefinition(trigger=SingleKey(mods=256, key=57364), definition='show_kitty_doc overview'), # command_palette @@ -986,19 +986,19 @@ defaults.map = [ # toggle_maximized KeyDefinition(trigger=SingleKey(mods=256, key=57373), definition='toggle_maximized'), # input_unicode_character - KeyDefinition(trigger=SingleKey(mods=256, key=117), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='kitten unicode_input'), + KeyDefinition(trigger=SingleKey(mods=256, key=117), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='kitten unicode_input'), # edit_config_file KeyDefinition(trigger=SingleKey(mods=256, key=57365), definition='edit_config_file'), # kitty_shell KeyDefinition(trigger=SingleKey(mods=256, key=57344), definition='kitty_shell window'), # increase_background_opacity - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=109),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='set_background_opacity +0.1'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=109),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='set_background_opacity +0.1'), # decrease_background_opacity - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=108),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='set_background_opacity -0.1'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=108),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='set_background_opacity -0.1'), # full_background_opacity - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=49),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='set_background_opacity 1'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=49),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='set_background_opacity 1'), # reset_background_opacity - KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=100),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='set_background_opacity default'), + KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=100),), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='set_background_opacity default'), # reset_terminal KeyDefinition(trigger=SingleKey(mods=256, key=57349), definition='clear_terminal reset active'), # reload_config_file @@ -1008,8 +1008,8 @@ defaults.map = [ ] if is_macos: - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=99), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='copy_or_noop')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=118), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='paste_from_clipboard')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=99), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='copy_or_noop')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=118), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='paste_from_clipboard')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=57354), definition='scroll_line_up')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57352), definition='scroll_line_up')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=57355), definition='scroll_line_down')) @@ -1018,11 +1018,11 @@ if is_macos: defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57355), definition='scroll_page_down')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57356), definition='scroll_home')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57357), definition='scroll_end')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=102), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='search_scrollback')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=102), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='search_scrollback')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57345), definition='new_window')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=110), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='new_os_window')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=100), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='close_window')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=114), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='start_resizing_window')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=110), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='new_os_window')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=100), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='close_window')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=114), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='start_resizing_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=49), definition='first_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=50), definition='second_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=51), definition='third_window')) @@ -1034,18 +1034,18 @@ if is_macos: defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57), definition='ninth_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=93), definition='next_tab')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=91), definition='previous_tab')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=116), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='new_tab')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=119), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='close_tab')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=119), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='close_os_window')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=105), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='set_tab_title')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=116), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='new_tab')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=119), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='close_tab')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=119), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='close_os_window')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=105), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='set_tab_title')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=43), definition='change_font_size all +2.0')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=61), definition='change_font_size all +2.0')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=61), definition='change_font_size all +2.0')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=45), definition='change_font_size all -2.0')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=45), definition='change_font_size all -2.0')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=48), definition='change_font_size all 0')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=102), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='toggle_fullscreen')) - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback='ascii,shifted'), definition='toggle_macos_secure_keyboard_entry')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=102), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='toggle_fullscreen')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), options=KeyMapOptions(when_focus_on='', new_mode='', mode='', on_unknown='beep', on_action='keep', timeout=None, allow_fallback=(KeyFallbackType.shifted, KeyFallbackType.alternate)), definition='toggle_macos_secure_keyboard_entry')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=96), definition='macos_cycle_through_os_windows')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=96), definition='macos_cycle_through_os_windows_backwards')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input')) diff --git a/kitty/options/utils.py b/kitty/options/utils.py index a5d2b1211..be7a00230 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -1292,7 +1292,7 @@ class LiteralField(Generic[T]): val = obj.__dict__.get(self._name) if val is None or isinstance(val, LiteralField): return self._vals[0] - return val + return cast(T, val) def __set__(self, obj: object, value: str) -> None: if value not in self._vals: @@ -1304,6 +1304,14 @@ OnUnknown = Literal['beep', 'end', 'ignore', 'passthrough'] OnAction = Literal['keep', 'end'] +class KeyFallbackType(enum.Enum): + shifted = 'shifted' + alternate = 'alternate' + + def __repr__(self) -> str: + return f'KeyFallbackType.{self.value}' + + @dataclass(frozen=True) class KeyMapOptions: when_focus_on: str = '' @@ -1312,7 +1320,7 @@ class KeyMapOptions: on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown)) on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction)) timeout: float | None = None - allow_fallback: str = 'shifted' + allow_fallback: tuple[KeyFallbackType, ...] = (KeyFallbackType.shifted,) default_key_map_options = KeyMapOptions() @@ -1385,17 +1393,19 @@ key_map_option_converters: defaultdict[str, Callable[[str], Any]] = defaultdict( key_map_option_converters['timeout'] = float -_allowed_fallback_values = frozenset(('shifted', 'ascii')) - - -def _convert_allow_fallback(val: str) -> str: - if not val or val == 'none': - return '' - parts = tuple(x.strip() for x in val.split(',')) - invalid = set(parts) - _allowed_fallback_values - if invalid: - raise ValueError(f'allow_fallback values must be a subset of {_allowed_fallback_values}, got: {invalid}') - return ','.join(parts) +def _convert_allow_fallback(val: str) -> tuple[KeyFallbackType, ...]: + match val: + case '' | 'none': + return () + case 'shifted,ascii': + return (KeyFallbackType.shifted, KeyFallbackType.alternate) + case 'ascii,shifted': + return (KeyFallbackType.alternate, KeyFallbackType.shifted) + case 'shifted': + return (KeyFallbackType.shifted,) + case 'ascii': + return (KeyFallbackType.alternate,) + raise ValueError(f'allow_fallback values must be a subset of shifted, ascii, got: {val}') key_map_option_converters['allow_fallback'] = _convert_allow_fallback diff --git a/kitty_tests/keys.py b/kitty_tests/keys.py index ac432a63e..bcacdb570 100644 --- a/kitty_tests/keys.py +++ b/kitty_tests/keys.py @@ -6,6 +6,7 @@ from functools import partial import kitty.fast_data_types as defines from kitty.key_encoding import EventType, KeyEvent, decode_key_event, encode_key_event from kitty.keys import Mappings +from kitty.options.utils import KeyFallbackType from . import BaseTest @@ -680,7 +681,7 @@ class TestKeys(BaseTest): def test_get_shortcut_per_mapping_fallback(self): from kitty.keys import get_shortcut - from kitty.options.utils import KeyDefinition, KeyMapOptions + from kitty.options.utils import KeyDefinition, KeyMapOptions, _convert_allow_fallback ctrl = defines.GLFW_MOD_CONTROL shift = defines.GLFW_MOD_SHIFT @@ -688,8 +689,7 @@ class TestKeys(BaseTest): latin_c = ord('c') def make_kd(definition='test_action', allow_fallback='shifted'): - opts = KeyMapOptions() - object.__setattr__(opts, 'allow_fallback', allow_fallback) + opts = KeyMapOptions(allow_fallback=_convert_allow_fallback(allow_fallback)) return KeyDefinition(definition=definition, options=opts) # non-ASCII key + alternate_key + allow_fallback includes ascii → match @@ -700,6 +700,18 @@ class TestKeys(BaseTest): self.assertIsNotNone(result) self.assertIs(result[0], kd_ascii) + # sorting by fallback order + kd1 = make_kd('ascii', allow_fallback='ascii,shifted') + kd2 = make_kd('shifted', allow_fallback='shifted,ascii') + keymap = {defines.SingleKey(ctrl, False, latin_c): [kd1, kd2]} + ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl) + result = get_shortcut(keymap, ev) + self.assertEqual(result, [kd1, kd2]) + keymap = {defines.SingleKey(0, False, latin_c): [kd1, kd2]} + ev = defines.KeyEvent(ord('C'), latin_c, 0, shift) + result = get_shortcut(keymap, ev) + self.assertEqual(result, [kd2, kd1]) + # non-ASCII key + alternate_key + allow_fallback='shifted' (no ascii) → no ascii match kd_shifted_only = make_kd('copy', allow_fallback='shifted') keymap = {defines.SingleKey(ctrl, False, latin_c): [kd_shifted_only]} @@ -842,34 +854,34 @@ class TestKeys(BaseTest): # default: no --allow-fallback → allow_fallback='shifted' kd = first_kd('ctrl+c copy_to_clipboard') - self.ae(kd.options.allow_fallback, 'shifted') + self.ae(kd.options.allow_fallback, (KeyFallbackType.shifted,)) # --allow-fallback=shifted,ascii kd = first_kd('--allow-fallback=shifted,ascii ctrl+c copy_to_clipboard') - self.assertIn('shifted', kd.options.allow_fallback) - self.assertIn('ascii', kd.options.allow_fallback) + self.assertIn(KeyFallbackType.shifted, kd.options.allow_fallback) + self.assertIn(KeyFallbackType.alternate, kd.options.allow_fallback) # --allow-fallback=ascii (only ascii, no shifted) kd = first_kd('--allow-fallback=ascii ctrl+c copy_to_clipboard') - self.ae(kd.options.allow_fallback, 'ascii') - self.assertNotIn('shifted', kd.options.allow_fallback) + self.ae(kd.options.allow_fallback, (KeyFallbackType.alternate,)) + self.assertNotIn(KeyFallbackType.shifted, kd.options.allow_fallback) # --allow-fallback=shifted (explicit, same as default) kd = first_kd('--allow-fallback=shifted ctrl+c copy_to_clipboard') - self.ae(kd.options.allow_fallback, 'shifted') + self.ae(kd.options.allow_fallback, (KeyFallbackType.shifted,)) # invalid value raises self.assertRaises(ValueError, first_kd, '--allow-fallback=bogus ctrl+c copy_to_clipboard') # order normalization: ascii,shifted → sorted as ascii,shifted kd = first_kd('--allow-fallback=ascii,shifted ctrl+c copy_to_clipboard') - self.ae(kd.options.allow_fallback, 'ascii,shifted') + self.ae(kd.options.allow_fallback, (KeyFallbackType.alternate, KeyFallbackType.shifted)) # --allow-fallback=none → empty string (no fallback) kd = first_kd('--allow-fallback=none ctrl+c copy_to_clipboard') - self.ae(kd.options.allow_fallback, '') + self.ae(kd.options.allow_fallback, ()) # combined with other options kd = first_kd('--when-focus-on 1 --allow-fallback=ascii ctrl+c copy_to_clipboard') - self.ae(kd.options.allow_fallback, 'ascii') + self.ae(kd.options.allow_fallback, (KeyFallbackType.alternate,)) self.ae(kd.options.when_focus_on, '1') diff --git a/tools/config/utils.go b/tools/config/utils.go index 09f656249..c8480ca54 100644 --- a/tools/config/utils.go +++ b/tools/config/utils.go @@ -259,7 +259,7 @@ func validateAllowFallback(value string) error { if value == "" || value == "none" { return nil } - for _, part := range strings.Split(value, ",") { + for part := range strings.SplitSeq(value, ",") { part = strings.TrimSpace(part) if part != "shifted" && part != "ascii" { return fmt.Errorf("Invalid allow-fallback value %#v, allowed values: shifted, ascii, none", part)