config_completion_test.go RawHistoryFolder

1package input
2
3import (
4	"context"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/bnema/dumber/internal/infrastructure/config"
10	"github.com/bnema/puregotk/v4/gdk"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13)
14
15// loadShortcutsFromTOML loads a config file through the real config loader
16// (legacy transforms, default completion, validation) and builds the shortcut
17// set the application would build from it.
18func loadShortcutsFromTOML(t *testing.T, content string) *ShortcutSet {
19	t.Helper()
20
21	home := t.TempDir()
22	t.Setenv("HOME", home)
23	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
24	t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
25	t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
26	t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
27
28	configFile, err := config.GetConfigFile()
29	require.NoError(t, err)
30	require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
31	require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
32
33	mgr, err := config.NewManager()
34	require.NoError(t, err)
35	require.NoError(t, mgr.Load())
36
37	cfg := mgr.Get()
38	return NewShortcutSet(context.Background(), &cfg.Workspace, &cfg.Session)
39}
40
41func lookupGlobal(set *ShortcutSet, keyval uint, mods Modifier) (Action, bool) {
42	return set.Lookup(KeyBinding{Keyval: keyval, Modifiers: mods}, ModeNormal)
43}
44
45func TestConfigCompletion_UserKeyConflictIsNotResurrectedByInput(t *testing.T) {
46	// ctrl+r is a default key of reload, so completion leaves reload out. The
47	// input layer must not bind reload's other default key (f5) on its own.
48	set := loadShortcutsFromTOML(t, `
49[workspace.shortcuts.actions.new-tab]
50keys = ["ctrl+r"]
51`)
52
53	action, ok := lookupGlobal(set, uint(gdk.KEY_r), ModCtrl)
54	require.True(t, ok)
55	assert.Equal(t, ActionNewTab, action)
56
57	_, ok = lookupGlobal(set, uint(gdk.KEY_F5), ModNone)
58	assert.False(t, ok, "f5 belongs to the skipped reload default and must stay unbound")
59
60	action, ok = lookupGlobal(set, uint(gdk.KEY_l), ModCtrl)
61	require.True(t, ok, "unrelated defaults are still completed")
62	assert.Equal(t, ActionOpenOmnibox, action)
63}
64
65func TestConfigCompletion_DisabledBuiltinStaysDisabled(t *testing.T) {
66	set := loadShortcutsFromTOML(t, `
67[workspace.shortcuts.actions.open-find]
68keys = []
69`)
70
71	_, ok := lookupGlobal(set, uint(gdk.KEY_f), ModCtrl)
72	assert.False(t, ok)
73
74	action, ok := lookupGlobal(set, uint(gdk.KEY_l), ModCtrl)
75	require.True(t, ok)
76	assert.Equal(t, ActionOpenOmnibox, action)
77}
78
79func TestConfigCompletion_AliasSpellingBlocksDefaultAndInputAgrees(t *testing.T) {
80	set := loadShortcutsFromTOML(t, `
81[workspace.pane_mode.actions.close-pane]
82keys = ["right"]
83`)
84
85	action, ok := set.Lookup(KeyBinding{Keyval: uint(gdk.KEY_Right)}, ModePane)
86	require.True(t, ok)
87	assert.Equal(t, ActionClosePane, action, "the user binding wins deterministically over split-right's arrowright")
88
89	action, ok = set.Lookup(KeyBinding{Keyval: uint('r')}, ModePane)
90	assert.False(t, ok, "split-right is skipped entirely, including its other key %v", action)
91}
92
93func TestConfigCompletion_UnderscoreNamesWorkEndToEnd(t *testing.T) {
94	set := loadShortcutsFromTOML(t, `
95[workspace.pane_mode.actions.stack_pane]
96keys = ["ctrl+alt+f12"]
97
98[workspace.pane_mode.actions.focus_left]
99keys = ["ctrl+alt+f11"]
100
101[workspace.resize_mode.actions.resize_increase_left]
102keys = ["z"]
103
104[workspace.prefix_mode.actions.enter_resize_mode]
105keys = ["ctrl+alt+f10"]
106
107[workspace.tab_mode.actions.new_tab]
108keys = ["ctrl+alt+f9"]
109
110[workspace.shortcuts.actions.toggle_floating_pane]
111keys = ["alt+g"]
112
113[workspace.shortcuts.actions.open_find]
114keys = ["ctrl+shift+f"]
115`)
116
117	paneBinding := func(keyval uint) KeyBinding {
118		return KeyBinding{Keyval: keyval, Modifiers: ModCtrl | ModAlt}
119	}
120
121	tests := []struct {
122		name    string
123		binding KeyBinding
124		mode    Mode
125		want    Action
126	}{
127		{"pane stack_pane", paneBinding(uint(gdk.KEY_F12)), ModePane, ActionStackPane},
128		{"pane focus_left", paneBinding(uint(gdk.KEY_F11)), ModePane, ActionFocusLeft},
129		{"resize resize_increase_left", KeyBinding{Keyval: uint('z')}, ModeResize, ActionResizeIncreaseLeft},
130		{"prefix enter_resize_mode", paneBinding(uint(gdk.KEY_F10)), ModePrefix, ActionEnterResizeMode},
131		{"tab new_tab", paneBinding(uint(gdk.KEY_F9)), ModeTab, ActionNewTab},
132		{"global toggle_floating_pane", KeyBinding{Keyval: uint('g'), Modifiers: ModAlt}, ModeNormal, ActionToggleFloatingPane},
133		{"global open_find", KeyBinding{Keyval: uint('f'), Modifiers: ModCtrl | ModShift}, ModeNormal, ActionOpenFind},
134	}
135	for _, tt := range tests {
136		t.Run(tt.name, func(t *testing.T) {
137			got, ok := set.Lookup(tt.binding, tt.mode)
138			require.True(t, ok)
139			assert.Equal(t, tt.want, got)
140		})
141	}
142
143	// The underscore-named user bindings replaced the defaults they renamed.
144	_, ok := lookupGlobal(set, uint(gdk.KEY_f), ModCtrl)
145	assert.False(t, ok, "default ctrl+f is replaced by the user's open_find binding")
146	_, ok = set.Lookup(KeyBinding{Keyval: uint('s')}, ModePane)
147	assert.False(t, ok, "default stack-pane key is replaced by the user's stack_pane binding")
148	_, ok = set.Lookup(KeyBinding{Keyval: uint('h')}, ModeResize)
149	assert.False(t, ok, "default resize-increase-left key is replaced")
150}

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