v0.20.1 d9a391f

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

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