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}