shortcuts.go RawHistoryFolder
1package browser
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7
8 "github.com/bnema/dumber/internal/app/control"
9 "github.com/bnema/dumber/internal/config"
10 "github.com/bnema/dumber/pkg/webkit"
11)
12
13// ShortcutHandler manages keyboard shortcuts for the browser
14type ShortcutHandler struct {
15 webView *webkit.WebView
16 clipboardController *control.ClipboardController
17 config *config.Config
18 app *BrowserApp
19}
20
21// NewShortcutHandler creates a new shortcut handler
22func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.ClipboardController, cfg *config.Config, app *BrowserApp) *ShortcutHandler {
23 return &ShortcutHandler{
24 webView: webView,
25 clipboardController: clipboardController,
26 config: cfg,
27 app: app,
28 }
29}
30
31func (s *ShortcutHandler) isActivePane() bool {
32 if s == nil || s.webView == nil {
33 return false
34 }
35 if s.app == nil || s.app.workspace == nil {
36 return true // No workspace context; allow
37 }
38
39 // Find node for this WebView
40 node := s.app.workspace.viewToNode[s.webView]
41 if node == nil {
42 return false
43 }
44
45 // For related popups, they're active if parent chain contains the active node
46 if node.isRelated && node.parentPane != nil {
47 activeNode := s.app.workspace.GetActiveNode()
48 cur := node
49 for cur != nil {
50 if cur == activeNode {
51 return true
52 }
53 cur = cur.parentPane
54 }
55 return false
56 }
57
58 // Independent panes: equal to active node
59 return node == s.app.workspace.GetActiveNode()
60}
61
62// RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards
63func (s *ShortcutHandler) RegisterShortcuts() {
64 s.exposeWorkspaceConfig()
65
66 // NOTE: Global shortcuts (Ctrl+L, Ctrl+F, Ctrl+Shift+C, F12) are now handled
67 // by WindowShortcutHandler at the window level to prevent duplicates
68
69 // Page refresh shortcuts
70 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+r", func() {
71 if !s.isActivePane() {
72 return
73 }
74 log.Printf("Shortcut: Reload page")
75 _ = s.webView.Reload()
76 })
77
78 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() {
79 if !s.isActivePane() {
80 return
81 }
82 log.Printf("Shortcut: Hard reload page")
83 _ = s.webView.ReloadBypassCache()
84 })
85
86 _ = s.webView.RegisterKeyboardShortcut("F5", func() {
87 if !s.isActivePane() {
88 return
89 }
90 log.Printf("Shortcut: F5 reload")
91 _ = s.webView.Reload()
92 })
93
94 // Note: Workspace pane navigation shortcuts (Alt + Arrow keys) are handled by WorkspaceManager
95 // and zoom shortcuts (Ctrl +/-/0) are handled by WindowShortcutHandler to ensure they work properly across all panes
96}
97
98func (s *ShortcutHandler) dispatchUIShortcut(action string, extra map[string]any) error {
99 if s == nil || s.webView == nil {
100 return fmt.Errorf("webview unavailable for action %s", action)
101 }
102
103 detail := map[string]any{
104 "action": action,
105 }
106 for k, v := range extra {
107 detail[k] = v
108 }
109
110 return s.webView.DispatchCustomEvent("dumber:ui:shortcut", detail)
111}
112
113func (s *ShortcutHandler) exposeWorkspaceConfig() {
114 if s.webView == nil || s.config == nil {
115 return
116 }
117
118 payload, err := json.Marshal(s.config.Workspace)
119 if err != nil {
120 log.Printf("Shortcut: failed to marshal workspace config: %v", err)
121 return
122 }
123
124 script := fmt.Sprintf(`(function(){
125 const cfg = %s;
126 window.__dumber_workspace_config = cfg;
127 document.dispatchEvent(new CustomEvent('dumber:workspace-config',{detail:cfg}));
128})();`, string(payload))
129
130 if err := s.webView.InjectScript(script); err != nil {
131 log.Printf("Shortcut: failed to expose workspace config: %v", err)
132 }
133}
134
135// Detach releases the WebView reference to prevent post-destruction usage.
136func (s *ShortcutHandler) Detach() {
137 if s == nil {
138 return
139 }
140 s.webView = nil
141}