v0.28.0 3cb3770

shortcuts_test.go RawHistoryFolder

1package input
2
3import (
4	"testing"
5
6	"github.com/bnema/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		// Modifiers and named keys are case-insensitive.
211		{"ctrl+t", "CTRL+t"},
212		{"Escape", "escape"},
213		{"Tab", "TAB"},
214		// If shift is explicitly present, the key letter case shouldn't matter.
215		{"Ctrl+Shift+T", "ctrl+shift+t"},
216	}
217
218	for _, tt := range tests {
219		t.Run(tt.input1+" vs "+tt.input2, func(t *testing.T) {
220			got1, ok1 := ParseKeyString(tt.input1)
221			got2, ok2 := ParseKeyString(tt.input2)
222
223			if ok1 != ok2 {
224				t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, ok1, tt.input2, ok2)
225				return
226			}
227
228			if got1 != got2 {
229				t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, got1, tt.input2, got2)
230			}
231		})
232	}
233}
234
235func TestParseKeyString_UppercaseSingleLetterAddsShift(t *testing.T) {
236	got1, ok1 := ParseKeyString("M")
237	got2, ok2 := ParseKeyString("shift+m")
238	if !ok1 || !ok2 {
239		t.Fatalf("ParseKeyString should succeed for M and shift+m")
240	}
241	if got1 != got2 {
242		t.Fatalf("ParseKeyString uppercase should imply shift: got %v want %v", got1, got2)
243	}
244}
245
246func TestShortcutSet_Lookup(t *testing.T) {
247	set := &ShortcutSet{
248		Global: ShortcutTable{
249			{uint(gdk.KEY_q), ModCtrl}: ActionQuit,
250			{uint(gdk.KEY_l), ModCtrl}: ActionOpenOmnibox,
251		},
252		TabMode: ShortcutTable{
253			{uint('n'), ModNone}:            ActionNewTab,
254			{uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
255		},
256		PaneMode: ShortcutTable{
257			{uint('x'), ModNone}:            ActionClosePane,
258			{uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
259		},
260	}
261
262	tests := []struct {
263		name    string
264		binding KeyBinding
265		mode    Mode
266		want    Action
267		wantOk  bool
268	}{
269		{
270			name:    "global in normal mode",
271			binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
272			mode:    ModeNormal,
273			want:    ActionQuit,
274			wantOk:  true,
275		},
276		{
277			name:    "global in tab mode",
278			binding: KeyBinding{uint(gdk.KEY_l), ModCtrl},
279			mode:    ModeTab,
280			want:    ActionOpenOmnibox,
281			wantOk:  true,
282		},
283		{
284			name:    "tab mode action",
285			binding: KeyBinding{uint('n'), ModNone},
286			mode:    ModeTab,
287			want:    ActionNewTab,
288			wantOk:  true,
289		},
290		{
291			name:    "pane mode action",
292			binding: KeyBinding{uint('x'), ModNone},
293			mode:    ModePane,
294			want:    ActionClosePane,
295			wantOk:  true,
296		},
297		{
298			name:    "escape exits tab mode",
299			binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
300			mode:    ModeTab,
301			want:    ActionExitMode,
302			wantOk:  true,
303		},
304		{
305			name:    "escape exits pane mode",
306			binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
307			mode:    ModePane,
308			want:    ActionExitMode,
309			wantOk:  true,
310		},
311		{
312			name:    "unknown key in normal mode",
313			binding: KeyBinding{uint('z'), ModNone},
314			mode:    ModeNormal,
315			want:    "",
316			wantOk:  false,
317		},
318		{
319			name:    "tab action not in normal mode",
320			binding: KeyBinding{uint('n'), ModNone},
321			mode:    ModeNormal,
322			want:    "",
323			wantOk:  false,
324		},
325	}
326
327	for _, tt := range tests {
328		t.Run(tt.name, func(t *testing.T) {
329			got, ok := set.Lookup(tt.binding, tt.mode)
330			if ok != tt.wantOk {
331				t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
332				return
333			}
334			if got != tt.want {
335				t.Errorf("Lookup() = %v, want %v", got, tt.want)
336			}
337		})
338	}
339}
340
341func TestShouldAutoExitMode(t *testing.T) {
342	exitActions := []Action{
343		ActionNewTab,
344		ActionCloseTab,
345		ActionRenameTab,
346		ActionSplitRight,
347		ActionSplitLeft,
348		ActionSplitUp,
349		ActionSplitDown,
350		ActionClosePane,
351		ActionStackPane,
352		ActionMovePaneToTab,
353		ActionMovePaneToNextTab,
354	}
355
356	stayActions := []Action{
357		ActionNextTab,
358		ActionPreviousTab,
359		ActionFocusRight,
360		ActionFocusLeft,
361		ActionGoBack,
362		ActionZoomIn,
363		ActionOpenOmnibox,
364	}
365
366	for _, action := range exitActions {
367		if !ShouldAutoExitMode(action) {
368			t.Errorf("ShouldAutoExitMode(%s) = false, want true", action)
369		}
370	}
371
372	for _, action := range stayActions {
373		if ShouldAutoExitMode(action) {
374			t.Errorf("ShouldAutoExitMode(%s) = true, want false", action)
375		}
376	}
377}
378
379func TestMapConfigAction_ToggleFloatingPane(t *testing.T) {
380	action := mapConfigAction("toggle_floating_pane")
381	if action != ActionToggleFloatingPane {
382		t.Fatalf("mapConfigAction(toggle_floating_pane) = %s, want %s", action, ActionToggleFloatingPane)
383	}
384}
385
386func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
387	action := mapConfigAction("toggle-floating-pane")
388	if action != ActionToggleFloatingPane {
389		t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane)
390	}
391}

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