keybindings_gateway.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"reflect"
6	"sort"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/logging"
10)
11
12const modeGlobal = "global"
13
14// KeybindingsGateway implements port.KeybindingsProvider and port.KeybindingsSaver.
15type KeybindingsGateway struct {
16	mgr *Manager
17}
18
19// NewKeybindingsGateway creates a new KeybindingsGateway.
20func NewKeybindingsGateway(mgr *Manager) *KeybindingsGateway {
21	return &KeybindingsGateway{mgr: mgr}
22}
23
24// GetKeybindings returns all keybindings grouped by mode.
25func (g *KeybindingsGateway) GetKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
26	log := logging.FromContext(ctx)
27	cfg := g.mgr.Get()
28	defaults := DefaultConfig()
29	log.Debug().Msg("keybindings gateway: fetching keybindings")
30
31	groups := []port.KeybindingGroup{
32		g.buildGlobalGroup(cfg, defaults),
33		g.buildPaneModeGroup(cfg, defaults),
34		g.buildTabModeGroup(cfg, defaults),
35		g.buildVimModeGroup(cfg, defaults),
36		g.buildResizeModeGroup(cfg, defaults),
37		g.buildSessionModeGroup(cfg, defaults),
38	}
39
40	return port.KeybindingsConfig{Groups: groups}, nil
41}
42
43// GetDefaultKeybindings returns the default keybindings configuration.
44func (g *KeybindingsGateway) GetDefaultKeybindings(ctx context.Context) (port.KeybindingsConfig, error) {
45	log := logging.FromContext(ctx)
46	defaults := DefaultConfig()
47
48	groups := []port.KeybindingGroup{
49		g.buildGlobalGroup(defaults, defaults),
50		g.buildPaneModeGroup(defaults, defaults),
51		g.buildTabModeGroup(defaults, defaults),
52		g.buildVimModeGroup(defaults, defaults),
53		g.buildResizeModeGroup(defaults, defaults),
54		g.buildSessionModeGroup(defaults, defaults),
55	}
56
57	log.Debug().Int("groups", len(groups)).Msg("keybindings gateway: returning default keybindings")
58	return port.KeybindingsConfig{Groups: groups}, nil
59}
60
61// CheckConflicts scans all modes for keybinding conflicts.
62func (g *KeybindingsGateway) CheckConflicts(ctx context.Context, mode, action string, keys []string) ([]port.KeybindingConflict, error) {
63	log := logging.FromContext(ctx)
64	cfg := g.mgr.Get()
65
66	conflicts := g.checkConflicts(cfg, mode, action, keys)
67	if len(conflicts) > 0 {
68		log.Warn().Int("conflicts", len(conflicts)).Msg("keybindings gateway: conflicts detected")
69	}
70
71	return conflicts, nil
72}
73
74// SetKeybinding updates a single keybinding.
75func (g *KeybindingsGateway) SetKeybinding(ctx context.Context, req port.SetKeybindingRequest) error {
76	log := logging.FromContext(ctx)
77	log.Debug().Str("mode", req.Mode).Str("action", req.Action).Msg("keybindings gateway: setting keybinding")
78	cfg := g.mgr.Get()
79	g.updateKeybinding(cfg, req)
80	return g.mgr.Save(cfg)
81}
82
83// ResetKeybinding resets a keybinding to its default value.
84func (g *KeybindingsGateway) ResetKeybinding(ctx context.Context, req port.ResetKeybindingRequest) error {
85	log := logging.FromContext(ctx)
86	log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("keybindings gateway: resetting keybinding to default")
87
88	defaults := DefaultConfig()
89	defaultKeys := g.getDefaultKeys(defaults, req.Mode, req.Action)
90
91	setReq := port.SetKeybindingRequest{
92		Mode:   req.Mode,
93		Action: req.Action,
94		Keys:   defaultKeys,
95	}
96
97	cfg := g.mgr.Get()
98	g.updateKeybinding(cfg, setReq)
99
100	return g.mgr.Save(cfg)
101}
102
103// ResetAllKeybindings resets all keybindings to their default values.
104func (g *KeybindingsGateway) ResetAllKeybindings(ctx context.Context) error {
105	log := logging.FromContext(ctx)
106	log.Info().Msg("keybindings gateway: resetting all keybindings to defaults")
107
108	cfg := g.mgr.Get()
109	defaults := DefaultConfig()
110
111	cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
112	cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
113	cfg.Workspace.VimMode.Actions = defaults.Workspace.VimMode.Actions
114	cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
115	cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
116	cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
117
118	return g.mgr.Save(cfg)
119}
120
121// buildGlobalGroup builds the global shortcuts group.
122func (g *KeybindingsGateway) buildGlobalGroup(cfg, defaults *Config) port.KeybindingGroup {
123	return port.KeybindingGroup{
124		Mode:        modeGlobal,
125		DisplayName: "Global Shortcuts",
126		Bindings:    g.buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
127	}
128}
129
130// buildPaneModeGroup builds the pane mode group.
131func (g *KeybindingsGateway) buildPaneModeGroup(cfg, defaults *Config) port.KeybindingGroup {
132	return port.KeybindingGroup{
133		Mode:        "pane",
134		DisplayName: "Pane Mode",
135		Bindings:    g.buildModeBindings(cfg.Workspace.PaneMode.Actions, defaults.Workspace.PaneMode.Actions),
136		Activation:  cfg.Workspace.PaneMode.ActivationShortcut,
137	}
138}
139
140// buildTabModeGroup builds the tab mode group.
141func (g *KeybindingsGateway) buildTabModeGroup(cfg, defaults *Config) port.KeybindingGroup {
142	return port.KeybindingGroup{
143		Mode:        "tab",
144		DisplayName: "Tab Mode",
145		Bindings:    g.buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
146		Activation:  cfg.Workspace.TabMode.ActivationShortcut,
147	}
148}
149
150// buildVimModeGroup builds the vim mode group.
151func (g *KeybindingsGateway) buildVimModeGroup(cfg, defaults *Config) port.KeybindingGroup {
152	return port.KeybindingGroup{
153		Mode:        "vim",
154		DisplayName: "Vim Mode",
155		Bindings:    g.buildModeBindings(cfg.Workspace.VimMode.Actions, defaults.Workspace.VimMode.Actions),
156		Activation:  cfg.Workspace.VimMode.ActivationShortcut,
157	}
158}
159
160// buildResizeModeGroup builds the resize mode group.
161func (g *KeybindingsGateway) buildResizeModeGroup(cfg, defaults *Config) port.KeybindingGroup {
162	return port.KeybindingGroup{
163		Mode:        "resize",
164		DisplayName: "Resize Mode",
165		Bindings:    g.buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
166		Activation:  cfg.Workspace.ResizeMode.ActivationShortcut,
167	}
168}
169
170// buildSessionModeGroup builds the session mode group.
171func (g *KeybindingsGateway) buildSessionModeGroup(cfg, defaults *Config) port.KeybindingGroup {
172	return port.KeybindingGroup{
173		Mode:        "session",
174		DisplayName: "Session Mode",
175		Bindings:    g.buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
176		Activation:  cfg.Session.SessionMode.ActivationShortcut,
177	}
178}
179
180// buildModeBindings builds keybinding entries for a modal mode.
181func (*KeybindingsGateway) buildModeBindings(actions, defaultActions map[string]ActionBinding) []port.KeybindingEntry {
182	var entries []port.KeybindingEntry
183
184	actionNames := make([]string, 0, len(actions))
185	for action := range actions {
186		actionNames = append(actionNames, action)
187	}
188	sort.Strings(actionNames)
189
190	for _, action := range actionNames {
191		binding := actions[action]
192		defaultBinding := defaultActions[action]
193
194		entries = append(entries, port.KeybindingEntry{
195			Action:      action,
196			Description: binding.Desc,
197			Keys:        binding.Keys,
198			DefaultKeys: defaultBinding.Keys,
199			IsCustom:    !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
200		})
201	}
202
203	return entries
204}
205
206// updateKeybinding updates a keybinding in the config.
207func (*KeybindingsGateway) updateKeybinding(cfg *Config, req port.SetKeybindingRequest) {
208	switch req.Mode {
209	case modeGlobal:
210		if existing, ok := cfg.Workspace.Shortcuts.Actions[req.Action]; ok {
211			existing.Keys = req.Keys
212			cfg.Workspace.Shortcuts.Actions[req.Action] = existing
213		}
214	case "pane":
215		if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
216			existing.Keys = req.Keys
217			cfg.Workspace.PaneMode.Actions[req.Action] = existing
218		}
219	case "tab":
220		if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
221			existing.Keys = req.Keys
222			cfg.Workspace.TabMode.Actions[req.Action] = existing
223		}
224	case "vim":
225		if existing, ok := cfg.Workspace.VimMode.Actions[req.Action]; ok {
226			existing.Keys = req.Keys
227			cfg.Workspace.VimMode.Actions[req.Action] = existing
228		}
229	case "resize":
230		if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
231			existing.Keys = req.Keys
232			cfg.Workspace.ResizeMode.Actions[req.Action] = existing
233		}
234	case "session":
235		if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
236			existing.Keys = req.Keys
237			cfg.Session.SessionMode.Actions[req.Action] = existing
238		}
239	}
240}
241
242// getDefaultKeys returns the default keys for an action.
243func (*KeybindingsGateway) getDefaultKeys(defaults *Config, mode, action string) []string {
244	switch mode {
245	case modeGlobal:
246		if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
247			return binding.Keys
248		}
249	case "pane":
250		if binding, ok := defaults.Workspace.PaneMode.Actions[action]; ok {
251			return binding.Keys
252		}
253	case "tab":
254		if binding, ok := defaults.Workspace.TabMode.Actions[action]; ok {
255			return binding.Keys
256		}
257	case "vim":
258		if binding, ok := defaults.Workspace.VimMode.Actions[action]; ok {
259			return binding.Keys
260		}
261	case "resize":
262		if binding, ok := defaults.Workspace.ResizeMode.Actions[action]; ok {
263			return binding.Keys
264		}
265	case "session":
266		if binding, ok := defaults.Session.SessionMode.Actions[action]; ok {
267			return binding.Keys
268		}
269	}
270	return nil
271}
272
273// checkConflicts scans all modes for keybinding conflicts.
274func (*KeybindingsGateway) checkConflicts(cfg *Config, targetMode, targetAction string, newKeys []string) []port.KeybindingConflict {
275	var conflicts []port.KeybindingConflict
276
277	type binding struct {
278		mode   string
279		action string
280	}
281	keyMap := make(map[string][]binding)
282
283	addBindings := func(mode string, actions map[string]ActionBinding) {
284		for action, ab := range actions {
285			for _, key := range ab.Keys {
286				keyMap[key] = append(keyMap[key], binding{mode: mode, action: action})
287			}
288		}
289	}
290
291	addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
292	addBindings("pane", cfg.Workspace.PaneMode.Actions)
293	addBindings("tab", cfg.Workspace.TabMode.Actions)
294	addBindings("vim", cfg.Workspace.VimMode.Actions)
295	addBindings("resize", cfg.Workspace.ResizeMode.Actions)
296	addBindings("session", cfg.Session.SessionMode.Actions)
297
298	for _, newKey := range newKeys {
299		if existing, ok := keyMap[newKey]; ok {
300			for _, b := range existing {
301				if b.mode == targetMode && b.action == targetAction {
302					continue
303				}
304				if targetMode == modeGlobal || b.mode == modeGlobal || b.mode == targetMode {
305					conflicts = append(conflicts, port.KeybindingConflict{
306						ConflictingAction: b.action,
307						ConflictingMode:   b.mode,
308						Key:               newKey,
309					})
310				}
311			}
312		}
313	}
314
315	return conflicts
316}

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