diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index 178eeb7bf..e38ccf6b1 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -478,10 +478,49 @@ def go_type_data(parser_func: ParserFuncType, ctype: str, is_multiple: bool = Fa return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)' +mod_map = { + "shift": "shift", + "⇧": "shift", + "alt": "alt", + "option": "alt", + "opt": "alt", + "⌥": "alt", + "super": "super", + "command": "super", + "cmd": "super", + "⌘": "super", + "control": "ctrl", + "ctrl": "ctrl", + "⌃": "ctrl", + "hyper": "hyper", + "meta": "meta", + "num_lock": "num_lock", + "caps_lock": "caps_lock", +} + +def normalize_shortcut(spec: str) -> str: + if spec.endswith('+'): + spec = spec[:-1] + 'plus' + parts = spec.lower().split('+') + key = parts[-1] + if len(parts) == 1: + return key + mods = parts[:-1] + return '+'.join(mod_map.get(x, x) for x in mods) + '+' + key + + +def normalize_shortcuts(spec: str) -> Iterator[str]: + spec = spec.replace('++', '+plus') + spec = re.sub(r'([^+])>', '\\1\0', spec) + for x in spec.split('\0'): + yield normalize_shortcut(x) + + def gen_go_code(defn: Definition) -> str: lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/config"', 'import "kitty/tools/utils/style"', 'var _ = fmt.Println', 'var _ = config.StringToBool', 'var _ = strconv.Atoi', 'var _ = style.ParseColor'] a = lines.append + keyboard_shortcuts = tuple(defn.iter_all_maps()) choices = {} go_types = {} go_parsers = {} @@ -509,6 +548,8 @@ def gen_go_code(defn: Definition) -> str: a(f'{name} []{gotype}') else: a(f'{name} {gotype}') + if keyboard_shortcuts: + a('KeyboardShortcuts []*config.KeyAction') a('}') def cval(x: str) -> str: @@ -540,6 +581,16 @@ def gen_go_code(defn: Definition) -> str: else: dval = repr(d) a(f'{name}: {dval},') + if keyboard_shortcuts: + a('KeyboardShortcuts: []*config.KeyAction{') + for sc in keyboard_shortcuts: + aname, aargs = map(serialize_as_go_string, sc.action_def.partition(' ')[::2]) + a('{'f'Name: "{aname}", Args: "{aargs}", Normalized_keys: []string''{') + ns = normalize_shortcuts(sc.key_text) + a(', '.join(f'"{serialize_as_go_string(x)}"' for x in ns) + ',') + a('}''},') + a('},') + a('}''}') for oname, choice_vals in choices.items(): @@ -582,6 +633,11 @@ def gen_go_code(defn: Definition) -> str: a(f'c.{oname} = append(c.{oname}, temp_val...)') else: a(f'c.{oname} = temp_val') + if keyboard_shortcuts: + a('case "map":') + a('tempsc, err := config.ParseMap(val)') + a('if err != nil { return fmt.Errorf("Failed to parse map = %#v with error: %w", val, err) }') + a('c.KeyboardShortcuts = append(c.KeyboardShortcuts, tempsc)') a('}') a('return}') return '\n'.join(lines) diff --git a/tools/config/api.go b/tools/config/api.go index eca970b54..89ecb7545 100644 --- a/tools/config/api.go +++ b/tools/config/api.go @@ -9,10 +9,11 @@ import ( "fmt" "io" "io/fs" - "kitty/tools/utils" "os" "path/filepath" "strings" + + "kitty/tools/utils" ) var _ = fmt.Print diff --git a/tools/config/utils.go b/tools/config/utils.go index b8e9c7a94..5fc7d24e5 100644 --- a/tools/config/utils.go +++ b/tools/config/utils.go @@ -5,6 +5,7 @@ package config import ( "fmt" "kitty/tools/utils" + "regexp" "strconv" "strings" ) @@ -183,3 +184,72 @@ func StringLiteral(val string) (string, error) { } return ans.String(), nil } + +var ModMap = (&utils.Once[map[string]string]{Run: func() map[string]string { + return map[string]string{ + "shift": "shift", + "⇧": "shift", + "alt": "alt", + "option": "alt", + "opt": "alt", + "⌥": "alt", + "super": "super", + "command": "super", + "cmd": "super", + "⌘": "super", + "control": "ctrl", + "ctrl": "ctrl", + "⌃": "ctrl", + "hyper": "hyper", + "meta": "meta", + "num_lock": "num_lock", + "caps_lock": "caps_lock", + } +}}).Get + +var ShortcutSpecPat = (&utils.Once[*regexp.Regexp]{Run: func() *regexp.Regexp { + return regexp.MustCompile(`([^+])>`) +}}).Get + +func NormalizeShortcut(spec string) string { + parts := strings.Split(strings.ToLower(spec), "+") + key := parts[len(parts)-1] + if len(parts) == 1 { + return key + } + mods := parts[:len(parts)-1] + mmap := ModMap() + mods = utils.Map(func(x string) string { + ans := mmap[x] + if ans == "" { + ans = x + } + return ans + }, mods) + utils.Sort(mods, func(a, b string) bool { return a < b }) + return strings.Join(mods, "+") + "+" + key +} + +func NormalizeShortcuts(spec string) []string { + if strings.HasSuffix(spec, "+") { + spec = spec[:len(spec)-1] + "plus" + } + spec = strings.ReplaceAll(spec, "++", "+plus") + spec = ShortcutSpecPat().ReplaceAllString(spec, "$1\x00") + return utils.Map(NormalizeShortcut, strings.Split(spec, "\x00")) +} + +type KeyAction struct { + Normalized_keys []string + Name string + Args string +} + +func ParseMap(val string) (*KeyAction, error) { + spec, action, found := strings.Cut(val, " ") + if !found { + return nil, fmt.Errorf("No action specified for shortcut %s", val) + } + action_name, action_args, _ := strings.Cut(action, " ") + return &KeyAction{Name: action_name, Args: action_args, Normalized_keys: NormalizeShortcuts(spec)}, nil +} diff --git a/tools/config/utils_test.go b/tools/config/utils_test.go index 11288f913..1f10df356 100644 --- a/tools/config/utils_test.go +++ b/tools/config/utils_test.go @@ -4,7 +4,10 @@ package config import ( "fmt" + "strings" "testing" + + "github.com/google/go-cmp/cmp" ) var _ = fmt.Print @@ -24,3 +27,18 @@ func TestStringLiteralParsing(t *testing.T) { } } } + +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) + } + } +}