mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-18 22:44:50 +02:00
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:
@@ -248,13 +248,74 @@ type KeyAction struct {
|
||||
Normalized_keys []string
|
||||
Name string
|
||||
Args string
|
||||
AllowFallback string
|
||||
}
|
||||
|
||||
func (self *KeyAction) String() string {
|
||||
return fmt.Sprintf("map %#v %#v %#v\n", strings.Join(self.Normalized_keys, ">"), self.Name, self.Args)
|
||||
}
|
||||
|
||||
func validateAllowFallback(value string) error {
|
||||
if value == "" || value == "none" {
|
||||
return nil
|
||||
}
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "shifted" && part != "ascii" {
|
||||
return fmt.Errorf("Invalid allow-fallback value %#v, allowed values: shifted, ascii, none", part)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseMap(val string) (*KeyAction, error) {
|
||||
allow_fallback := "shifted"
|
||||
for strings.HasPrefix(val, "--") {
|
||||
flag, rest, found := strings.Cut(val, " ")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("No key specified after flag %s", flag)
|
||||
}
|
||||
rest = strings.TrimSpace(rest)
|
||||
if name, value, ok := strings.Cut(flag, "="); ok {
|
||||
// --flag=value form
|
||||
name = strings.ReplaceAll(name[2:], "-", "_")
|
||||
switch name {
|
||||
case "allow_fallback":
|
||||
if err := validateAllowFallback(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "none" {
|
||||
allow_fallback = ""
|
||||
} else {
|
||||
allow_fallback = value
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown map option: %s", flag)
|
||||
}
|
||||
} else {
|
||||
// --flag value form (space-separated)
|
||||
name := strings.ReplaceAll(flag[2:], "-", "_")
|
||||
switch name {
|
||||
case "allow_fallback":
|
||||
value, after, has_more := strings.Cut(rest, " ")
|
||||
if !has_more {
|
||||
return nil, fmt.Errorf("No key specified after %s %s", flag, value)
|
||||
}
|
||||
if err := validateAllowFallback(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "none" {
|
||||
allow_fallback = ""
|
||||
} else {
|
||||
allow_fallback = value
|
||||
}
|
||||
rest = strings.TrimSpace(after)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown map option: %s", flag)
|
||||
}
|
||||
}
|
||||
val = rest
|
||||
}
|
||||
spec, action, found := strings.Cut(val, " ")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("No action specified for shortcut %s", val)
|
||||
@@ -262,7 +323,7 @@ func ParseMap(val string) (*KeyAction, error) {
|
||||
action = strings.TrimSpace(action)
|
||||
action_name, action_args, _ := strings.Cut(action, " ")
|
||||
action_args = strings.TrimSpace(action_args)
|
||||
return &KeyAction{Name: action_name, Args: action_args, Normalized_keys: NormalizeShortcuts(spec)}, nil
|
||||
return &KeyAction{Name: action_name, Args: action_args, Normalized_keys: NormalizeShortcuts(spec), AllowFallback: allow_fallback}, nil
|
||||
}
|
||||
|
||||
type ShortcutTracker struct {
|
||||
@@ -274,7 +335,7 @@ func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction)
|
||||
if self.partial_num_consumed > 0 {
|
||||
ev.Handled = true
|
||||
self.partial_matches = utils.Filter(self.partial_matches, func(ac *KeyAction) bool {
|
||||
return self.partial_num_consumed < len(ac.Normalized_keys) && ev.MatchesPressOrRepeat(ac.Normalized_keys[self.partial_num_consumed])
|
||||
return self.partial_num_consumed < len(ac.Normalized_keys) && ev.MatchesPressOrRepeatWithFallback(ac.Normalized_keys[self.partial_num_consumed], ac.AllowFallback)
|
||||
})
|
||||
if len(self.partial_matches) == 0 {
|
||||
self.partial_num_consumed = 0
|
||||
@@ -282,7 +343,7 @@ func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction)
|
||||
}
|
||||
} else {
|
||||
self.partial_matches = utils.Filter(all_actions, func(ac *KeyAction) bool {
|
||||
return ev.MatchesPressOrRepeat(ac.Normalized_keys[0])
|
||||
return ev.MatchesPressOrRepeatWithFallback(ac.Normalized_keys[0], ac.AllowFallback)
|
||||
})
|
||||
if len(self.partial_matches) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user