v0.27.2 5960dd8

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

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