detached 5d469a0

feat(webkit): add keybindings IPC handlers

commit
5d469a0a5f55eb7f79367c239c05c5c1160460b6
author
bnema · · 2026-01-11 14:21 UTC
committer
brice · · 2026-01-24 06:22 UTC
parent
e787fc1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +330 -0
A internal/infrastructure/webkit/handlers/keybindings.go +325 -0
@@ -0,0 +1,325 @@
1+package handlers
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "reflect"
8+ "sort"
9+
10+ "github.com/bnema/dumber/internal/application/port"
11+ "github.com/bnema/dumber/internal/infrastructure/config"
12+ "github.com/bnema/dumber/internal/infrastructure/webkit"
13+ "github.com/bnema/dumber/internal/logging"
14+)
15+
16+
17+// KeybindingsHandler handles keybinding-related messages.
18+type KeybindingsHandler struct{}
19+
20+// NewKeybindingsHandler creates a new KeybindingsHandler.
21+func NewKeybindingsHandler() *KeybindingsHandler {
22+ return &KeybindingsHandler{}
23+}
24+
25+// HandleGetKeybindings returns all keybindings grouped by mode.
26+func (*KeybindingsHandler) HandleGetKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
27+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
28+
29+ mgr := config.GetManager()
30+ if mgr == nil {
31+ return nil, fmt.Errorf("config manager not initialized")
32+ }
33+
34+ cfg := mgr.Get()
35+ defaults := config.DefaultConfig()
36+
37+ groups := []port.KeybindingGroup{
38+ buildGlobalGroup(cfg, defaults),
39+ buildPaneModeGroup(cfg, defaults),
40+ buildTabModeGroup(cfg, defaults),
41+ buildResizeModeGroup(cfg, defaults),
42+ buildSessionModeGroup(cfg, defaults),
43+ }
44+
45+ log.Debug().Int("groups", len(groups)).Msg("returning keybindings")
46+ return port.KeybindingsConfig{Groups: groups}, nil
47+}
48+
49+// HandleSetKeybinding updates a single keybinding.
50+func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
51+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
52+
53+ var req port.SetKeybindingRequest
54+ if err := json.Unmarshal(payload, &req); err != nil {
55+ return nil, fmt.Errorf("invalid request: %w", err)
56+ }
57+
58+ log.Info().Str("mode", req.Mode).Str("action", req.Action).Strs("keys", req.Keys).Msg("setting keybinding")
59+
60+ mgr := config.GetManager()
61+ if mgr == nil {
62+ return nil, fmt.Errorf("config manager not initialized")
63+ }
64+
65+ cfg := mgr.Get()
66+ updateKeybinding(cfg, req)
67+
68+ if err := mgr.SaveKeybindings(cfg); err != nil {
69+ return nil, fmt.Errorf("failed to save: %w", err)
70+ }
71+
72+ return map[string]any{"status": "success"}, nil
73+}
74+
75+// HandleResetKeybinding resets a keybinding to default.
76+func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
77+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
78+
79+ var req port.ResetKeybindingRequest
80+ if err := json.Unmarshal(payload, &req); err != nil {
81+ return nil, fmt.Errorf("invalid request: %w", err)
82+ }
83+
84+ log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("resetting keybinding to default")
85+
86+ defaults := config.DefaultConfig()
87+ defaultKeys := 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+ mgr := config.GetManager()
96+ if mgr == nil {
97+ return nil, fmt.Errorf("config manager not initialized")
98+ }
99+
100+ cfg := mgr.Get()
101+ updateKeybinding(cfg, setReq)
102+
103+ if err := mgr.SaveKeybindings(cfg); err != nil {
104+ return nil, fmt.Errorf("failed to save: %w", err)
105+ }
106+
107+ return map[string]any{"status": "success"}, nil
108+}
109+
110+// HandleResetAllKeybindings resets all keybindings to defaults.
111+func (*KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
112+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
113+ log.Info().Msg("resetting all keybindings to defaults")
114+
115+ mgr := config.GetManager()
116+ if mgr == nil {
117+ return nil, fmt.Errorf("config manager not initialized")
118+ }
119+
120+ cfg := mgr.Get()
121+ defaults := config.DefaultConfig()
122+
123+ // Reset all keybinding sections
124+ cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
125+ cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
126+ cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
127+ cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
128+ cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
129+
130+ if err := mgr.SaveKeybindings(cfg); err != nil {
131+ return nil, fmt.Errorf("failed to save: %w", err)
132+ }
133+
134+ return map[string]any{"status": "success"}, nil
135+}
136+
137+// buildGlobalGroup builds the global shortcuts group.
138+func buildGlobalGroup(cfg, defaults *config.Config) port.KeybindingGroup {
139+ return port.KeybindingGroup{
140+ Mode: "global",
141+ DisplayName: "Global Shortcuts",
142+ Bindings: buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
143+ }
144+}
145+
146+// buildPaneModeGroup builds the pane mode group.
147+func buildPaneModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
148+ return port.KeybindingGroup{
149+ Mode: "pane",
150+ DisplayName: "Pane Mode",
151+ Bindings: 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.
157+func buildTabModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
158+ return port.KeybindingGroup{
159+ Mode: "tab",
160+ DisplayName: "Tab Mode",
161+ Bindings: buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
162+ Activation: cfg.Workspace.TabMode.ActivationShortcut,
163+ }
164+}
165+
166+// buildResizeModeGroup builds the resize mode group.
167+func buildResizeModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
168+ return port.KeybindingGroup{
169+ Mode: "resize",
170+ DisplayName: "Resize Mode",
171+ Bindings: buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
172+ Activation: cfg.Workspace.ResizeMode.ActivationShortcut,
173+ }
174+}
175+
176+// buildSessionModeGroup builds the session mode group.
177+func buildSessionModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
178+ return port.KeybindingGroup{
179+ Mode: "session",
180+ DisplayName: "Session Mode",
181+ Bindings: buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
182+ Activation: cfg.Session.SessionMode.ActivationShortcut,
183+ }
184+}
185+
186+// buildModeBindings builds keybinding entries for a modal mode.
187+func buildModeBindings(actions, defaultActions map[string]config.ActionBinding) []port.KeybindingEntry {
188+ var entries []port.KeybindingEntry
189+
190+ // Get all action names and sort them
191+ actionNames := make([]string, 0, len(actions))
192+ for action := range actions {
193+ actionNames = append(actionNames, action)
194+ }
195+ sort.Strings(actionNames)
196+
197+ for _, action := range actionNames {
198+ binding := actions[action]
199+ defaultBinding := defaultActions[action]
200+
201+ entries = append(entries, port.KeybindingEntry{
202+ Action: action,
203+ Description: binding.Desc,
204+ Keys: binding.Keys,
205+ DefaultKeys: defaultBinding.Keys,
206+ IsCustom: !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
207+ })
208+ }
209+
210+ return entries
211+}
212+
213+// updateKeybinding updates a keybinding in the config.
214+func updateKeybinding(cfg *config.Config, req port.SetKeybindingRequest) {
215+ switch req.Mode {
216+ case "global":
217+ updateGlobalShortcut(cfg, req.Action, req.Keys)
218+ case "pane":
219+ if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
220+ existing.Keys = req.Keys
221+ cfg.Workspace.PaneMode.Actions[req.Action] = existing
222+ }
223+ case "tab":
224+ if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
225+ existing.Keys = req.Keys
226+ cfg.Workspace.TabMode.Actions[req.Action] = existing
227+ }
228+ case "resize":
229+ if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
230+ existing.Keys = req.Keys
231+ cfg.Workspace.ResizeMode.Actions[req.Action] = existing
232+ }
233+ case "session":
234+ if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
235+ existing.Keys = req.Keys
236+ cfg.Session.SessionMode.Actions[req.Action] = existing
237+ }
238+ }
239+}
240+
241+// updateGlobalShortcut updates a global shortcut in the config.
242+func updateGlobalShortcut(cfg *config.Config, action string, keys []string) {
243+ if existing, ok := cfg.Workspace.Shortcuts.Actions[action]; ok {
244+ existing.Keys = keys
245+ cfg.Workspace.Shortcuts.Actions[action] = existing
246+ }
247+}
248+
249+// getDefaultKeys returns the default keys for an action.
250+func getDefaultKeys(defaults *config.Config, mode, action string) []string {
251+ switch mode {
252+ case "global":
253+ return getDefaultGlobalKeys(defaults, action)
254+ case "pane":
255+ return defaults.Workspace.PaneMode.Actions[action].Keys
256+ case "tab":
257+ return defaults.Workspace.TabMode.Actions[action].Keys
258+ case "resize":
259+ return defaults.Workspace.ResizeMode.Actions[action].Keys
260+ case "session":
261+ return defaults.Session.SessionMode.Actions[action].Keys
262+ }
263+ return nil
264+}
265+
266+// getDefaultGlobalKeys returns the default key for a global shortcut.
267+func getDefaultGlobalKeys(defaults *config.Config, action string) []string {
268+ if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
269+ return binding.Keys
270+ }
271+ return nil
272+}
273+
274+// RegisterKeybindingsHandlers registers keybindings handlers with the router.
275+func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRouter) error {
276+ handler := NewKeybindingsHandler()
277+ log := logging.FromContext(ctx).With().Str("component", "handlers").Logger()
278+
279+ // Get all keybindings
280+ if err := router.RegisterHandlerWithCallbacks(
281+ "get_keybindings",
282+ "__dumber_keybindings_loaded",
283+ "__dumber_keybindings_error",
284+ "",
285+ webkit.MessageHandlerFunc(handler.HandleGetKeybindings),
286+ ); err != nil {
287+ return err
288+ }
289+
290+ // Set a single keybinding
291+ if err := router.RegisterHandlerWithCallbacks(
292+ "set_keybinding",
293+ "__dumber_keybinding_set",
294+ "__dumber_keybinding_error",
295+ "",
296+ webkit.MessageHandlerFunc(handler.HandleSetKeybinding),
297+ ); err != nil {
298+ return err
299+ }
300+
301+ // Reset a single keybinding
302+ if err := router.RegisterHandlerWithCallbacks(
303+ "reset_keybinding",
304+ "__dumber_keybinding_reset",
305+ "__dumber_keybinding_error",
306+ "",
307+ webkit.MessageHandlerFunc(handler.HandleResetKeybinding),
308+ ); err != nil {
309+ return err
310+ }
311+
312+ // Reset all keybindings
313+ if err := router.RegisterHandlerWithCallbacks(
314+ "reset_all_keybindings",
315+ "__dumber_keybindings_reset_all",
316+ "__dumber_keybindings_error",
317+ "",
318+ webkit.MessageHandlerFunc(handler.HandleResetAllKeybindings),
319+ ); err != nil {
320+ return err
321+ }
322+
323+ log.Info().Msg("registered keybindings handlers")
324+ return nil
325+}
@@ -0,0 +1,325 @@
1+package handlers
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "reflect"
8+ "sort"
9+
10+ "github.com/bnema/dumber/internal/application/port"
11+ "github.com/bnema/dumber/internal/infrastructure/config"
12+ "github.com/bnema/dumber/internal/infrastructure/webkit"
13+ "github.com/bnema/dumber/internal/logging"
14+)
15+
16+
17+// KeybindingsHandler handles keybinding-related messages.
18+type KeybindingsHandler struct{}
19+
20+// NewKeybindingsHandler creates a new KeybindingsHandler.
21+func NewKeybindingsHandler() *KeybindingsHandler {
22+ return &KeybindingsHandler{}
23+}
24+
25+// HandleGetKeybindings returns all keybindings grouped by mode.
26+func (*KeybindingsHandler) HandleGetKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
27+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
28+
29+ mgr := config.GetManager()
30+ if mgr == nil {
31+ return nil, fmt.Errorf("config manager not initialized")
32+ }
33+
34+ cfg := mgr.Get()
35+ defaults := config.DefaultConfig()
36+
37+ groups := []port.KeybindingGroup{
38+ buildGlobalGroup(cfg, defaults),
39+ buildPaneModeGroup(cfg, defaults),
40+ buildTabModeGroup(cfg, defaults),
41+ buildResizeModeGroup(cfg, defaults),
42+ buildSessionModeGroup(cfg, defaults),
43+ }
44+
45+ log.Debug().Int("groups", len(groups)).Msg("returning keybindings")
46+ return port.KeybindingsConfig{Groups: groups}, nil
47+}
48+
49+// HandleSetKeybinding updates a single keybinding.
50+func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
51+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
52+
53+ var req port.SetKeybindingRequest
54+ if err := json.Unmarshal(payload, &req); err != nil {
55+ return nil, fmt.Errorf("invalid request: %w", err)
56+ }
57+
58+ log.Info().Str("mode", req.Mode).Str("action", req.Action).Strs("keys", req.Keys).Msg("setting keybinding")
59+
60+ mgr := config.GetManager()
61+ if mgr == nil {
62+ return nil, fmt.Errorf("config manager not initialized")
63+ }
64+
65+ cfg := mgr.Get()
66+ updateKeybinding(cfg, req)
67+
68+ if err := mgr.SaveKeybindings(cfg); err != nil {
69+ return nil, fmt.Errorf("failed to save: %w", err)
70+ }
71+
72+ return map[string]any{"status": "success"}, nil
73+}
74+
75+// HandleResetKeybinding resets a keybinding to default.
76+func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
77+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
78+
79+ var req port.ResetKeybindingRequest
80+ if err := json.Unmarshal(payload, &req); err != nil {
81+ return nil, fmt.Errorf("invalid request: %w", err)
82+ }
83+
84+ log.Info().Str("mode", req.Mode).Str("action", req.Action).Msg("resetting keybinding to default")
85+
86+ defaults := config.DefaultConfig()
87+ defaultKeys := 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+ mgr := config.GetManager()
96+ if mgr == nil {
97+ return nil, fmt.Errorf("config manager not initialized")
98+ }
99+
100+ cfg := mgr.Get()
101+ updateKeybinding(cfg, setReq)
102+
103+ if err := mgr.SaveKeybindings(cfg); err != nil {
104+ return nil, fmt.Errorf("failed to save: %w", err)
105+ }
106+
107+ return map[string]any{"status": "success"}, nil
108+}
109+
110+// HandleResetAllKeybindings resets all keybindings to defaults.
111+func (*KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webkit.WebViewID, _ json.RawMessage) (any, error) {
112+ log := logging.FromContext(ctx).With().Str("handler", "keybindings").Logger()
113+ log.Info().Msg("resetting all keybindings to defaults")
114+
115+ mgr := config.GetManager()
116+ if mgr == nil {
117+ return nil, fmt.Errorf("config manager not initialized")
118+ }
119+
120+ cfg := mgr.Get()
121+ defaults := config.DefaultConfig()
122+
123+ // Reset all keybinding sections
124+ cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
125+ cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
126+ cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
127+ cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
128+ cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
129+
130+ if err := mgr.SaveKeybindings(cfg); err != nil {
131+ return nil, fmt.Errorf("failed to save: %w", err)
132+ }
133+
134+ return map[string]any{"status": "success"}, nil
135+}
136+
137+// buildGlobalGroup builds the global shortcuts group.
138+func buildGlobalGroup(cfg, defaults *config.Config) port.KeybindingGroup {
139+ return port.KeybindingGroup{
140+ Mode: "global",
141+ DisplayName: "Global Shortcuts",
142+ Bindings: buildModeBindings(cfg.Workspace.Shortcuts.Actions, defaults.Workspace.Shortcuts.Actions),
143+ }
144+}
145+
146+// buildPaneModeGroup builds the pane mode group.
147+func buildPaneModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
148+ return port.KeybindingGroup{
149+ Mode: "pane",
150+ DisplayName: "Pane Mode",
151+ Bindings: 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.
157+func buildTabModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
158+ return port.KeybindingGroup{
159+ Mode: "tab",
160+ DisplayName: "Tab Mode",
161+ Bindings: buildModeBindings(cfg.Workspace.TabMode.Actions, defaults.Workspace.TabMode.Actions),
162+ Activation: cfg.Workspace.TabMode.ActivationShortcut,
163+ }
164+}
165+
166+// buildResizeModeGroup builds the resize mode group.
167+func buildResizeModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
168+ return port.KeybindingGroup{
169+ Mode: "resize",
170+ DisplayName: "Resize Mode",
171+ Bindings: buildModeBindings(cfg.Workspace.ResizeMode.Actions, defaults.Workspace.ResizeMode.Actions),
172+ Activation: cfg.Workspace.ResizeMode.ActivationShortcut,
173+ }
174+}
175+
176+// buildSessionModeGroup builds the session mode group.
177+func buildSessionModeGroup(cfg, defaults *config.Config) port.KeybindingGroup {
178+ return port.KeybindingGroup{
179+ Mode: "session",
180+ DisplayName: "Session Mode",
181+ Bindings: buildModeBindings(cfg.Session.SessionMode.Actions, defaults.Session.SessionMode.Actions),
182+ Activation: cfg.Session.SessionMode.ActivationShortcut,
183+ }
184+}
185+
186+// buildModeBindings builds keybinding entries for a modal mode.
187+func buildModeBindings(actions, defaultActions map[string]config.ActionBinding) []port.KeybindingEntry {
188+ var entries []port.KeybindingEntry
189+
190+ // Get all action names and sort them
191+ actionNames := make([]string, 0, len(actions))
192+ for action := range actions {
193+ actionNames = append(actionNames, action)
194+ }
195+ sort.Strings(actionNames)
196+
197+ for _, action := range actionNames {
198+ binding := actions[action]
199+ defaultBinding := defaultActions[action]
200+
201+ entries = append(entries, port.KeybindingEntry{
202+ Action: action,
203+ Description: binding.Desc,
204+ Keys: binding.Keys,
205+ DefaultKeys: defaultBinding.Keys,
206+ IsCustom: !reflect.DeepEqual(binding.Keys, defaultBinding.Keys),
207+ })
208+ }
209+
210+ return entries
211+}
212+
213+// updateKeybinding updates a keybinding in the config.
214+func updateKeybinding(cfg *config.Config, req port.SetKeybindingRequest) {
215+ switch req.Mode {
216+ case "global":
217+ updateGlobalShortcut(cfg, req.Action, req.Keys)
218+ case "pane":
219+ if existing, ok := cfg.Workspace.PaneMode.Actions[req.Action]; ok {
220+ existing.Keys = req.Keys
221+ cfg.Workspace.PaneMode.Actions[req.Action] = existing
222+ }
223+ case "tab":
224+ if existing, ok := cfg.Workspace.TabMode.Actions[req.Action]; ok {
225+ existing.Keys = req.Keys
226+ cfg.Workspace.TabMode.Actions[req.Action] = existing
227+ }
228+ case "resize":
229+ if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
230+ existing.Keys = req.Keys
231+ cfg.Workspace.ResizeMode.Actions[req.Action] = existing
232+ }
233+ case "session":
234+ if existing, ok := cfg.Session.SessionMode.Actions[req.Action]; ok {
235+ existing.Keys = req.Keys
236+ cfg.Session.SessionMode.Actions[req.Action] = existing
237+ }
238+ }
239+}
240+
241+// updateGlobalShortcut updates a global shortcut in the config.
242+func updateGlobalShortcut(cfg *config.Config, action string, keys []string) {
243+ if existing, ok := cfg.Workspace.Shortcuts.Actions[action]; ok {
244+ existing.Keys = keys
245+ cfg.Workspace.Shortcuts.Actions[action] = existing
246+ }
247+}
248+
249+// getDefaultKeys returns the default keys for an action.
250+func getDefaultKeys(defaults *config.Config, mode, action string) []string {
251+ switch mode {
252+ case "global":
253+ return getDefaultGlobalKeys(defaults, action)
254+ case "pane":
255+ return defaults.Workspace.PaneMode.Actions[action].Keys
256+ case "tab":
257+ return defaults.Workspace.TabMode.Actions[action].Keys
258+ case "resize":
259+ return defaults.Workspace.ResizeMode.Actions[action].Keys
260+ case "session":
261+ return defaults.Session.SessionMode.Actions[action].Keys
262+ }
263+ return nil
264+}
265+
266+// getDefaultGlobalKeys returns the default key for a global shortcut.
267+func getDefaultGlobalKeys(defaults *config.Config, action string) []string {
268+ if binding, ok := defaults.Workspace.Shortcuts.Actions[action]; ok {
269+ return binding.Keys
270+ }
271+ return nil
272+}
273+
274+// RegisterKeybindingsHandlers registers keybindings handlers with the router.
275+func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRouter) error {
276+ handler := NewKeybindingsHandler()
277+ log := logging.FromContext(ctx).With().Str("component", "handlers").Logger()
278+
279+ // Get all keybindings
280+ if err := router.RegisterHandlerWithCallbacks(
281+ "get_keybindings",
282+ "__dumber_keybindings_loaded",
283+ "__dumber_keybindings_error",
284+ "",
285+ webkit.MessageHandlerFunc(handler.HandleGetKeybindings),
286+ ); err != nil {
287+ return err
288+ }
289+
290+ // Set a single keybinding
291+ if err := router.RegisterHandlerWithCallbacks(
292+ "set_keybinding",
293+ "__dumber_keybinding_set",
294+ "__dumber_keybinding_error",
295+ "",
296+ webkit.MessageHandlerFunc(handler.HandleSetKeybinding),
297+ ); err != nil {
298+ return err
299+ }
300+
301+ // Reset a single keybinding
302+ if err := router.RegisterHandlerWithCallbacks(
303+ "reset_keybinding",
304+ "__dumber_keybinding_reset",
305+ "__dumber_keybinding_error",
306+ "",
307+ webkit.MessageHandlerFunc(handler.HandleResetKeybinding),
308+ ); err != nil {
309+ return err
310+ }
311+
312+ // Reset all keybindings
313+ if err := router.RegisterHandlerWithCallbacks(
314+ "reset_all_keybindings",
315+ "__dumber_keybindings_reset_all",
316+ "__dumber_keybindings_error",
317+ "",
318+ webkit.MessageHandlerFunc(handler.HandleResetAllKeybindings),
319+ ); err != nil {
320+ return err
321+ }
322+
323+ log.Info().Msg("registered keybindings handlers")
324+ return nil
325+}
M internal/infrastructure/webkit/handlers/register.go +5 -0
@@ -36,6 +36,11 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
3636 return err
3737 }
3838
39+ // Keybindings handlers (always available)
40+ if err := RegisterKeybindingsHandlers(ctx, router); err != nil {
41+ return err
42+ }
43+
3944 // Clipboard handlers (for auto-copy on selection feature)
4045 if cfg.Clipboard != nil && cfg.ConfigGetter != nil {
4146 if err := RegisterClipboardHandlers(ctx, router, cfg.Clipboard, cfg.ConfigGetter, cfg.OnClipboardCopied); err != nil {
@@ -36,6 +36,11 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
36 return err36 return err
37 }37 }
38 38
39+ // Keybindings handlers (always available)
40+ if err := RegisterKeybindingsHandlers(ctx, router); err != nil {
41+ return err
42+ }
43+
39 // Clipboard handlers (for auto-copy on selection feature)44 // Clipboard handlers (for auto-copy on selection feature)
40 if cfg.Clipboard != nil && cfg.ConfigGetter != nil {45 if cfg.Clipboard != nil && cfg.ConfigGetter != nil {
41 if err := RegisterClipboardHandlers(ctx, router, cfg.Clipboard, cfg.ConfigGetter, cfg.OnClipboardCopied); err != nil {46 if err := RegisterClipboardHandlers(ctx, router, cfg.Clipboard, cfg.ConfigGetter, cfg.OnClipboardCopied); err != nil {

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