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

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