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

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

View File

@@ -6,11 +6,38 @@ import sys
from functools import partial
from typing import Any
from kitty.conf.types import Definition
from kitty.fast_data_types import add_timer, get_boss
from kitty.typing_compat import BossType
from ..tui.handler import result_handler
definition = Definition(
'!kittens.command_palette',
)
agr = definition.add_group
egr = definition.end_group
map = definition.add_map
# shortcuts {{{
agr('shortcuts', 'Keyboard shortcuts')
map('Move selection up',
'selection_up --allow-fallback=shifted,ascii ctrl+k selection_up',
)
map('Move selection up',
'selection_up --allow-fallback=shifted,ascii ctrl+p selection_up',
)
map('Move selection down',
'selection_down --allow-fallback=shifted,ascii ctrl+j selection_down',
)
map('Move selection down',
'selection_down --allow-fallback=shifted,ascii ctrl+n selection_down',
)
egr() # }}}
def collect_keys_data(opts: Any) -> dict[str, Any]:
"""Collect all keybinding data from options into a JSON-serializable dict."""
@@ -188,3 +215,5 @@ elif __name__ == '__doc__':
cd['options'] = OPTIONS
cd['help_text'] = help_text
cd['short_desc'] = help_text
elif __name__ == '__conf__':
sys.options_definition = definition # type: ignore