Files
kitty/tools/config/utils_test.go
Павел Мешалкин 8ffdf7d7ee 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).
2026-03-25 19:34:13 +03:00

179 lines
5.1 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package config
import (
"fmt"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
var _ = fmt.Print
func TestStringLiteralParsing(t *testing.T) {
for q, expected := range map[string]string{
`abc`: `abc`,
`a\nb\M`: "a\nb\\M",
`a\x20\x1\u1234\123\12|`: "a \\x1\u1234\123\x0a|",
} {
actual, err := StringLiteral(q)
if err != nil {
t.Fatal(err)
}
if expected != actual {
t.Fatalf("Failed with input: %#v\n%#v != %#v", q, expected, actual)
}
}
}
func TestParseMap(t *testing.T) {
// Test without --allow-fallback (default "shifted")
ka, err := ParseMap("ctrl+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "shifted" {
t.Fatalf("Expected AllowFallback 'shifted', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
if diff := cmp.Diff([]string{"ctrl+c"}, ka.Normalized_keys); diff != "" {
t.Fatalf("Keys mismatch:\n%s", diff)
}
// Test with --allow-fallback=ascii
ka, err = ParseMap("--allow-fallback=ascii ctrl+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "ascii" {
t.Fatalf("Expected AllowFallback 'ascii', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
// Test with --allow-fallback=shifted,ascii
ka, err = ParseMap("--allow-fallback=shifted,ascii cmd+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "shifted,ascii" {
t.Fatalf("Expected AllowFallback 'shifted,ascii', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
if diff := cmp.Diff([]string{"super+c"}, ka.Normalized_keys); diff != "" {
t.Fatalf("Keys mismatch:\n%s", diff)
}
// Test with --allow-fallback and action args
ka, err = ParseMap("--allow-fallback=shifted,ascii ctrl+shift+f launch --type=tab grep")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "shifted,ascii" {
t.Fatalf("Expected AllowFallback 'shifted,ascii', got %#v", ka.AllowFallback)
}
if ka.Name != "launch" {
t.Fatalf("Expected Name 'launch', got %#v", ka.Name)
}
if ka.Args != "--type=tab grep" {
t.Fatalf("Expected Args '--type=tab grep', got %#v", ka.Args)
}
// Test space form: --allow-fallback ascii (without =)
ka, err = ParseMap("--allow-fallback ascii ctrl+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "ascii" {
t.Fatalf("Expected AllowFallback 'ascii', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
if diff := cmp.Diff([]string{"ctrl+c"}, ka.Normalized_keys); diff != "" {
t.Fatalf("Keys mismatch:\n%s", diff)
}
// Test space form: --allow-fallback shifted,ascii
ka, err = ParseMap("--allow-fallback shifted,ascii cmd+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "shifted,ascii" {
t.Fatalf("Expected AllowFallback 'shifted,ascii', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
// Test --allow-fallback=none (equals form)
ka, err = ParseMap("--allow-fallback=none ctrl+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "" {
t.Fatalf("Expected AllowFallback '', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
// Test --allow-fallback none (space form)
ka, err = ParseMap("--allow-fallback none ctrl+c copy_to_clipboard")
if err != nil {
t.Fatal(err)
}
if ka.AllowFallback != "" {
t.Fatalf("Expected AllowFallback '', got %#v", ka.AllowFallback)
}
if ka.Name != "copy_to_clipboard" {
t.Fatalf("Expected Name 'copy_to_clipboard', got %#v", ka.Name)
}
// Test error: unknown flag
_, err = ParseMap("--allow-fallbak=ascii ctrl+c copy")
if err == nil {
t.Fatal("Expected error for unknown flag --allow-fallbak")
}
// Test error: unknown flag without =
_, err = ParseMap("--unknown ctrl+c copy")
if err == nil {
t.Fatal("Expected error for unknown flag --unknown")
}
// Test error: invalid allow-fallback value
_, err = ParseMap("--allow-fallback=typo ctrl+c copy")
if err == nil {
t.Fatal("Expected error for invalid allow-fallback value 'typo'")
}
// Test error: invalid allow-fallback value in space form
_, err = ParseMap("--allow-fallback typo ctrl+c copy")
if err == nil {
t.Fatal("Expected error for invalid allow-fallback value 'typo' in space form")
}
}
func TestNormalizeShortcuts(t *testing.T) {
for q, expected_ := range map[string]string{
`a`: `a`,
`+`: `plus`,
`cmd+b>opt+>`: `super+b alt+>`,
`cmd+>>opt+>`: `super+> alt+>`,
} {
expected := strings.Split(expected_, " ")
actual := NormalizeShortcuts(q)
if diff := cmp.Diff(expected, actual); diff != "" {
t.Fatalf("failed with input: %#v\n%s", q, diff)
}
}
}