1package messaging
2
3import (
4 "context"
5 "encoding/json"
6 "log"
7 "strconv"
8 "time"
9
10 "github.com/bnema/dumber/internal/app/constants"
11 "github.com/bnema/dumber/internal/app/control"
12 "github.com/bnema/dumber/internal/config"
13 "github.com/bnema/dumber/internal/logging"
14 "github.com/bnema/dumber/internal/services"
15 "github.com/bnema/dumber/pkg/webkit"
16)
17
18// Handler processes script messages from the WebView
19type WorkspaceObserver interface {
20 OnWorkspaceMessage(source *webkit.WebView, msg Message)
21}
22
23type Handler struct {
24 parserService *services.ParserService
25 browserService *services.BrowserService
26 webView *webkit.WebView
27 navigationController *control.NavigationController
28 lastTheme string
29 workspaceObserver WorkspaceObserver
30}
31
32// Message represents a script message from the WebView
33type Message struct {
34 Type string `json:"type"`
35 URL string `json:"url"`
36 Q string `json:"q"`
37 Limit int `json:"limit"`
38 Offset int `json:"offset"`
39 Value string `json:"value"`
40 Event string `json:"event"`
41 Action string `json:"action"`
42 Direction string `json:"direction"`
43 // History operations
44 HistoryID string `json:"historyId"`
45 // Request tracking
46 RequestID string `json:"requestId"`
47 // Popup close tracking
48 WebViewID string `json:"webviewId"`
49 Reason string `json:"reason"`
50 // Wails fetch bridge
51 ID string `json:"id"`
52 Payload json.RawMessage `json:"payload"`
53}
54
55// NewHandler creates a new message handler
56func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {
57 return &Handler{
58 parserService: parserService,
59 browserService: browserService,
60 }
61}
62
63// SetNavigationController injects the navigation controller for unified navigation flow.
64func (h *Handler) SetNavigationController(controller *control.NavigationController) {
65 h.navigationController = controller
66}
67
68// SetWorkspaceObserver registers a workspace event observer.
69func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
70 h.workspaceObserver = observer
71}
72
73// Handle processes incoming script messages
74func (h *Handler) Handle(payload string) {
75 var msg Message
76 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
77 log.Printf("[ERROR] Failed to unmarshal message: %v", err)
78 return
79 }
80
81 switch msg.Type {
82 case "navigate":
83 h.handleNavigation(msg)
84 case "query":
85 log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit)
86 if h.webView == nil {
87 log.Printf("[WARN] Dropping query response: webview unavailable for suggestions")
88 return
89 }
90 h.handleQuery(msg)
91 case "wails":
92 h.handleWailsBridge(msg)
93 case "theme":
94 h.handleTheme(msg)
95 case "history_recent":
96 h.handleHistoryRecent(msg)
97 case "history_stats":
98 h.handleHistoryStats(msg)
99 case "history_search":
100 h.handleHistorySearch(msg)
101 case "history_delete":
102 h.handleHistoryDelete(msg)
103 case "workspace":
104 h.handleWorkspace(msg)
105 case "close-popup":
106 h.handleClosePopup(msg)
107 case "console-message":
108 h.handleConsoleMessage(msg)
109 case "request-webview-id":
110 h.handleWebViewIDRequest(msg)
111 case "get_search_shortcuts":
112 h.handleGetSearchShortcuts(msg)
113 case "get_color_palettes":
114 h.handleGetColorPalettes(msg)
115 }
116}
117
118// SetWebView sets the WebView reference (needed for script injection)
119func (h *Handler) SetWebView(webView *webkit.WebView) {
120 h.webView = webView
121}
122
123// handleNavigation processes navigation requests from the frontend
124func (h *Handler) handleNavigation(msg Message) {
125 if h.navigationController != nil {
126 if err := h.navigationController.NavigateToURL(msg.URL); err != nil {
127 log.Printf("[messaging] Navigation controller failed for input %q: %v", msg.URL, err)
128 h.legacyNavigate(msg)
129 }
130 return
131 }
132
133 h.legacyNavigate(msg)
134}
135
136func (h *Handler) legacyNavigate(msg Message) {
137 ctx := context.Background()
138 res, err := h.parserService.ParseInput(ctx, msg.URL)
139 if err != nil {
140 log.Printf("[messaging] Legacy navigation parse failed for %q: %v", msg.URL, err)
141 return
142 }
143
144 if _, navErr := h.browserService.Navigate(ctx, res.URL); navErr != nil {
145 log.Printf("Warning: failed to navigate to %s: %v", res.URL, navErr)
146 }
147
148 if h.webView == nil {
149 return
150 }
151
152 if err := h.webView.LoadURL(res.URL); err != nil {
153 log.Printf("[messaging] Legacy LoadURL failed for %s: %v", res.URL, err)
154 }
155
156 if z, zerr := h.browserService.GetZoomLevel(ctx, res.URL); zerr == nil {
157 if err := h.webView.SetZoom(z); err != nil {
158 log.Printf("[messaging] Legacy SetZoom failed for %s: %v", res.URL, err)
159 }
160 }
161}
162
163// (legacy query handler removed; omnibox suggestions now fetched via dumb://api/omnibox/suggestions)
164// handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch
165func (h *Handler) handleWorkspace(msg Message) {
166 if h.workspaceObserver == nil {
167 log.Printf("[workspace] Received workspace event %q but no observer registered", msg.Event)
168 return
169 }
170 if h.webView == nil {
171 log.Printf("[workspace] Ignoring workspace event %q: webview not attached", msg.Event)
172 return
173 }
174 log.Printf("[workspace] Forwarding workspace event: event=%s direction=%s action=%s webviewId=%s", msg.Event, msg.Direction, msg.Action, msg.WebViewID)
175 h.workspaceObserver.OnWorkspaceMessage(h.webView, msg)
176}
177
178func (h *Handler) handleClosePopup(msg Message) {
179 log.Printf("[messaging] Received close-popup request: webviewId=%s reason=%s", msg.WebViewID, msg.Reason)
180
181 if h.workspaceObserver == nil {
182 log.Printf("[messaging] No workspace observer registered for close-popup request")
183 return
184 }
185 if h.webView == nil {
186 log.Printf("[messaging] No webview attached for close-popup request")
187 return
188 }
189
190 // Forward close-popup request to workspace manager via observer
191 closeMsg := Message{
192 Type: "workspace",
193 Event: "close-popup",
194 WebViewID: msg.WebViewID,
195 Reason: msg.Reason,
196 }
197
198 log.Printf("[messaging] Forwarding close-popup to workspace: webviewId=%s reason=%s", msg.WebViewID, msg.Reason)
199 h.workspaceObserver.OnWorkspaceMessage(h.webView, closeMsg)
200}
201
202func (h *Handler) handleQuery(msg Message) {
203 if h.webView == nil {
204 log.Printf("[WARN] Skipping query handling: webview is nil")
205 return
206 }
207 log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)
208 if h.browserService == nil {
209 log.Printf("[ERROR] handleQuery: browserService is nil")
210 return
211 }
212 ctx := context.Background()
213 q := msg.Q
214 if q == "" {
215 q = ""
216 }
217 limit := msg.Limit
218 if limit <= 0 {
219 limit = 10
220 }
221
222 // Build suggestions: shortcuts first, then history search
223 type suggestion struct {
224 URL string `json:"url"`
225 Favicon string `json:"favicon,omitempty"`
226 }
227
228 results := make([]suggestion, 0, limit)
229 seen := make(map[string]struct{}, limit*2)
230
231 // Shortcuts intentionally omitted; rely on explicit prefix commands (e.g., gh:) or history results
232
233 // History
234 if len(results) < limit {
235 remaining := limit - len(results)
236 log.Printf("[DEBUG] Searching history for '%s' with limit %d", q, remaining)
237 if entries, err := h.browserService.SearchHistory(ctx, q, remaining); err != nil {
238 log.Printf("[ERROR] Failed to search history: %v", err)
239 } else {
240 log.Printf("[DEBUG] Found %d history entries", len(entries))
241 for i, e := range entries {
242 // JSON roundtrip to map to get url field agnostic of struct tag
243 bb, _ := json.Marshal(e)
244 var m map[string]any
245 _ = json.Unmarshal(bb, &m)
246 var url string
247 if s, ok := m["url"].(string); ok {
248 url = s
249 } else if s, ok := m["URL"].(string); ok {
250 url = s
251 }
252 log.Printf("[DEBUG] History entry %d: url=%s", i, url)
253 if url == "" {
254 continue
255 }
256 if _, ok := seen[url]; ok {
257 continue
258 }
259 // Use favicon_url from database (populated by FaviconService)
260 favicon := ""
261 if s, ok := m["favicon_url"].(string); ok && s != "" {
262 favicon = s
263 }
264 results = append(results, suggestion{URL: url, Favicon: favicon})
265 seen[url] = struct{}{}
266 if len(results) >= limit {
267 break
268 }
269 }
270 }
271 }
272
273 // Inject back to GUI
274 log.Printf("[DEBUG] Final results count: %d", len(results))
275 if b, err := json.Marshal(results); err != nil {
276 log.Printf("[ERROR] Failed to marshal results: %v", err)
277 } else {
278 // Prefer unified page-world API; fallback to legacy global function
279 script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))"
280 log.Print("[DEBUG] Injecting omnibox suggestions")
281 if injErr := h.webView.InjectScript(script); injErr != nil {
282 log.Printf("[ERROR] Failed to inject suggestions script: %v", injErr)
283 } else {
284 log.Printf("[DEBUG] Successfully injected omnibox suggestions")
285 }
286 }
287}
288
289// handleWailsBridge processes Wails runtime bridge calls for homepage
290func (h *Handler) handleWailsBridge(msg Message) {
291 // Payload contains { methodID, methodName?, args }
292 var p struct {
293 MethodID uint32 `json:"methodID"`
294 MethodName string `json:"methodName"`
295 Args json.RawMessage `json:"args"`
296 }
297 if err := json.Unmarshal(msg.Payload, &p); err != nil {
298 return
299 }
300
301 // Only implement the IDs we need
302 switch p.MethodID {
303 case constants.HashGetRecentHistory: // BrowserService.GetRecentHistory(limit)
304 var args []interface{}
305 _ = json.Unmarshal(p.Args, &args)
306 limit := 50
307 if len(args) > 0 {
308 if f, ok := args[0].(float64); ok {
309 limit = int(f)
310 }
311 }
312 ctx := context.Background()
313 entries, err := h.browserService.GetRecentHistory(ctx, limit)
314 if err != nil {
315 return
316 }
317 resp, _ := json.Marshal(entries)
318 if h.webView != nil {
319 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
320 }
321 case constants.HashGetSearchShortcuts: // BrowserService.GetSearchShortcuts()
322 ctx := context.Background()
323 shortcuts, err := h.browserService.GetSearchShortcuts(ctx)
324 if err != nil {
325 return
326 }
327 resp, _ := json.Marshal(shortcuts)
328 if h.webView != nil {
329 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
330 }
331 default:
332 // Return empty JSON to avoid breaking UI
333 if h.webView != nil {
334 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', '{}')")
335 }
336 }
337}
338
339// handleTheme processes theme-related messages
340func (h *Handler) handleTheme(msg Message) {
341 if msg.Value != "" && msg.Value != h.lastTheme {
342 log.Printf("[theme] color-scheme changed: %s", msg.Value)
343 h.lastTheme = msg.Value
344 }
345}
346
347// handleHistoryRecent processes recent history requests
348func (h *Handler) handleHistoryRecent(msg Message) {
349 if h.browserService == nil || h.webView == nil {
350 return
351 }
352
353 ctx := context.Background()
354 limit := msg.Limit
355 if limit <= 0 {
356 limit = 20
357 }
358 offset := msg.Offset
359
360 var entries []interface{}
361 var err error
362
363 // Use paginated version if offset is provided
364 if offset > 0 {
365 histEntries, e := h.browserService.GetRecentHistoryWithOffset(ctx, limit, offset)
366 if e != nil {
367 err = e
368 } else {
369 entries = make([]interface{}, len(histEntries))
370 for i, entry := range histEntries {
371 entries[i] = entry
372 }
373 }
374 } else {
375 // Use original method for backward compatibility
376 histEntries, e := h.browserService.GetRecentHistory(ctx, limit)
377 if e != nil {
378 err = e
379 } else {
380 entries = make([]interface{}, len(histEntries))
381 for i, entry := range histEntries {
382 entries[i] = entry
383 }
384 }
385 }
386
387 if err != nil {
388 log.Printf("[messaging] Failed to get recent history: %v", err)
389 requestIdParam := ""
390 if msg.RequestID != "" {
391 requestIdParam = ", '" + msg.RequestID + "'"
392 }
393 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load recent history'" + requestIdParam + ")")
394 return
395 }
396
397 b, _ := json.Marshal(entries)
398 requestIdParam := ""
399 if msg.RequestID != "" {
400 requestIdParam = ", '" + msg.RequestID + "'"
401 }
402 _ = h.webView.InjectScript("window.__dumber_history_recent && window.__dumber_history_recent(" + string(b) + requestIdParam + ")")
403}
404
405// handleHistoryStats processes history stats requests
406func (h *Handler) handleHistoryStats(_ Message) {
407 if h.browserService == nil || h.webView == nil {
408 return
409 }
410
411 ctx := context.Background()
412 stats, err := h.browserService.GetHistoryStats(ctx)
413 if err != nil {
414 log.Printf("[messaging] Failed to get history stats: %v", err)
415 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load history stats')")
416 return
417 }
418
419 b, _ := json.Marshal(stats)
420 _ = h.webView.InjectScript("window.__dumber_history_stats && window.__dumber_history_stats(" + string(b) + ")")
421}
422
423// handleHistorySearch processes history search requests
424func (h *Handler) handleHistorySearch(msg Message) {
425 if h.browserService == nil || h.webView == nil {
426 return
427 }
428
429 ctx := context.Background()
430 query := msg.Q
431 limit := msg.Limit
432 if limit <= 0 {
433 limit = 5
434 }
435
436 entries, err := h.browserService.SearchHistory(ctx, query, limit)
437 if err != nil {
438 log.Printf("[messaging] Failed to search history: %v", err)
439 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to search history')")
440 return
441 }
442
443 b, _ := json.Marshal(entries)
444 _ = h.webView.InjectScript("window.__dumber_history_search && window.__dumber_history_search(" + string(b) + ")")
445}
446
447// handleHistoryDelete processes history deletion requests
448func (h *Handler) handleHistoryDelete(msg Message) {
449 if h.browserService == nil || h.webView == nil {
450 return
451 }
452
453 if msg.HistoryID == "" {
454 log.Printf("[messaging] History delete: missing historyId")
455 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Missing history ID')")
456 return
457 }
458
459 // Convert string ID to int64
460 id, err := strconv.ParseInt(msg.HistoryID, 10, 64)
461 if err != nil {
462 log.Printf("[messaging] History delete: invalid ID format: %v", err)
463 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Invalid history ID format')")
464 return
465 }
466
467 ctx := context.Background()
468 err = h.browserService.DeleteHistoryEntry(ctx, id)
469 if err != nil {
470 log.Printf("[messaging] Failed to delete history entry: %v", err)
471 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to delete history entry')")
472 return
473 }
474
475 // Send success response with the deleted ID
476 successData := map[string]string{"deletedId": msg.HistoryID}
477 b, _ := json.Marshal(successData)
478 _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")")
479}
480
481// handleConsoleMessage processes console-message from JavaScript
482func (h *Handler) handleConsoleMessage(msg Message) {
483 // Check if console capture is enabled
484 cfg := config.Get()
485 if !cfg.Logging.CaptureConsole {
486 return
487 }
488
489 // Parse the console message payload
490 var consolePayload struct {
491 Level string `json:"level"`
492 Message string `json:"message"`
493 URL string `json:"url"`
494 }
495
496 if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil {
497 log.Printf("[messaging] Failed to unmarshal console-message: %v", err)
498 return
499 }
500
501 // Send to logging system with [CONSOLE] tag
502 logging.CaptureWebKitLog(consolePayload.Message)
503}
504
505// handleWebViewIDRequest responds to JavaScript requests for the webview ID
506func (h *Handler) handleWebViewIDRequest(msg Message) {
507 if h.webView == nil {
508 log.Printf("[messaging] Cannot provide webview ID - no webview available")
509 return
510 }
511
512 // Check if WebView is destroyed before attempting any operations
513 if h.webView.IsDestroyed() {
514 log.Printf("[messaging] Cannot provide webview ID - webview is destroyed")
515 return
516 }
517
518 webViewID := h.webView.ID()
519 log.Printf("[messaging] Sending webview ID %d to JavaScript", webViewID)
520
521 // Send the webview ID back to JavaScript via custom event
522 if err := h.webView.DispatchCustomEvent("dumber:webview-id", map[string]any{
523 "webviewId": webViewID,
524 "timestamp": time.Now().UnixMilli(),
525 }); err != nil {
526 log.Printf("[messaging] Failed to send webview ID: %v", err)
527 }
528}
529
530// handleGetSearchShortcuts sends search shortcuts configuration to JavaScript
531func (h *Handler) handleGetSearchShortcuts(msg Message) {
532 if h.webView == nil {
533 log.Printf("[messaging] Cannot provide search shortcuts - no webview available")
534 return
535 }
536
537 if h.webView.IsDestroyed() {
538 log.Printf("[messaging] Cannot provide search shortcuts - webview is destroyed")
539 return
540 }
541
542 // Get search shortcuts from browser service config
543 shortcuts, err := h.browserService.GetSearchShortcuts(context.Background())
544 if err != nil {
545 log.Printf("[messaging] Failed to get search shortcuts: %v", err)
546 _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to get search shortcuts')")
547 return
548 }
549
550 // Marshal to JSON
551 b, err := json.Marshal(shortcuts)
552 if err != nil {
553 log.Printf("[messaging] Failed to marshal search shortcuts: %v", err)
554 _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to load search shortcuts')")
555 return
556 }
557
558 // Inject the search shortcuts into the page
559 log.Printf("[messaging] Sending search shortcuts to JavaScript")
560 _ = h.webView.InjectScript("window.__dumber_search_shortcuts && window.__dumber_search_shortcuts(" + string(b) + ")")
561}
562
563// handleGetColorPalettes sends color palettes configuration to JavaScript
564func (h *Handler) handleGetColorPalettes(msg Message) {
565 if h.webView == nil {
566 log.Printf("[messaging] Cannot provide color palettes - no webview available")
567 return
568 }
569
570 if h.webView.IsDestroyed() {
571 log.Printf("[messaging] Cannot provide color palettes - webview is destroyed")
572 return
573 }
574
575 // Get color palettes from browser service config
576 palettes := h.browserService.GetColorPalettesForMessaging()
577
578 // Marshal to JSON
579 b, err := json.Marshal(palettes)
580 if err != nil {
581 log.Printf("[messaging] Failed to marshal color palettes: %v", err)
582 _ = h.webView.InjectScript("window.__dumber_color_palettes_error && window.__dumber_color_palettes_error('Failed to load color palettes')")
583 return
584 }
585
586 // Inject the color palettes into the page
587 log.Printf("[messaging] Sending color palettes to JavaScript")
588 _ = h.webView.InjectScript("window.__dumber_color_palettes && window.__dumber_color_palettes(" + string(b) + ")")
589}