keybindings_gateway_test.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"path/filepath"
6	"sync"
7	"testing"
8
9	"github.com/bnema/dumber/internal/application/port"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12)
13
14// newTestKeybindingsManager creates a Manager with a temp config dir.
15func newTestKeybindingsManager(t *testing.T) *Manager {
16	t.Helper()
17	configHome := t.TempDir()
18
19	t.Setenv("XDG_CONFIG_HOME", configHome)
20	t.Setenv("XDG_DATA_HOME", filepath.Join(configHome, "data"))
21	t.Setenv("XDG_STATE_HOME", filepath.Join(configHome, "state"))
22	t.Setenv("XDG_CACHE_HOME", filepath.Join(configHome, "cache"))
23
24	mgr, err := NewManager()
25	require.NoError(t, err)
26	require.NoError(t, mgr.Load())
27	return mgr
28}
29
30// inMemoryManager creates a lightweight Manager without file I/O for tests
31// that only query config data without saving.
32func inMemoryManager() *Manager {
33	return &Manager{
34		config: DefaultConfig(),
35		mu:     sync.RWMutex{},
36	}
37}
38
39func TestKeybindingsGateway_VimModeIncludedInGetKeybindings(t *testing.T) {
40	gw := NewKeybindingsGateway(inMemoryManager())
41	cfg, err := gw.GetKeybindings(context.Background())
42	require.NoError(t, err)
43
44	var pageGroup *port.KeybindingGroup
45	for i, g := range cfg.Groups {
46		if g.Mode == "vim" {
47			pageGroup = &cfg.Groups[i]
48			break
49		}
50	}
51	require.NotNil(t, pageGroup, "vim mode group must be present in GetKeybindings output")
52	assert.Equal(t, "Vim Mode", pageGroup.DisplayName)
53	assert.Equal(t, "ctrl+y", pageGroup.Activation)
54
55	// Verify all expected actions are present.
56	expectedActions := map[string]struct{}{
57		"vim-scroll-left":      {},
58		"vim-scroll-down":      {},
59		"vim-scroll-up":        {},
60		"vim-scroll-right":     {},
61		"vim-scroll-down-fast": {},
62		"vim-scroll-up-fast":   {},
63		"confirm":              {},
64		"cancel":               {},
65	}
66
67	for _, entry := range pageGroup.Bindings {
68		delete(expectedActions, entry.Action)
69		assert.NotEmpty(t, entry.Keys, "action %q has empty keys", entry.Action)
70		assert.False(t, entry.IsCustom, "default vim mode config should not have IsCustom=true")
71	}
72	assert.Empty(t, expectedActions, "vim mode group missing actions: %v", expectedActions)
73
74	// Must be placed after global, pane, tab groups (consistent ordering).
75	groups := cfg.Groups
76	foundIdx := -1
77	for i, g := range groups {
78		if g.Mode == "vim" {
79			foundIdx = i
80			break
81		}
82	}
83	assert.Greater(t, foundIdx, 2, "vim mode group should be after pane/tab groups")
84}
85
86func TestKeybindingsGateway_VimModeIncludedInGetDefaultKeybindings(t *testing.T) {
87	gw := NewKeybindingsGateway(inMemoryManager())
88	cfg, err := gw.GetDefaultKeybindings(context.Background())
89	require.NoError(t, err)
90
91	var pageGroup *port.KeybindingGroup
92	for i, g := range cfg.Groups {
93		if g.Mode == "vim" {
94			pageGroup = &cfg.Groups[i]
95			break
96		}
97	}
98	require.NotNil(t, pageGroup, "vim mode group must be present in GetDefaultKeybindings output")
99	assert.Equal(t, "Vim Mode", pageGroup.DisplayName)
100	assert.Equal(t, "ctrl+y", pageGroup.Activation)
101
102	// Verify default keys match actual keys.
103	for _, entry := range pageGroup.Bindings {
104		assert.Equal(t, entry.DefaultKeys, entry.Keys,
105			"default vim mode action %q should have keys == default_keys", entry.Action)
106	}
107}
108
109func TestKeybindingsGateway_VimModeSetKeybindingUpdatesFile(t *testing.T) {
110	mgr := newTestKeybindingsManager(t)
111	gw := NewKeybindingsGateway(mgr)
112
113	err := gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
114		Mode:   "vim",
115		Action: "vim-scroll-left",
116		Keys:   []string{"a"},
117	})
118	require.NoError(t, err)
119
120	// Reload and assert on the parsed Vim-mode configuration rather than raw TOML text.
121	reloaded, reloadErr := NewManager()
122	require.NoError(t, reloadErr)
123	require.NoError(t, reloaded.Load())
124	assert.Equal(t, []string{"a"}, reloaded.Get().Workspace.VimMode.Actions["vim-scroll-left"].Keys)
125}
126
127func TestKeybindingsGateway_VimModeResetKeybinding(t *testing.T) {
128	mgr := newTestKeybindingsManager(t)
129	gw := NewKeybindingsGateway(mgr)
130
131	// Set a keybinding.
132	require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
133		Mode:   "vim",
134		Action: "vim-scroll-left",
135		Keys:   []string{"a"},
136	}))
137
138	// Reset it.
139	err := gw.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{
140		Mode:   "vim",
141		Action: "vim-scroll-left",
142	})
143	require.NoError(t, err)
144
145	// Reload and assert on the parsed default binding.
146	reloaded, reloadErr := NewManager()
147	require.NoError(t, reloadErr)
148	require.NoError(t, reloaded.Load())
149	assert.Equal(t, []string{"h"}, reloaded.Get().Workspace.VimMode.Actions["vim-scroll-left"].Keys)
150}
151
152func TestKeybindingsGateway_VimModeIsPartOfResetAll(t *testing.T) {
153	mgr := newTestKeybindingsManager(t)
154	gw := NewKeybindingsGateway(mgr)
155
156	// Change a vim mode binding.
157	require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
158		Mode:   "vim",
159		Action: "vim-scroll-left",
160		Keys:   []string{"a"},
161	}))
162
163	// Change another action.
164	require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
165		Mode:   "vim",
166		Action: "vim-scroll-down",
167		Keys:   []string{"b"},
168	}))
169
170	// Reset all.
171	err := gw.ResetAllKeybindings(context.Background())
172	require.NoError(t, err)
173
174	// Reload and assert on the parsed default bindings.
175	reloaded, reloadErr := NewManager()
176	require.NoError(t, reloadErr)
177	require.NoError(t, reloaded.Load())
178	assert.Equal(t, []string{"h"}, reloaded.Get().Workspace.VimMode.Actions["vim-scroll-left"].Keys, "vim-scroll-left should be restored to default")
179	assert.Equal(t, []string{"j"}, reloaded.Get().Workspace.VimMode.Actions["vim-scroll-down"].Keys, "vim-scroll-down should be restored to default")
180}
181
182func TestKeybindingsGateway_VimModeConflictsWithinSelf(t *testing.T) {
183	gw := NewKeybindingsGateway(inMemoryManager())
184	conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"h"})
185	require.NoError(t, err)
186	require.NotEmpty(t, conflicts, "should detect conflict with vim-scroll-left which uses 'h'")
187
188	// The conflict should reference the same mode.
189	assert.Equal(t, "vim-scroll-left", conflicts[0].ConflictingAction)
190	assert.Equal(t, "vim", conflicts[0].ConflictingMode)
191}
192
193func TestKeybindingsGateway_VimModeConflictsWithOtherModes(t *testing.T) {
194	gw := NewKeybindingsGateway(inMemoryManager())
195	// "enter" is used by vim mode's confirm - but also by other modes like pane_mode.
196	conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"enter"})
197	require.NoError(t, err)
198
199	// There should be conflicts since "enter" is used in other modes too.
200	assert.NotEmpty(t, conflicts, "should detect cross-mode conflict for 'enter'")
201}
202
203func TestKeybindingsGateway_VimModeNoConflictForUnusedKey(t *testing.T) {
204	gw := NewKeybindingsGateway(inMemoryManager())
205	conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"ctrl+9"})
206	require.NoError(t, err)
207	assert.Empty(t, conflicts, "no conflict expected for unused key")
208}

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