v0.21.0 73bf644

shortcuts_test.go RawHistoryFolder

1package input
2
3import (
4	"testing"
5
6	"github.com/jwijenbergh/puregotk/v4/gdk"
7)
8
9func TestParseKeyString_SingleKeys(t *testing.T) {
10	tests := []struct {
11		name   string
12		input  string
13		want   KeyBinding
14		wantOk bool
15	}{
16		{
17			name:   "escape",
18			input:  "escape",
19			want:   KeyBinding{Keyval: uint(gdk.KEY_Escape), Modifiers: ModNone},
20			wantOk: true,
21		},
22		{
23			name:   "esc alias",
24			input:  "esc",
25			want:   KeyBinding{Keyval: uint(gdk.KEY_Escape), Modifiers: ModNone},
26			wantOk: true,
27		},
28		{
29			name:   "return",
30			input:  "return",
31			want:   KeyBinding{Keyval: uint(gdk.KEY_Return), Modifiers: ModNone},
32			wantOk: true,
33		},
34		{
35			name:   "enter alias",
36			input:  "enter",
37			want:   KeyBinding{Keyval: uint(gdk.KEY_Return), Modifiers: ModNone},
38			wantOk: true,
39		},
40		{
41			name:   "tab",
42			input:  "tab",
43			want:   KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModNone},
44			wantOk: true,
45		},
46		{
47			name:   "space",
48			input:  "space",
49			want:   KeyBinding{Keyval: uint(gdk.KEY_space), Modifiers: ModNone},
50			wantOk: true,
51		},
52		{
53			name:   "plus symbol",
54			input:  "+",
55			want:   KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModNone},
56			wantOk: true,
57		},
58		{
59			name:   "f5",
60			input:  "f5",
61			want:   KeyBinding{Keyval: uint(gdk.KEY_F5), Modifiers: ModNone},
62			wantOk: true,
63		},
64		{
65			name:   "f12",
66			input:  "f12",
67			want:   KeyBinding{Keyval: uint(gdk.KEY_F12), Modifiers: ModNone},
68			wantOk: true,
69		},
70		{
71			name:   "single letter",
72			input:  "t",
73			want:   KeyBinding{Keyval: uint('t'), Modifiers: ModNone},
74			wantOk: true,
75		},
76		{
77			name:   "single number",
78			input:  "0",
79			want:   KeyBinding{Keyval: uint(gdk.KEY_0), Modifiers: ModNone},
80			wantOk: true,
81		},
82		{
83			name:   "arrow left",
84			input:  "left",
85			want:   KeyBinding{Keyval: uint(gdk.KEY_Left), Modifiers: ModNone},
86			wantOk: true,
87		},
88		{
89			name:   "arrow right",
90			input:  "right",
91			want:   KeyBinding{Keyval: uint(gdk.KEY_Right), Modifiers: ModNone},
92			wantOk: true,
93		},
94	}
95
96	for _, tt := range tests {
97		t.Run(tt.name, func(t *testing.T) {
98			got, ok := ParseKeyString(tt.input)
99			if ok != tt.wantOk {
100				t.Errorf("ParseKeyString(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
101				return
102			}
103			if !tt.wantOk {
104				return
105			}
106			if got.Keyval != tt.want.Keyval {
107				t.Errorf("ParseKeyString(%q) keyval = %d, want %d", tt.input, got.Keyval, tt.want.Keyval)
108			}
109			if got.Modifiers != tt.want.Modifiers {
110				t.Errorf("ParseKeyString(%q) modifiers = %d, want %d", tt.input, got.Modifiers, tt.want.Modifiers)
111			}
112		})
113	}
114}
115
116func TestParseKeyString_WithModifiers(t *testing.T) {
117	tests := []struct {
118		name   string
119		input  string
120		want   KeyBinding
121		wantOk bool
122	}{
123		{
124			name:   "ctrl+t",
125			input:  "ctrl+t",
126			want:   KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl},
127			wantOk: true,
128		},
129		{
130			name:   "control+t",
131			input:  "control+t",
132			want:   KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl},
133			wantOk: true,
134		},
135		{
136			name:   "shift+tab",
137			input:  "shift+tab",
138			want:   KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModShift},
139			wantOk: true,
140		},
141		{
142			name:   "alt+left",
143			input:  "alt+left",
144			want:   KeyBinding{Keyval: uint(gdk.KEY_Left), Modifiers: ModAlt},
145			wantOk: true,
146		},
147		{
148			name:   "ctrl+shift+t",
149			input:  "ctrl+shift+t",
150			want:   KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl | ModShift},
151			wantOk: true,
152		},
153		{
154			name:   "ctrl+r",
155			input:  "ctrl+r",
156			want:   KeyBinding{Keyval: uint('r'), Modifiers: ModCtrl},
157			wantOk: true,
158		},
159		{
160			name:   "ctrl++",
161			input:  "ctrl++",
162			want:   KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModCtrl},
163			wantOk: true,
164		},
165	}
166
167	for _, tt := range tests {
168		t.Run(tt.name, func(t *testing.T) {
169			got, ok := ParseKeyString(tt.input)
170			if ok != tt.wantOk {
171				t.Errorf("ParseKeyString(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
172				return
173			}
174			if !tt.wantOk {
175				return
176			}
177			if got.Keyval != tt.want.Keyval {
178				t.Errorf("ParseKeyString(%q) keyval = %d, want %d", tt.input, got.Keyval, tt.want.Keyval)
179			}
180			if got.Modifiers != tt.want.Modifiers {
181				t.Errorf("ParseKeyString(%q) modifiers = %d, want %d", tt.input, got.Modifiers, tt.want.Modifiers)
182			}
183		})
184	}
185}
186
187func TestParseKeyString_Invalid(t *testing.T) {
188	tests := []string{
189		"",
190		"ctrl+",
191		"unknownkey",
192		"ctrl+unknownkey",
193	}
194
195	for _, input := range tests {
196		t.Run(input, func(t *testing.T) {
197			_, ok := ParseKeyString(input)
198			if ok {
199				t.Errorf("ParseKeyString(%q) should have failed", input)
200			}
201		})
202	}
203}
204
205func TestParseKeyString_CaseInsensitive(t *testing.T) {
206	tests := []struct {
207		input1 string
208		input2 string
209	}{
210		{"ctrl+t", "CTRL+T"},
211		{"Escape", "escape"},
212		{"Tab", "TAB"},
213		{"Ctrl+Shift+T", "ctrl+shift+t"},
214	}
215
216	for _, tt := range tests {
217		t.Run(tt.input1+" vs "+tt.input2, func(t *testing.T) {
218			got1, ok1 := ParseKeyString(tt.input1)
219			got2, ok2 := ParseKeyString(tt.input2)
220
221			if ok1 != ok2 {
222				t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, ok1, tt.input2, ok2)
223				return
224			}
225
226			if got1 != got2 {
227				t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, got1, tt.input2, got2)
228			}
229		})
230	}
231}
232
233func TestShortcutSet_Lookup(t *testing.T) {
234	set := &ShortcutSet{
235		Global: ShortcutTable{
236			{uint(gdk.KEY_q), ModCtrl}: ActionQuit,
237			{uint(gdk.KEY_l), ModCtrl}: ActionOpenOmnibox,
238		},
239		TabMode: ShortcutTable{
240			{uint('n'), ModNone}:            ActionNewTab,
241			{uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
242		},
243		PaneMode: ShortcutTable{
244			{uint('x'), ModNone}:            ActionClosePane,
245			{uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
246		},
247	}
248
249	tests := []struct {
250		name    string
251		binding KeyBinding
252		mode    Mode
253		want    Action
254		wantOk  bool
255	}{
256		{
257			name:    "global in normal mode",
258			binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
259			mode:    ModeNormal,
260			want:    ActionQuit,
261			wantOk:  true,
262		},
263		{
264			name:    "global in tab mode",
265			binding: KeyBinding{uint(gdk.KEY_l), ModCtrl},
266			mode:    ModeTab,
267			want:    ActionOpenOmnibox,
268			wantOk:  true,
269		},
270		{
271			name:    "tab mode action",
272			binding: KeyBinding{uint('n'), ModNone},
273			mode:    ModeTab,
274			want:    ActionNewTab,
275			wantOk:  true,
276		},
277		{
278			name:    "pane mode action",
279			binding: KeyBinding{uint('x'), ModNone},
280			mode:    ModePane,
281			want:    ActionClosePane,
282			wantOk:  true,
283		},
284		{
285			name:    "escape exits tab mode",
286			binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
287			mode:    ModeTab,
288			want:    ActionExitMode,
289			wantOk:  true,
290		},
291		{
292			name:    "escape exits pane mode",
293			binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
294			mode:    ModePane,
295			want:    ActionExitMode,
296			wantOk:  true,
297		},
298		{
299			name:    "unknown key in normal mode",
300			binding: KeyBinding{uint('z'), ModNone},
301			mode:    ModeNormal,
302			want:    "",
303			wantOk:  false,
304		},
305		{
306			name:    "tab action not in normal mode",
307			binding: KeyBinding{uint('n'), ModNone},
308			mode:    ModeNormal,
309			want:    "",
310			wantOk:  false,
311		},
312	}
313
314	for _, tt := range tests {
315		t.Run(tt.name, func(t *testing.T) {
316			got, ok := set.Lookup(tt.binding, tt.mode)
317			if ok != tt.wantOk {
318				t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
319				return
320			}
321			if got != tt.want {
322				t.Errorf("Lookup() = %v, want %v", got, tt.want)
323			}
324		})
325	}
326}
327
328func TestShouldAutoExitMode(t *testing.T) {
329	exitActions := []Action{
330		ActionNewTab,
331		ActionCloseTab,
332		ActionRenameTab,
333		ActionSplitRight,
334		ActionSplitLeft,
335		ActionSplitUp,
336		ActionSplitDown,
337		ActionClosePane,
338		ActionStackPane,
339	}
340
341	stayActions := []Action{
342		ActionNextTab,
343		ActionPreviousTab,
344		ActionFocusRight,
345		ActionFocusLeft,
346		ActionGoBack,
347		ActionZoomIn,
348		ActionOpenOmnibox,
349	}
350
351	for _, action := range exitActions {
352		if !ShouldAutoExitMode(action) {
353			t.Errorf("ShouldAutoExitMode(%s) = false, want true", action)
354		}
355	}
356
357	for _, action := range stayActions {
358		if ShouldAutoExitMode(action) {
359			t.Errorf("ShouldAutoExitMode(%s) = true, want false", action)
360		}
361	}
362}

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