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}