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:
Павел Мешалкин
2026-03-25 19:34:13 +03:00
parent 79bde7f9a9
commit 8ffdf7d7ee
20 changed files with 987 additions and 227 deletions

View File

@@ -274,6 +274,55 @@ for copying to clipboard.
and you cannot have mappings with and without conditions applying to multi-keys and you cannot have mappings with and without conditions applying to multi-keys
with the same first key. 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 Sending arbitrary text or keys to the program running in kitty
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@@ -156,15 +156,15 @@ map('Change to root directory', 'cd_root ctrl+/ cd /')
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+` cd ~') map('Change to home directory', 'cd_home ctrl+` cd ~')
map('Change to home directory', 'cd_home ctrl+shift+` 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('Next filter', 'next_filter --allow-fallback=shifted,ascii ctrl+f 1')
map('Previous filter', 'prev_filter alt+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 dotfiles', 'toggle_dotfiles --allow-fallback=shifted,ascii alt+h toggle dotfiles')
map('Toggle showing ignored files', 'toggle_ignorefiles alt+i toggle ignorefiles') map('Toggle showing ignored files', 'toggle_ignorefiles --allow-fallback=shifted,ascii alt+i toggle ignorefiles')
map('Toggle sorting by dates', 'toggle_sort_by_dates alt+d toggle sort_by_dates') 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 alt+p toggle preview') map('Toggle showing preview', 'toggle_preview --allow-fallback=shifted,ascii alt+p toggle preview')
egr() # }}} egr() # }}}

View File

@@ -11,6 +11,7 @@ import (
"strings" "strings"
"github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/config"
"github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui" "github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop" "github.com/kovidgoyal/kitty/tools/tui/loop"
@@ -256,21 +257,23 @@ type CachedSettings struct {
} }
type Handler struct { type Handler struct {
lp *loop.Loop lp *loop.Loop
screen_size loop.ScreenSize screen_size loop.ScreenSize
all_items []DisplayItem all_items []DisplayItem
filtered_idx []int // indices into all_items for current results filtered_idx []int // indices into all_items for current results
match_infos []matchInfo // parallel to filtered_idx, valid when query != "" match_infos []matchInfo // parallel to filtered_idx, valid when query != ""
query string query string
selected_idx int selected_idx int
scroll_offset int scroll_offset int
input_data InputData input_data InputData
result string // action definition to execute after exit result string // action definition to execute after exit
display_lines []displayLine display_lines []displayLine
results_start_y int results_start_y int
results_height int results_height int
show_unmapped bool show_unmapped bool
cv *utils.CachedValues[*CachedSettings] cv *utils.CachedValues[*CachedSettings]
shortcut_tracker config.ShortcutTracker
keyboard_shortcuts []*config.KeyAction
} }
func (h *Handler) initialize() (string, error) { func (h *Handler) initialize() (string, error) {
@@ -290,6 +293,8 @@ func (h *Handler) initialize() (string, error) {
settings := h.cv.Load() settings := h.cv.Load()
h.show_unmapped = settings.ShowUnmapped h.show_unmapped = settings.ShowUnmapped
h.keyboard_shortcuts = config.ResolveShortcuts(NewConfig().KeyboardShortcuts)
if err := h.loadData(); err != nil { if err := h.loadData(); err != nil {
return "", err return "", err
} }
@@ -852,16 +857,27 @@ func (h *Handler) onKeyEvent(ev *loop.KeyEvent) error {
h.triggerSelected() h.triggerSelected()
return nil return nil
} }
if ev.MatchesPressOrRepeat("up") || ev.MatchesPressOrRepeat("ctrl+k") || ev.MatchesPressOrRepeat("ctrl+p") { if ev.MatchesPressOrRepeat("up") {
ev.Handled = true ev.Handled = true
h.moveSelection(-1) h.moveSelection(-1)
return nil return nil
} }
if ev.MatchesPressOrRepeat("down") || ev.MatchesPressOrRepeat("ctrl+j") || ev.MatchesPressOrRepeat("ctrl+n") { if ev.MatchesPressOrRepeat("down") {
ev.Handled = true ev.Handled = true
h.moveSelection(1) h.moveSelection(1)
return nil 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") { if ev.MatchesPressOrRepeat("page_up") {
ev.Handled = true ev.Handled = true
delta := max(1, int(h.screen_size.HeightCells)-4) delta := max(1, int(h.screen_size.HeightCells)-4)

View File

@@ -6,11 +6,38 @@ import sys
from functools import partial from functools import partial
from typing import Any from typing import Any
from kitty.conf.types import Definition
from kitty.fast_data_types import add_timer, get_boss from kitty.fast_data_types import add_timer, get_boss
from kitty.typing_compat import BossType from kitty.typing_compat import BossType
from ..tui.handler import result_handler 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]: def collect_keys_data(opts: Any) -> dict[str, Any]:
"""Collect all keybinding data from options into a JSON-serializable dict.""" """Collect all keybinding data from options into a JSON-serializable dict."""
@@ -188,3 +215,5 @@ elif __name__ == '__doc__':
cd['options'] = OPTIONS cd['options'] = OPTIONS
cd['help_text'] = help_text cd['help_text'] = help_text
cd['short_desc'] = help_text cd['short_desc'] = help_text
elif __name__ == '__conf__':
sys.options_definition = definition # type: ignore

View File

@@ -192,21 +192,21 @@ egr() # }}}
agr('shortcuts', 'Keyboard shortcuts') agr('shortcuts', 'Keyboard shortcuts')
map('Quit', map('Quit',
'quit q quit', 'quit --allow-fallback=shifted,ascii q quit',
) )
map('Quit', map('Quit',
'quit esc quit', 'quit esc quit',
) )
map('Scroll down', map('Scroll down',
'scroll_down j scroll_by 1', 'scroll_down --allow-fallback=shifted,ascii j scroll_by 1',
) )
map('Scroll down', map('Scroll down',
'scroll_down down scroll_by 1', 'scroll_down down scroll_by 1',
) )
map('Scroll up', map('Scroll up',
'scroll_up k scroll_by -1', 'scroll_up --allow-fallback=shifted,ascii k scroll_by -1',
) )
map('Scroll up', map('Scroll up',
'scroll_up up scroll_by -1', 'scroll_up up scroll_by -1',
@@ -227,41 +227,41 @@ map('Scroll to next page',
'scroll_page_down space scroll_to next-page', 'scroll_page_down space scroll_to next-page',
) )
map('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', map('Scroll to previous page',
'scroll_page_up page_up scroll_to prev-page', 'scroll_page_up page_up scroll_to prev-page',
) )
map('Scroll to previous 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', 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', 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', 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', 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', 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', 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', map('Show all context',
'all_context a change_context all', 'all_context --allow-fallback=shifted,ascii a change_context all',
) )
map('Show default context', map('Show default context',
@@ -299,15 +299,17 @@ map('Scroll to previous search match',
) )
map('Search forward (no regex)', 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)', 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', '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 ctrl+c copy_to_clipboard_or_exit') 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() # }}} egr() # }}}

View File

@@ -3,8 +3,59 @@
import sys import sys
from kitty.conf.types import Definition
from kitty.simple_cli_definitions import CompletionSpec 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 = ( help_text = (
'Change the kitty theme. If no theme name is supplied, run interactively, otherwise' 'Change the kitty theme. If no theme name is supplied, run interactively, otherwise'
' change the current theme to the specified theme name.' ' change the current theme to the specified theme name.'
@@ -57,3 +108,5 @@ elif __name__ == '__doc__':
cd['help_text'] = help_text cd['help_text'] = help_text
cd['short_desc'] = 'Manage kitty color schemes easily' cd['short_desc'] = 'Manage kitty color schemes easily'
cd['args_completion'] = CompletionSpec.from_string('type:special group:complete_themes') cd['args_completion'] = CompletionSpec.from_string('type:special group:complete_themes')
elif __name__ == '__conf__':
sys.options_definition = definition # type: ignore

View File

@@ -62,15 +62,17 @@ type handler struct {
opts *Options opts *Options
cached_data *CachedData cached_data *CachedData
state State state State
fetch_result chan fetch_data fetch_result chan fetch_data
all_themes *themes.Themes all_themes *themes.Themes
themes_closer io.Closer themes_closer io.Closer
themes_list *ThemesList themes_list *ThemesList
category_filters map[string]func(*themes.Theme) bool category_filters map[string]func(*themes.Theme) bool
colors_set_once bool colors_set_once bool
tabs []string tabs []string
rl *readline.Readline rl *readline.Readline
shortcut_tracker config.ShortcutTracker
keyboard_shortcuts []*config.KeyAction
} }
// fetching {{{ // fetching {{{
@@ -123,6 +125,7 @@ func (self *handler) initialize() {
self.category_filters = make(map[string]func(*themes.Theme) bool, len(category_filters)+1) self.category_filters = make(map[string]func(*themes.Theme) bool, len(category_filters)+1)
maps.Copy(self.category_filters, category_filters) maps.Copy(self.category_filters, category_filters)
self.category_filters["recent"] = recent_filter(self.cached_data.Recent) self.category_filters["recent"] = recent_filter(self.cached_data.Recent)
self.keyboard_shortcuts = config.ResolveShortcuts(NewConfig().KeyboardShortcuts)
go self.fetch_themes() go self.fetch_themes()
self.draw_screen() 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 { 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) self.lp.Quit(0)
ev.Handled = true ev.Handled = true
return nil return nil
@@ -255,12 +258,12 @@ func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
ev.Handled = true ev.Handled = true
return nil return nil
} }
if ev.MatchesCaseInsensitiveTextOrKey("j") || ev.MatchesPressOrRepeat("down") { if ev.MatchesPressOrRepeat("down") {
self.next(1, true) self.next(1, true)
ev.Handled = true ev.Handled = true
return nil return nil
} }
if ev.MatchesCaseInsensitiveTextOrKey("k") || ev.MatchesPressOrRepeat("up") { if ev.MatchesPressOrRepeat("up") {
self.next(-1, true) self.next(-1, true)
ev.Handled = true ev.Handled = true
return nil return nil
@@ -281,12 +284,12 @@ func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
} }
return nil return nil
} }
if ev.MatchesCaseInsensitiveTextOrKey("s") || ev.MatchesCaseInsensitiveTextOrKey("/") { if ev.MatchesCaseInsensitiveTextOrKey("/") {
ev.Handled = true ev.Handled = true
self.start_search() self.start_search()
return nil return nil
} }
if ev.MatchesCaseInsensitiveTextOrKey("c") || ev.MatchesPressOrRepeat("enter") { if ev.MatchesPressOrRepeat("enter") {
ev.Handled = true ev.Handled = true
if self.themes_list == nil || self.themes_list.Len() == 0 { if self.themes_list == nil || self.themes_list.Len() == 0 {
self.lp.Beep() self.lp.Beep()
@@ -294,6 +297,27 @@ func (self *handler) on_browsing_key_event(ev *loop.KeyEvent) error {
self.state = ACCEPTING self.state = ACCEPTING
self.draw_screen() 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 return nil
} }
@@ -495,49 +519,41 @@ func (self *handler) draw_theme_demo() {
// accepting {{{ // accepting {{{
func (self *handler) on_accepting_key_event(ev *loop.KeyEvent) error { 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 ev.Handled = true
self.lp.Quit(0) self.lp.Quit(0)
return nil return nil
} }
if ev.MatchesCaseInsensitiveTextOrKey("a") || ev.MatchesPressOrRepeat("shift+a") { if ac := self.shortcut_tracker.Match(ev, self.keyboard_shortcuts); ac != nil {
ev.Handled = true switch ac.Name {
self.state = BROWSING case "quit":
self.draw_screen() 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 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 return nil
} }

View File

@@ -87,8 +87,13 @@ def as_rst() -> str:
return group_title(x).lower() return group_title(x).lower()
def kitten_link(text: str) -> str: def kitten_link(text: str) -> str:
x = text.split() # skip any --flag tokens at the start, then key, then 'kitten', then kitten name
return f':doc:`kittens/{x[2]}`' if len(x) > 2 else '' 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): for group in sorted(allg, key=key):
title = group_title(group) title = group_title(group)

View File

@@ -10,6 +10,7 @@ from collections.abc import Callable, Iterator
from typing import Any, get_type_hints from typing import Any, get_type_hints
from kitty.conf.types import Definition, MultiOption, Option, ParserFuncType, unset 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.simple_cli_definitions import serialize_as_go_string
from kitty.types import _T from kitty.types import _T
@@ -618,9 +619,15 @@ def gen_go_code(defn: Definition) -> str:
if keyboard_shortcuts: if keyboard_shortcuts:
a('KeyboardShortcuts: []*config.KeyAction{') a('KeyboardShortcuts: []*config.KeyAction{')
for sc in keyboard_shortcuts: for sc in keyboard_shortcuts:
aname, aargs = map(serialize_as_go_string, sc.action_def.partition(' ')[::2]) options, leftover = parse_options_for_map(sc.parseable_text)
a('{'f'Name: "{aname}", Args: "{aargs}", Normalized_keys: []string''{') allow_fallback = options.allow_fallback
ns = normalize_shortcuts(sc.key_text) 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(', '.join(f'"{serialize_as_go_string(x)}"' for x in ns) + ',')
a('}''},') a('}''},')
a('},') a('},')
@@ -653,23 +660,22 @@ def gen_go_code(defn: Definition) -> str:
has_parsers = bool(go_parsers or keyboard_shortcuts) has_parsers = bool(go_parsers or keyboard_shortcuts)
a('func (c *Config) Parse(key, val string) (err error) {') a('func (c *Config) Parse(key, val string) (err error) {')
if has_parsers: if has_parsers:
if go_parsers: a('switch key {')
a('switch key {') a('default: return fmt.Errorf("Unknown configuration key: %#v", key)')
a('default: return fmt.Errorf("Unknown configuration key: %#v", key)') for oname, pname in go_parsers.items():
for oname, pname in go_parsers.items(): ol = oname.lower()
ol = oname.lower() is_multiple = oname in multiopts
is_multiple = oname in multiopts a(f'case "{ol}":')
a(f'case "{ol}":') if is_multiple:
if is_multiple: a(f'var temp_val []{go_types[oname]}')
a(f'var temp_val []{go_types[oname]}') else:
else: a(f'var temp_val {go_types[oname]}')
a(f'var temp_val {go_types[oname]}') a(f'temp_val, err = {pname}')
a(f'temp_val, err = {pname}') a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}')
a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}') if is_multiple:
if is_multiple: a(f'c.{oname} = append(c.{oname}, temp_val...)')
a(f'c.{oname} = append(c.{oname}, temp_val...)') else:
else: a(f'c.{oname} = temp_val')
a(f'c.{oname} = temp_val')
if keyboard_shortcuts: if keyboard_shortcuts:
a('case "map":') a('case "map":')
a('tempsc, err := config.ParseMap(val)') a('tempsc, err := config.ParseMap(val)')

View File

@@ -431,21 +431,26 @@ class ShortcutMapping(Mapping):
setting_name: str = 'map' setting_name: str = 'map'
def __init__( 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.name = name
self.only = only self.only = only
self.key = key self._raw_definition = raw_definition
self.action_def = action_def
self.short_text = short_text self.short_text = short_text
self.long_text = long_text self.long_text = long_text
self.documented = documented self.documented = documented
self.add_to_default = add_to_default self.add_to_default = add_to_default
self.group = group 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 @property
def parseable_text(self) -> str: def parseable_text(self) -> str:
return f'{self.key} {self.action_def}' return self._raw_definition
@property @property
def key_text(self) -> str: def key_text(self) -> str:
@@ -733,8 +738,8 @@ class Definition:
def add_map( def add_map(
self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = '' self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = ''
) -> None: ) -> None:
name, key, action_def = defn.split(maxsplit=2) name, rest = defn.split(maxsplit=1)
sc = ShortcutMapping(name, key, action_def, short_text, long_text.strip(), add_to_default, documented, self.current_group, only) sc = ShortcutMapping(name, rest, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
self.current_group.append(sc) self.current_group.append(sc)
self.shortcut_map.setdefault(name, []).append(sc) self.shortcut_map.setdefault(name, []).append(sc)

2
kitty/key_encoding.py generated
View File

@@ -225,6 +225,8 @@ class KeyEvent(NamedTuple):
is_shifted = bool(self.shifted_key and self.shift) is_shifted = bool(self.shifted_key and self.shift)
if is_shifted and (mods & ~SHIFT, self.shifted_key) == spec: if is_shifted and (mods & ~SHIFT, self.shifted_key) == spec:
return True 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 return False
def matches_without_mods(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool: def matches_without_mods(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool:

View File

@@ -23,13 +23,14 @@ from .fast_data_types import (
set_ignore_os_keyboard_processing, set_ignore_os_keyboard_processing,
) )
from .options.types import Options 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 from .typing_compat import ScreenType
if TYPE_CHECKING: if TYPE_CHECKING:
from .window import Window from .window import Window
mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER 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: 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 mods = ev.mods & mod_mask
ans = keymap.get(SingleKey(mods, False, ev.key)) ans = keymap.get(SingleKey(mods, False, ev.key))
if ans is None and ev.shifted_key and mods & GLFW_MOD_SHIFT: 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: if ans is None:
ans = keymap.get(SingleKey(mods, True, ev.native_key)) ans = keymap.get(SingleKey(mods, True, ev.native_key))
return ans return ans
@@ -58,6 +69,8 @@ def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool:
return True return True
if ev.shifted_key and mods & GLFW_MOD_SHIFT and (mods & ~GLFW_MOD_SHIFT) == smods and ev.shifted_key == s.key: if ev.shifted_key and mods & GLFW_MOD_SHIFT and (mods & ~GLFW_MOD_SHIFT) == smods and ev.shifted_key == s.key:
return True return True
if ev.alternate_key and 127 < ev.key < 0xE000 and ev.alternate_key == s.key and mods == smods:
return True
return False return False
@@ -77,7 +90,7 @@ class Mappings:
def update_keymap(self, global_shortcuts: dict[str, SingleKey] | None = None) -> None: def update_keymap(self, global_shortcuts: dict[str, SingleKey] | None = None) -> None:
if global_shortcuts is None: if global_shortcuts is None:
global_shortcuts = self.set_cocoa_global_shortcuts(self.get_options()) if is_macos else {} 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.global_shortcuts = global_shortcuts
self.keyboard_modes = self.get_options().keyboard_modes.copy() self.keyboard_modes = self.get_options().keyboard_modes.copy()
km = self.keyboard_modes[''].keymap km = self.keyboard_modes[''].keymap

View File

@@ -12,7 +12,7 @@ from kitty.options.utils import pointer_shape_names
definition = Definition( definition = Definition(
'kitty', 'kitty',
Action('map', 'parse_map', {'keyboard_modes': 'KeyboardModeMap', 'alias_map': 'AliasMap'}, 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']), Action('mouse_map', 'parse_mouse_map', {'mousemap': 'MouseMap'}, ['MouseMapping']),
has_color_table=True, has_color_table=True,
) )
@@ -3926,6 +3926,14 @@ Some quick examples to illustrate common tasks::
# multi-key shortcuts # multi-key shortcuts
map ctrl+x>ctrl+y>z action 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 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 run the command palette. The full list of actions that can be mapped to
key presses is available :doc:`here </actions>`. key presses is available :doc:`here </actions>`.
@@ -3998,7 +4006,7 @@ option applied.
agr('shortcuts.clipboard', 'Clipboard') agr('shortcuts.clipboard', 'Clipboard')
map('Copy to 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=''' long_text='''
There is also a :ac:`copy_or_interrupt` action that can be optionally mapped 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 to :kbd:`Ctrl+C`. It will copy only if there is a selection and send an
@@ -4009,27 +4017,27 @@ the key through to the application running in the terminal if there is no select
''' '''
) )
map('Copy to clipboard or pass through', 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', only='macos',
) )
map('Paste from clipboard', 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', 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', only='macos',
) )
map('Paste from selection', 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', map('Paste from selection',
'paste_from_selection shift+insert paste_from_selection', 'paste_from_selection shift+insert paste_from_selection',
) )
map('Pass selection to program', 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=''' long_text='''
You can also pass the contents of the current selection to any program with 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, :ac:`pass_selection_to_program`. By default, the system's open program is used,
@@ -4054,7 +4062,7 @@ map('Scroll line up',
'scroll_line_up kitty_mod+up scroll_line_up', 'scroll_line_up kitty_mod+up scroll_line_up',
) )
map('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', map('Scroll line up',
'scroll_line_up opt+cmd+page_up scroll_line_up', 'scroll_line_up opt+cmd+page_up scroll_line_up',
@@ -4069,7 +4077,7 @@ map('Scroll line down',
'scroll_line_down kitty_mod+down scroll_line_down', 'scroll_line_down kitty_mod+down scroll_line_down',
) )
map('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', map('Scroll line down',
'scroll_line_down opt+cmd+page_down scroll_line_down', 'scroll_line_down opt+cmd+page_down scroll_line_down',
@@ -4113,7 +4121,7 @@ map('Scroll to bottom',
) )
map('Scroll to previous shell prompt', 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=''' long_text='''
Use a parameter of :code:`0` for :ac:`scroll_to_prompt` to scroll to the last 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 jumped to or the last clicked position. Requires :ref:`shell integration
@@ -4121,10 +4129,10 @@ jumped to or the last clicked position. Requires :ref:`shell integration
''' '''
) )
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', 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=''' long_text='''
You can pipe the contents of the current screen and history buffer as You can pipe the contents of the current screen and history buffer as
:file:`STDIN` to an arbitrary program using :option:`launch --stdin-source`. :file:`STDIN` to an arbitrary program using :option:`launch --stdin-source`.
@@ -4139,7 +4147,7 @@ see :doc:`launch`.
) )
map('Browse output of the last shell command in pager', 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=''' long_text='''
You can also define additional shortcuts to get the command output. You can also define additional shortcuts to get the command output.
For example, to get the first command output on screen:: For example, to get the first command output on screen::
@@ -4176,7 +4184,7 @@ a manual mapping with a special pager for this, you can use something like:
For more sophisticated control, such as using the current selection, use :ac:`remote_control_script`. 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() # }}} egr() # }}}
@@ -4220,7 +4228,7 @@ map('New window',
) )
map('New OS window', 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=''' long_text='''
Works like :ac:`new_window` above, except that it opens a top-level :term:`OS 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 window <os_window>`. In particular you can use :ac:`new_os_window_with_cwd` to
@@ -4228,15 +4236,15 @@ open a window with the current working directory.
''' '''
) )
map('New OS window', 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', only='macos',
) )
map('Close window', map('Close window',
'close_window kitty_mod+w close_window', 'close_window --allow-fallback=shifted,ascii kitty_mod+w close_window',
) )
map('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', only='macos',
) )
@@ -4249,11 +4257,11 @@ map('Previous window',
) )
map('Move window forward', 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', 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', map('Move window to top',
@@ -4261,10 +4269,10 @@ map('Move window to top',
) )
map('Start resizing window', 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', 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', only='macos',
) )
@@ -4386,23 +4394,23 @@ map('Previous tab',
) )
map('New tab', map('New tab',
'new_tab kitty_mod+t new_tab', 'new_tab --allow-fallback=shifted,ascii kitty_mod+t new_tab',
) )
map('New tab', map('New tab',
'new_tab cmd+t new_tab', 'new_tab --allow-fallback=shifted,ascii cmd+t new_tab',
only='macos', only='macos',
) )
map('Close tab', map('Close tab',
'close_tab kitty_mod+q close_tab', 'close_tab --allow-fallback=shifted,ascii kitty_mod+q close_tab',
) )
map('Close tab', map('Close tab',
'close_tab cmd+w close_tab', 'close_tab --allow-fallback=shifted,ascii cmd+w close_tab',
only='macos', only='macos',
) )
map('Close OS window', 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', only='macos',
) )
@@ -4415,10 +4423,10 @@ map('Move tab backward',
) )
map('Set tab title', 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', 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', only='macos',
) )
egr(''' egr('''
@@ -4444,7 +4452,7 @@ end of the tabs list, use::
agr('shortcuts.layout', 'Layout management') agr('shortcuts.layout', 'Layout management')
map('Next layout', map('Next layout',
'next_layout kitty_mod+l next_layout', 'next_layout --allow-fallback=shifted,ascii kitty_mod+l next_layout',
) )
egr(''' egr('''
You can also create shortcuts to switch to specific :term:`layouts <layout>`:: You can also create shortcuts to switch to specific :term:`layouts <layout>`::
@@ -4537,7 +4545,7 @@ insert it into the terminal or copy it to the clipboard.
''') ''')
map('Open URL', 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=''' long_text='''
Open a currently visible URL using the keyboard. The program used to open the Open a currently visible URL using the keyboard. The program used to open the
URL is specified in :opt:`open_url_with`. URL is specified in :opt:`open_url_with`.
@@ -4545,7 +4553,7 @@ URL is specified in :opt:`open_url_with`.
) )
map('Insert selected path', 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=''' long_text='''
Select a path/filename and insert it into the terminal. Useful, for instance to 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` run :program:`git` commands on a filename output from a previous :program:`git`
@@ -4554,12 +4562,12 @@ command.
) )
map('Open selected path', 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.' long_text='Select a path/filename and open it with the default open program.'
) )
map('Insert chosen file', 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=''' long_text='''
Select a file using the :doc:`choose-files </kittens/choose-files>` kitten and insert Select a file using the :doc:`choose-files </kittens/choose-files>` kitten and insert
it into the terminal. it into the terminal.
@@ -4567,7 +4575,7 @@ it into the terminal.
) )
map('Insert chosen directory', 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=''' long_text='''
Select a directory using the :doc:`choose-files </kittens/choose-files>` kitten and insert Select a directory using the :doc:`choose-files </kittens/choose-files>` kitten and insert
it into the terminal. it into the terminal.
@@ -4576,7 +4584,7 @@ it into the terminal.
map('Insert selected line', 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=''' long_text='''
Select a line of text and insert it into the terminal. Useful for the output of Select a line of text and insert it into the terminal. Useful for the output of
things like: ``ls -1``. things like: ``ls -1``.
@@ -4584,12 +4592,12 @@ things like: ``ls -1``.
) )
map('Insert selected word', 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.' long_text='Select words and insert into terminal.'
) )
map('Insert selected hash', 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=''' long_text='''
Select something that looks like a hash and insert it into the terminal. Useful Select something that looks like a hash and insert it into the terminal. Useful
with :program:`git`, which uses SHA1 hashes to identify commits. with :program:`git`, which uses SHA1 hashes to identify commits.
@@ -4597,7 +4605,7 @@ with :program:`git`, which uses SHA1 hashes to identify commits.
) )
map('Open the selected file at the selected line', 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=''' long_text='''
Select something that looks like :code:`filename:linenum` and open it in Select something that looks like :code:`filename:linenum` and open it in
your default editor at the specified line number. your default editor at the specified line number.
@@ -4605,7 +4613,7 @@ your default editor at the specified line number.
) )
map('Open the selected hyperlink', 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=''' long_text='''
Select a :term:`hyperlink <hyperlinks>` (i.e. a URL that has been marked as such 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``). by the terminal program, for example, by ``ls --hyperlink=auto``).
@@ -4630,7 +4638,7 @@ map('Toggle fullscreen',
'toggle_fullscreen kitty_mod+f11 toggle_fullscreen', 'toggle_fullscreen kitty_mod+f11 toggle_fullscreen',
) )
map('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', only='macos',
) )
@@ -4639,7 +4647,7 @@ map('Toggle maximized',
) )
map('Toggle macOS secure keyboard entry', 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', only='macos',
) )
@@ -4647,7 +4655,7 @@ map('macOS Cycle through OS Windows', 'macos_cycle_through_os_windows cmd+` maco
map('macOS Cycle through OS Windows backwards', 'macos_cycle_through_os_windows_backwards cmd+shift+` macos_cycle_through_os_windows_backwards', only='macos') 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', 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', map('Unicode input',
'input_unicode_character ctrl+cmd+space kitten unicode_input', 'input_unicode_character ctrl+cmd+space kitten unicode_input',
@@ -4671,19 +4679,19 @@ Open the kitty shell in a new :code:`window` / :code:`tab` / :code:`overlay` /
) )
map('Increase background opacity', 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', 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', 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', 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', map('Reset the terminal',

96
kitty/options/types.py generated
View File

@@ -11,8 +11,8 @@ import kitty.fast_data_types
from kitty.fonts import FontSpec from kitty.fonts import FontSpec
import kitty.fonts import kitty.fonts
from kitty.options.utils import ( from kitty.options.utils import (
AliasMap, KeyDefinition, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping, NotifyOnCmdFinish, AliasMap, KeyDefinition, KeyMapOptions, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping,
TabBarMarginHeight NotifyOnCmdFinish, TabBarMarginHeight
) )
import kitty.options.utils import kitty.options.utils
from kitty.types import FloatEdges from kitty.types import FloatEdges
@@ -848,23 +848,23 @@ defaults.watcher = {}
defaults.map = [ defaults.map = [
# copy_to_clipboard # 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 # 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 # 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 # paste_from_selection
KeyDefinition(trigger=SingleKey(mods=1, key=57348), definition='paste_from_selection'), KeyDefinition(trigger=SingleKey(mods=1, key=57348), definition='paste_from_selection'),
# pass_selection_to_program # 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 # scroll_line_up
KeyDefinition(trigger=SingleKey(mods=256, key=57352), definition='scroll_line_up'), KeyDefinition(trigger=SingleKey(mods=256, key=57352), definition='scroll_line_up'),
# 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 # scroll_line_down
KeyDefinition(trigger=SingleKey(mods=256, key=57353), definition='scroll_line_down'), KeyDefinition(trigger=SingleKey(mods=256, key=57353), definition='scroll_line_down'),
# 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 # scroll_page_up
KeyDefinition(trigger=SingleKey(mods=256, key=57354), definition='scroll_page_up'), KeyDefinition(trigger=SingleKey(mods=256, key=57354), definition='scroll_page_up'),
# scroll_page_down # scroll_page_down
@@ -874,33 +874,33 @@ defaults.map = [
# scroll_end # scroll_end
KeyDefinition(trigger=SingleKey(mods=256, key=57357), definition='scroll_end'), KeyDefinition(trigger=SingleKey(mods=256, key=57357), definition='scroll_end'),
# scroll_to_previous_prompt # 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 # 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 # 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 # 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 # search_scrollback
KeyDefinition(trigger=SingleKey(mods=256, key=47), definition='search_scrollback'), KeyDefinition(trigger=SingleKey(mods=256, key=47), definition='search_scrollback'),
# new_window # new_window
KeyDefinition(trigger=SingleKey(mods=256, key=57345), definition='new_window'), KeyDefinition(trigger=SingleKey(mods=256, key=57345), definition='new_window'),
# new_os_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 # 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 # next_window
KeyDefinition(trigger=SingleKey(mods=256, key=93), definition='next_window'), KeyDefinition(trigger=SingleKey(mods=256, key=93), definition='next_window'),
# previous_window # previous_window
KeyDefinition(trigger=SingleKey(mods=256, key=91), definition='previous_window'), KeyDefinition(trigger=SingleKey(mods=256, key=91), definition='previous_window'),
# move_window_forward # 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 # 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 # move_window_to_top
KeyDefinition(trigger=SingleKey(mods=256, key=96), definition='move_window_to_top'), KeyDefinition(trigger=SingleKey(mods=256, key=96), definition='move_window_to_top'),
# start_resizing_window # 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 # first_window
KeyDefinition(trigger=SingleKey(mods=256, key=49), definition='first_window'), KeyDefinition(trigger=SingleKey(mods=256, key=49), definition='first_window'),
# second_window # second_window
@@ -934,17 +934,17 @@ defaults.map = [
# previous_tab # previous_tab
KeyDefinition(trigger=SingleKey(mods=5, key=57346), definition='previous_tab'), KeyDefinition(trigger=SingleKey(mods=5, key=57346), definition='previous_tab'),
# new_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 # 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 # move_tab_forward
KeyDefinition(trigger=SingleKey(mods=256, key=46), definition='move_tab_forward'), KeyDefinition(trigger=SingleKey(mods=256, key=46), definition='move_tab_forward'),
# move_tab_backward # move_tab_backward
KeyDefinition(trigger=SingleKey(mods=256, key=44), definition='move_tab_backward'), KeyDefinition(trigger=SingleKey(mods=256, key=44), definition='move_tab_backward'),
# set_tab_title # 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 # 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 # increase_font_size
KeyDefinition(trigger=SingleKey(mods=256, key=61), definition='change_font_size all +2.0'), KeyDefinition(trigger=SingleKey(mods=256, key=61), definition='change_font_size all +2.0'),
# increase_font_size # increase_font_size
@@ -958,25 +958,25 @@ defaults.map = [
# reset_font_size # reset_font_size
KeyDefinition(trigger=SingleKey(mods=256, key=57347), definition='change_font_size all 0'), KeyDefinition(trigger=SingleKey(mods=256, key=57347), definition='change_font_size all 0'),
# open_url # 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 # 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 # 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 # 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 # 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 # 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 # 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 # 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 # 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 # 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 # show_kitty_doc
KeyDefinition(trigger=SingleKey(mods=256, key=57364), definition='show_kitty_doc overview'), KeyDefinition(trigger=SingleKey(mods=256, key=57364), definition='show_kitty_doc overview'),
# command_palette # command_palette
@@ -986,19 +986,19 @@ defaults.map = [
# toggle_maximized # toggle_maximized
KeyDefinition(trigger=SingleKey(mods=256, key=57373), definition='toggle_maximized'), KeyDefinition(trigger=SingleKey(mods=256, key=57373), definition='toggle_maximized'),
# input_unicode_character # 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 # edit_config_file
KeyDefinition(trigger=SingleKey(mods=256, key=57365), definition='edit_config_file'), KeyDefinition(trigger=SingleKey(mods=256, key=57365), definition='edit_config_file'),
# kitty_shell # kitty_shell
KeyDefinition(trigger=SingleKey(mods=256, key=57344), definition='kitty_shell window'), KeyDefinition(trigger=SingleKey(mods=256, key=57344), definition='kitty_shell window'),
# increase_background_opacity # 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 # 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 # 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 # 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 # reset_terminal
KeyDefinition(trigger=SingleKey(mods=256, key=57349), definition='clear_terminal reset active'), KeyDefinition(trigger=SingleKey(mods=256, key=57349), definition='clear_terminal reset active'),
# reload_config_file # reload_config_file
@@ -1008,8 +1008,8 @@ defaults.map = [
] ]
if is_macos: 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=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), definition='paste_from_clipboard')) 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=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=8, key=57352), definition='scroll_line_up'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=57355), definition='scroll_line_down')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=57355), definition='scroll_line_down'))
@@ -1018,11 +1018,11 @@ if is_macos:
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57355), definition='scroll_page_down')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=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=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=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=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=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), definition='close_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), definition='start_resizing_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=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=50), definition='second_window'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=51), definition='third_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=51), definition='third_window'))
@@ -1034,18 +1034,18 @@ if is_macos:
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57), definition='ninth_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=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=93), definition='next_tab'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=91), definition='previous_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=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), definition='close_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), definition='close_os_window')) 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), definition='set_tab_title')) 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=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=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=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=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=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=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=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), definition='toggle_macos_secure_keyboard_entry')) 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=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=9, key=96), definition='macos_cycle_through_os_windows_backwards'))
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input'))

View File

@@ -1284,24 +1284,27 @@ class LiteralField(Generic[T]):
self._vals = vals self._vals = vals
def __set_name__(self, owner: object, name: str) -> None: 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: def __get__(self, obj: object, type: type | None = None) -> T:
if obj is None: if obj is None:
return self._vals[0] 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: def __set__(self, obj: object, value: str) -> None:
if value not in self._vals: if value not in self._vals:
raise KeyError(f'Invalid value for {self._name[1:]}: {value!r}') raise KeyError(f'Invalid value for {self._name}: {value!r}')
object.__setattr__(obj, self._name, value) obj.__dict__[self._name] = value
OnUnknown = Literal['beep', 'end', 'ignore', 'passthrough'] OnUnknown = Literal['beep', 'end', 'ignore', 'passthrough']
OnAction = Literal['keep', 'end'] OnAction = Literal['keep', 'end']
@dataclass(init=False, frozen=True) @dataclass(frozen=True)
class KeyMapOptions: class KeyMapOptions:
when_focus_on: str = '' when_focus_on: str = ''
new_mode: str = '' new_mode: str = ''
@@ -1309,6 +1312,7 @@ class KeyMapOptions:
on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown)) on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown))
on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction)) on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction))
timeout: float | None = None timeout: float | None = None
allow_fallback: str = 'shifted'
default_key_map_options = KeyMapOptions() default_key_map_options = KeyMapOptions()
@@ -1381,6 +1385,22 @@ key_map_option_converters: defaultdict[str, Callable[[str], Any]] = defaultdict(
key_map_option_converters['timeout'] = float 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]: def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]:
expecting_arg = '' expecting_arg = ''
ans = KeyMapOptions() ans = KeyMapOptions()

View File

@@ -654,3 +654,222 @@ class TestKeys(BaseTest):
self.ae(tm('m', 'a'), [True, True]) self.ae(tm('m', 'a'), [True, True])
self.ae(tm.actions, ['push_keyboard_mode mw', 'new_window']) self.ae(tm.actions, ['push_keyboard_mode mw', 'new_window'])
af(tm.ignore_os_keyboard_processing) 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')

View File

@@ -248,13 +248,74 @@ type KeyAction struct {
Normalized_keys []string Normalized_keys []string
Name string Name string
Args string Args string
AllowFallback string
} }
func (self *KeyAction) String() string { func (self *KeyAction) String() string {
return fmt.Sprintf("map %#v %#v %#v\n", strings.Join(self.Normalized_keys, ">"), self.Name, self.Args) 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) { 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, " ") spec, action, found := strings.Cut(val, " ")
if !found { if !found {
return nil, fmt.Errorf("No action specified for shortcut %s", val) 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 = strings.TrimSpace(action)
action_name, action_args, _ := strings.Cut(action, " ") action_name, action_args, _ := strings.Cut(action, " ")
action_args = strings.TrimSpace(action_args) 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 { type ShortcutTracker struct {
@@ -274,7 +335,7 @@ func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction)
if self.partial_num_consumed > 0 { if self.partial_num_consumed > 0 {
ev.Handled = true ev.Handled = true
self.partial_matches = utils.Filter(self.partial_matches, func(ac *KeyAction) bool { 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 { if len(self.partial_matches) == 0 {
self.partial_num_consumed = 0 self.partial_num_consumed = 0
@@ -282,7 +343,7 @@ func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction)
} }
} else { } else {
self.partial_matches = utils.Filter(all_actions, func(ac *KeyAction) bool { 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 { if len(self.partial_matches) == 0 {
return nil return nil

View File

@@ -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) { func TestNormalizeShortcuts(t *testing.T) {
for q, expected_ := range map[string]string{ for q, expected_ := range map[string]string{
`a`: `a`, `a`: `a`,

View File

@@ -7,6 +7,7 @@ import (
"math" "math"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8"
"github.com/kovidgoyal/kitty" "github.com/kovidgoyal/kitty"
) )
@@ -285,7 +286,12 @@ func ParseShortcut(spec string) *ParsedShortcut {
return &ans 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 { if self.Type&event_type == 0 {
return false return false
} }
@@ -293,12 +299,19 @@ func (self *KeyEvent) MatchesParsedShortcut(ps *ParsedShortcut, event_type KeyEv
if mods == ps.Mods && self.Key == ps.KeyName { if mods == ps.Mods && self.Key == ps.KeyName {
return true 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 true
} }
return false 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 { func (self *KeyEvent) Matches(spec string, event_type KeyEventType) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), event_type) 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) 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 { func (self *KeyEvent) MatchesCaseSensitiveTextOrKey(spec string) bool {
if self.MatchesParsedShortcut(ParseShortcut(spec), PRESS|REPEAT) { if self.MatchesParsedShortcut(ParseShortcut(spec), PRESS|REPEAT) {
return true return true

View File

@@ -28,3 +28,108 @@ func TestKeyEventFromCSI(t *testing.T) {
test_text("121;;121u", "y", "") test_text("121;;121u", "y", "")
test_text("121::122;;121u", "y", "z") 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")
}
}