v0.29.0 21b3150

global_shortcuts_test.go RawHistoryFolder

1package input
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/puregotk/v4/gdk"
8)
9
10func TestGlobalShortcutHandlerSuppressesRepeatedOneShotActions(t *testing.T) {
11	h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
12	now := time.Unix(100, 0)
13
14	if h.suppressRepeatedShortcut(ActionZoomReset, now) {
15		t.Fatal("first zoom reset dispatch was suppressed")
16	}
17	if !h.suppressRepeatedShortcut(ActionZoomReset, now.Add(globalShortcutRepeatSuppressWindow/2)) {
18		t.Fatal("repeated zoom reset dispatch inside suppression window was not suppressed")
19	}
20	if h.suppressRepeatedShortcut(ActionZoomReset, now.Add(globalShortcutRepeatSuppressWindow)) {
21		t.Fatal("zoom reset dispatch at suppression boundary was suppressed")
22	}
23}
24
25func TestGlobalShortcutHandlerAllowsRepeatingFocusNavigationActions(t *testing.T) {
26	h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
27	now := time.Unix(100, 0)
28
29	if h.suppressRepeatedShortcut(ActionFocusRight, now) {
30		t.Fatal("first focus-right dispatch was suppressed")
31	}
32	if h.suppressRepeatedShortcut(ActionFocusRight, now.Add(time.Millisecond)) {
33		t.Fatal("focus-right repeat was suppressed")
34	}
35}
36
37func TestGlobalShortcutHandlerSuppressesRepeatedBrowserNavigationActions(t *testing.T) {
38	h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
39	now := time.Unix(100, 0)
40
41	if h.suppressRepeatedShortcut(ActionGoBack, now) {
42		t.Fatal("first go-back dispatch was suppressed")
43	}
44	if !h.suppressRepeatedShortcut(ActionGoBack, now.Add(time.Millisecond)) {
45		t.Fatal("repeated go-back dispatch was not suppressed")
46	}
47}
48
49func TestGlobalShortcutHandlerSuppressesRepeatedTabIndexSwitchActions(t *testing.T) {
50	actions := []Action{
51		ActionSwitchTabIndex1,
52		ActionSwitchTabIndex2,
53		ActionSwitchTabIndex3,
54		ActionSwitchTabIndex4,
55		ActionSwitchTabIndex5,
56		ActionSwitchTabIndex6,
57		ActionSwitchTabIndex7,
58		ActionSwitchTabIndex8,
59		ActionSwitchTabIndex9,
60		ActionSwitchTabIndex10,
61	}
62
63	for _, action := range actions {
64		t.Run(string(action), func(t *testing.T) {
65			h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
66			now := time.Unix(100, 0)
67
68			if h.suppressRepeatedShortcut(action, now) {
69				t.Fatalf("first %s dispatch was suppressed", action)
70			}
71			if !h.suppressRepeatedShortcut(action, now.Add(time.Millisecond)) {
72				t.Fatalf("repeated %s dispatch was not suppressed", action)
73			}
74		})
75	}
76}
77
78func TestGlobalShortcutHandlerSuppressesOneShotActionsAfterDetach(t *testing.T) {
79	h := &GlobalShortcutHandler{
80		registered:     make(map[KeyBinding]Action),
81		lastDispatchAt: make(map[Action]time.Time),
82	}
83
84	if h.suppressRepeatedShortcut(ActionZoomReset, time.Unix(100, 0)) {
85		t.Fatal("one-shot shortcut was suppressed before detach")
86	}
87	h.Detach()
88	if !h.suppressRepeatedShortcut(ActionZoomReset, time.Unix(100, 0)) {
89		t.Fatal("one-shot shortcut was not suppressed with detached handler state")
90	}
91}
92
93func TestGlobalShortcutHandlerGenerationMarksOldCallbacksStale(t *testing.T) {
94	h := &GlobalShortcutHandler{}
95	generation := h.generation
96
97	if h.isStaleGeneration(generation) {
98		t.Fatal("current generation was marked stale")
99	}
100	h.generation++
101	if !h.isStaleGeneration(generation) {
102		t.Fatal("old generation was not marked stale")
103	}
104}
105
106func TestGlobalShortcutHandlerReturnsInactiveForNilOrDetached(t *testing.T) {
107	var nilHandler *GlobalShortcutHandler
108	if nilHandler.isActiveWindowShortcutHandler() {
109		t.Fatal("nil handler should return inactive")
110	}
111	if (&GlobalShortcutHandler{}).isActiveWindowShortcutHandler() {
112		t.Fatal("handler with nil window should return inactive")
113	}
114
115	h := &GlobalShortcutHandler{
116		registered:     make(map[KeyBinding]Action),
117		lastDispatchAt: make(map[Action]time.Time),
118	}
119	h.Detach()
120	if h.isActiveWindowShortcutHandler() {
121		t.Fatal("detached handler with nil controller should return inactive")
122	}
123}
124
125func TestGlobalShortcutHandlerSuppressesRepeatedAdditionalOneShotUIActions(t *testing.T) {
126	actions := []Action{
127		ActionPrintPage,
128		ActionReload,
129		ActionHardReload,
130		ActionToggleFullscreen,
131		ActionToggleHistorySystemView,
132		ActionOpenOmnibox,
133		ActionOpenFind,
134		ActionFindNext,
135		ActionFindPrev,
136		ActionOpenDevTools,
137		ActionToggleFloatingPane,
138		ActionToggleFavoritesSystemView,
139		ActionToggleConfigSystemView,
140		ActionCopyURL,
141		ActionConsumeOrExpelLeft,
142		ActionConsumeOrExpelRight,
143		ActionConsumeOrExpelUp,
144		ActionConsumeOrExpelDown,
145	}
146
147	for _, action := range actions {
148		t.Run(string(action), func(t *testing.T) {
149			h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
150			now := time.Unix(100, 0)
151
152			if h.suppressRepeatedShortcut(action, now) {
153				t.Fatalf("first %s dispatch was suppressed", action)
154			}
155			if !h.suppressRepeatedShortcut(action, now.Add(time.Millisecond)) {
156				t.Fatalf("repeated %s dispatch was not suppressed", action)
157			}
158		})
159	}
160}
161
162func TestGlobalShortcutHandlerSuppressesRepeatedFloatingProfileAction(t *testing.T) {
163	h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
164	now := time.Unix(100, 0)
165	action := NewFloatingProfileAction("work", "https://example.com")
166
167	if h.suppressRepeatedShortcut(action, now) {
168		t.Fatal("first floating-profile dispatch was suppressed")
169	}
170	if !h.suppressRepeatedShortcut(action, now.Add(time.Millisecond)) {
171		t.Fatal("repeated floating-profile dispatch was not suppressed")
172	}
173}
174
175func TestGlobalShortcutHandlerSuppressesActionSwitchLastTab(t *testing.T) {
176	if !isRepeatedGlobalShortcutSuppressed(ActionSwitchLastTab) {
177		t.Fatal("ActionSwitchLastTab should be suppressed")
178	}
179
180	h := &GlobalShortcutHandler{lastDispatchAt: make(map[Action]time.Time)}
181	now := time.Unix(100, 0)
182
183	if h.suppressRepeatedShortcut(ActionSwitchLastTab, now) {
184		t.Fatal("first ActionSwitchLastTab dispatch was suppressed")
185	}
186	if !h.suppressRepeatedShortcut(ActionSwitchLastTab, now.Add(time.Millisecond)) {
187		t.Fatal("repeated ActionSwitchLastTab dispatch was not suppressed")
188	}
189}
190
191func TestGlobalShortcutHandlerSuppressesHeldGlobalShortcutUntilRelease(t *testing.T) {
192	h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
193	info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('f'), eventKeycode: 41}
194
195	if h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
196		t.Fatal("first held global shortcut dispatch was suppressed")
197	}
198	if !h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
199		t.Fatal("held global shortcut repeat after cooldown was not suppressed")
200	}
201
202	h.releaseHeldGlobalShortcuts(uint('x'), 0)
203	if !h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
204		t.Fatal("unrelated key release re-armed held global shortcut")
205	}
206
207	h.releaseHeldGlobalShortcuts(uint('f'), 0)
208	if h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
209		t.Fatal("matching key release did not re-arm held global shortcut")
210	}
211}
212
213func TestGlobalShortcutHandlerSuppressesHeldModeAction(t *testing.T) {
214	h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
215	info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('p'), eventKeycode: 33}
216
217	if h.suppressHeldShortcut(ActionEnterPaneMode, info) {
218		t.Fatal("first mode shortcut dispatch was suppressed")
219	}
220	if !h.suppressHeldShortcut(ActionEnterPaneMode, info) {
221		t.Fatal("held mode shortcut repeat was not suppressed")
222	}
223}
224
225func TestGlobalShortcutHandlerSuppressesHeldFloatingProfileAction(t *testing.T) {
226	h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
227	info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('o'), eventKeycode: 32}
228	action := NewFloatingProfileAction("work", "https://example.com")
229
230	if h.suppressHeldShortcut(action, info) {
231		t.Fatal("first floating-profile shortcut dispatch was suppressed")
232	}
233	if !h.suppressHeldShortcut(action, info) {
234		t.Fatal("held floating-profile shortcut repeat was not suppressed")
235	}
236}
237
238func TestShouldDispatchGlobalShortcutEventRequiresCurrentKeyEvent(t *testing.T) {
239	if shouldDispatchGlobalShortcutEvent(globalShortcutEventInfo{}) {
240		t.Fatal("global shortcut without current event should not dispatch")
241	}
242	if shouldDispatchGlobalShortcutEvent(globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyReleaseValue}) {
243		t.Fatal("global shortcut from key-release event should not dispatch")
244	}
245	if !shouldDispatchGlobalShortcutEvent(globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue}) {
246		t.Fatal("global shortcut from key-press event should dispatch")
247	}
248}
249
250func TestFormatModifierMask(t *testing.T) {
251	got := formatModifierMask(gdk.ControlMaskValue | gdk.ShiftMaskValue | gdk.ModifierType(0x2000))
252	if got != "ctrl+shift+0x2000" {
253		t.Fatalf("unexpected modifier mask: %q", got)
254	}
255	if formatModifierMask(0) != "none" {
256		t.Fatal("zero modifier mask should render as none")
257	}
258}
259
260func TestFormatEventType(t *testing.T) {
261	if got := formatEventType(gdk.KeyPressValue); got != "key-press" {
262		t.Fatalf("unexpected key press event type: %q", got)
263	}
264	if got := formatEventType(gdk.EventType(99)); got != "event-99" {
265		t.Fatalf("unexpected fallback event type: %q", got)
266	}
267}

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