1package messaging
2
3import (
4 "context"
5 "encoding/json"
6 "log"
7 neturl "net/url"
8 "sort"
9 "strconv"
10 "strings"
11
12 "github.com/bnema/dumber/internal/app/constants"
13 "github.com/bnema/dumber/pkg/webkit"
14 "github.com/bnema/dumber/services"
15)
16
17// Handler processes script messages from the WebView
18type Handler struct {
19 parserService *services.ParserService
20 browserService *services.BrowserService
21 webView *webkit.WebView
22 lastTheme string
23}
24
25// Message represents a script message from the WebView
26type Message struct {
27 Type string `json:"type"`
28 URL string `json:"url"`
29 Q string `json:"q"`
30 Limit int `json:"limit"`
31 Offset int `json:"offset"`
32 Value string `json:"value"`
33 // History operations
34 HistoryID string `json:"historyId"`
35 // Request tracking
36 RequestID string `json:"requestId"`
37 // Wails fetch bridge
38 ID string `json:"id"`
39 Payload json.RawMessage `json:"payload"`
40}
41
42// NewHandler creates a new message handler
43func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {
44 return &Handler{
45 parserService: parserService,
46 browserService: browserService,
47 }
48}
49
50// Handle processes incoming script messages
51func (h *Handler) Handle(payload string) {
52 log.Printf("[DEBUG] Received message: %s", payload)
53 var msg Message
54 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
55 log.Printf("[ERROR] Failed to unmarshal message: %v", err)
56 return
57 }
58 log.Printf("[DEBUG] Parsed message type: %s", msg.Type)
59
60 switch msg.Type {
61 case "navigate":
62 h.handleNavigation(msg)
63 case "query":
64 log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit)
65 h.handleQuery(msg)
66 case "wails":
67 h.handleWailsBridge(msg)
68 case "theme":
69 h.handleTheme(msg)
70 case "history_recent":
71 h.handleHistoryRecent(msg)
72 case "history_stats":
73 h.handleHistoryStats(msg)
74 case "history_search":
75 h.handleHistorySearch(msg)
76 case "history_delete":
77 h.handleHistoryDelete(msg)
78 }
79}
80
81// SetWebView sets the WebView reference (needed for script injection)
82func (h *Handler) SetWebView(webView *webkit.WebView) {
83 h.webView = webView
84}
85
86// handleNavigation processes navigation requests from the frontend
87func (h *Handler) handleNavigation(msg Message) {
88 ctx := context.Background()
89 res, err := h.parserService.ParseInput(ctx, msg.URL)
90 if err == nil {
91 if _, navErr := h.browserService.Navigate(ctx, res.URL); navErr != nil {
92 log.Printf("Warning: failed to navigate to %s: %v", res.URL, navErr)
93 }
94 if h.webView != nil {
95 _ = h.webView.LoadURL(res.URL)
96 if z, zerr := h.browserService.GetZoomLevel(ctx, res.URL); zerr == nil {
97 _ = h.webView.SetZoom(z)
98 }
99 }
100 }
101}
102
103// (legacy query handler removed; omnibox suggestions now fetched via dumb://api/omnibox/suggestions)
104// handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch
105func (h *Handler) handleQuery(msg Message) {
106 log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)
107 if h.webView == nil {
108 log.Printf("[ERROR] handleQuery: webView is nil")
109 return
110 }
111 if h.browserService == nil {
112 log.Printf("[ERROR] handleQuery: browserService is nil")
113 return
114 }
115 ctx := context.Background()
116 q := msg.Q
117 if q == "" {
118 q = ""
119 }
120 limit := msg.Limit
121 if limit <= 0 {
122 limit = 10
123 }
124
125 // Build suggestions: shortcuts first, then history search
126 type suggestion struct {
127 URL string `json:"url"`
128 Favicon string `json:"favicon,omitempty"`
129 }
130
131 buildFavicon := func(raw string) string {
132 if raw == "" {
133 return ""
134 }
135 scheme := "https"
136 host := raw
137 if i := strings.Index(raw, "://"); i >= 0 {
138 if i > 0 {
139 scheme = raw[:i]
140 }
141 rest := raw[i+3:]
142 if j := strings.IndexByte(rest, '/'); j >= 0 {
143 host = rest[:j]
144 } else {
145 host = rest
146 }
147 } else {
148 if j := strings.IndexByte(raw, '/'); j >= 0 {
149 host = raw[:j]
150 }
151 }
152 if host == "" {
153 return ""
154 }
155 return scheme + "://" + host + "/favicon.ico"
156 }
157
158 expandShortcut := func(tpl string, query string) string {
159 esc := neturl.QueryEscape(query)
160 u := strings.ReplaceAll(tpl, "{query}", esc)
161 u = strings.ReplaceAll(u, "%s", esc)
162 return u
163 }
164
165 results := make([]suggestion, 0, limit)
166 seen := make(map[string]struct{}, limit*2)
167
168 // Shortcuts
169 if shortcuts, err := h.browserService.GetSearchShortcuts(ctx); err != nil {
170 log.Printf("[ERROR] Failed to get search shortcuts: %v", err)
171 } else if len(shortcuts) > 0 {
172 log.Printf("[DEBUG] Found %d search shortcuts", len(shortcuts))
173 keys := make([]string, 0, len(shortcuts))
174 for k := range shortcuts {
175 keys = append(keys, k)
176 }
177 sort.Strings(keys)
178 for _, k := range keys {
179 sc := shortcuts[k]
180 url := expandShortcut(sc.URL, q)
181 log.Printf("[DEBUG] Shortcut %s -> %s", k, url)
182 if url == "" {
183 continue
184 }
185 if _, ok := seen[url]; ok {
186 continue
187 }
188 results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)})
189 seen[url] = struct{}{}
190 if len(results) >= limit {
191 break
192 }
193 }
194 } else {
195 log.Printf("[DEBUG] No search shortcuts found")
196 }
197
198 // History
199 if len(results) < limit {
200 remaining := limit - len(results)
201 log.Printf("[DEBUG] Searching history for '%s' with limit %d", q, remaining)
202 if entries, err := h.browserService.SearchHistory(ctx, q, remaining); err != nil {
203 log.Printf("[ERROR] Failed to search history: %v", err)
204 } else {
205 log.Printf("[DEBUG] Found %d history entries", len(entries))
206 for i, e := range entries {
207 // JSON roundtrip to map to get url field agnostic of struct tag
208 bb, _ := json.Marshal(e)
209 var m map[string]any
210 _ = json.Unmarshal(bb, &m)
211 var url string
212 if s, ok := m["url"].(string); ok {
213 url = s
214 } else if s, ok := m["URL"].(string); ok {
215 url = s
216 }
217 log.Printf("[DEBUG] History entry %d: url=%s", i, url)
218 if url == "" {
219 continue
220 }
221 if _, ok := seen[url]; ok {
222 continue
223 }
224 results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)})
225 seen[url] = struct{}{}
226 if len(results) >= limit {
227 break
228 }
229 }
230 }
231 }
232
233 // Inject back to GUI
234 log.Printf("[DEBUG] Final results count: %d", len(results))
235 if b, err := json.Marshal(results); err != nil {
236 log.Printf("[ERROR] Failed to marshal results: %v", err)
237 } else {
238 // Prefer unified page-world API; fallback to legacy global function
239 script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))"
240 log.Printf("[DEBUG] Injecting omnibox suggestions: %s", script)
241 if injErr := h.webView.InjectScript(script); injErr != nil {
242 log.Printf("[ERROR] Failed to inject suggestions script: %v", injErr)
243 } else {
244 log.Printf("[DEBUG] Successfully injected omnibox suggestions")
245 }
246 }
247}
248
249// handleWailsBridge processes Wails runtime bridge calls for homepage
250func (h *Handler) handleWailsBridge(msg Message) {
251 // Payload contains { methodID, methodName?, args }
252 var p struct {
253 MethodID uint32 `json:"methodID"`
254 MethodName string `json:"methodName"`
255 Args json.RawMessage `json:"args"`
256 }
257 if err := json.Unmarshal(msg.Payload, &p); err != nil {
258 return
259 }
260
261 // Only implement the IDs we need
262 switch p.MethodID {
263 case constants.HashGetRecentHistory: // BrowserService.GetRecentHistory(limit)
264 var args []interface{}
265 _ = json.Unmarshal(p.Args, &args)
266 limit := 50
267 if len(args) > 0 {
268 if f, ok := args[0].(float64); ok {
269 limit = int(f)
270 }
271 }
272 ctx := context.Background()
273 entries, err := h.browserService.GetRecentHistory(ctx, limit)
274 if err != nil {
275 return
276 }
277 resp, _ := json.Marshal(entries)
278 if h.webView != nil {
279 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
280 }
281 case constants.HashGetSearchShortcuts: // BrowserService.GetSearchShortcuts()
282 ctx := context.Background()
283 shortcuts, err := h.browserService.GetSearchShortcuts(ctx)
284 if err != nil {
285 return
286 }
287 resp, _ := json.Marshal(shortcuts)
288 if h.webView != nil {
289 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', " + string(resp) + ")")
290 }
291 default:
292 // Return empty JSON to avoid breaking UI
293 if h.webView != nil {
294 _ = h.webView.InjectScript("window.__dumber_wails_resolve('" + msg.ID + "', '{}')")
295 }
296 }
297}
298
299// handleTheme processes theme-related messages
300func (h *Handler) handleTheme(msg Message) {
301 if msg.Value != "" && msg.Value != h.lastTheme {
302 log.Printf("[theme] color-scheme changed: %s", msg.Value)
303 h.lastTheme = msg.Value
304 }
305}
306
307// handleHistoryRecent processes recent history requests
308func (h *Handler) handleHistoryRecent(msg Message) {
309 if h.browserService == nil || h.webView == nil {
310 return
311 }
312
313 ctx := context.Background()
314 limit := msg.Limit
315 if limit <= 0 {
316 limit = 20
317 }
318 offset := msg.Offset
319
320 var entries []interface{}
321 var err error
322
323 // Use paginated version if offset is provided
324 if offset > 0 {
325 histEntries, e := h.browserService.GetRecentHistoryWithOffset(ctx, limit, offset)
326 if e != nil {
327 err = e
328 } else {
329 entries = make([]interface{}, len(histEntries))
330 for i, entry := range histEntries {
331 entries[i] = entry
332 }
333 }
334 } else {
335 // Use original method for backward compatibility
336 histEntries, e := h.browserService.GetRecentHistory(ctx, limit)
337 if e != nil {
338 err = e
339 } else {
340 entries = make([]interface{}, len(histEntries))
341 for i, entry := range histEntries {
342 entries[i] = entry
343 }
344 }
345 }
346
347 if err != nil {
348 log.Printf("[messaging] Failed to get recent history: %v", err)
349 requestIdParam := ""
350 if msg.RequestID != "" {
351 requestIdParam = ", '" + msg.RequestID + "'"
352 }
353 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load recent history'" + requestIdParam + ")")
354 return
355 }
356
357 b, _ := json.Marshal(entries)
358 requestIdParam := ""
359 if msg.RequestID != "" {
360 requestIdParam = ", '" + msg.RequestID + "'"
361 }
362 _ = h.webView.InjectScript("window.__dumber_history_recent && window.__dumber_history_recent(" + string(b) + requestIdParam + ")")
363}
364
365// handleHistoryStats processes history stats requests
366func (h *Handler) handleHistoryStats(msg Message) {
367 if h.browserService == nil || h.webView == nil {
368 return
369 }
370
371 ctx := context.Background()
372 stats, err := h.browserService.GetHistoryStats(ctx)
373 if err != nil {
374 log.Printf("[messaging] Failed to get history stats: %v", err)
375 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load history stats')")
376 return
377 }
378
379 b, _ := json.Marshal(stats)
380 _ = h.webView.InjectScript("window.__dumber_history_stats && window.__dumber_history_stats(" + string(b) + ")")
381}
382
383// handleHistorySearch processes history search requests
384func (h *Handler) handleHistorySearch(msg Message) {
385 if h.browserService == nil || h.webView == nil {
386 return
387 }
388
389 ctx := context.Background()
390 query := msg.Q
391 limit := msg.Limit
392 if limit <= 0 {
393 limit = 5
394 }
395
396 entries, err := h.browserService.SearchHistory(ctx, query, limit)
397 if err != nil {
398 log.Printf("[messaging] Failed to search history: %v", err)
399 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to search history')")
400 return
401 }
402
403 b, _ := json.Marshal(entries)
404 _ = h.webView.InjectScript("window.__dumber_history_search && window.__dumber_history_search(" + string(b) + ")")
405}
406
407// handleHistoryDelete processes history deletion requests
408func (h *Handler) handleHistoryDelete(msg Message) {
409 if h.browserService == nil || h.webView == nil {
410 return
411 }
412
413 if msg.HistoryID == "" {
414 log.Printf("[messaging] History delete: missing historyId")
415 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Missing history ID')")
416 return
417 }
418
419 // Convert string ID to int64
420 id, err := strconv.ParseInt(msg.HistoryID, 10, 64)
421 if err != nil {
422 log.Printf("[messaging] History delete: invalid ID format: %v", err)
423 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Invalid history ID format')")
424 return
425 }
426
427 ctx := context.Background()
428 err = h.browserService.DeleteHistoryEntry(ctx, id)
429 if err != nil {
430 log.Printf("[messaging] Failed to delete history entry: %v", err)
431 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to delete history entry')")
432 return
433 }
434
435 // Send success response with the deleted ID
436 successData := map[string]string{"deletedId": msg.HistoryID}
437 b, _ := json.Marshal(successData)
438 _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")")
439}