floating_shortcuts_test.go RawHistoryFolder

1package input
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/dumber/internal/domain/entity"
8	"github.com/bnema/puregotk/v4/gdk"
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11)
12
13// defaultWorkspaceConfig returns a WorkspaceConfig with the default shortcuts,
14// mirroring the relevant portion of config.DefaultConfig().
15func defaultWorkspaceConfig() *entity.WorkspaceConfig {
16	return &entity.WorkspaceConfig{
17		Shortcuts: entity.GlobalShortcutsConfig{
18			Actions: map[string]entity.ActionBinding{
19				"toggle_floating_pane":   {Keys: []string{"alt+f"}},
20				"consume_or_expel_left":  {Keys: []string{"alt+["}},
21				"consume_or_expel_right": {Keys: []string{"alt+]"}},
22			},
23		},
24	}
25}
26
27func TestShortcutSet_GlobalToggleFloatingPane(t *testing.T) {
28	ws := defaultWorkspaceConfig()
29	set := NewShortcutSet(context.Background(), ws, nil)
30
31	action, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_f), Modifiers: ModAlt}, ModeNormal)
32	require.True(t, ok)
33	assert.Equal(t, ActionToggleFloatingPane, action)
34}
35
36func TestShortcutSet_FloatingProfiles_RegisterMultiple(t *testing.T) {
37	ws := defaultWorkspaceConfig()
38	ws.FloatingPane.Profiles = map[string]entity.FloatingPaneProfile{
39		"google": {
40			Keys: []string{"alt+g"},
41			URL:  "https://google.com",
42		},
43		"github": {
44			Keys: []string{"alt+y"},
45			URL:  "https://github.com",
46		},
47	}
48
49	set := NewShortcutSet(context.Background(), ws, nil)
50
51	googleAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_g), Modifiers: ModAlt}, ModeNormal)
52	require.True(t, ok)
53	googleTarget, ok := ParseFloatingProfileTarget(googleAction)
54	require.True(t, ok)
55	assert.Equal(t, "google", googleTarget.SessionID)
56	assert.Equal(t, "https://google.com", googleTarget.URL)
57
58	githubAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_y), Modifiers: ModAlt}, ModeNormal)
59	require.True(t, ok)
60	githubTarget, ok := ParseFloatingProfileTarget(githubAction)
61	require.True(t, ok)
62	assert.Equal(t, "github", githubTarget.SessionID)
63	assert.Equal(t, "https://github.com", githubTarget.URL)
64}
65
66func TestCollectFloatingProfileShortcuts_SkipsConflicts(t *testing.T) {
67	ws := defaultWorkspaceConfig()
68	ws.FloatingPane.Profiles = map[string]entity.FloatingPaneProfile{
69		"tab-conflict": {
70			Keys: []string{"alt+1"},
71			URL:  "https://example.com/tab",
72		},
73		"pane-conflict": {
74			Keys: []string{"alt+["},
75			URL:  "https://example.com/pane",
76		},
77		"ok": {
78			Keys: []string{"alt+g"},
79			URL:  "https://example.com/ok",
80		},
81	}
82
83	occupied := map[KeyBinding]Action{
84		{Keyval: uint(gdk.KEY_1), Modifiers: ModAlt}:           ActionSwitchTabIndex1,
85		{Keyval: uint(gdk.KEY_bracketleft), Modifiers: ModAlt}: ActionConsumeOrExpelLeft,
86	}
87
88	shortcuts := collectFloatingProfileShortcutsFromWorkspace(context.Background(), ws, occupied)
89	require.Len(t, shortcuts, 1)
90	assert.Equal(t, KeyBinding{Keyval: uint(gdk.KEY_g), Modifiers: ModAlt}, shortcuts[0].Binding)
91	target, ok := ParseFloatingProfileTarget(shortcuts[0].Action)
92	require.True(t, ok)
93	assert.Equal(t, "ok", target.SessionID)
94	assert.Equal(t, "https://example.com/ok", target.URL)
95}
96
97func TestShortcutSet_FloatingProfilesPreserveExistingGlobalShortcuts(t *testing.T) {
98	ws := defaultWorkspaceConfig()
99	ws.FloatingPane.Profiles = map[string]entity.FloatingPaneProfile{
100		"conflict": {
101			Keys: []string{"alt+["},
102			URL:  "https://example.com/override-attempt",
103		},
104	}
105
106	set := NewShortcutSet(context.Background(), ws, nil)
107
108	tabAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_bracketleft), Modifiers: ModAlt}, ModeNormal)
109	require.True(t, ok)
110	assert.Equal(t, ActionConsumeOrExpelLeft, tabAction)
111
112	omniboxAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_l), Modifiers: ModCtrl}, ModeNormal)
113	require.True(t, ok)
114	assert.Equal(t, ActionOpenOmnibox, omniboxAction)
115}
116
117func TestShortcutSet_FloatingProfilesSkipGlobalOnlyConflicts(t *testing.T) {
118	ws := defaultWorkspaceConfig()
119	ws.FloatingPane.Profiles = map[string]entity.FloatingPaneProfile{
120		"alt-one-conflict": {
121			Keys: []string{"alt+1"},
122			URL:  "https://example.com/one",
123		},
124		"alt-tab-conflict": {
125			Keys: []string{"alt+tab"},
126			URL:  "https://example.com/tab",
127		},
128		"ok": {
129			Keys: []string{"alt+g"},
130			URL:  "https://example.com/ok",
131		},
132	}
133
134	set := NewShortcutSet(context.Background(), ws, nil)
135
136	_, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_1), Modifiers: ModAlt}, ModeNormal)
137	assert.False(t, ok)
138
139	_, ok = set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModAlt}, ModeNormal)
140	assert.False(t, ok)
141
142	action, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_g), Modifiers: ModAlt}, ModeNormal)
143	require.True(t, ok)
144	target, ok := ParseFloatingProfileTarget(action)
145	require.True(t, ok)
146	assert.Equal(t, "ok", target.SessionID)
147	assert.Equal(t, "https://example.com/ok", target.URL)
148}
149
150func TestShortcutSet_FloatingProfilesWithSameURLGetDistinctSessionIDs(t *testing.T) {
151	ws := defaultWorkspaceConfig()
152	ws.FloatingPane.Profiles = map[string]entity.FloatingPaneProfile{
153		"work-mail": {
154			Keys: []string{"alt+w"},
155			URL:  "https://mail.google.com",
156		},
157		"personal-mail": {
158			Keys: []string{"alt+p"},
159			URL:  "https://mail.google.com",
160		},
161	}
162
163	set := NewShortcutSet(context.Background(), ws, nil)
164
165	workAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_w), Modifiers: ModAlt}, ModeNormal)
166	require.True(t, ok)
167	workTarget, ok := ParseFloatingProfileTarget(workAction)
168	require.True(t, ok)
169
170	personalAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_p), Modifiers: ModAlt}, ModeNormal)
171	require.True(t, ok)
172	personalTarget, ok := ParseFloatingProfileTarget(personalAction)
173	require.True(t, ok)
174
175	assert.Equal(t, "https://mail.google.com", workTarget.URL)
176	assert.Equal(t, "https://mail.google.com", personalTarget.URL)
177	assert.NotEqual(t, workTarget.SessionID, personalTarget.SessionID)
178}
179
180func TestShortcutSet_FloatingProfiles_SupportModifierCombos(t *testing.T) {
181	ws := defaultWorkspaceConfig()
182	ws.FloatingPane.Profiles = map[string]entity.FloatingPaneProfile{
183		"mail": {
184			Keys: []string{"ctrl+alt+m"},
185			URL:  "https://mail.example.com",
186		},
187		"search": {
188			Keys: []string{"ctrl+shift+y"},
189			URL:  "https://search.example.com",
190		},
191	}
192
193	set := NewShortcutSet(context.Background(), ws, nil)
194
195	mailAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_m), Modifiers: ModCtrl | ModAlt}, ModeNormal)
196	require.True(t, ok)
197	mailTarget, ok := ParseFloatingProfileTarget(mailAction)
198	require.True(t, ok)
199	assert.Equal(t, "mail", mailTarget.SessionID)
200	assert.Equal(t, "https://mail.example.com", mailTarget.URL)
201
202	searchAction, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_y), Modifiers: ModCtrl | ModShift}, ModeNormal)
203	require.True(t, ok)
204	searchTarget, ok := ParseFloatingProfileTarget(searchAction)
205	require.True(t, ok)
206	assert.Equal(t, "search", searchTarget.SessionID)
207	assert.Equal(t, "https://search.example.com", searchTarget.URL)
208}
209
210func TestParseFloatingProfileTarget_AllowsPipeInURL(t *testing.T) {
211	action := NewFloatingProfileAction("pipe-test", "https://example.com/a|b")
212
213	target, ok := ParseFloatingProfileTarget(action)
214	require.True(t, ok)
215	assert.Equal(t, "pipe-test", target.SessionID)
216	assert.Equal(t, "https://example.com/a|b", target.URL)
217}

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