1package messaging
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "strconv"
9 "time"
10
11 "github.com/bnema/dumber/internal/app/constants"
12 "github.com/bnema/dumber/internal/app/control"
13 "github.com/bnema/dumber/internal/config"
14 "github.com/bnema/dumber/internal/logging"
15 "github.com/bnema/dumber/internal/services"
16 "github.com/bnema/dumber/pkg/webkit"
17)
18
19// Handler processes script messages from the WebView
20type WorkspaceObserver interface {
21 OnWorkspaceMessage(source *webkit.WebView, msg Message)
22}
23
24type Handler struct {
25 parserService *services.ParserService
26 browserService *services.BrowserService
27 webView *webkit.WebView
28 navigationController *control.NavigationController
29 lastTheme string
30 workspaceObserver WorkspaceObserver
31}
32
33// Message represents a script message from the WebView
34type Message struct {
35 Type string `json:"type"`
36 URL string `json:"url"`
37 Q string `json:"q"`
38 Limit int `json:"limit"`
39 Offset int `json:"offset"`
40 Value string `json:"value"`
41 Event string `json:"event"`
42 Action string `json:"action"`
43 Direction string `json:"direction"`
44 // History operations
45 HistoryID string `json:"historyId"`
46 // Request tracking
47 RequestID string `json:"requestId"`
48 // Popup close tracking
49 WebViewID string `json:"webviewId"`
50 Reason string `json:"reason"`
51 // Wails fetch bridge
52 ID string `json:"id"`
53 Payload json.RawMessage `json:"payload"`
54 // Favorites operations
55 Title string `json:"title"`
56 FaviconURL string `json:"faviconURL"`
57}
58
59// NewHandler creates a new message handler
60func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {
61 return &Handler{
62 parserService: parserService,
63 browserService: browserService,
64 }
65}
66
67// SetNavigationController injects the navigation controller for unified navigation flow.
68func (h *Handler) SetNavigationController(controller *control.NavigationController) {
69 h.navigationController = controller
70}
71
72// SetWorkspaceObserver registers a workspace event observer.
73func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
74 h.workspaceObserver = observer
75}
76
77// Handle processes incoming script messages
78func (h *Handler) Handle(payload string) {
79 msg, err := parseIncomingMessage(payload)
80 if err != nil {
81 logging.Error(fmt.Sprintf("[ERROR] Failed to unmarshal message: %v", err))
82 return
83 }
84
85 switch msg.Type {
86 case "navigate":
87 h.handleNavigation(msg)
88 case "query":
89 if h.webView == nil {
90 return
91 }
92 h.handleQuery(msg)
93 case "omnibox_initial_history":
94 if h.webView == nil {
95 return
96 }
97 h.handleOmniboxInitialHistory(msg)
98 case "wails":
99 h.handleWailsBridge(msg)
100 case "theme":
101 h.handleTheme(msg)
102 case "history_recent":
103 h.handleHistoryRecent(msg)
104 case "history_stats":
105 h.handleHistoryStats(msg)
106 case "history_search":
107 h.handleHistorySearch(msg)
108 case "history_delete":
109 h.handleHistoryDelete(msg)
110 case "workspace":
111 h.handleWorkspace(msg)
112 case "close-popup":
113 h.handleClosePopup(msg)
114 case "console-message":
115 h.handleConsoleMessage(msg)
116 case "request-webview-id":
117 h.handleWebViewIDRequest(msg)
118 case "get_search_shortcuts":
119 h.handleGetSearchShortcuts(msg)
120 case "get_color_palettes":
121 h.handleGetColorPalettes(msg)
122 case "get_favorites":
123 h.handleGetFavorites(msg)
124 case "toggle_favorite":
125 h.handleToggleFavorite(msg)
126 case "is_favorite":
127 h.handleIsFavorite(msg)
128 case "keyboard_blocking":
129 h.handleKeyboardBlocking(msg)
130 case "prefix_query":
131 h.handlePrefixQuery(msg)
132 }
133}
134
135func parseIncomingMessage(payload string) (Message, error) {
136 data := []byte(payload)
137 var msg Message
138 if err := json.Unmarshal(data, &msg); err == nil {
139 return msg, nil
140 } else {
141 normalized, normErr := normalizeWebViewIDPayload(data)
142 if normErr != nil {
143 return Message{}, err
144 }
145
146 if err := json.Unmarshal(normalized, &msg); err != nil {
147 return Message{}, err
148 }
149
150 return msg, nil
151 }
152}
153
154func normalizeWebViewIDPayload(data []byte) ([]byte, error) {
155 var raw map[string]json.RawMessage
156 if err := json.Unmarshal(data, &raw); err != nil {
157 return nil, err
158 }
159
160 rawID, ok := raw["webviewId"]
161 if !ok {
162 return nil, fmt.Errorf("webviewId missing in payload")
163 }
164
165 normalizedID, err := parseWebViewIDRaw(rawID)
166 if err != nil {
167 return nil, err
168 }
169
170 raw["webviewId"] = json.RawMessage(strconv.Quote(normalizedID))
171
172 return json.Marshal(raw)
173}
174
175func parseWebViewIDRaw(raw json.RawMessage) (string, error) {
176 trimmed := bytes.TrimSpace(raw)
177 if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
178 return "", nil
179 }
180
181 if trimmed[0] == '"' {
182 var id string
183 if err := json.Unmarshal(trimmed, &id); err != nil {
184 return "", err
185 }
186 return id, nil
187 }
188
189 var number json.Number
190 if err := json.Unmarshal(trimmed, &number); err == nil {
191 return number.String(), nil
192 }
193
194 return "", fmt.Errorf("unsupported webviewId format: %s", string(trimmed))
195}
196
197// SetWebView sets the WebView reference (needed for script injection)
198func (h *Handler) SetWebView(webView *webkit.WebView) {
199 h.webView = webView
200}
201
202// handleNavigation processes navigation requests from the frontend
203func (h *Handler) handleNavigation(msg Message) {
204 if h.navigationController != nil {
205 if err := h.navigationController.NavigateToURL(msg.URL); err != nil {
206 logging.Error(fmt.Sprintf("[messaging] Navigation controller failed for input %q: %v", msg.URL, err))
207 h.legacyNavigate(msg)
208 }
209 return
210 }
211
212 h.legacyNavigate(msg)
213}
214
215func (h *Handler) legacyNavigate(msg Message) {
216 ctx := context.Background()
217 res, err := h.parserService.ParseInput(ctx, msg.URL)
218 if err != nil {
219 logging.Error(fmt.Sprintf("[messaging] Legacy navigation parse failed for %q: %v", msg.URL, err))
220 return
221 }
222
223 if _, navErr := h.browserService.Navigate(ctx, res.URL); navErr != nil {
224 logging.Warn(fmt.Sprintf("Warning: failed to navigate to %s: %v", res.URL, navErr))
225 }
226
227 if h.webView == nil {
228 return
229 }
230
231 if err := h.webView.LoadURL(res.URL); err != nil {
232 logging.Error(fmt.Sprintf("[messaging] Legacy LoadURL failed for %s: %v", res.URL, err))
233 }
234
235 if z, zerr := h.browserService.GetZoomLevel(ctx, res.URL); zerr == nil {
236 if err := h.webView.SetZoom(z); err != nil {
237 logging.Error(fmt.Sprintf("[messaging] Legacy SetZoom failed for %s: %v", res.URL, err))
238 }
239 }
240}
241
242// (legacy query handler removed; omnibox suggestions now fetched via dumb://api/omnibox/suggestions)
243// handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch
244func (h *Handler) handleWorkspace(msg Message) {
245 if h.workspaceObserver == nil {
246 logging.Warn(fmt.Sprintf("[workspace] Received workspace event %q but no observer registered", msg.Event))
247 return
248 }
249 if h.webView == nil {
250 logging.Debug(fmt.Sprintf("[workspace] Ignoring workspace event %q: webview not attached", msg.Event))
251 return
252 }
253 logging.Debug(fmt.Sprintf("[workspace] Forwarding workspace event: event=%s direction=%s action=%s webviewId=%s", msg.Event, msg.Direction, msg.Action, msg.WebViewID))
254 h.workspaceObserver.OnWorkspaceMessage(h.webView, msg)
255}
256
257func (h *Handler) handleClosePopup(msg Message) {
258 logging.Debug(fmt.Sprintf("[messaging] Received close-popup request: webviewId=%s reason=%s", msg.WebViewID, msg.Reason))
259
260 if h.workspaceObserver == nil {
261 logging.Warn(fmt.Sprintf("[messaging] No workspace observer registered for close-popup request"))
262 return
263 }
264 if h.webView == nil {
265 logging.Debug(fmt.Sprintf("[messaging] No webview attached for close-popup request"))
266 return
267 }
268
269 // Forward close-popup request to workspace manager via observer
270 closeMsg := Message{
271 Type: "workspace",
272 Event: "close-popup",
273 WebViewID: msg.WebViewID,
274 Reason: msg.Reason,
275 }
276
277 logging.Debug(fmt.Sprintf("[messaging] Forwarding close-popup to workspace: webviewId=%s reason=%s", msg.WebViewID, msg.Reason))
278 h.workspaceObserver.OnWorkspaceMessage(h.webView, closeMsg)
279}
280
281func (h *Handler) handleQuery(msg Message) {
282 if h.webView == nil || h.browserService == nil {
283 return
284 }
285 ctx := context.Background()
286 q := msg.Q
287 if q == "" {
288 q = ""
289 }
290 limit := msg.Limit
291 if limit <= 0 {
292 limit = 10
293 }
294
295 // Build suggestions: shortcuts first, then history search
296 type suggestion struct {
297 URL string `json:"url"`
298 Favicon string `json:"favicon,omitempty"`
299 }
300
301 results := make([]suggestion, 0, limit)
302 seen := make(map[string]struct{}, limit*2)
303
304 // Shortcuts intentionally omitted; rely on explicit prefix commands (e.g., gh:) or history results
305
306 // History
307 if len(results) < limit {
308 remaining := limit - len(results)
309 if entries, err := h.browserService.SearchHistory(ctx, q, remaining); err == nil {
310 for _, e := range entries {
311 // JSON roundtrip to map to get url field agnostic of struct tag
312 bb, _ := json.Marshal(e)
313 var m map[string]any
314 _ = json.Unmarshal(bb, &m)
315 var url string
316 if s, ok := m["url"].(string); ok {
317 url = s
318 } else if s, ok := m["URL"].(string); ok {
319 url = s
320 }
321 if url == "" {
322 continue
323 }
324 if _, ok := seen[url]; ok {
325 continue
326 }
327 // Use favicon_url from database (populated by FaviconService)
328 favicon := ""
329 if s, ok := m["favicon_url"].(string); ok && s != "" {
330 favicon = s
331 }
332 results = append(results, suggestion{URL: url, Favicon: favicon})
333 seen[url] = struct{}{}
334 if len(results) >= limit {
335 break
336 }
337 }
338 }
339 }
340
341 // Inject back to GUI
342 if b, err := json.Marshal(results); err == nil {
343 // Prefer unified page-world API; fallback to legacy global function
344 script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))"
345 if err := h.webView.InjectScript(script); err != nil {
346 logging.Error(fmt.Sprintf("[ERROR] Failed to inject omnibox suggestions: %v", err))
347 }
348 }
349}
350
351// handleOmniboxInitialHistory processes initial history display for empty omnibox
352func (h *Handler) handleOmniboxInitialHistory(msg Message) {
353 if h.webView == nil || h.browserService == nil {
354 return
355 }
356
357 ctx := context.Background()
358 limit := msg.Limit
359 if limit <= 0 {
360 limit = 10
361 }
362
363 // Get config to determine behavior
364 cfg, err := h.browserService.GetConfig(ctx)
365 if err != nil {
366 logging.Error(fmt.Sprintf("[ERROR] Failed to get config: %v", err))
367 return
368 }
369 behavior := cfg.Omnibox.InitialBehavior
370
371 type suggestion struct {
372 URL string `json:"url"`
373 Favicon string `json:"favicon,omitempty"`
374 }
375
376 results := make([]suggestion, 0, limit)
377 seen := make(map[string]struct{}, limit*2)
378
379 // Fetch history based on config
380 var entries []interface{}
381
382 if behavior == "recent" {
383 historyEntries, histErr := h.browserService.GetRecentHistory(ctx, limit)
384 err = histErr
385 for _, e := range historyEntries {
386 entries = append(entries, e)
387 }
388 } else if behavior == "most_visited" {
389 historyEntries, histErr := h.browserService.GetMostVisited(ctx, limit)
390 err = histErr
391 for _, e := range historyEntries {
392 entries = append(entries, e)
393 }
394 } else if behavior == "none" {
395 // Return empty results
396 entries = []interface{}{}
397 }
398
399 if err != nil {
400 logging.Error(fmt.Sprintf("[ERROR] Failed to fetch initial history: %v", err))
401 return
402 }
403
404 // Build suggestions from entries with deduplication
405 for _, entry := range entries {
406 bb, _ := json.Marshal(entry)
407 var m map[string]any
408 _ = json.Unmarshal(bb, &m)
409
410 var url string
411 if s, ok := m["url"].(string); ok {
412 url = s
413 } else if s, ok := m["URL"].(string); ok {
414 url = s
415 }
416 if url == "" {
417 continue
418 }
419
420 // Skip duplicates
421 if _, ok := seen[url]; ok {
422 continue
423 }
424
425 favicon := ""
426 if s, ok := m["favicon_url"].(string); ok && s != "" {
427 favicon = s
428 }
429
430 results = append(results, suggestion{URL: url, Favicon: favicon})
431 seen[url] = struct{}{}
432 if len(results) >= limit {
433 break
434 }
435 }
436
437 // Inject back to GUI
438 if b, err := json.Marshal(results); err == nil {
439 script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))"
440 if err := h.webView.InjectScript(script); err != nil {
441 logging.Error(fmt.Sprintf("[ERROR] Failed to inject initial history: %v", err))
442 }
443 }
444}
445
446// handleWailsBridge processes Wails runtime bridge calls for homepage
447func (h *Handler) handleWailsBridge(msg Message) {
448 // Payload contains { methodID, methodName?, args }
449 var p struct {
450 MethodID uint32 `json:"methodID"`
451 MethodName string `json:"methodName"`
452 Args json.RawMessage `json:"args"`
453 }
454 if err := json.Unmarshal(msg.Payload, &p); err != nil {
455 return
456 }
457
458 // Only implement the IDs we need
459 switch p.MethodID {
460 case constants.HashGetRecentHistory: // BrowserService.GetRecentHistory(limit)
461 var args []interface{}
462 _ = json.Unmarshal(p.Args, &args)
463 limit := 50
464 if len(args) > 0 {
465 if f, ok := args[0].(float64); ok {
466 limit = int(f)
467 }
468 }
469 ctx := context.Background()
470 entries, err := h.browserService.GetRecentHistory(ctx, limit)
471 if err != nil {
472 return
473 }
474 resp, _ := json.Marshal(entries)
475 if h.webView != nil {
476 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
477 }
478 case constants.HashGetSearchShortcuts: // BrowserService.GetSearchShortcuts()
479 ctx := context.Background()
480 shortcuts, err := h.browserService.GetSearchShortcuts(ctx)
481 if err != nil {
482 return
483 }
484 resp, _ := json.Marshal(shortcuts)
485 if h.webView != nil {
486 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
487 }
488 default:
489 // Return empty JSON to avoid breaking UI
490 if h.webView != nil {
491 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', '{}')")
492 }
493 }
494}
495
496// handleTheme processes theme-related messages
497func (h *Handler) handleTheme(msg Message) {
498 if msg.Value != "" && msg.Value != h.lastTheme {
499 logging.Debug(fmt.Sprintf("[theme] color-scheme changed: %s", msg.Value))
500 h.lastTheme = msg.Value
501 }
502}
503
504// handleHistoryRecent processes recent history requests
505func (h *Handler) handleHistoryRecent(msg Message) {
506 if h.browserService == nil || h.webView == nil {
507 return
508 }
509
510 ctx := context.Background()
511 limit := msg.Limit
512 if limit <= 0 {
513 limit = 20
514 }
515 offset := msg.Offset
516
517 var entries []interface{}
518 var err error
519
520 // Use paginated version if offset is provided
521 if offset > 0 {
522 histEntries, e := h.browserService.GetRecentHistoryWithOffset(ctx, limit, offset)
523 if e != nil {
524 err = e
525 } else {
526 entries = make([]interface{}, len(histEntries))
527 for i, entry := range histEntries {
528 entries[i] = entry
529 }
530 }
531 } else {
532 // Use original method for backward compatibility
533 histEntries, e := h.browserService.GetRecentHistory(ctx, limit)
534 if e != nil {
535 err = e
536 } else {
537 entries = make([]interface{}, len(histEntries))
538 for i, entry := range histEntries {
539 entries[i] = entry
540 }
541 }
542 }
543
544 if err != nil {
545 logging.Error(fmt.Sprintf("[messaging] Failed to get recent history: %v", err))
546 requestIdParam := ""
547 if msg.RequestID != "" {
548 requestIdParam = ", '" + msg.RequestID + "'"
549 }
550 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load recent history'" + requestIdParam + ")")
551 return
552 }
553
554 b, _ := json.Marshal(entries)
555 requestIdParam := ""
556 if msg.RequestID != "" {
557 requestIdParam = ", '" + msg.RequestID + "'"
558 }
559 _ = h.webView.InjectScript("window.__dumber_history_recent && window.__dumber_history_recent(" + string(b) + requestIdParam + ")")
560}
561
562// handleHistoryStats processes history stats requests
563func (h *Handler) handleHistoryStats(_ Message) {
564 if h.browserService == nil || h.webView == nil {
565 return
566 }
567
568 ctx := context.Background()
569 stats, err := h.browserService.GetHistoryStats(ctx)
570 if err != nil {
571 logging.Error(fmt.Sprintf("[messaging] Failed to get history stats: %v", err))
572 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load history stats')")
573 return
574 }
575
576 b, _ := json.Marshal(stats)
577 _ = h.webView.InjectScript("window.__dumber_history_stats && window.__dumber_history_stats(" + string(b) + ")")
578}
579
580// handleHistorySearch processes history search requests
581func (h *Handler) handleHistorySearch(msg Message) {
582 if h.browserService == nil || h.webView == nil {
583 return
584 }
585
586 ctx := context.Background()
587 query := msg.Q
588 limit := msg.Limit
589 if limit <= 0 {
590 limit = 5
591 }
592
593 entries, err := h.browserService.SearchHistory(ctx, query, limit)
594 if err != nil {
595 logging.Error(fmt.Sprintf("[messaging] Failed to search history: %v", err))
596 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to search history')")
597 return
598 }
599
600 b, _ := json.Marshal(entries)
601 _ = h.webView.InjectScript("window.__dumber_history_search && window.__dumber_history_search(" + string(b) + ")")
602}
603
604// handleHistoryDelete processes history deletion requests
605func (h *Handler) handleHistoryDelete(msg Message) {
606 if h.browserService == nil || h.webView == nil {
607 return
608 }
609
610 if msg.HistoryID == "" {
611 logging.Warn(fmt.Sprintf("[messaging] History delete: missing historyId"))
612 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Missing history ID')")
613 return
614 }
615
616 // Convert string ID to int64
617 id, err := strconv.ParseInt(msg.HistoryID, 10, 64)
618 if err != nil {
619 logging.Error(fmt.Sprintf("[messaging] History delete: invalid ID format: %v", err))
620 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Invalid history ID format')")
621 return
622 }
623
624 ctx := context.Background()
625 err = h.browserService.DeleteHistoryEntry(ctx, id)
626 if err != nil {
627 logging.Error(fmt.Sprintf("[messaging] Failed to delete history entry: %v", err))
628 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to delete history entry')")
629 return
630 }
631
632 // Send success response with the deleted ID
633 successData := map[string]string{"deletedId": msg.HistoryID}
634 b, _ := json.Marshal(successData)
635 _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")")
636}
637
638// handleConsoleMessage processes console-message from JavaScript
639func (h *Handler) handleConsoleMessage(msg Message) {
640 // Check if console capture is enabled
641 cfg := config.Get()
642 if !cfg.Logging.CaptureConsole {
643 return
644 }
645
646 // Parse the console message payload
647 var consolePayload struct {
648 Level string `json:"level"`
649 Message string `json:"message"`
650 URL string `json:"url"`
651 }
652
653 if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil {
654 logging.Error(fmt.Sprintf("[messaging] Failed to unmarshal console-message: %v", err))
655 return
656 }
657
658 // Send to logging system with [CONSOLE] tag
659 logging.CaptureWebKitLog(consolePayload.Message)
660}
661
662// handleWebViewIDRequest responds to JavaScript requests for the webview ID
663func (h *Handler) handleWebViewIDRequest(msg Message) {
664 if h.webView == nil {
665 logging.Warn(fmt.Sprintf("[messaging] Cannot provide webview ID - no webview available"))
666 return
667 }
668
669 // Check if WebView is destroyed before attempting any operations
670 if h.webView.IsDestroyed() {
671 logging.Warn(fmt.Sprintf("[messaging] Cannot provide webview ID - webview is destroyed"))
672 return
673 }
674
675 webViewID := h.webView.ID()
676 logging.Debug(fmt.Sprintf("[messaging] Sending webview ID %d to JavaScript", webViewID))
677
678 // Send the webview ID back to JavaScript via custom event
679 if err := h.webView.DispatchCustomEvent("dumber:webview-id", map[string]any{
680 "webviewId": webViewID,
681 "timestamp": time.Now().UnixMilli(),
682 }); err != nil {
683 logging.Error(fmt.Sprintf("[messaging] Failed to send webview ID: %v", err))
684 }
685}
686
687// handleGetSearchShortcuts sends search shortcuts configuration to JavaScript
688func (h *Handler) handleGetSearchShortcuts(msg Message) {
689 if h.webView == nil {
690 logging.Warn(fmt.Sprintf("[messaging] Cannot provide search shortcuts - no webview available"))
691 return
692 }
693
694 if h.webView.IsDestroyed() {
695 logging.Warn(fmt.Sprintf("[messaging] Cannot provide search shortcuts - webview is destroyed"))
696 return
697 }
698
699 // Get search shortcuts from browser service config
700 shortcuts, err := h.browserService.GetSearchShortcuts(context.Background())
701 if err != nil {
702 logging.Error(fmt.Sprintf("[messaging] Failed to get search shortcuts: %v", err))
703 _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to get search shortcuts')")
704 return
705 }
706
707 // Marshal to JSON
708 b, err := json.Marshal(shortcuts)
709 if err != nil {
710 logging.Error(fmt.Sprintf("[messaging] Failed to marshal search shortcuts: %v", err))
711 _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to load search shortcuts')")
712 return
713 }
714
715 // Inject the search shortcuts into the page
716 logging.Debug(fmt.Sprintf("[messaging] Sending search shortcuts to JavaScript"))
717 _ = h.webView.InjectScript("window.__dumber_search_shortcuts && window.__dumber_search_shortcuts(" + string(b) + ")")
718}
719
720// handleGetColorPalettes sends color palettes configuration to JavaScript
721func (h *Handler) handleGetColorPalettes(msg Message) {
722 if h.webView == nil {
723 logging.Warn(fmt.Sprintf("[messaging] Cannot provide color palettes - no webview available"))
724 return
725 }
726
727 if h.webView.IsDestroyed() {
728 logging.Warn(fmt.Sprintf("[messaging] Cannot provide color palettes - webview is destroyed"))
729 return
730 }
731
732 // Get color palettes from browser service config
733 palettes := h.browserService.GetColorPalettesForMessaging()
734
735 // Marshal to JSON
736 b, err := json.Marshal(palettes)
737 if err != nil {
738 logging.Error(fmt.Sprintf("[messaging] Failed to marshal color palettes: %v", err))
739 _ = h.webView.InjectScript("window.__dumber_color_palettes_error && window.__dumber_color_palettes_error('Failed to load color palettes')")
740 return
741 }
742
743 // Inject the color palettes into the page
744 logging.Debug(fmt.Sprintf("[messaging] Sending color palettes to JavaScript"))
745 _ = h.webView.InjectScript("window.__dumber_color_palettes && window.__dumber_color_palettes(" + string(b) + ")")
746}
747
748// handleGetFavorites sends all favorites to JavaScript
749func (h *Handler) handleGetFavorites(msg Message) {
750 if h.webView == nil {
751 logging.Warn(fmt.Sprintf("[messaging] Cannot provide favorites - no webview available"))
752 return
753 }
754
755 if h.webView.IsDestroyed() {
756 logging.Warn(fmt.Sprintf("[messaging] Cannot provide favorites - webview is destroyed"))
757 return
758 }
759
760 // Get favorites from browser service
761 favorites, err := h.browserService.GetFavorites(context.Background())
762 if err != nil {
763 logging.Error(fmt.Sprintf("[messaging] Failed to get favorites: %v", err))
764 _ = h.webView.InjectScript("window.__dumber_favorites_error && window.__dumber_favorites_error('Failed to get favorites')")
765 return
766 }
767
768 // Marshal to JSON
769 b, err := json.Marshal(favorites)
770 if err != nil {
771 logging.Error(fmt.Sprintf("[messaging] Failed to marshal favorites: %v", err))
772 _ = h.webView.InjectScript("window.__dumber_favorites_error && window.__dumber_favorites_error('Failed to load favorites')")
773 return
774 }
775
776 // Inject the favorites into the page
777 logging.Debug(fmt.Sprintf("[messaging] Sending %d favorites to JavaScript", len(favorites)))
778 _ = h.webView.InjectScript("window.__dumber_favorites && window.__dumber_favorites(" + string(b) + ")")
779}
780
781// handleToggleFavorite adds or removes a URL from favorites
782func (h *Handler) handleToggleFavorite(msg Message) {
783 if h.webView == nil {
784 logging.Warn(fmt.Sprintf("[messaging] Cannot toggle favorite - no webview available"))
785 return
786 }
787
788 if h.webView.IsDestroyed() {
789 logging.Warn(fmt.Sprintf("[messaging] Cannot toggle favorite - webview is destroyed"))
790 return
791 }
792
793 if msg.URL == "" {
794 logging.Warn(fmt.Sprintf("[messaging] Cannot toggle favorite - URL is empty"))
795 return
796 }
797
798 // Toggle the favorite
799 added, err := h.browserService.ToggleFavorite(context.Background(), msg.URL, msg.Title, msg.FaviconURL)
800 if err != nil {
801 logging.Error(fmt.Sprintf("[messaging] Failed to toggle favorite for %s: %v", msg.URL, err))
802 _ = h.webView.InjectScript("window.__dumber_favorite_toggled_error && window.__dumber_favorite_toggled_error('Failed to toggle favorite')")
803 return
804 }
805
806 // Send result back to JavaScript
807 result := map[string]interface{}{
808 "url": msg.URL,
809 "added": added,
810 }
811 b, err := json.Marshal(result)
812 if err != nil {
813 logging.Error(fmt.Sprintf("[messaging] Failed to marshal toggle result: %v", err))
814 return
815 }
816
817 logging.Debug(fmt.Sprintf("[messaging] Favorite toggled for %s (added: %v)", msg.URL, added))
818 _ = h.webView.InjectScript("window.__dumber_favorite_toggled && window.__dumber_favorite_toggled(" + string(b) + ")")
819}
820
821// handleIsFavorite checks if a URL is favorited
822func (h *Handler) handleIsFavorite(msg Message) {
823 if h.webView == nil {
824 logging.Warn(fmt.Sprintf("[messaging] Cannot check favorite - no webview available"))
825 return
826 }
827
828 if h.webView.IsDestroyed() {
829 logging.Warn(fmt.Sprintf("[messaging] Cannot check favorite - webview is destroyed"))
830 return
831 }
832
833 if msg.URL == "" {
834 logging.Warn(fmt.Sprintf("[messaging] Cannot check favorite - URL is empty"))
835 return
836 }
837
838 // Check if the URL is favorited
839 isFavorite, err := h.browserService.IsFavorite(context.Background(), msg.URL)
840 if err != nil {
841 logging.Error(fmt.Sprintf("[messaging] Failed to check if favorite for %s: %v", msg.URL, err))
842 return
843 }
844
845 // Send result back to JavaScript
846 result := map[string]interface{}{
847 "url": msg.URL,
848 "isFavorite": isFavorite,
849 }
850 b, err := json.Marshal(result)
851 if err != nil {
852 logging.Error(fmt.Sprintf("[messaging] Failed to marshal is_favorite result: %v", err))
853 return
854 }
855
856 _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")")
857}
858
859// handleKeyboardBlocking enables or disables keyboard event blocking for omnibox
860func (h *Handler) handleKeyboardBlocking(msg Message) {
861 if h.webView == nil {
862 return
863 }
864
865 var script string
866 if msg.Action == "enable" {
867 script = "window.__dumber_enableKeyboardBlocking?.()"
868 } else {
869 script = "window.__dumber_disableKeyboardBlocking?.()"
870 }
871
872 if err := h.webView.InjectScript(script); err != nil {
873 logging.Error(fmt.Sprintf("[ERROR] Failed to inject keyboard blocking script: %v", err))
874 }
875}
876
877// handlePrefixQuery returns the best prefix-matching URL for inline suggestions
878func (h *Handler) handlePrefixQuery(msg Message) {
879 if h.webView == nil || h.browserService == nil {
880 return
881 }
882
883 query := msg.Q
884 if query == "" {
885 _ = h.webView.InjectScript("window.__dumber_omnibox_inline_suggestion && window.__dumber_omnibox_inline_suggestion(null)")
886 return
887 }
888
889 ctx := context.Background()
890 prefixURL := h.browserService.GetBestPrefixMatch(ctx, query)
891
892 if prefixURL != "" {
893 // Escape the URL for JavaScript string
894 escaped, err := json.Marshal(prefixURL)
895 if err != nil {
896 _ = h.webView.InjectScript("window.__dumber_omnibox_inline_suggestion && window.__dumber_omnibox_inline_suggestion(null)")
897 return
898 }
899 _ = h.webView.InjectScript("window.__dumber_omnibox_inline_suggestion && window.__dumber_omnibox_inline_suggestion(" + string(escaped) + ")")
900 } else {
901 _ = h.webView.InjectScript("window.__dumber_omnibox_inline_suggestion && window.__dumber_omnibox_inline_suggestion(null)")
902 }
903}