1package messaging
2
3import (
4 "context"
5 "encoding/json"
6 "log"
7 neturl "net/url"
8
9 "github.com/bnema/dumber/internal/app/constants"
10 "github.com/bnema/dumber/pkg/webkit"
11 "github.com/bnema/dumber/services"
12)
13
14// Handler processes script messages from the WebView
15type Handler struct {
16 parserService *services.ParserService
17 browserService *services.BrowserService
18 webView *webkit.WebView
19 lastTheme string
20}
21
22// Message represents a script message from the WebView
23type Message struct {
24 Type string `json:"type"`
25 URL string `json:"url"`
26 Q string `json:"q"`
27 Limit int `json:"limit"`
28 Value string `json:"value"`
29 // Wails fetch bridge
30 ID string `json:"id"`
31 Payload json.RawMessage `json:"payload"`
32}
33
34// NewHandler creates a new message handler
35func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {
36 return &Handler{
37 parserService: parserService,
38 browserService: browserService,
39 }
40}
41
42// Handle processes incoming script messages
43func (h *Handler) Handle(payload string) {
44 var msg Message
45 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
46 return
47 }
48
49 switch msg.Type {
50 case "navigate":
51 h.handleNavigation(msg)
52 case "query":
53 h.handleQuery(msg)
54 case "wails":
55 h.handleWailsBridge(msg)
56 case "theme":
57 h.handleTheme(msg)
58 }
59}
60
61// SetWebView sets the WebView reference (needed for script injection)
62func (h *Handler) SetWebView(webView *webkit.WebView) {
63 h.webView = webView
64}
65
66// handleNavigation processes navigation requests from the frontend
67func (h *Handler) handleNavigation(msg Message) {
68 ctx := context.Background()
69 res, err := h.parserService.ParseInput(ctx, msg.URL)
70 if err == nil {
71 if _, navErr := h.browserService.Navigate(ctx, res.URL); navErr != nil {
72 log.Printf("Warning: failed to navigate to %s: %v", res.URL, navErr)
73 }
74 if h.webView != nil {
75 _ = h.webView.LoadURL(res.URL)
76 if z, zerr := h.browserService.GetZoomLevel(ctx, res.URL); zerr == nil {
77 _ = h.webView.SetZoom(z)
78 }
79 }
80 }
81}
82
83// handleQuery processes history search queries from the frontend
84func (h *Handler) handleQuery(msg Message) {
85 ctx := context.Background()
86 limit := msg.Limit
87 if limit <= 0 || limit > 25 {
88 limit = 10
89 }
90 entries, err := h.browserService.SearchHistory(ctx, msg.Q, limit)
91 if err != nil {
92 return
93 }
94
95 // Map to lightweight items for frontend
96 type item struct {
97 URL string `json:"url"`
98 Favicon string `json:"favicon"`
99 }
100
101 buildFavicon := func(raw string) string {
102 u, err := h.parserService.ParseInput(ctx, raw)
103 if err != nil || u.URL == "" {
104 return ""
105 }
106 parsed, perr := neturl.Parse(u.URL)
107 if perr != nil || parsed.Host == "" {
108 return ""
109 }
110 scheme := parsed.Scheme
111 if scheme == "" {
112 scheme = "https"
113 }
114 return scheme + "://" + parsed.Host + "/favicon.ico"
115 }
116
117 items := make([]item, 0, len(entries))
118 for _, e := range entries {
119 items = append(items, item{URL: e.URL, Favicon: buildFavicon(e.URL)})
120 }
121 b, _ := json.Marshal(items)
122 // Update suggestions in page
123 if h.webView != nil {
124 _ = h.webView.InjectScript("window.__dumber_setSuggestions && window.__dumber_setSuggestions(" + string(b) + ")")
125 }
126}
127
128// handleWailsBridge processes Wails runtime bridge calls for homepage
129func (h *Handler) handleWailsBridge(msg Message) {
130 // Payload contains { methodID, methodName?, args }
131 var p struct {
132 MethodID uint32 `json:"methodID"`
133 MethodName string `json:"methodName"`
134 Args json.RawMessage `json:"args"`
135 }
136 if err := json.Unmarshal(msg.Payload, &p); err != nil {
137 return
138 }
139
140 // Only implement the IDs we need
141 switch p.MethodID {
142 case constants.HashGetRecentHistory: // BrowserService.GetRecentHistory(limit)
143 var args []interface{}
144 _ = json.Unmarshal(p.Args, &args)
145 limit := 50
146 if len(args) > 0 {
147 if f, ok := args[0].(float64); ok {
148 limit = int(f)
149 }
150 }
151 ctx := context.Background()
152 entries, err := h.browserService.GetRecentHistory(ctx, limit)
153 if err != nil {
154 return
155 }
156 resp, _ := json.Marshal(entries)
157 if h.webView != nil {
158 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
159 }
160 case constants.HashGetSearchShortcuts: // BrowserService.GetSearchShortcuts()
161 ctx := context.Background()
162 shortcuts, err := h.browserService.GetSearchShortcuts(ctx)
163 if err != nil {
164 return
165 }
166 resp, _ := json.Marshal(shortcuts)
167 if h.webView != nil {
168 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
169 }
170 default:
171 // Return empty JSON to avoid breaking UI
172 if h.webView != nil {
173 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', '{}')")
174 }
175 }
176}
177
178// handleTheme processes theme-related messages
179func (h *Handler) handleTheme(msg Message) {
180 if msg.Value != "" && msg.Value != h.lastTheme {
181 log.Printf("[theme] color-scheme changed: %s", msg.Value)
182 h.lastTheme = msg.Value
183 }
184}