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}