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}