keychord.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close