1// Package keychord parses and canonicalizes single key chords such as
2// "ctrl+shift+t" or "arrowright".
3//
4// It is the one place that knows which spellings name the same key (aliases,
5// modifier names, letter case), so every consumer agrees on when two config
6// strings are the same binding. It has no toolkit dependency: the input layer
7// maps the canonical key names to its own key values.
8package keychord
9
10import (
11 "slices"
12 "strings"
13)
14
15// Mods holds the modifier flags of a chord.
16type Mods uint8
17
18// Modifier flags.
19const (
20 Ctrl Mods = 1 << iota
21 Alt
22 Shift
23)
24
25// Chord is a parsed key chord. Key is the canonical key name: one of the
26// names returned by KeyNames, or a single printable ASCII character.
27type Chord struct {
28 Mods Mods
29 Key string
30}
31
32// String renders the chord in canonical form: modifiers in the fixed order
33// ctrl, alt, shift, followed by the key.
34func (c Chord) String() string {
35 var b strings.Builder
36 if c.Mods&Ctrl != 0 {
37 b.WriteString("ctrl+")
38 }
39 if c.Mods&Alt != 0 {
40 b.WriteString("alt+")
41 }
42 if c.Mods&Shift != 0 {
43 b.WriteString("shift+")
44 }
45 b.WriteString(c.Key)
46 return b.String()
47}
48
49// keyAliases maps every accepted lower-case spelling of a named key to its
50// canonical name. Canonical names map to themselves.
51var keyAliases = map[string]string{
52 "escape": "escape",
53 "esc": "escape",
54 "return": "return",
55 "enter": "return",
56 "tab": "tab",
57 "space": "space",
58 "backspace": "backspace",
59 "delete": "delete",
60 "del": "delete",
61 "home": "home",
62 "end": "end",
63 "pageup": "pageup",
64 "page_up": "pageup",
65 "pagedown": "pagedown",
66 "page_down": "pagedown",
67 "left": "left",
68 "arrowleft": "left",
69 "right": "right",
70 "arrowright": "right",
71 "up": "up",
72 "arrowup": "up",
73 "down": "down",
74 "arrowdown": "down",
75 "f1": "f1",
76 "f2": "f2",
77 "f3": "f3",
78 "f4": "f4",
79 "f5": "f5",
80 "f6": "f6",
81 "f7": "f7",
82 "f8": "f8",
83 "f9": "f9",
84 "f10": "f10",
85 "f11": "f11",
86 "f12": "f12",
87 "plus": "plus",
88 "+": "plus",
89 "minus": "minus",
90 "-": "minus",
91 "equal": "equal",
92 "=": "equal",
93 "bracketleft": "bracketleft",
94 "[": "bracketleft",
95 "bracketright": "bracketright",
96 "]": "bracketright",
97 "braceleft": "braceleft",
98 "{": "braceleft",
99 "braceright": "braceright",
100 "}": "braceright",
101}
102
103// KeyNames returns the canonical names of the named keys, sorted. Printable
104// ASCII characters are also valid keys but are not listed.
105func KeyNames() []string {
106 seen := make(map[string]struct{})
107 for _, canonical := range keyAliases {
108 seen[canonical] = struct{}{}
109 }
110 names := make([]string, 0, len(seen))
111 for name := range seen {
112 names = append(names, name)
113 }
114 slices.Sort(names)
115 return names
116}
117
118// canonicalKey resolves a key spelling to its canonical name.
119func canonicalKey(name string) (string, bool) {
120 if canonical, ok := keyAliases[strings.ToLower(name)]; ok {
121 return canonical, true
122 }
123 // Single printable ASCII keys (letters, digits, symbols such as %, ", &).
124 if len(name) == 1 && name[0] > ' ' && name[0] <= '~' {
125 return name, true
126 }
127 return "", false
128}
129
130// Parse parses a config key string such as "ctrl+t", "Control+Shift+T" or
131// "alt+left". Modifier names and named keys are case-insensitive; an upper-case
132// single letter implies Shift. It returns false if the string is not a valid
133// single chord.
134func Parse(s string) (Chord, bool) {
135 s = strings.TrimSpace(s)
136 if s == "" {
137 return Chord{}, false
138 }
139 if s == "+" {
140 return Chord{Key: "plus"}, true
141 }
142
143 var mods Mods
144 var keyPart string
145 for _, part := range strings.Split(s, "+") {
146 part = strings.TrimSpace(part)
147 if part == "" {
148 continue
149 }
150 switch strings.ToLower(part) {
151 case "ctrl", "control":
152 mods |= Ctrl
153 case "shift":
154 mods |= Shift
155 case "alt":
156 mods |= Alt
157 default:
158 if keyPart != "" {
159 return Chord{}, false
160 }
161 keyPart = part
162 }
163 }
164
165 // Allow "ctrl++" / "alt+shift++" where the key is "+".
166 if keyPart == "" && strings.HasSuffix(s, "++") {
167 keyPart = "+"
168 }
169 if keyPart == "" {
170 return Chord{}, false
171 }
172
173 // An upper-case single letter is Shift+<letter>.
174 if len(keyPart) == 1 && keyPart[0] >= 'A' && keyPart[0] <= 'Z' {
175 mods |= Shift
176 keyPart = strings.ToLower(keyPart)
177 }
178
179 key, ok := canonicalKey(keyPart)
180 if !ok {
181 return Chord{}, false
182 }
183 return Chord{Mods: mods, Key: key}, true
184}
185
186// Canonicalize returns the canonical spelling of a key string, so that two
187// strings that name the same binding compare equal. It returns false if the
188// string is not a valid single chord.
189func Canonicalize(s string) (string, bool) {
190 chord, ok := Parse(s)
191 if !ok {
192 return "", false
193 }
194 return chord.String(), true
195}