feat: add per-mapping --allow-fallback for layout-independent shortcuts
Add --allow-fallback option to the map command that controls shifted and ascii (alternate_key) fallback for individual key mappings. For non-Latin keyboard layouts, when the current layout key is non-ascii (codepoint > 127 and < 0xE000), the alternate_key from the base layout is used for matching if the mapping opts in via --allow-fallback=shifted,ascii. Default kitty bindings use --allow-fallback=shifted,ascii so they work out of the box with non-Latin layouts. User custom mappings default to --allow-fallback=shifted (preserving existing shifted_key behavior without ascii fallback). --allow-fallback=none disables all fallback for a mapping. Python side: parse_options_for_map() in options/utils.py handles flag parsing, ShortcutMapping uses it in __init__. get_shortcut() filters candidates by per-mapping allow_fallback. Go side: ParseMap() handles --allow-fallback, KeyAction stores AllowFallback, ShortcutTracker.Match passes it to matching. MatchesParsedShortcut defaults to shifted,ascii for hardcoded shortcuts. Migrated kittens (themes, command_palette, diff, choose_files) to use ShortcutTracker with configurable map entries. Tests added for Python (5 test methods) and Go (ParseMap + key matching).
This commit is contained in:
parent
79bde7f9a9
commit
8ffdf7d7ee
20 changed files with 987 additions and 227 deletions
|
|
@ -274,6 +274,55 @@ for copying to clipboard.
|
|||
and you cannot have mappings with and without conditions applying to multi-keys
|
||||
with the same first key.
|
||||
|
||||
Non-Latin keyboard layout support
|
||||
--------------------------------------
|
||||
|
||||
When using a non-Latin keyboard layout (e.g. Russian, Arabic, Greek), letter-key
|
||||
shortcuts like :kbd:`Ctrl+C` stop working because the key produces a non-Latin
|
||||
character. kitty solves this with the ``--allow-fallback`` option on the ``map``
|
||||
directive, which controls how shortcuts fall back to the physical key position.
|
||||
|
||||
The ``--allow-fallback`` option accepts a comma-separated list of fallback types:
|
||||
|
||||
``shifted``
|
||||
Fall back to the *shifted key* — the character produced when Shift is held
|
||||
with the key. This is the default for all mappings and preserves the existing
|
||||
behavior.
|
||||
|
||||
``ascii``
|
||||
Fall back to the *alternate key* — the character that the physical key would
|
||||
produce in a standard US layout. This only triggers when the key produces a
|
||||
non-ASCII character, so it has no effect on Latin-based layouts like Dvorak
|
||||
or Colemak.
|
||||
|
||||
``none``
|
||||
Disable all fallback matching. The mapping will only match the exact key
|
||||
specified, ignoring both shifted and alternate key positions.
|
||||
|
||||
For example::
|
||||
|
||||
# Enable both shifted and ASCII fallback (used by default kitty shortcuts)
|
||||
map --allow-fallback=shifted,ascii kitty_mod+c copy_to_clipboard
|
||||
|
||||
# Only ASCII fallback, no shifted key fallback
|
||||
map --allow-fallback=ascii ctrl+s save_something
|
||||
|
||||
# Disable all fallback (neither shifted nor alternate key matching)
|
||||
map --allow-fallback=none ctrl+x some_action
|
||||
|
||||
All default kitty shortcuts use ``--allow-fallback=shifted,ascii``, so they work
|
||||
out of the box with non-Latin layouts. Custom mappings without an explicit
|
||||
``--allow-fallback`` get the default value of ``shifted``, which preserves
|
||||
backward compatibility.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``ascii`` fallback uses a non-ASCII guard: it only activates when
|
||||
the key produces a character with a Unicode code point above 127. This means
|
||||
alternative Latin layouts (Dvorak, Colemak, etc.) are never affected by the
|
||||
``ascii`` fallback — only non-Latin layouts trigger it.
|
||||
|
||||
|
||||
Sending arbitrary text or keys to the program running in kitty
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -156,15 +156,15 @@
|
|||
map('Change to home directory', 'cd_home ctrl+~ cd ~')
|
||||
map('Change to home directory', 'cd_home ctrl+` cd ~')
|
||||
map('Change to home directory', 'cd_home ctrl+shift+` cd ~')
|
||||
map('Change to temp directory', 'cd_tmp ctrl+t cd /tmp')
|
||||
map('Change to temp directory', 'cd_tmp --allow-fallback=shifted,ascii ctrl+t cd /tmp')
|
||||
|
||||
map('Next filter', 'next_filter ctrl+f 1')
|
||||
map('Previous filter', 'prev_filter alt+f -1')
|
||||
map('Next filter', 'next_filter --allow-fallback=shifted,ascii ctrl+f 1')
|
||||
map('Previous filter', 'prev_filter --allow-fallback=shifted,ascii alt+f -1')
|
||||
|
||||
map('Toggle showing dotfiles', 'toggle_dotfiles alt+h toggle dotfiles')
|
||||
map('Toggle showing ignored files', 'toggle_ignorefiles alt+i toggle ignorefiles')
|
||||
map('Toggle sorting by dates', 'toggle_sort_by_dates alt+d toggle sort_by_dates')
|
||||
map('Toggle showing preview', 'toggle_preview alt+p toggle preview')
|
||||
map('Toggle showing dotfiles', 'toggle_dotfiles --allow-fallback=shifted,ascii alt+h toggle dotfiles')
|
||||
map('Toggle showing ignored files', 'toggle_ignorefiles --allow-fallback=shifted,ascii alt+i toggle ignorefiles')
|
||||
map('Toggle sorting by dates', 'toggle_sort_by_dates --allow-fallback=shifted,ascii alt+d toggle sort_by_dates')
|
||||
map('Toggle showing preview', 'toggle_preview --allow-fallback=shifted,ascii alt+p toggle preview')
|
||||
|
||||
egr() # }}}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/config"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
|
|
@ -256,21 +257,23 @@ type CachedSettings struct {
|
|||
}
|
||||
|
||||
type Handler struct {
|
||||
lp *loop.Loop
|
||||
screen_size loop.ScreenSize
|
||||
all_items []DisplayItem
|
||||
filtered_idx []int // indices into all_items for current results
|
||||
match_infos []matchInfo // parallel to filtered_idx, valid when query != ""
|
||||
query string
|
||||
selected_idx int
|
||||
scroll_offset int
|
||||
input_data InputData
|
||||
result string // action definition to execute after exit
|
||||
display_lines []displayLine
|
||||
results_start_y int
|
||||
results_height int
|
||||
show_unmapped bool
|
||||
cv *utils.CachedValues[*CachedSettings]
|
||||
lp *loop.Loop
|
||||
screen_size loop.ScreenSize
|
||||
all_items []DisplayItem
|
||||
filtered_idx []int // indices into all_items for current results
|
||||
match_infos []matchInfo // parallel to filtered_idx, valid when query != ""
|
||||
query string
|
||||
selected_idx int
|
||||
scroll_offset int
|
||||
input_data InputData
|
||||
result string // action definition to execute after exit
|
||||
display_lines []displayLine
|
||||
results_start_y int
|
||||
results_height int
|
||||
show_unmapped bool
|
||||
cv *utils.CachedValues[*CachedSettings]
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
keyboard_shortcuts []*config.KeyAction
|
||||
}
|
||||
|
||||
func (h *Handler) initialize() (string, error) {
|
||||
|
|
@ -290,6 +293,8 @@ func (h *Handler) initialize() (string, error) {
|
|||
settings := h.cv.Load()
|
||||
h.show_unmapped = settings.ShowUnmapped
|
||||
|
||||
h.keyboard_shortcuts = config.ResolveShortcuts(NewConfig().KeyboardShortcuts)
|
||||
|
||||
if err := h.loadData(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -852,16 +857,27 @@ func (h *Handler) onKeyEvent(ev *loop.KeyEvent) error {
|
|||
h.triggerSelected()
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesPressOrRepeat("up") || ev.MatchesPressOrRepeat("ctrl+k") || ev.MatchesPressOrRepeat("ctrl+p") {
|
||||
if ev.MatchesPressOrRepeat("up") {
|
||||
ev.Handled = true
|
||||
h.moveSelection(-1)
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesPressOrRepeat("down") || ev.MatchesPressOrRepeat("ctrl+j") || ev.MatchesPressOrRepeat("ctrl+n") {
|
||||
if ev.MatchesPressOrRepeat("down") {
|
||||
ev.Handled = true
|
||||
h.moveSelection(1)
|
||||
return nil
|
||||
}
|
||||
if ac := h.shortcut_tracker.Match(ev, h.keyboard_shortcuts); ac != nil {
|
||||
ev.Handled = true
|
||||
switch ac.Name {
|
||||
case "selection_up":
|
||||
h.moveSelection(-1)
|
||||
case "selection_down":
|
||||
h.moveSelection(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if ev.MatchesPressOrRepeat("page_up") {
|
||||
ev.Handled = true
|
||||
delta := max(1, int(h.screen_size.HeightCells)-4)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,38 @@
|
|||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from kitty.conf.types import Definition
|
||||
from kitty.fast_data_types import add_timer, get_boss
|
||||
from kitty.typing_compat import BossType
|
||||
|
||||
from ..tui.handler import result_handler
|
||||
|
||||
definition = Definition(
|
||||
'!kittens.command_palette',
|
||||
)
|
||||
|
||||
agr = definition.add_group
|
||||
egr = definition.end_group
|
||||
map = definition.add_map
|
||||
|
||||
# shortcuts {{{
|
||||
agr('shortcuts', 'Keyboard shortcuts')
|
||||
|
||||
map('Move selection up',
|
||||
'selection_up --allow-fallback=shifted,ascii ctrl+k selection_up',
|
||||
)
|
||||
map('Move selection up',
|
||||
'selection_up --allow-fallback=shifted,ascii ctrl+p selection_up',
|
||||
)
|
||||
map('Move selection down',
|
||||
'selection_down --allow-fallback=shifted,ascii ctrl+j selection_down',
|
||||
)
|
||||
map('Move selection down',
|
||||
'selection_down --allow-fallback=shifted,ascii ctrl+n selection_down',
|
||||
)
|
||||
|
||||
egr() # }}}
|
||||
|
||||
|
||||
def collect_keys_data(opts: Any) -> dict[str, Any]:
|
||||
"""Collect all keybinding data from options into a JSON-serializable dict."""
|
||||
|
|
@ -188,3 +215,5 @@ def handle_result(args: list[str], data: dict[str, Any], target_window_id: int,
|
|||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
cd['short_desc'] = help_text
|
||||
elif __name__ == '__conf__':
|
||||
sys.options_definition = definition # type: ignore
|
||||
|
|
|
|||
|
|
@ -192,21 +192,21 @@ def main(args: list[str]) -> None:
|
|||
agr('shortcuts', 'Keyboard shortcuts')
|
||||
|
||||
map('Quit',
|
||||
'quit q quit',
|
||||
'quit --allow-fallback=shifted,ascii q quit',
|
||||
)
|
||||
map('Quit',
|
||||
'quit esc quit',
|
||||
)
|
||||
|
||||
map('Scroll down',
|
||||
'scroll_down j scroll_by 1',
|
||||
'scroll_down --allow-fallback=shifted,ascii j scroll_by 1',
|
||||
)
|
||||
map('Scroll down',
|
||||
'scroll_down down scroll_by 1',
|
||||
)
|
||||
|
||||
map('Scroll up',
|
||||
'scroll_up k scroll_by -1',
|
||||
'scroll_up --allow-fallback=shifted,ascii k scroll_by -1',
|
||||
)
|
||||
map('Scroll up',
|
||||
'scroll_up up scroll_by -1',
|
||||
|
|
@ -227,41 +227,41 @@ def main(args: list[str]) -> None:
|
|||
'scroll_page_down space scroll_to next-page',
|
||||
)
|
||||
map('Scroll to next page',
|
||||
'scroll_page_down ctrl+f scroll_to next-page',
|
||||
'scroll_page_down --allow-fallback=shifted,ascii ctrl+f scroll_to next-page',
|
||||
)
|
||||
|
||||
map('Scroll to previous page',
|
||||
'scroll_page_up page_up scroll_to prev-page',
|
||||
)
|
||||
map('Scroll to previous page',
|
||||
'scroll_page_up ctrl+b scroll_to prev-page',
|
||||
'scroll_page_up --allow-fallback=shifted,ascii ctrl+b scroll_to prev-page',
|
||||
)
|
||||
|
||||
map('Scroll down half page',
|
||||
'scroll_half_page_down ctrl+d scroll_to next-half-page',
|
||||
'scroll_half_page_down --allow-fallback=shifted,ascii ctrl+d scroll_to next-half-page',
|
||||
)
|
||||
map('Scroll up half page',
|
||||
'scroll_half_page_up ctrl+u scroll_to prev-half-page',
|
||||
'scroll_half_page_up --allow-fallback=shifted,ascii ctrl+u scroll_to prev-half-page',
|
||||
)
|
||||
|
||||
map('Scroll to next change',
|
||||
'next_change n scroll_to next-change',
|
||||
'next_change --allow-fallback=shifted,ascii n scroll_to next-change',
|
||||
)
|
||||
|
||||
map('Scroll to previous change',
|
||||
'prev_change p scroll_to prev-change',
|
||||
'prev_change --allow-fallback=shifted,ascii p scroll_to prev-change',
|
||||
)
|
||||
|
||||
map('Scroll to next file',
|
||||
'next_file shift+j scroll_to next-file',
|
||||
'next_file --allow-fallback=shifted,ascii shift+j scroll_to next-file',
|
||||
)
|
||||
|
||||
map('Scroll to previous file',
|
||||
'prev_file shift+k scroll_to prev-file',
|
||||
'prev_file --allow-fallback=shifted,ascii shift+k scroll_to prev-file',
|
||||
)
|
||||
|
||||
map('Show all context',
|
||||
'all_context a change_context all',
|
||||
'all_context --allow-fallback=shifted,ascii a change_context all',
|
||||
)
|
||||
|
||||
map('Show default context',
|
||||
|
|
@ -299,15 +299,17 @@ def main(args: list[str]) -> None:
|
|||
)
|
||||
|
||||
map('Search forward (no regex)',
|
||||
'search_forward_simple f start_search substring forward',
|
||||
'search_forward_simple --allow-fallback=shifted,ascii f start_search substring forward',
|
||||
)
|
||||
|
||||
map('Search backward (no regex)',
|
||||
'search_backward_simple b start_search substring backward',
|
||||
'search_backward_simple --allow-fallback=shifted,ascii b start_search substring backward',
|
||||
)
|
||||
|
||||
map('Copy selection to clipboard', 'copy_to_clipboard y copy_to_clipboard')
|
||||
map('Copy selection to clipboard or exit if no selection is present', 'copy_to_clipboard_or_exit ctrl+c copy_to_clipboard_or_exit')
|
||||
map('Copy selection to clipboard', 'copy_to_clipboard --allow-fallback=shifted,ascii y copy_to_clipboard')
|
||||
map('Copy selection to clipboard or exit if no selection is present',
|
||||
'copy_to_clipboard_or_exit --allow-fallback=shifted,ascii ctrl+c copy_to_clipboard_or_exit',
|
||||
)
|
||||
|
||||
egr() # }}}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,59 @@
|
|||
|
||||
import sys
|
||||
|
||||
from kitty.conf.types import Definition
|
||||
from kitty.simple_cli_definitions import CompletionSpec
|
||||
|
||||
definition = Definition(
|
||||
'!kittens.themes',
|
||||
)
|
||||
|
||||
agr = definition.add_group
|
||||
egr = definition.end_group
|
||||
map = definition.add_map
|
||||
|
||||
# shortcuts {{{
|
||||
agr('shortcuts', 'Keyboard shortcuts')
|
||||
|
||||
# Browsing mode shortcuts
|
||||
map('Quit',
|
||||
'quit --allow-fallback=shifted,ascii q quit',
|
||||
)
|
||||
map('Scroll down',
|
||||
'scroll_down --allow-fallback=shifted,ascii j scroll_down',
|
||||
)
|
||||
map('Scroll up',
|
||||
'scroll_up --allow-fallback=shifted,ascii k scroll_up',
|
||||
)
|
||||
map('Start search',
|
||||
'search --allow-fallback=shifted,ascii s search',
|
||||
)
|
||||
map('Accept theme',
|
||||
'accept --allow-fallback=shifted,ascii c accept',
|
||||
)
|
||||
|
||||
# Accepting mode shortcuts
|
||||
map('Abort and return to browsing',
|
||||
'abort --allow-fallback=shifted,ascii a abort',
|
||||
)
|
||||
map('Place theme file',
|
||||
'place_theme --allow-fallback=shifted,ascii p place_theme',
|
||||
)
|
||||
map('Modify config file',
|
||||
'modify_conf --allow-fallback=shifted,ascii m modify_conf',
|
||||
)
|
||||
map('Save as dark scheme',
|
||||
'dark_scheme --allow-fallback=shifted,ascii d dark_scheme',
|
||||
)
|
||||
map('Save as light scheme',
|
||||
'light_scheme --allow-fallback=shifted,ascii l light_scheme',
|
||||
)
|
||||
map('Save as no preference scheme',
|
||||
'no_preference --allow-fallback=shifted,ascii n no_preference',
|
||||
)
|
||||
|
||||
egr() # }}}
|
||||
|
||||
help_text = (
|
||||
'Change the kitty theme. If no theme name is supplied, run interactively, otherwise'
|
||||
' change the current theme to the specified theme name.'
|
||||
|
|
@ -57,3 +108,5 @@ def main(args: list[str]) -> None:
|
|||
cd['help_text'] = help_text
|
||||
cd['short_desc'] = 'Manage kitty color schemes easily'
|
||||
cd['args_completion'] = CompletionSpec.from_string('type:special group:complete_themes')
|
||||
elif __name__ == '__conf__':
|
||||
sys.options_definition = definition # type: ignore
|
||||
|
|
|
|||
|
|
@ -62,15 +62,17 @@ type handler struct {
|
|||
opts *Options
|
||||
cached_data *CachedData
|
||||
|
||||
state State
|
||||
fetch_result chan fetch_data
|
||||
all_themes *themes.Themes
|
||||
themes_closer io.Closer
|
||||
themes_list *ThemesList
|
||||
category_filters map[string]func(*themes.Theme) bool
|
||||
colors_set_once bool
|
||||
tabs []string
|
||||
rl *readline.Readline
|
||||
state State
|
||||
fetch_result chan fetch_data
|
||||
all_themes *themes.Themes
|
||||
themes_closer io.Closer
|
||||
themes_list *ThemesList
|
||||
category_filters map[string]func(*themes.Theme) bool
|
||||
colors_set_once bool
|
||||
tabs []string
|
||||
rl *readline.Readline
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
keyboard_shortcuts []*config.KeyAction
|
||||
}
|
||||
|
||||
// fetching {{{
|
||||
|
|
@ -123,6 +125,7 @@ func (self *handler) initialize() {
|
|||
self.category_filters = make(map[string]func(*themes.Theme) bool, len(category_filters)+1)
|
||||
maps.Copy(self.category_filters, category_filters)
|
||||
self.category_filters["recent"] = recent_filter(self.cached_data.Recent)
|
||||
self.keyboard_shortcuts = config.ResolveShortcuts(NewConfig().KeyboardShortcuts)
|
||||
go self.fetch_themes()
|
||||
self.draw_screen()
|
||||
}
|
||||
|
|
@ -230,7 +233,7 @@ func (self *handler) next(delta int, allow_wrapping bool) {
|
|||
}
|
||||
|
||||
func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
|
||||
if ev.MatchesPressOrRepeat("esc") || ev.MatchesCaseInsensitiveTextOrKey("q") {
|
||||
if ev.MatchesPressOrRepeat("esc") {
|
||||
self.lp.Quit(0)
|
||||
ev.Handled = true
|
||||
return nil
|
||||
|
|
@ -255,12 +258,12 @@ func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
|
|||
ev.Handled = true
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("j") || ev.MatchesPressOrRepeat("down") {
|
||||
if ev.MatchesPressOrRepeat("down") {
|
||||
self.next(1, true)
|
||||
ev.Handled = true
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("k") || ev.MatchesPressOrRepeat("up") {
|
||||
if ev.MatchesPressOrRepeat("up") {
|
||||
self.next(-1, true)
|
||||
ev.Handled = true
|
||||
return nil
|
||||
|
|
@ -281,12 +284,12 @@ func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("s") || ev.MatchesCaseInsensitiveTextOrKey("/") {
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("/") {
|
||||
ev.Handled = true
|
||||
self.start_search()
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("c") || ev.MatchesPressOrRepeat("enter") {
|
||||
if ev.MatchesPressOrRepeat("enter") {
|
||||
ev.Handled = true
|
||||
if self.themes_list == nil || self.themes_list.Len() == 0 {
|
||||
self.lp.Beep()
|
||||
|
|
@ -294,6 +297,27 @@ func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
|
|||
self.state = ACCEPTING
|
||||
self.draw_screen()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if ac := self.shortcut_tracker.Match(ev, self.keyboard_shortcuts); ac != nil {
|
||||
switch ac.Name {
|
||||
case "quit":
|
||||
self.lp.Quit(0)
|
||||
case "scroll_down":
|
||||
self.next(1, true)
|
||||
case "scroll_up":
|
||||
self.next(-1, true)
|
||||
case "search":
|
||||
self.start_search()
|
||||
case "accept":
|
||||
if self.themes_list == nil || self.themes_list.Len() == 0 {
|
||||
self.lp.Beep()
|
||||
} else {
|
||||
self.state = ACCEPTING
|
||||
self.draw_screen()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -495,49 +519,41 @@ func (self *handler) draw_theme_demo() {
|
|||
// accepting {{{
|
||||
|
||||
func (self *handler) on_accepting_key_event(ev *loop.KeyEvent) error {
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("q") || ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("shift+q") {
|
||||
if ev.MatchesPressOrRepeat("esc") {
|
||||
ev.Handled = true
|
||||
self.lp.Quit(0)
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("a") || ev.MatchesPressOrRepeat("shift+a") {
|
||||
ev.Handled = true
|
||||
self.state = BROWSING
|
||||
self.draw_screen()
|
||||
if ac := self.shortcut_tracker.Match(ev, self.keyboard_shortcuts); ac != nil {
|
||||
switch ac.Name {
|
||||
case "quit":
|
||||
self.lp.Quit(0)
|
||||
case "abort":
|
||||
self.state = BROWSING
|
||||
self.draw_screen()
|
||||
case "place_theme":
|
||||
self.themes_list.CurrentTheme().SaveInDir(utils.ConfigDir())
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
case "modify_conf":
|
||||
self.themes_list.CurrentTheme().SaveInConf(utils.ConfigDir(), self.opts.ReloadIn, self.opts.ConfigFileName)
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
case "dark_scheme":
|
||||
self.themes_list.CurrentTheme().SaveInFile(utils.ConfigDir(), kitty.DarkThemeFileName)
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
case "light_scheme":
|
||||
self.themes_list.CurrentTheme().SaveInFile(utils.ConfigDir(), kitty.LightThemeFileName)
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
case "no_preference":
|
||||
self.themes_list.CurrentTheme().SaveInFile(utils.ConfigDir(), kitty.NoPreferenceThemeFileName)
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("p") || ev.MatchesPressOrRepeat("shift+p") {
|
||||
ev.Handled = true
|
||||
self.themes_list.CurrentTheme().SaveInDir(utils.ConfigDir())
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
return nil
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("m") || ev.MatchesPressOrRepeat("shift+m") {
|
||||
ev.Handled = true
|
||||
self.themes_list.CurrentTheme().SaveInConf(utils.ConfigDir(), self.opts.ReloadIn, self.opts.ConfigFileName)
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
scheme := func(name string) error {
|
||||
ev.Handled = true
|
||||
self.themes_list.CurrentTheme().SaveInFile(utils.ConfigDir(), name)
|
||||
self.update_recent()
|
||||
self.lp.Quit(0)
|
||||
return nil
|
||||
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("d") || ev.MatchesPressOrRepeat("shift+d") {
|
||||
return scheme(kitty.DarkThemeFileName)
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("l") || ev.MatchesPressOrRepeat("shift+l") {
|
||||
return scheme(kitty.LightThemeFileName)
|
||||
}
|
||||
if ev.MatchesCaseInsensitiveTextOrKey("n") || ev.MatchesPressOrRepeat("shift+n") {
|
||||
return scheme(kitty.NoPreferenceThemeFileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,8 +87,13 @@ def key(x: ActionGroup) -> str:
|
|||
return group_title(x).lower()
|
||||
|
||||
def kitten_link(text: str) -> str:
|
||||
x = text.split()
|
||||
return f':doc:`kittens/{x[2]}`' if len(x) > 2 else ''
|
||||
# skip any --flag tokens at the start, then key, then 'kitten', then kitten name
|
||||
parts = text.split()
|
||||
# find 'kitten' token and take the next one as the kitten name
|
||||
for i, p in enumerate(parts):
|
||||
if p == 'kitten' and i + 1 < len(parts):
|
||||
return f':doc:`kittens/{parts[i + 1]}`'
|
||||
return ''
|
||||
|
||||
for group in sorted(allg, key=key):
|
||||
title = group_title(group)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
from typing import Any, get_type_hints
|
||||
|
||||
from kitty.conf.types import Definition, MultiOption, Option, ParserFuncType, unset
|
||||
from kitty.options.utils import parse_options_for_map
|
||||
from kitty.simple_cli_definitions import serialize_as_go_string
|
||||
from kitty.types import _T
|
||||
|
||||
|
|
@ -618,9 +619,15 @@ def basic_defval(d: Any) -> str:
|
|||
if keyboard_shortcuts:
|
||||
a('KeyboardShortcuts: []*config.KeyAction{')
|
||||
for sc in keyboard_shortcuts:
|
||||
aname, aargs = map(serialize_as_go_string, sc.action_def.partition(' ')[::2])
|
||||
a('{'f'Name: "{aname}", Args: "{aargs}", Normalized_keys: []string''{')
|
||||
ns = normalize_shortcuts(sc.key_text)
|
||||
options, leftover = parse_options_for_map(sc.parseable_text)
|
||||
allow_fallback = options.allow_fallback
|
||||
key_spec, action = leftover.split(None, 1)
|
||||
aname, _, aargs = action.partition(' ')
|
||||
aname = serialize_as_go_string(aname)
|
||||
aargs = serialize_as_go_string(aargs)
|
||||
fb = f', AllowFallback: "{serialize_as_go_string(allow_fallback)}"'
|
||||
a('{'f'Name: "{aname}", Args: "{aargs}"{fb}, Normalized_keys: []string''{')
|
||||
ns = normalize_shortcuts(key_spec)
|
||||
a(', '.join(f'"{serialize_as_go_string(x)}"' for x in ns) + ',')
|
||||
a('}''},')
|
||||
a('},')
|
||||
|
|
@ -653,23 +660,22 @@ def basic_defval(d: Any) -> str:
|
|||
has_parsers = bool(go_parsers or keyboard_shortcuts)
|
||||
a('func (c *Config) Parse(key, val string) (err error) {')
|
||||
if has_parsers:
|
||||
if go_parsers:
|
||||
a('switch key {')
|
||||
a('default: return fmt.Errorf("Unknown configuration key: %#v", key)')
|
||||
for oname, pname in go_parsers.items():
|
||||
ol = oname.lower()
|
||||
is_multiple = oname in multiopts
|
||||
a(f'case "{ol}":')
|
||||
if is_multiple:
|
||||
a(f'var temp_val []{go_types[oname]}')
|
||||
else:
|
||||
a(f'var temp_val {go_types[oname]}')
|
||||
a(f'temp_val, err = {pname}')
|
||||
a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}')
|
||||
if is_multiple:
|
||||
a(f'c.{oname} = append(c.{oname}, temp_val...)')
|
||||
else:
|
||||
a(f'c.{oname} = temp_val')
|
||||
a('switch key {')
|
||||
a('default: return fmt.Errorf("Unknown configuration key: %#v", key)')
|
||||
for oname, pname in go_parsers.items():
|
||||
ol = oname.lower()
|
||||
is_multiple = oname in multiopts
|
||||
a(f'case "{ol}":')
|
||||
if is_multiple:
|
||||
a(f'var temp_val []{go_types[oname]}')
|
||||
else:
|
||||
a(f'var temp_val {go_types[oname]}')
|
||||
a(f'temp_val, err = {pname}')
|
||||
a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}')
|
||||
if is_multiple:
|
||||
a(f'c.{oname} = append(c.{oname}, temp_val...)')
|
||||
else:
|
||||
a(f'c.{oname} = temp_val')
|
||||
if keyboard_shortcuts:
|
||||
a('case "map":')
|
||||
a('tempsc, err := config.ParseMap(val)')
|
||||
|
|
|
|||
|
|
@ -431,21 +431,26 @@ class ShortcutMapping(Mapping):
|
|||
setting_name: str = 'map'
|
||||
|
||||
def __init__(
|
||||
self, name: str, key: str, action_def: str, short_text: str, long_text: str, add_to_default: bool, documented: bool, group: 'Group', only: Only
|
||||
self, name: str, raw_definition: str, short_text: str, long_text: str,
|
||||
add_to_default: bool, documented: bool, group: 'Group', only: Only,
|
||||
):
|
||||
self.name = name
|
||||
self.only = only
|
||||
self.key = key
|
||||
self.action_def = action_def
|
||||
self._raw_definition = raw_definition
|
||||
self.short_text = short_text
|
||||
self.long_text = long_text
|
||||
self.documented = documented
|
||||
self.add_to_default = add_to_default
|
||||
self.group = group
|
||||
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.action_def = parts[1] if len(parts) > 1 else ''
|
||||
|
||||
@property
|
||||
def parseable_text(self) -> str:
|
||||
return f'{self.key} {self.action_def}'
|
||||
return self._raw_definition
|
||||
|
||||
@property
|
||||
def key_text(self) -> str:
|
||||
|
|
@ -733,8 +738,8 @@ def add_option(
|
|||
def add_map(
|
||||
self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = ''
|
||||
) -> None:
|
||||
name, key, action_def = defn.split(maxsplit=2)
|
||||
sc = ShortcutMapping(name, key, action_def, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
|
||||
name, rest = defn.split(maxsplit=1)
|
||||
sc = ShortcutMapping(name, rest, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
|
||||
self.current_group.append(sc)
|
||||
self.shortcut_map.setdefault(name, []).append(sc)
|
||||
|
||||
|
|
|
|||
2
kitty/key_encoding.py
generated
2
kitty/key_encoding.py
generated
|
|
@ -225,6 +225,8 @@ def matches(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | Eve
|
|||
is_shifted = bool(self.shifted_key and self.shift)
|
||||
if is_shifted and (mods & ~SHIFT, self.shifted_key) == spec:
|
||||
return True
|
||||
if self.alternate_key and self.key and len(self.key) == 1 and 127 < ord(self.key) < 0xE000 and (mods, self.alternate_key) == spec:
|
||||
return True
|
||||
return False
|
||||
|
||||
def matches_without_mods(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool:
|
||||
|
|
|
|||
|
|
@ -23,13 +23,14 @@
|
|||
set_ignore_os_keyboard_processing,
|
||||
)
|
||||
from .options.types import Options
|
||||
from .options.utils import KeyboardMode, KeyDefinition, KeyMap
|
||||
from .options.utils import KeyboardMode, KeyDefinition, 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')
|
||||
|
||||
|
||||
def keyboard_mode_name(screen: ScreenType) -> str:
|
||||
|
|
@ -43,7 +44,17 @@ 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:
|
||||
ans = keymap.get(SingleKey(mods & (~GLFW_MOD_SHIFT), False, ev.shifted_key))
|
||||
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:
|
||||
ans = keymap.get(SingleKey(mods, True, ev.native_key))
|
||||
return ans
|
||||
|
|
@ -58,6 +69,8 @@ def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool:
|
|||
return True
|
||||
if ev.shifted_key and mods & GLFW_MOD_SHIFT and (mods & ~GLFW_MOD_SHIFT) == smods and ev.shifted_key == s.key:
|
||||
return True
|
||||
if ev.alternate_key and 127 < ev.key < 0xE000 and ev.alternate_key == s.key and mods == smods:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -77,7 +90,7 @@ def current_keyboard_mode_name(self) -> str:
|
|||
def update_keymap(self, global_shortcuts: dict[str, SingleKey] | None = None) -> None:
|
||||
if global_shortcuts is None:
|
||||
global_shortcuts = self.set_cocoa_global_shortcuts(self.get_options()) if is_macos else {}
|
||||
self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k)] for k, v in global_shortcuts.items()}
|
||||
self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k, options=_global_shortcut_options)] for k, v in global_shortcuts.items()}
|
||||
self.global_shortcuts = global_shortcuts
|
||||
self.keyboard_modes = self.get_options().keyboard_modes.copy()
|
||||
km = self.keyboard_modes[''].keymap
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
definition = Definition(
|
||||
'kitty',
|
||||
Action('map', 'parse_map', {'keyboard_modes': 'KeyboardModeMap', 'alias_map': 'AliasMap'},
|
||||
['KeyDefinition', 'kitty.fast_data_types.SingleKey']),
|
||||
['KeyDefinition', 'KeyMapOptions', 'kitty.fast_data_types.SingleKey']),
|
||||
Action('mouse_map', 'parse_mouse_map', {'mousemap': 'MouseMap'}, ['MouseMapping']),
|
||||
has_color_table=True,
|
||||
)
|
||||
|
|
@ -3926,6 +3926,14 @@
|
|||
# multi-key shortcuts
|
||||
map ctrl+x>ctrl+y>z action
|
||||
|
||||
For non-Latin keyboard layouts (Russian, Arabic, etc.), the :code:`--allow-fallback`
|
||||
option controls physical key fallback. Values: :code:`shifted` (default — shifted key
|
||||
fallback), :code:`ascii` (alternate key / physical position fallback, only for non-ASCII
|
||||
keys), :code:`none` (disable all fallback), or a comma-separated combination.
|
||||
All default shortcuts use :code:`--allow-fallback=shifted,ascii`::
|
||||
|
||||
map --allow-fallback=shifted,ascii kitty_mod+c copy_to_clipboard
|
||||
|
||||
You can browse and trigger these actions by pressing :sc:`command_palette` to
|
||||
run the command palette. The full list of actions that can be mapped to
|
||||
key presses is available :doc:`here </actions>`.
|
||||
|
|
@ -3998,7 +4006,7 @@
|
|||
agr('shortcuts.clipboard', 'Clipboard')
|
||||
|
||||
map('Copy to clipboard',
|
||||
'copy_to_clipboard kitty_mod+c copy_to_clipboard',
|
||||
'copy_to_clipboard --allow-fallback=shifted,ascii kitty_mod+c copy_to_clipboard',
|
||||
long_text='''
|
||||
There is also a :ac:`copy_or_interrupt` action that can be optionally mapped
|
||||
to :kbd:`Ctrl+C`. It will copy only if there is a selection and send an
|
||||
|
|
@ -4009,27 +4017,27 @@
|
|||
'''
|
||||
)
|
||||
map('Copy to clipboard or pass through',
|
||||
'copy_or_noop cmd+c copy_or_noop',
|
||||
'copy_or_noop --allow-fallback=shifted,ascii cmd+c copy_or_noop',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Paste from clipboard',
|
||||
'paste_from_clipboard kitty_mod+v paste_from_clipboard',
|
||||
'paste_from_clipboard --allow-fallback=shifted,ascii kitty_mod+v paste_from_clipboard',
|
||||
)
|
||||
map('Paste from clipboard',
|
||||
'paste_from_clipboard cmd+v paste_from_clipboard',
|
||||
'paste_from_clipboard --allow-fallback=shifted,ascii cmd+v paste_from_clipboard',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Paste from selection',
|
||||
'paste_from_selection kitty_mod+s paste_from_selection',
|
||||
'paste_from_selection --allow-fallback=shifted,ascii kitty_mod+s paste_from_selection',
|
||||
)
|
||||
map('Paste from selection',
|
||||
'paste_from_selection shift+insert paste_from_selection',
|
||||
)
|
||||
|
||||
map('Pass selection to program',
|
||||
'pass_selection_to_program kitty_mod+o pass_selection_to_program',
|
||||
'pass_selection_to_program --allow-fallback=shifted,ascii kitty_mod+o pass_selection_to_program',
|
||||
long_text='''
|
||||
You can also pass the contents of the current selection to any program with
|
||||
:ac:`pass_selection_to_program`. By default, the system's open program is used,
|
||||
|
|
@ -4054,7 +4062,7 @@
|
|||
'scroll_line_up kitty_mod+up scroll_line_up',
|
||||
)
|
||||
map('Scroll line up',
|
||||
'scroll_line_up kitty_mod+k scroll_line_up',
|
||||
'scroll_line_up --allow-fallback=shifted,ascii kitty_mod+k scroll_line_up',
|
||||
)
|
||||
map('Scroll line up',
|
||||
'scroll_line_up opt+cmd+page_up scroll_line_up',
|
||||
|
|
@ -4069,7 +4077,7 @@
|
|||
'scroll_line_down kitty_mod+down scroll_line_down',
|
||||
)
|
||||
map('Scroll line down',
|
||||
'scroll_line_down kitty_mod+j scroll_line_down',
|
||||
'scroll_line_down --allow-fallback=shifted,ascii kitty_mod+j scroll_line_down',
|
||||
)
|
||||
map('Scroll line down',
|
||||
'scroll_line_down opt+cmd+page_down scroll_line_down',
|
||||
|
|
@ -4113,7 +4121,7 @@
|
|||
)
|
||||
|
||||
map('Scroll to previous shell prompt',
|
||||
'scroll_to_previous_prompt kitty_mod+z scroll_to_prompt -1',
|
||||
'scroll_to_previous_prompt --allow-fallback=shifted,ascii kitty_mod+z scroll_to_prompt -1',
|
||||
long_text='''
|
||||
Use a parameter of :code:`0` for :ac:`scroll_to_prompt` to scroll to the last
|
||||
jumped to or the last clicked position. Requires :ref:`shell integration
|
||||
|
|
@ -4121,10 +4129,10 @@
|
|||
'''
|
||||
)
|
||||
|
||||
map('Scroll to next shell prompt', 'scroll_to_next_prompt kitty_mod+x scroll_to_prompt 1')
|
||||
map('Scroll to next shell prompt', 'scroll_to_next_prompt --allow-fallback=shifted,ascii kitty_mod+x scroll_to_prompt 1')
|
||||
|
||||
map('Browse scrollback buffer in pager',
|
||||
'show_scrollback kitty_mod+h show_scrollback',
|
||||
'show_scrollback --allow-fallback=shifted,ascii kitty_mod+h show_scrollback',
|
||||
long_text='''
|
||||
You can pipe the contents of the current screen and history buffer as
|
||||
:file:`STDIN` to an arbitrary program using :option:`launch --stdin-source`.
|
||||
|
|
@ -4139,7 +4147,7 @@
|
|||
)
|
||||
|
||||
map('Browse output of the last shell command in pager',
|
||||
'show_last_command_output kitty_mod+g show_last_command_output',
|
||||
'show_last_command_output --allow-fallback=shifted,ascii kitty_mod+g show_last_command_output',
|
||||
long_text='''
|
||||
You can also define additional shortcuts to get the command output.
|
||||
For example, to get the first command output on screen::
|
||||
|
|
@ -4176,7 +4184,7 @@
|
|||
For more sophisticated control, such as using the current selection, use :ac:`remote_control_script`.
|
||||
''')
|
||||
|
||||
map('Search the scrollback within a pager', 'search_scrollback cmd+f search_scrollback', only='macos')
|
||||
map('Search the scrollback within a pager', 'search_scrollback --allow-fallback=shifted,ascii cmd+f search_scrollback', only='macos')
|
||||
|
||||
|
||||
egr() # }}}
|
||||
|
|
@ -4220,7 +4228,7 @@
|
|||
)
|
||||
|
||||
map('New OS window',
|
||||
'new_os_window kitty_mod+n new_os_window',
|
||||
'new_os_window --allow-fallback=shifted,ascii kitty_mod+n new_os_window',
|
||||
long_text='''
|
||||
Works like :ac:`new_window` above, except that it opens a top-level :term:`OS
|
||||
window <os_window>`. In particular you can use :ac:`new_os_window_with_cwd` to
|
||||
|
|
@ -4228,15 +4236,15 @@
|
|||
'''
|
||||
)
|
||||
map('New OS window',
|
||||
'new_os_window cmd+n new_os_window',
|
||||
'new_os_window --allow-fallback=shifted,ascii cmd+n new_os_window',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Close window',
|
||||
'close_window kitty_mod+w close_window',
|
||||
'close_window --allow-fallback=shifted,ascii kitty_mod+w close_window',
|
||||
)
|
||||
map('Close window',
|
||||
'close_window shift+cmd+d close_window',
|
||||
'close_window --allow-fallback=shifted,ascii shift+cmd+d close_window',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
|
|
@ -4249,11 +4257,11 @@
|
|||
)
|
||||
|
||||
map('Move window forward',
|
||||
'move_window_forward kitty_mod+f move_window_forward',
|
||||
'move_window_forward --allow-fallback=shifted,ascii kitty_mod+f move_window_forward',
|
||||
)
|
||||
|
||||
map('Move window backward',
|
||||
'move_window_backward kitty_mod+b move_window_backward',
|
||||
'move_window_backward --allow-fallback=shifted,ascii kitty_mod+b move_window_backward',
|
||||
)
|
||||
|
||||
map('Move window to top',
|
||||
|
|
@ -4261,10 +4269,10 @@
|
|||
)
|
||||
|
||||
map('Start resizing window',
|
||||
'start_resizing_window kitty_mod+r start_resizing_window',
|
||||
'start_resizing_window --allow-fallback=shifted,ascii kitty_mod+r start_resizing_window',
|
||||
)
|
||||
map('Start resizing window',
|
||||
'start_resizing_window cmd+r start_resizing_window',
|
||||
'start_resizing_window --allow-fallback=shifted,ascii cmd+r start_resizing_window',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
|
|
@ -4386,23 +4394,23 @@
|
|||
)
|
||||
|
||||
map('New tab',
|
||||
'new_tab kitty_mod+t new_tab',
|
||||
'new_tab --allow-fallback=shifted,ascii kitty_mod+t new_tab',
|
||||
)
|
||||
map('New tab',
|
||||
'new_tab cmd+t new_tab',
|
||||
'new_tab --allow-fallback=shifted,ascii cmd+t new_tab',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Close tab',
|
||||
'close_tab kitty_mod+q close_tab',
|
||||
'close_tab --allow-fallback=shifted,ascii kitty_mod+q close_tab',
|
||||
)
|
||||
map('Close tab',
|
||||
'close_tab cmd+w close_tab',
|
||||
'close_tab --allow-fallback=shifted,ascii cmd+w close_tab',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Close OS window',
|
||||
'close_os_window shift+cmd+w close_os_window',
|
||||
'close_os_window --allow-fallback=shifted,ascii shift+cmd+w close_os_window',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
|
|
@ -4415,10 +4423,10 @@
|
|||
)
|
||||
|
||||
map('Set tab title',
|
||||
'set_tab_title kitty_mod+alt+t set_tab_title',
|
||||
'set_tab_title --allow-fallback=shifted,ascii kitty_mod+alt+t set_tab_title',
|
||||
)
|
||||
map('Set tab title',
|
||||
'set_tab_title shift+cmd+i set_tab_title',
|
||||
'set_tab_title --allow-fallback=shifted,ascii shift+cmd+i set_tab_title',
|
||||
only='macos',
|
||||
)
|
||||
egr('''
|
||||
|
|
@ -4444,7 +4452,7 @@
|
|||
agr('shortcuts.layout', 'Layout management')
|
||||
|
||||
map('Next layout',
|
||||
'next_layout kitty_mod+l next_layout',
|
||||
'next_layout --allow-fallback=shifted,ascii kitty_mod+l next_layout',
|
||||
)
|
||||
egr('''
|
||||
You can also create shortcuts to switch to specific :term:`layouts <layout>`::
|
||||
|
|
@ -4537,7 +4545,7 @@
|
|||
''')
|
||||
|
||||
map('Open URL',
|
||||
'open_url kitty_mod+e open_url_with_hints',
|
||||
'open_url --allow-fallback=shifted,ascii kitty_mod+e open_url_with_hints',
|
||||
long_text='''
|
||||
Open a currently visible URL using the keyboard. The program used to open the
|
||||
URL is specified in :opt:`open_url_with`.
|
||||
|
|
@ -4545,7 +4553,7 @@
|
|||
)
|
||||
|
||||
map('Insert selected path',
|
||||
'insert_selected_path kitty_mod+p>f kitten hints --type path --program -',
|
||||
'insert_selected_path --allow-fallback=shifted,ascii kitty_mod+p>f kitten hints --type path --program -',
|
||||
long_text='''
|
||||
Select a path/filename and insert it into the terminal. Useful, for instance to
|
||||
run :program:`git` commands on a filename output from a previous :program:`git`
|
||||
|
|
@ -4554,12 +4562,12 @@
|
|||
)
|
||||
|
||||
map('Open selected path',
|
||||
'open_selected_path kitty_mod+p>shift+f kitten hints --type path',
|
||||
'open_selected_path --allow-fallback=shifted,ascii kitty_mod+p>shift+f kitten hints --type path',
|
||||
long_text='Select a path/filename and open it with the default open program.'
|
||||
)
|
||||
|
||||
map('Insert chosen file',
|
||||
'insert_chosen_file kitty_mod+p>c kitten choose-files',
|
||||
'insert_chosen_file --allow-fallback=shifted,ascii kitty_mod+p>c kitten choose-files',
|
||||
long_text='''
|
||||
Select a file using the :doc:`choose-files </kittens/choose-files>` kitten and insert
|
||||
it into the terminal.
|
||||
|
|
@ -4567,7 +4575,7 @@
|
|||
)
|
||||
|
||||
map('Insert chosen directory',
|
||||
'insert_chosen_directory kitty_mod+p>d kitten choose-files --mode=dir',
|
||||
'insert_chosen_directory --allow-fallback=shifted,ascii kitty_mod+p>d kitten choose-files --mode=dir',
|
||||
long_text='''
|
||||
Select a directory using the :doc:`choose-files </kittens/choose-files>` kitten and insert
|
||||
it into the terminal.
|
||||
|
|
@ -4576,7 +4584,7 @@
|
|||
|
||||
|
||||
map('Insert selected line',
|
||||
'insert_selected_line kitty_mod+p>l kitten hints --type line --program -',
|
||||
'insert_selected_line --allow-fallback=shifted,ascii kitty_mod+p>l kitten hints --type line --program -',
|
||||
long_text='''
|
||||
Select a line of text and insert it into the terminal. Useful for the output of
|
||||
things like: ``ls -1``.
|
||||
|
|
@ -4584,12 +4592,12 @@
|
|||
)
|
||||
|
||||
map('Insert selected word',
|
||||
'insert_selected_word kitty_mod+p>w kitten hints --type word --program -',
|
||||
'insert_selected_word --allow-fallback=shifted,ascii kitty_mod+p>w kitten hints --type word --program -',
|
||||
long_text='Select words and insert into terminal.'
|
||||
)
|
||||
|
||||
map('Insert selected hash',
|
||||
'insert_selected_hash kitty_mod+p>h kitten hints --type hash --program -',
|
||||
'insert_selected_hash --allow-fallback=shifted,ascii kitty_mod+p>h kitten hints --type hash --program -',
|
||||
long_text='''
|
||||
Select something that looks like a hash and insert it into the terminal. Useful
|
||||
with :program:`git`, which uses SHA1 hashes to identify commits.
|
||||
|
|
@ -4597,7 +4605,7 @@
|
|||
)
|
||||
|
||||
map('Open the selected file at the selected line',
|
||||
'goto_file_line kitty_mod+p>n kitten hints --type linenum',
|
||||
'goto_file_line --allow-fallback=shifted,ascii kitty_mod+p>n kitten hints --type linenum',
|
||||
long_text='''
|
||||
Select something that looks like :code:`filename:linenum` and open it in
|
||||
your default editor at the specified line number.
|
||||
|
|
@ -4605,7 +4613,7 @@
|
|||
)
|
||||
|
||||
map('Open the selected hyperlink',
|
||||
'open_selected_hyperlink kitty_mod+p>y kitten hints --type hyperlink',
|
||||
'open_selected_hyperlink --allow-fallback=shifted,ascii kitty_mod+p>y kitten hints --type hyperlink',
|
||||
long_text='''
|
||||
Select a :term:`hyperlink <hyperlinks>` (i.e. a URL that has been marked as such
|
||||
by the terminal program, for example, by ``ls --hyperlink=auto``).
|
||||
|
|
@ -4630,7 +4638,7 @@
|
|||
'toggle_fullscreen kitty_mod+f11 toggle_fullscreen',
|
||||
)
|
||||
map('Toggle fullscreen',
|
||||
'toggle_fullscreen ctrl+cmd+f toggle_fullscreen',
|
||||
'toggle_fullscreen --allow-fallback=shifted,ascii ctrl+cmd+f toggle_fullscreen',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
|
|
@ -4639,7 +4647,7 @@
|
|||
)
|
||||
|
||||
map('Toggle macOS secure keyboard entry',
|
||||
'toggle_macos_secure_keyboard_entry opt+cmd+s toggle_macos_secure_keyboard_entry',
|
||||
'toggle_macos_secure_keyboard_entry --allow-fallback=shifted,ascii opt+cmd+s toggle_macos_secure_keyboard_entry',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
|
|
@ -4647,7 +4655,7 @@
|
|||
map('macOS Cycle through OS Windows backwards', 'macos_cycle_through_os_windows_backwards cmd+shift+` macos_cycle_through_os_windows_backwards', only='macos')
|
||||
|
||||
map('Unicode input',
|
||||
'input_unicode_character kitty_mod+u kitten unicode_input',
|
||||
'input_unicode_character --allow-fallback=shifted,ascii kitty_mod+u kitten unicode_input',
|
||||
)
|
||||
map('Unicode input',
|
||||
'input_unicode_character ctrl+cmd+space kitten unicode_input',
|
||||
|
|
@ -4671,19 +4679,19 @@
|
|||
)
|
||||
|
||||
map('Increase background opacity',
|
||||
'increase_background_opacity kitty_mod+a>m set_background_opacity +0.1',
|
||||
'increase_background_opacity --allow-fallback=shifted,ascii kitty_mod+a>m set_background_opacity +0.1',
|
||||
)
|
||||
|
||||
map('Decrease background opacity',
|
||||
'decrease_background_opacity kitty_mod+a>l set_background_opacity -0.1',
|
||||
'decrease_background_opacity --allow-fallback=shifted,ascii kitty_mod+a>l set_background_opacity -0.1',
|
||||
)
|
||||
|
||||
map('Make background fully opaque',
|
||||
'full_background_opacity kitty_mod+a>1 set_background_opacity 1',
|
||||
'full_background_opacity --allow-fallback=shifted,ascii kitty_mod+a>1 set_background_opacity 1',
|
||||
)
|
||||
|
||||
map('Reset background opacity',
|
||||
'reset_background_opacity kitty_mod+a>d set_background_opacity default',
|
||||
'reset_background_opacity --allow-fallback=shifted,ascii kitty_mod+a>d set_background_opacity default',
|
||||
)
|
||||
|
||||
map('Reset the terminal',
|
||||
|
|
|
|||
96
kitty/options/types.py
generated
96
kitty/options/types.py
generated
|
|
@ -11,8 +11,8 @@
|
|||
from kitty.fonts import FontSpec
|
||||
import kitty.fonts
|
||||
from kitty.options.utils import (
|
||||
AliasMap, KeyDefinition, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping, NotifyOnCmdFinish,
|
||||
TabBarMarginHeight
|
||||
AliasMap, KeyDefinition, KeyMapOptions, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping,
|
||||
NotifyOnCmdFinish, TabBarMarginHeight
|
||||
)
|
||||
import kitty.options.utils
|
||||
from kitty.types import FloatEdges
|
||||
|
|
@ -848,23 +848,23 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
|
||||
defaults.map = [
|
||||
# copy_to_clipboard
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=99), 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='ascii,shifted'), definition='copy_to_clipboard'),
|
||||
# paste_from_clipboard
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=118), 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='ascii,shifted'), definition='paste_from_clipboard'),
|
||||
# paste_from_selection
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=115), 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='ascii,shifted'), 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), 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='ascii,shifted'), 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), 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='ascii,shifted'), 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), 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='ascii,shifted'), 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 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
# scroll_end
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=57357), definition='scroll_end'),
|
||||
# scroll_to_previous_prompt
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=122), 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='ascii,shifted'), definition='scroll_to_prompt -1'),
|
||||
# scroll_to_next_prompt
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=120), 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='ascii,shifted'), definition='scroll_to_prompt 1'),
|
||||
# show_scrollback
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=104), 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='ascii,shifted'), definition='show_scrollback'),
|
||||
# show_last_command_output
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=103), 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='ascii,shifted'), 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), 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='ascii,shifted'), definition='new_os_window'),
|
||||
# close_window
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=119), 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='ascii,shifted'), 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), 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='ascii,shifted'), definition='move_window_forward'),
|
||||
# move_window_backward
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=98), 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='ascii,shifted'), 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), 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='ascii,shifted'), definition='start_resizing_window'),
|
||||
# first_window
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=49), definition='first_window'),
|
||||
# second_window
|
||||
|
|
@ -934,17 +934,17 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
# previous_tab
|
||||
KeyDefinition(trigger=SingleKey(mods=5, key=57346), definition='previous_tab'),
|
||||
# new_tab
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=116), 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='ascii,shifted'), definition='new_tab'),
|
||||
# close_tab
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=113), 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='ascii,shifted'), 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), 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='ascii,shifted'), definition='set_tab_title'),
|
||||
# next_layout
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=108), 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='ascii,shifted'), 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 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
# 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), 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='ascii,shifted'), definition='open_url_with_hints'),
|
||||
# insert_selected_path
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=102),), 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='ascii,shifted'), 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),), 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='ascii,shifted'), definition='kitten hints --type path'),
|
||||
# insert_chosen_file
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=99),), 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='ascii,shifted'), definition='kitten choose-files'),
|
||||
# insert_chosen_directory
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=100),), 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='ascii,shifted'), definition='kitten choose-files --mode=dir'),
|
||||
# insert_selected_line
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=108),), 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='ascii,shifted'), definition='kitten hints --type line --program -'),
|
||||
# insert_selected_word
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=119),), 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='ascii,shifted'), definition='kitten hints --type word --program -'),
|
||||
# insert_selected_hash
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=104),), 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='ascii,shifted'), definition='kitten hints --type hash --program -'),
|
||||
# goto_file_line
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=110),), 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='ascii,shifted'), definition='kitten hints --type linenum'),
|
||||
# open_selected_hyperlink
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=112), rest=(SingleKey(key=121),), 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='ascii,shifted'), 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 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
# toggle_maximized
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=57373), definition='toggle_maximized'),
|
||||
# input_unicode_character
|
||||
KeyDefinition(trigger=SingleKey(mods=256, key=117), 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='ascii,shifted'), 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),), 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='ascii,shifted'), definition='set_background_opacity +0.1'),
|
||||
# decrease_background_opacity
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=108),), 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='ascii,shifted'), definition='set_background_opacity -0.1'),
|
||||
# full_background_opacity
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=49),), 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='ascii,shifted'), definition='set_background_opacity 1'),
|
||||
# reset_background_opacity
|
||||
KeyDefinition(is_sequence=True, trigger=SingleKey(mods=256, key=97), rest=(SingleKey(key=100),), 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='ascii,shifted'), 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 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
]
|
||||
|
||||
if is_macos:
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=99), definition='copy_or_noop'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=118), 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='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=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 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
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), 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='ascii,shifted'), 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), definition='new_os_window'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=100), definition='close_window'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=114), 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='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=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 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any:
|
|||
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), definition='new_tab'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=119), definition='close_tab'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=119), definition='close_os_window'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=105), 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='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=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), definition='toggle_fullscreen'))
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), 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='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=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'))
|
||||
|
|
|
|||
|
|
@ -1284,24 +1284,27 @@ def __init__(self, vals: tuple[T, ...]):
|
|||
self._vals = vals
|
||||
|
||||
def __set_name__(self, owner: object, name: str) -> None:
|
||||
self._name = "_" + name
|
||||
self._name = name
|
||||
|
||||
def __get__(self, obj: object, type: type | None = None) -> T:
|
||||
if obj is None:
|
||||
return self._vals[0]
|
||||
return getattr(obj, self._name, self._vals[0])
|
||||
val = obj.__dict__.get(self._name)
|
||||
if val is None or isinstance(val, LiteralField):
|
||||
return self._vals[0]
|
||||
return val
|
||||
|
||||
def __set__(self, obj: object, value: str) -> None:
|
||||
if value not in self._vals:
|
||||
raise KeyError(f'Invalid value for {self._name[1:]}: {value!r}')
|
||||
object.__setattr__(obj, self._name, value)
|
||||
raise KeyError(f'Invalid value for {self._name}: {value!r}')
|
||||
obj.__dict__[self._name] = value
|
||||
|
||||
|
||||
OnUnknown = Literal['beep', 'end', 'ignore', 'passthrough']
|
||||
OnAction = Literal['keep', 'end']
|
||||
|
||||
|
||||
@dataclass(init=False, frozen=True)
|
||||
@dataclass(frozen=True)
|
||||
class KeyMapOptions:
|
||||
when_focus_on: str = ''
|
||||
new_mode: str = ''
|
||||
|
|
@ -1309,6 +1312,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'
|
||||
|
||||
|
||||
default_key_map_options = KeyMapOptions()
|
||||
|
|
@ -1381,6 +1385,22 @@ def __init__(self, name: str = '') -> None:
|
|||
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)
|
||||
|
||||
|
||||
key_map_option_converters['allow_fallback'] = _convert_allow_fallback
|
||||
|
||||
|
||||
def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]:
|
||||
expecting_arg = ''
|
||||
ans = KeyMapOptions()
|
||||
|
|
|
|||
|
|
@ -654,3 +654,222 @@ def __call__(self, *keys: str):
|
|||
self.ae(tm('m', 'a'), [True, True])
|
||||
self.ae(tm.actions, ['push_keyboard_mode mw', 'new_window'])
|
||||
af(tm.ignore_os_keyboard_processing)
|
||||
|
||||
def test_match_physical_keys_removed(self):
|
||||
# match_physical_keys global option has been removed in favor of per-mapping --allow-fallback
|
||||
# Verify that get_shortcut does NOT match via alternate_key without per-mapping allow_fallback='ascii'
|
||||
from kitty.keys import get_shortcut
|
||||
from kitty.options.utils import KeyDefinition
|
||||
|
||||
ctrl = defines.GLFW_MOD_CONTROL
|
||||
cyrillic_s = 0x441 # Cyrillic 'с'
|
||||
latin_c = ord('c')
|
||||
|
||||
kd = KeyDefinition(definition='copy_to_clipboard') # default allow_fallback='shifted'
|
||||
keymap = {defines.SingleKey(ctrl, False, latin_c): [kd]}
|
||||
|
||||
# alternate_key should NOT match since default allow_fallback='shifted' (no 'ascii')
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl)
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
|
||||
# direct key match still works
|
||||
ev = defines.KeyEvent(latin_c, 0, latin_c, ctrl)
|
||||
result = get_shortcut(keymap, ev)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIs(result[0], kd)
|
||||
|
||||
def test_get_shortcut_per_mapping_fallback(self):
|
||||
from kitty.keys import get_shortcut
|
||||
from kitty.options.utils import KeyDefinition, KeyMapOptions
|
||||
|
||||
ctrl = defines.GLFW_MOD_CONTROL
|
||||
shift = defines.GLFW_MOD_SHIFT
|
||||
cyrillic_s = 0x441 # Cyrillic 'с' (on physical 'c' key in Russian layout)
|
||||
latin_c = ord('c')
|
||||
|
||||
def make_kd(definition='test_action', allow_fallback='shifted'):
|
||||
opts = KeyMapOptions()
|
||||
object.__setattr__(opts, 'allow_fallback', allow_fallback)
|
||||
return KeyDefinition(definition=definition, options=opts)
|
||||
|
||||
# non-ASCII key + alternate_key + allow_fallback includes ascii → match
|
||||
kd_ascii = make_kd('copy', allow_fallback='ascii,shifted')
|
||||
keymap = {defines.SingleKey(ctrl, False, latin_c): [kd_ascii]}
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl)
|
||||
result = get_shortcut(keymap, ev)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIs(result[0], kd_ascii)
|
||||
|
||||
# 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]}
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl)
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
|
||||
# shifted_key + allow_fallback='shifted' → match
|
||||
# When Shift+key pressed: key='C'(67), shifted_key='c'(99), lookup SingleKey(0, False, 'c')
|
||||
kd_shifted = make_kd('zoom', allow_fallback='shifted')
|
||||
keymap = {defines.SingleKey(0, False, latin_c): [kd_shifted]}
|
||||
ev = defines.KeyEvent(ord('C'), latin_c, 0, shift)
|
||||
result = get_shortcut(keymap, ev)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIs(result[0], kd_shifted)
|
||||
|
||||
# shifted_key + allow_fallback='ascii' (no shifted) → no shifted match
|
||||
kd_ascii_only = make_kd('zoom', allow_fallback='ascii')
|
||||
keymap = {defines.SingleKey(0, False, latin_c): [kd_ascii_only]}
|
||||
ev = defines.KeyEvent(ord('C'), latin_c, 0, shift)
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
|
||||
# allow_fallback='' (empty) → no fallback at all
|
||||
kd_none = make_kd('copy', allow_fallback='')
|
||||
keymap = {defines.SingleKey(ctrl, False, latin_c): [kd_none]}
|
||||
# ascii fallback blocked
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl)
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
# shifted fallback blocked
|
||||
ev = defines.KeyEvent(ord('C'), latin_c, 0, ctrl | shift)
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
|
||||
# ASCII key (Dvorak) + alternate_key → no fallback (non-ASCII guard: key must be > 127)
|
||||
kd_dvorak = make_kd('test', allow_fallback='ascii,shifted')
|
||||
keymap = {defines.SingleKey(ctrl, False, latin_c): [kd_dvorak]}
|
||||
ev = defines.KeyEvent(ord('k'), 0, latin_c, ctrl) # key='k' is ASCII
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
|
||||
# functional key (PUA range 0xE000+) + alternate_key → no fallback (functional keys excluded)
|
||||
kd_functional = make_kd('escape_action', allow_fallback='ascii,shifted')
|
||||
keymap = {defines.SingleKey(ctrl, False, latin_c): [kd_functional]}
|
||||
ev = defines.KeyEvent(0xE000, 0, latin_c, ctrl) # ESCAPE key (functional, PUA range)
|
||||
self.assertIsNone(get_shortcut(keymap, ev))
|
||||
|
||||
# boundary: 0xDFFF (last codepoint before PUA) → should match via ascii fallback
|
||||
kd_boundary = make_kd('boundary_action', allow_fallback='ascii,shifted')
|
||||
keymap = {defines.SingleKey(ctrl, False, latin_c): [kd_boundary]}
|
||||
ev = defines.KeyEvent(0xDFFF, 0, latin_c, ctrl)
|
||||
self.assertIsNotNone(get_shortcut(keymap, ev))
|
||||
|
||||
# boundary: 128 (first non-ASCII codepoint) → should match via ascii fallback
|
||||
ev = defines.KeyEvent(128, 0, latin_c, ctrl)
|
||||
self.assertIsNotNone(get_shortcut(keymap, ev))
|
||||
|
||||
# direct key match takes priority over alternate_key fallback
|
||||
kd_direct = make_kd('direct_action', allow_fallback='ascii,shifted')
|
||||
kd_alt = make_kd('alt_action', allow_fallback='ascii,shifted')
|
||||
keymap = {
|
||||
defines.SingleKey(ctrl, False, cyrillic_s): [kd_direct],
|
||||
defines.SingleKey(ctrl, False, latin_c): [kd_alt],
|
||||
}
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl)
|
||||
result = get_shortcut(keymap, ev)
|
||||
self.assertIs(result[0], kd_direct) # direct match, not ascii fallback
|
||||
|
||||
def test_shortcut_matches_alternate_key(self):
|
||||
from kitty.keys import shortcut_matches
|
||||
|
||||
ctrl = defines.GLFW_MOD_CONTROL
|
||||
cyrillic_s = 0x441 # Cyrillic 'с'
|
||||
latin_c = ord('c')
|
||||
|
||||
s = defines.SingleKey(ctrl, False, latin_c)
|
||||
|
||||
# non-ASCII key + alternate_key → match (unconditional with non-ASCII guard)
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, ctrl)
|
||||
self.assertTrue(shortcut_matches(s, ev))
|
||||
|
||||
# ASCII key + alternate_key → no match (non-ASCII guard blocks it)
|
||||
ev = defines.KeyEvent(ord('k'), 0, latin_c, ctrl)
|
||||
self.assertFalse(shortcut_matches(s, ev))
|
||||
|
||||
# direct key match still works
|
||||
ev = defines.KeyEvent(latin_c, 0, 0, ctrl)
|
||||
self.assertTrue(shortcut_matches(s, ev))
|
||||
|
||||
# no alternate_key → no match
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, 0, ctrl)
|
||||
self.assertFalse(shortcut_matches(s, ev))
|
||||
|
||||
# mods mismatch → no match even with alternate_key
|
||||
ev = defines.KeyEvent(cyrillic_s, 0, latin_c, defines.GLFW_MOD_ALT)
|
||||
self.assertFalse(shortcut_matches(s, ev))
|
||||
|
||||
# functional key (PUA range 0xE000+) + alternate_key → no match
|
||||
ev = defines.KeyEvent(0xE000, 0, latin_c, ctrl) # ESCAPE key (functional)
|
||||
self.assertFalse(shortcut_matches(s, ev))
|
||||
|
||||
def test_key_event_matches_alternate_key(self):
|
||||
from kitty.key_encoding import EventType, KeyEvent
|
||||
|
||||
ctrl = 0x4 # CTRL modifier in kitty protocol encoding
|
||||
|
||||
# non-ASCII key + alternate_key → match via alternate_key fallback
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=ctrl, key='\u0441', alternate_key='c', ctrl=True) # Cyrillic 'с'
|
||||
self.assertTrue(ev.matches('ctrl+c'))
|
||||
|
||||
# direct key match still works
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=ctrl, key='c', ctrl=True)
|
||||
self.assertTrue(ev.matches('ctrl+c'))
|
||||
|
||||
# ASCII key + alternate_key → no match (non-ASCII guard: key must be non-ASCII)
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=ctrl, key='k', alternate_key='c', ctrl=True)
|
||||
self.assertFalse(ev.matches('ctrl+c'))
|
||||
|
||||
# no alternate_key → no match for non-ASCII key
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=ctrl, key='\u0441', ctrl=True) # Cyrillic 'с', no alternate_key
|
||||
self.assertFalse(ev.matches('ctrl+c'))
|
||||
|
||||
# mods mismatch → no match even with alternate_key
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=0x2, key='\u0441', alternate_key='c', alt=True) # ALT, not CTRL
|
||||
self.assertFalse(ev.matches('ctrl+c'))
|
||||
|
||||
# shifted_key still works alongside alternate_key
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=0x1, key='C', shifted_key='c', shift=True)
|
||||
self.assertTrue(ev.matches('c'))
|
||||
|
||||
# functional key name (multi-char key like "ENTER") → no alternate_key fallback (guard blocks it)
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=ctrl, key='ENTER', alternate_key='c', ctrl=True)
|
||||
self.assertFalse(ev.matches('ctrl+c'))
|
||||
|
||||
# functional key (single-char PUA 0xE000+) → no alternate_key fallback
|
||||
ev = KeyEvent(type=EventType.PRESS, mods=ctrl, key='\ue000', alternate_key='c', ctrl=True)
|
||||
self.assertFalse(ev.matches('ctrl+c'))
|
||||
|
||||
def test_allow_fallback_parsing(self):
|
||||
from kitty.options.utils import parse_map
|
||||
|
||||
def first_kd(val):
|
||||
return next(iter(parse_map(val)))
|
||||
|
||||
# default: no --allow-fallback → allow_fallback='shifted'
|
||||
kd = first_kd('ctrl+c copy_to_clipboard')
|
||||
self.ae(kd.options.allow_fallback, '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)
|
||||
|
||||
# --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)
|
||||
|
||||
# --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')
|
||||
|
||||
# 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')
|
||||
|
||||
# --allow-fallback=none → empty string (no fallback)
|
||||
kd = first_kd('--allow-fallback=none ctrl+c copy_to_clipboard')
|
||||
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.when_focus_on, '1')
|
||||
|
|
|
|||
|
|
@ -248,13 +248,74 @@ type KeyAction struct {
|
|||
Normalized_keys []string
|
||||
Name string
|
||||
Args string
|
||||
AllowFallback string
|
||||
}
|
||||
|
||||
func (self *KeyAction) String() string {
|
||||
return fmt.Sprintf("map %#v %#v %#v\n", strings.Join(self.Normalized_keys, ">"), self.Name, self.Args)
|
||||
}
|
||||
|
||||
func validateAllowFallback(value string) error {
|
||||
if value == "" || value == "none" {
|
||||
return nil
|
||||
}
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "shifted" && part != "ascii" {
|
||||
return fmt.Errorf("Invalid allow-fallback value %#v, allowed values: shifted, ascii, none", part)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseMap(val string) (*KeyAction, error) {
|
||||
allow_fallback := "shifted"
|
||||
for strings.HasPrefix(val, "--") {
|
||||
flag, rest, found := strings.Cut(val, " ")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("No key specified after flag %s", flag)
|
||||
}
|
||||
rest = strings.TrimSpace(rest)
|
||||
if name, value, ok := strings.Cut(flag, "="); ok {
|
||||
// --flag=value form
|
||||
name = strings.ReplaceAll(name[2:], "-", "_")
|
||||
switch name {
|
||||
case "allow_fallback":
|
||||
if err := validateAllowFallback(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "none" {
|
||||
allow_fallback = ""
|
||||
} else {
|
||||
allow_fallback = value
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown map option: %s", flag)
|
||||
}
|
||||
} else {
|
||||
// --flag value form (space-separated)
|
||||
name := strings.ReplaceAll(flag[2:], "-", "_")
|
||||
switch name {
|
||||
case "allow_fallback":
|
||||
value, after, has_more := strings.Cut(rest, " ")
|
||||
if !has_more {
|
||||
return nil, fmt.Errorf("No key specified after %s %s", flag, value)
|
||||
}
|
||||
if err := validateAllowFallback(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "none" {
|
||||
allow_fallback = ""
|
||||
} else {
|
||||
allow_fallback = value
|
||||
}
|
||||
rest = strings.TrimSpace(after)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown map option: %s", flag)
|
||||
}
|
||||
}
|
||||
val = rest
|
||||
}
|
||||
spec, action, found := strings.Cut(val, " ")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("No action specified for shortcut %s", val)
|
||||
|
|
@ -262,7 +323,7 @@ func ParseMap(val string) (*KeyAction, error) {
|
|||
action = strings.TrimSpace(action)
|
||||
action_name, action_args, _ := strings.Cut(action, " ")
|
||||
action_args = strings.TrimSpace(action_args)
|
||||
return &KeyAction{Name: action_name, Args: action_args, Normalized_keys: NormalizeShortcuts(spec)}, nil
|
||||
return &KeyAction{Name: action_name, Args: action_args, Normalized_keys: NormalizeShortcuts(spec), AllowFallback: allow_fallback}, nil
|
||||
}
|
||||
|
||||
type ShortcutTracker struct {
|
||||
|
|
@ -274,7 +335,7 @@ func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction)
|
|||
if self.partial_num_consumed > 0 {
|
||||
ev.Handled = true
|
||||
self.partial_matches = utils.Filter(self.partial_matches, func(ac *KeyAction) bool {
|
||||
return self.partial_num_consumed < len(ac.Normalized_keys) && ev.MatchesPressOrRepeat(ac.Normalized_keys[self.partial_num_consumed])
|
||||
return self.partial_num_consumed < len(ac.Normalized_keys) && ev.MatchesPressOrRepeatWithFallback(ac.Normalized_keys[self.partial_num_consumed], ac.AllowFallback)
|
||||
})
|
||||
if len(self.partial_matches) == 0 {
|
||||
self.partial_num_consumed = 0
|
||||
|
|
@ -282,7 +343,7 @@ func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction)
|
|||
}
|
||||
} else {
|
||||
self.partial_matches = utils.Filter(all_actions, func(ac *KeyAction) bool {
|
||||
return ev.MatchesPressOrRepeat(ac.Normalized_keys[0])
|
||||
return ev.MatchesPressOrRepeatWithFallback(ac.Normalized_keys[0], ac.AllowFallback)
|
||||
})
|
||||
if len(self.partial_matches) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -28,6 +28,140 @@ func TestStringLiteralParsing(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseMap(t *testing.T) {
|
||||
// Test without --allow-fallback (default "shifted")
|
||||
ka, err := ParseMap("ctrl+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "shifted" {
|
||||
t.Fatalf("Expected AllowFallback 'shifted', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
if diff := cmp.Diff([]string{"ctrl+c"}, ka.Normalized_keys); diff != "" {
|
||||
t.Fatalf("Keys mismatch:\n%s", diff)
|
||||
}
|
||||
|
||||
// Test with --allow-fallback=ascii
|
||||
ka, err = ParseMap("--allow-fallback=ascii ctrl+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "ascii" {
|
||||
t.Fatalf("Expected AllowFallback 'ascii', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
|
||||
// Test with --allow-fallback=shifted,ascii
|
||||
ka, err = ParseMap("--allow-fallback=shifted,ascii cmd+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "shifted,ascii" {
|
||||
t.Fatalf("Expected AllowFallback 'shifted,ascii', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
if diff := cmp.Diff([]string{"super+c"}, ka.Normalized_keys); diff != "" {
|
||||
t.Fatalf("Keys mismatch:\n%s", diff)
|
||||
}
|
||||
|
||||
// Test with --allow-fallback and action args
|
||||
ka, err = ParseMap("--allow-fallback=shifted,ascii ctrl+shift+f launch --type=tab grep")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "shifted,ascii" {
|
||||
t.Fatalf("Expected AllowFallback 'shifted,ascii', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "launch" {
|
||||
t.Fatalf("Expected Name 'launch', got %#v", ka.Name)
|
||||
}
|
||||
if ka.Args != "--type=tab grep" {
|
||||
t.Fatalf("Expected Args '--type=tab grep', got %#v", ka.Args)
|
||||
}
|
||||
|
||||
// Test space form: --allow-fallback ascii (without =)
|
||||
ka, err = ParseMap("--allow-fallback ascii ctrl+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "ascii" {
|
||||
t.Fatalf("Expected AllowFallback 'ascii', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
if diff := cmp.Diff([]string{"ctrl+c"}, ka.Normalized_keys); diff != "" {
|
||||
t.Fatalf("Keys mismatch:\n%s", diff)
|
||||
}
|
||||
|
||||
// Test space form: --allow-fallback shifted,ascii
|
||||
ka, err = ParseMap("--allow-fallback shifted,ascii cmd+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "shifted,ascii" {
|
||||
t.Fatalf("Expected AllowFallback 'shifted,ascii', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
|
||||
// Test --allow-fallback=none (equals form)
|
||||
ka, err = ParseMap("--allow-fallback=none ctrl+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "" {
|
||||
t.Fatalf("Expected AllowFallback '', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
|
||||
// Test --allow-fallback none (space form)
|
||||
ka, err = ParseMap("--allow-fallback none ctrl+c copy_to_clipboard")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ka.AllowFallback != "" {
|
||||
t.Fatalf("Expected AllowFallback '', got %#v", ka.AllowFallback)
|
||||
}
|
||||
if ka.Name != "copy_to_clipboard" {
|
||||
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
|
||||
}
|
||||
|
||||
// Test error: unknown flag
|
||||
_, err = ParseMap("--allow-fallbak=ascii ctrl+c copy")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for unknown flag --allow-fallbak")
|
||||
}
|
||||
|
||||
// Test error: unknown flag without =
|
||||
_, err = ParseMap("--unknown ctrl+c copy")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for unknown flag --unknown")
|
||||
}
|
||||
|
||||
// Test error: invalid allow-fallback value
|
||||
_, err = ParseMap("--allow-fallback=typo ctrl+c copy")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid allow-fallback value 'typo'")
|
||||
}
|
||||
|
||||
// Test error: invalid allow-fallback value in space form
|
||||
_, err = ParseMap("--allow-fallback typo ctrl+c copy")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid allow-fallback value 'typo' in space form")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeShortcuts(t *testing.T) {
|
||||
for q, expected_ := range map[string]string{
|
||||
`a`: `a`,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kovidgoyal/kitty"
|
||||
)
|
||||
|
|
@ -285,7 +286,12 @@ func ParseShortcut(spec string) *ParsedShortcut {
|
|||
return &ans
|
||||
}
|
||||
|
||||
func (self *KeyEvent) MatchesParsedShortcut(ps *ParsedShortcut, event_type KeyEventType) bool {
|
||||
func isNonASCIIKey(key string) bool {
|
||||
r, size := utf8.DecodeRuneInString(key)
|
||||
return size > 0 && size == len(key) && r > 127 && r < 0xE000 && r != utf8.RuneError
|
||||
}
|
||||
|
||||
func (self *KeyEvent) MatchesParsedShortcutWithFallback(ps *ParsedShortcut, event_type KeyEventType, allowFallback string) bool {
|
||||
if self.Type&event_type == 0 {
|
||||
return false
|
||||
}
|
||||
|
|
@ -293,12 +299,19 @@ func (self *KeyEvent) MatchesParsedShortcut(ps *ParsedShortcut, event_type KeyEv
|
|||
if mods == ps.Mods && self.Key == ps.KeyName {
|
||||
return true
|
||||
}
|
||||
if self.ShiftedKey != "" && mods&SHIFT != 0 && (mods & ^SHIFT) == ps.Mods && self.ShiftedKey == ps.KeyName {
|
||||
if strings.Contains(allowFallback, "shifted") && self.ShiftedKey != "" && mods&SHIFT != 0 && (mods & ^SHIFT) == ps.Mods && self.ShiftedKey == ps.KeyName {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(allowFallback, "ascii") && self.AlternateKey != "" && isNonASCIIKey(self.Key) && mods == ps.Mods && self.AlternateKey == ps.KeyName {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *KeyEvent) MatchesParsedShortcut(ps *ParsedShortcut, event_type KeyEventType) bool {
|
||||
return self.MatchesParsedShortcutWithFallback(ps, event_type, "shifted,ascii")
|
||||
}
|
||||
|
||||
func (self *KeyEvent) Matches(spec string, event_type KeyEventType) bool {
|
||||
return self.MatchesParsedShortcut(ParseShortcut(spec), event_type)
|
||||
}
|
||||
|
|
@ -307,6 +320,10 @@ func (self *KeyEvent) MatchesPressOrRepeat(spec string) bool {
|
|||
return self.MatchesParsedShortcut(ParseShortcut(spec), PRESS|REPEAT)
|
||||
}
|
||||
|
||||
func (self *KeyEvent) MatchesPressOrRepeatWithFallback(spec string, allowFallback string) bool {
|
||||
return self.MatchesParsedShortcutWithFallback(ParseShortcut(spec), PRESS|REPEAT, allowFallback)
|
||||
}
|
||||
|
||||
func (self *KeyEvent) MatchesCaseSensitiveTextOrKey(spec string) bool {
|
||||
if self.MatchesParsedShortcut(ParseShortcut(spec), PRESS|REPEAT) {
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -28,3 +28,108 @@ func TestKeyEventFromCSI(t *testing.T) {
|
|||
test_text("121;;121u", "y", "")
|
||||
test_text("121::122;;121u", "y", "z")
|
||||
}
|
||||
|
||||
func TestIsNonASCIIKey(t *testing.T) {
|
||||
if !isNonASCIIKey("с") { // Cyrillic с (U+0441)
|
||||
t.Fatal("Cyrillic с should be non-ASCII")
|
||||
}
|
||||
if isNonASCIIKey("c") { // Latin c
|
||||
t.Fatal("Latin c should be ASCII")
|
||||
}
|
||||
if isNonASCIIKey("") {
|
||||
t.Fatal("empty string should not be non-ASCII")
|
||||
}
|
||||
// boundary: U+0080 (first non-ASCII) should be true
|
||||
if !isNonASCIIKey("\u0080") {
|
||||
t.Fatal("U+0080 should be non-ASCII")
|
||||
}
|
||||
// boundary: U+007F (DEL, last ASCII) should be false
|
||||
if isNonASCIIKey("\u007f") {
|
||||
t.Fatal("U+007F should be ASCII")
|
||||
}
|
||||
// boundary: U+D7FF (last valid BMP char before surrogates) should be true
|
||||
if !isNonASCIIKey("\uD7FF") {
|
||||
t.Fatal("U+D7FF should be non-ASCII (before PUA)")
|
||||
}
|
||||
// boundary: U+E000 (first PUA, functional key range) should be false
|
||||
if isNonASCIIKey("\uE000") {
|
||||
t.Fatal("U+E000 should be excluded (functional key range)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesParsedShortcutWithFallback(t *testing.T) {
|
||||
ps := ParseShortcut("ctrl+c")
|
||||
|
||||
// Per-mapping ascii match: Cyrillic key + AlternateKey=c + allow_fallback contains ascii
|
||||
ev := &KeyEvent{Type: PRESS, Mods: CTRL, Key: "с", AlternateKey: "c"}
|
||||
if !ev.MatchesParsedShortcutWithFallback(ps, PRESS, "shifted,ascii") {
|
||||
t.Fatal("should match via AlternateKey with allow_fallback=shifted,ascii")
|
||||
}
|
||||
if !ev.MatchesParsedShortcutWithFallback(ps, PRESS, "ascii") {
|
||||
t.Fatal("should match via AlternateKey with allow_fallback=ascii")
|
||||
}
|
||||
|
||||
// Per-mapping no ascii match when allow_fallback=shifted only
|
||||
if ev.MatchesParsedShortcutWithFallback(ps, PRESS, "shifted") {
|
||||
t.Fatal("should NOT match via AlternateKey with allow_fallback=shifted (no ascii)")
|
||||
}
|
||||
|
||||
// No fallback at all
|
||||
if ev.MatchesParsedShortcutWithFallback(ps, PRESS, "") {
|
||||
t.Fatal("should NOT match with empty allow_fallback")
|
||||
}
|
||||
|
||||
// Direct Key match takes priority (always works regardless of allow_fallback)
|
||||
evDirect := &KeyEvent{Type: PRESS, Mods: CTRL, Key: "c"}
|
||||
if !evDirect.MatchesParsedShortcutWithFallback(ps, PRESS, "") {
|
||||
t.Fatal("direct Key match should work even with empty allow_fallback")
|
||||
}
|
||||
if !evDirect.MatchesParsedShortcutWithFallback(ps, PRESS, "shifted") {
|
||||
t.Fatal("direct Key match should work with any allow_fallback")
|
||||
}
|
||||
|
||||
// No AlternateKey match when Key is ASCII (Dvorak safety: non-ASCII guard)
|
||||
evDvorak := &KeyEvent{Type: PRESS, Mods: CTRL, Key: "x", AlternateKey: "c"}
|
||||
if evDvorak.MatchesParsedShortcutWithFallback(ps, PRESS, "ascii") {
|
||||
t.Fatal("should NOT match via AlternateKey when Key is ASCII (Dvorak)")
|
||||
}
|
||||
|
||||
// Shifted fallback respects per-mapping allow_fallback
|
||||
psA := ParseShortcut("a")
|
||||
evShifted := &KeyEvent{Type: PRESS, Mods: SHIFT, Key: "A", ShiftedKey: "a"}
|
||||
if !evShifted.MatchesParsedShortcutWithFallback(psA, PRESS, "shifted") {
|
||||
t.Fatal("shifted fallback should work with allow_fallback=shifted")
|
||||
}
|
||||
if evShifted.MatchesParsedShortcutWithFallback(psA, PRESS, "ascii") {
|
||||
t.Fatal("shifted fallback should NOT work with allow_fallback=ascii only")
|
||||
}
|
||||
if evShifted.MatchesParsedShortcutWithFallback(psA, PRESS, "") {
|
||||
t.Fatal("shifted fallback should NOT work with empty allow_fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesParsedShortcutUnconditionalAlternateKey(t *testing.T) {
|
||||
// Unconditional match via MatchesPressOrRepeat (hardcoded shortcuts)
|
||||
ev := &KeyEvent{Type: PRESS, Mods: CTRL, Key: "с", AlternateKey: "c"}
|
||||
if !ev.MatchesPressOrRepeat("ctrl+c") {
|
||||
t.Fatal("MatchesPressOrRepeat should match via AlternateKey with non-ASCII guard")
|
||||
}
|
||||
|
||||
// Direct Key match takes priority in unconditional mode too
|
||||
evDirect := &KeyEvent{Type: PRESS, Mods: CTRL, Key: "c"}
|
||||
if !evDirect.MatchesPressOrRepeat("ctrl+c") {
|
||||
t.Fatal("direct Key match should work in unconditional mode")
|
||||
}
|
||||
|
||||
// No AlternateKey match when Key is ASCII (Dvorak safety)
|
||||
evDvorak := &KeyEvent{Type: PRESS, Mods: CTRL, Key: "x", AlternateKey: "c"}
|
||||
if evDvorak.MatchesPressOrRepeat("ctrl+c") {
|
||||
t.Fatal("should NOT match via AlternateKey when Key is ASCII in unconditional mode")
|
||||
}
|
||||
|
||||
// ShiftedKey still works unconditionally
|
||||
evShifted := &KeyEvent{Type: PRESS, Mods: SHIFT, Key: "A", ShiftedKey: "a"}
|
||||
if !evShifted.MatchesPressOrRepeat("a") {
|
||||
t.Fatal("ShiftedKey should match unconditionally in MatchesParsedShortcut")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue