feat(messaging): expand webkit message bridge with unified handler system
- commit
- 3f99708148c974bad5658e72039306fe51f80616
- author
- bamen · · 2025-09-15 16:28 UTC
- committer
- bamen · · 2025-09-15 17:26 UTC
- parent
- 4bd9f0f
- signature
- unsigned
3 files · +413 -36
M internal/app/browser/webview.go +1 -0
| @@ -60,6 +60,7 @@ func (app *BrowserApp) createWebView() error { | |||
| 60 | 60 | EnableDeveloperExtras: true, | |
| 61 | 61 | DataDir: webkitData, | |
| 62 | 62 | CacheDir: webkitCache, | |
| 63 | + | APIToken: app.config.APISecurity.Token, | |
| 63 | 64 | DefaultSansFont: app.config.Appearance.SansFont, | |
| 64 | 65 | DefaultSerifFont: app.config.Appearance.SerifFont, | |
| 65 | 66 | DefaultMonospaceFont: app.config.Appearance.MonospaceFont, | |
| @@ -60,6 +60,7 @@ func (app *BrowserApp) createWebView() error { | |||||
| 60 | EnableDeveloperExtras: true, | 60 | EnableDeveloperExtras: true, | ||
| 61 | DataDir: webkitData, | 61 | DataDir: webkitData, | ||
| 62 | CacheDir: webkitCache, | 62 | CacheDir: webkitCache, | ||
| 63 | + | APIToken: app.config.APISecurity.Token, | |||
| 63 | DefaultSansFont: app.config.Appearance.SansFont, | 64 | DefaultSansFont: app.config.Appearance.SansFont, | ||
| 64 | DefaultSerifFont: app.config.Appearance.SerifFont, | 65 | DefaultSerifFont: app.config.Appearance.SerifFont, | ||
| 65 | DefaultMonospaceFont: app.config.Appearance.MonospaceFont, | 66 | DefaultMonospaceFont: app.config.Appearance.MonospaceFont, | ||
M internal/app/messaging/handler.go +284 -29
| @@ -5,6 +5,9 @@ import ( | |||
| 5 | 5 | "encoding/json" | |
| 6 | 6 | "log" | |
| 7 | 7 | neturl "net/url" | |
| 8 | + | "sort" | |
| 9 | + | "strconv" | |
| 10 | + | "strings" | |
| 8 | 11 | ||
| 9 | 12 | "github.com/bnema/dumber/internal/app/constants" | |
| 10 | 13 | "github.com/bnema/dumber/pkg/webkit" | |
| @@ -21,11 +24,16 @@ type Handler struct { | |||
| 21 | 24 | ||
| 22 | 25 | // Message represents a script message from the WebView | |
| 23 | 26 | type 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"` | |
| 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"` | |
| 29 | 37 | // Wails fetch bridge | |
| 30 | 38 | ID string `json:"id"` | |
| 31 | 39 | Payload json.RawMessage `json:"payload"` | |
| @@ -41,20 +49,32 @@ func NewHandler(parserService *services.ParserService, browserService *services. | |||
| 41 | 49 | ||
| 42 | 50 | // Handle processes incoming script messages | |
| 43 | 51 | func (h *Handler) Handle(payload string) { | |
| 52 | + | log.Printf("[DEBUG] Received message: %s", payload) | |
| 44 | 53 | var msg Message | |
| 45 | 54 | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | |
| 55 | + | log.Printf("[ERROR] Failed to unmarshal message: %v", err) | |
| 46 | 56 | return | |
| 47 | 57 | } | |
| 58 | + | log.Printf("[DEBUG] Parsed message type: %s", msg.Type) | |
| 48 | 59 | ||
| 49 | 60 | switch msg.Type { | |
| 50 | 61 | case "navigate": | |
| 51 | 62 | h.handleNavigation(msg) | |
| 52 | 63 | case "query": | |
| 64 | + | log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit) | |
| 53 | 65 | h.handleQuery(msg) | |
| 54 | 66 | case "wails": | |
| 55 | 67 | h.handleWailsBridge(msg) | |
| 56 | 68 | case "theme": | |
| 57 | 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) | |
| 58 | 78 | } | |
| 59 | 79 | } | |
| 60 | 80 | ||
| @@ -80,48 +100,149 @@ func (h *Handler) handleNavigation(msg Message) { | |||
| 80 | 100 | } | |
| 81 | 101 | } | |
| 82 | 102 | ||
| 83 | - | // handleQuery processes history search queries from the frontend | |
| 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 | |
| 84 | 105 | func (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 | + | } | |
| 85 | 115 | ctx := context.Background() | |
| 116 | + | q := msg.Q | |
| 117 | + | if q == "" { | |
| 118 | + | q = "" | |
| 119 | + | } | |
| 86 | 120 | limit := msg.Limit | |
| 87 | - | if limit <= 0 || limit > 25 { | |
| 121 | + | if limit <= 0 { | |
| 88 | 122 | limit = 10 | |
| 89 | 123 | } | |
| 90 | - | entries, err := h.browserService.SearchHistory(ctx, msg.Q, limit) | |
| 91 | - | if err != nil { | |
| 92 | - | return | |
| 93 | - | } | |
| 94 | 124 | ||
| 95 | - | // Map to lightweight items for frontend | |
| 96 | - | type item struct { | |
| 125 | + | // Build suggestions: shortcuts first, then history search | |
| 126 | + | type suggestion struct { | |
| 97 | 127 | URL string `json:"url"` | |
| 98 | - | Favicon string `json:"favicon"` | |
| 128 | + | Favicon string `json:"favicon,omitempty"` | |
| 99 | 129 | } | |
| 100 | 130 | ||
| 101 | 131 | buildFavicon := func(raw string) string { | |
| 102 | - | u, err := h.parserService.ParseInput(ctx, raw) | |
| 103 | - | if err != nil || u.URL == "" { | |
| 132 | + | if raw == "" { | |
| 104 | 133 | return "" | |
| 105 | 134 | } | |
| 106 | - | parsed, perr := neturl.Parse(u.URL) | |
| 107 | - | if perr != nil || parsed.Host == "" { | |
| 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 == "" { | |
| 108 | 153 | return "" | |
| 109 | 154 | } | |
| 110 | - | scheme := parsed.Scheme | |
| 111 | - | if scheme == "" { | |
| 112 | - | scheme = "https" | |
| 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) | |
| 113 | 176 | } | |
| 114 | - | return scheme + "://" + parsed.Host + "/favicon.ico" | |
| 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") | |
| 115 | 196 | } | |
| 116 | 197 | ||
| 117 | - | items := make([]item, 0, len(entries)) | |
| 118 | - | for _, e := range entries { | |
| 119 | - | items = append(items, item{URL: e.URL, Favicon: buildFavicon(e.URL)}) | |
| 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 | + | } | |
| 120 | 231 | } | |
| 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) + ")") | |
| 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 | + | } | |
| 125 | 246 | } | |
| 126 | 247 | } | |
| 127 | 248 | ||
| @@ -182,3 +303,137 @@ func (h *Handler) handleTheme(msg Message) { | |||
| 182 | 303 | h.lastTheme = msg.Value | |
| 183 | 304 | } | |
| 184 | 305 | } | |
| 306 | + | ||
| 307 | + | // handleHistoryRecent processes recent history requests | |
| 308 | + | func (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 | |
| 366 | + | func (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 | |
| 384 | + | func (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 | |
| 408 | + | func (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 | + | } | |
| @@ -5,6 +5,9 @@ import ( | |||||
| 5 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | "log" | 6 | "log" | ||
| 7 | neturl "net/url" | 7 | neturl "net/url" | ||
| 8 | + | "sort" | |||
| 9 | + | "strconv" | |||
| 10 | + | "strings" | |||
| 8 | 11 | ||||
| 9 | "github.com/bnema/dumber/internal/app/constants" | 12 | "github.com/bnema/dumber/internal/app/constants" | ||
| 10 | "github.com/bnema/dumber/pkg/webkit" | 13 | "github.com/bnema/dumber/pkg/webkit" | ||
| @@ -21,11 +24,16 @@ type Handler struct { | |||||
| 21 | 24 | ||||
| 22 | // Message represents a script message from the WebView | 25 | // Message represents a script message from the WebView | ||
| 23 | type Message struct { | 26 | type Message struct { | ||
| 24 | - | Type string `json:"type"` | 27 | + | Type string `json:"type"` |
| 25 | - | URL string `json:"url"` | 28 | + | URL string `json:"url"` |
| 26 | - | Q string `json:"q"` | 29 | + | Q string `json:"q"` |
| 27 | - | Limit int `json:"limit"` | 30 | + | Limit int `json:"limit"` |
| 28 | - | Value string `json:"value"` | 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"` | |||
| 29 | // Wails fetch bridge | 37 | // Wails fetch bridge | ||
| 30 | ID string `json:"id"` | 38 | ID string `json:"id"` | ||
| 31 | Payload json.RawMessage `json:"payload"` | 39 | Payload json.RawMessage `json:"payload"` | ||
| @@ -41,20 +49,32 @@ func NewHandler(parserService *services.ParserService, browserService *services. | |||||
| 41 | 49 | ||||
| 42 | // Handle processes incoming script messages | 50 | // Handle processes incoming script messages | ||
| 43 | func (h *Handler) Handle(payload string) { | 51 | func (h *Handler) Handle(payload string) { | ||
| 52 | + | log.Printf("[DEBUG] Received message: %s", payload) | |||
| 44 | var msg Message | 53 | var msg Message | ||
| 45 | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | 54 | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | ||
| 55 | + | log.Printf("[ERROR] Failed to unmarshal message: %v", err) | |||
| 46 | return | 56 | return | ||
| 47 | } | 57 | } | ||
| 58 | + | log.Printf("[DEBUG] Parsed message type: %s", msg.Type) | |||
| 48 | 59 | ||||
| 49 | switch msg.Type { | 60 | switch msg.Type { | ||
| 50 | case "navigate": | 61 | case "navigate": | ||
| 51 | h.handleNavigation(msg) | 62 | h.handleNavigation(msg) | ||
| 52 | case "query": | 63 | case "query": | ||
| 64 | + | log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit) | |||
| 53 | h.handleQuery(msg) | 65 | h.handleQuery(msg) | ||
| 54 | case "wails": | 66 | case "wails": | ||
| 55 | h.handleWailsBridge(msg) | 67 | h.handleWailsBridge(msg) | ||
| 56 | case "theme": | 68 | case "theme": | ||
| 57 | h.handleTheme(msg) | 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) | |||
| 58 | } | 78 | } | ||
| 59 | } | 79 | } | ||
| 60 | 80 | ||||
| @@ -80,48 +100,149 @@ func (h *Handler) handleNavigation(msg Message) { | |||||
| 80 | } | 100 | } | ||
| 81 | } | 101 | } | ||
| 82 | 102 | ||||
| 83 | - | // handleQuery processes history search queries from the frontend | 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 | |||
| 84 | func (h *Handler) handleQuery(msg Message) { | 105 | func (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 | + | } | |||
| 85 | ctx := context.Background() | 115 | ctx := context.Background() | ||
| 116 | + | q := msg.Q | |||
| 117 | + | if q == "" { | |||
| 118 | + | q = "" | |||
| 119 | + | } | |||
| 86 | limit := msg.Limit | 120 | limit := msg.Limit | ||
| 87 | - | if limit <= 0 || limit > 25 { | 121 | + | if limit <= 0 { |
| 88 | limit = 10 | 122 | limit = 10 | ||
| 89 | } | 123 | } | ||
| 90 | - | entries, err := h.browserService.SearchHistory(ctx, msg.Q, limit) | |||
| 91 | - | if err != nil { | |||
| 92 | - | return | |||
| 93 | - | } | |||
| 94 | 124 | ||||
| 95 | - | // Map to lightweight items for frontend | 125 | + | // Build suggestions: shortcuts first, then history search |
| 96 | - | type item struct { | 126 | + | type suggestion struct { |
| 97 | URL string `json:"url"` | 127 | URL string `json:"url"` | ||
| 98 | - | Favicon string `json:"favicon"` | 128 | + | Favicon string `json:"favicon,omitempty"` |
| 99 | } | 129 | } | ||
| 100 | 130 | ||||
| 101 | buildFavicon := func(raw string) string { | 131 | buildFavicon := func(raw string) string { | ||
| 102 | - | u, err := h.parserService.ParseInput(ctx, raw) | 132 | + | if raw == "" { |
| 103 | - | if err != nil || u.URL == "" { | |||
| 104 | return "" | 133 | return "" | ||
| 105 | } | 134 | } | ||
| 106 | - | parsed, perr := neturl.Parse(u.URL) | 135 | + | scheme := "https" |
| 107 | - | if perr != nil || parsed.Host == "" { | 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 == "" { | |||
| 108 | return "" | 153 | return "" | ||
| 109 | } | 154 | } | ||
| 110 | - | scheme := parsed.Scheme | 155 | + | return scheme + "://" + host + "/favicon.ico" |
| 111 | - | if scheme == "" { | 156 | + | } |
| 112 | - | scheme = "https" | 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) | |||
| 113 | } | 176 | } | ||
| 114 | - | return scheme + "://" + parsed.Host + "/favicon.ico" | 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") | |||
| 115 | } | 196 | } | ||
| 116 | 197 | ||||
| 117 | - | items := make([]item, 0, len(entries)) | 198 | + | // History |
| 118 | - | for _, e := range entries { | 199 | + | if len(results) < limit { |
| 119 | - | items = append(items, item{URL: e.URL, Favicon: buildFavicon(e.URL)}) | 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 | + | } | |||
| 120 | } | 231 | } | ||
| 121 | - | b, _ := json.Marshal(items) | 232 | + | |
| 122 | - | // Update suggestions in page | 233 | + | // Inject back to GUI |
| 123 | - | if h.webView != nil { | 234 | + | log.Printf("[DEBUG] Final results count: %d", len(results)) |
| 124 | - | _ = h.webView.InjectScript("window.__dumber_setSuggestions && window.__dumber_setSuggestions(" + string(b) + ")") | 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 | + | } | |||
| 125 | } | 246 | } | ||
| 126 | } | 247 | } | ||
| 127 | 248 | ||||
| @@ -182,3 +303,137 @@ func (h *Handler) handleTheme(msg Message) { | |||||
| 182 | h.lastTheme = msg.Value | 303 | h.lastTheme = msg.Value | ||
| 183 | } | 304 | } | ||
| 184 | } | 305 | } | ||
| 306 | + | ||||
| 307 | + | // handleHistoryRecent processes recent history requests | |||
| 308 | + | func (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 | |||
| 366 | + | func (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 | |||
| 384 | + | func (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 | |||
| 408 | + | func (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 | + | } | |||
M pkg/webkit/webview_cgo.go +128 -7
| @@ -212,6 +212,18 @@ static void register_uri_scheme_handler(WebKitWebContext* context, const char* s | |||
| 212 | 212 | if (!context || !scheme) return; | |
| 213 | 213 | webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL); | |
| 214 | 214 | } | |
| 215 | + | static void register_uri_scheme_security(WebKitWebContext* context, const char* scheme) { | |
| 216 | + | if (!context || !scheme) return; | |
| 217 | + | WebKitSecurityManager* sm = webkit_web_context_get_security_manager(context); | |
| 218 | + | if (!sm) return; | |
| 219 | + | ||
| 220 | + | // Mark as secure to avoid mixed-content warnings when requested by https pages | |
| 221 | + | webkit_security_manager_register_uri_scheme_as_secure(sm, scheme); | |
| 222 | + | // Treat as local scheme | |
| 223 | + | webkit_security_manager_register_uri_scheme_as_local(sm, scheme); | |
| 224 | + | // Allow cross-origin requests to this scheme (bypass CORS header checks for this scheme) | |
| 225 | + | webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme); | |
| 226 | + | } | |
| 215 | 227 | ||
| 216 | 228 | // Script message handler wiring for WebKitGTK 6 | |
| 217 | 229 | // Forward declaration for callback used in g_signal_connect_data | |
| @@ -238,6 +250,17 @@ static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* | |||
| 238 | 250 | return TRUE; | |
| 239 | 251 | } | |
| 240 | 252 | ||
| 253 | + | // Register a script message handler for a specific JavaScript world | |
| 254 | + | static gboolean register_ucm_handler_world(WebKitUserContentManager* ucm, const char* name, const char* world_name, unsigned long id) { | |
| 255 | + | if (!ucm || !name || !world_name) return FALSE; | |
| 256 | + | webkit_user_content_manager_register_script_message_handler(ucm, name, world_name); | |
| 257 | + | gchar* signal = g_strdup_printf("script-message-received::%s", name); | |
| 258 | + | if (!signal) return FALSE; | |
| 259 | + | g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0); | |
| 260 | + | g_free(signal); | |
| 261 | + | return TRUE; | |
| 262 | + | } | |
| 263 | + | ||
| 241 | 264 | // ----- Conditional helpers for settings across WebKit versions ----- | |
| 242 | 265 | #ifndef WEBKIT_CHECK_VERSION | |
| 243 | 266 | #define WEBKIT_CHECK_VERSION(major,minor,micro) (0) | |
| @@ -842,6 +865,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 842 | 865 | { | |
| 843 | 866 | sch := C.CString("dumb") | |
| 844 | 867 | C.register_uri_scheme_handler(ctx, sch) | |
| 868 | + | C.register_uri_scheme_security(ctx, sch) | |
| 845 | 869 | C.free(unsafe.Pointer(sch)) | |
| 846 | 870 | } | |
| 847 | 871 | // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session | |
| @@ -950,14 +974,14 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 950 | 974 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | |
| 951 | 975 | // Find in page (Ctrl/Cmd+F): use new keyboard service bridge | |
| 952 | 976 | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | |
| 953 | - | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')") | |
| 977 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))") | |
| 954 | 978 | }) | |
| 955 | 979 | // Navigation with Alt+Arrow keys: use new keyboard service bridge | |
| 956 | 980 | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | |
| 957 | - | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowleft')") | |
| 981 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowleft'}}))") | |
| 958 | 982 | }) | |
| 959 | 983 | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | |
| 960 | - | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowright')") | |
| 984 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowright'}}))") | |
| 961 | 985 | }) | |
| 962 | 986 | // Update window title when page title changes | |
| 963 | 987 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | |
| @@ -1246,7 +1270,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 1246 | 1270 | if w == nil || w.native == nil || w.native.ucm == nil { | |
| 1247 | 1271 | return | |
| 1248 | 1272 | } | |
| 1249 | - | // Register handler "dumber" | |
| 1273 | + | // Register handler "dumber" for main world | |
| 1250 | 1274 | cname := C.CString("dumber") | |
| 1251 | 1275 | ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id)) | |
| 1252 | 1276 | if ok == 0 { | |
| @@ -1254,6 +1278,16 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 1254 | 1278 | } | |
| 1255 | 1279 | C.free(unsafe.Pointer(cname)) | |
| 1256 | 1280 | ||
| 1281 | + | // Also register handler for the isolated GUI world so GUI can post messages to native | |
| 1282 | + | cworld := C.CString("dumber-gui") | |
| 1283 | + | cname2 := C.CString("dumber") | |
| 1284 | + | ok2 := C.register_ucm_handler_world(w.native.ucm, cname2, cworld, C.ulong(w.id)) | |
| 1285 | + | if ok2 == 0 { | |
| 1286 | + | log.Printf("[webkit] Failed to register UCM handler for isolated world 'dumber-gui'") | |
| 1287 | + | } | |
| 1288 | + | C.free(unsafe.Pointer(cworld)) | |
| 1289 | + | C.free(unsafe.Pointer(cname2)) | |
| 1290 | + | ||
| 1257 | 1291 | // Inject color-scheme preference script at document-start to inform sites of system theme | |
| 1258 | 1292 | preferDark := C.gtk_prefers_dark() != 0 | |
| 1259 | 1293 | if preferDark { | |
| @@ -1285,15 +1319,102 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 1285 | 1319 | cGui := C.CString(guiScript) | |
| 1286 | 1320 | defer C.free(unsafe.Pointer(cGui)) | |
| 1287 | 1321 | ||
| 1288 | - | log.Printf("[webkit] Creating GUI user script for document-start injection in main world") | |
| 1289 | - | guiUserScript := C.webkit_user_script_new((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1322 | + | log.Printf("[webkit] Creating GUI user script for document-start injection in isolated world 'dumber-gui'") | |
| 1323 | + | world := C.CString("dumber-gui") | |
| 1324 | + | defer C.free(unsafe.Pointer(world)) | |
| 1325 | + | guiUserScript := C.webkit_user_script_new_for_world((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil) | |
| 1290 | 1326 | if guiUserScript != nil { | |
| 1291 | 1327 | C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript) | |
| 1292 | 1328 | C.webkit_user_script_unref(guiUserScript) | |
| 1293 | - | log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in main world") | |
| 1329 | + | log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in isolated world") | |
| 1294 | 1330 | } else { | |
| 1295 | 1331 | log.Printf("[webkit] ERROR: Failed to create GUI user script") | |
| 1296 | 1332 | } | |
| 1333 | + | ||
| 1334 | + | // Inject a unified page-world bridge to dispatch standard events and expose unified API | |
| 1335 | + | bridge := `(() => { try { | |
| 1336 | + | if (window.__dumber_page_bridge_installed) return; | |
| 1337 | + | window.__dumber_page_bridge_installed = true; | |
| 1338 | + | // Ensure unified API object | |
| 1339 | + | window.__dumber = window.__dumber || {}; | |
| 1340 | + | // Toast namespace | |
| 1341 | + | window.__dumber.toast = window.__dumber.toast || { | |
| 1342 | + | show: function(message, duration, type) { | |
| 1343 | + | try { | |
| 1344 | + | document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } })); | |
| 1345 | + | // Legacy compatibility | |
| 1346 | + | document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } })); | |
| 1347 | + | } catch(e) { /* ignore */ } | |
| 1348 | + | }, | |
| 1349 | + | zoom: function(level) { | |
| 1350 | + | try { | |
| 1351 | + | document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } })); | |
| 1352 | + | // Legacy compatibility | |
| 1353 | + | document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } })); | |
| 1354 | + | } catch(e) { /* ignore */ } | |
| 1355 | + | } | |
| 1356 | + | }; | |
| 1357 | + | // Back-compat global helpers | |
| 1358 | + | window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); }; | |
| 1359 | + | window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); }; | |
| 1360 | + | ||
| 1361 | + | // Omnibox suggestions bridge (with queue before ready) | |
| 1362 | + | let __omniboxQueue = []; | |
| 1363 | + | let __omniboxReady = false; | |
| 1364 | + | function __omniboxDispatch(suggestions) { | |
| 1365 | + | try { | |
| 1366 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } })); | |
| 1367 | + | // Legacy compatibility event name | |
| 1368 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } })); | |
| 1369 | + | } catch (e) { /* ignore */ } | |
| 1370 | + | } | |
| 1371 | + | window.__dumber_omnibox_suggestions = function(suggestions) { | |
| 1372 | + | if (__omniboxReady) { | |
| 1373 | + | __omniboxDispatch(suggestions); | |
| 1374 | + | } else { | |
| 1375 | + | try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ } | |
| 1376 | + | } | |
| 1377 | + | }; | |
| 1378 | + | document.addEventListener('dumber:omnibox-ready', function() { | |
| 1379 | + | __omniboxReady = true; | |
| 1380 | + | if (__omniboxQueue && __omniboxQueue.length) { | |
| 1381 | + | const items = __omniboxQueue.slice(); | |
| 1382 | + | __omniboxQueue.length = 0; | |
| 1383 | + | for (const s of items) __omniboxDispatch(s); | |
| 1384 | + | } | |
| 1385 | + | }); | |
| 1386 | + | // Unified omnibox API for page-world callers | |
| 1387 | + | window.__dumber.omnibox = window.__dumber.omnibox || { | |
| 1388 | + | suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); } | |
| 1389 | + | }; | |
| 1390 | + | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | |
| 1391 | + | cBridge := C.CString(bridge) | |
| 1392 | + | defer C.free(unsafe.Pointer(cBridge)) | |
| 1393 | + | bridgeScript := C.webkit_user_script_new((*C.gchar)(cBridge), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1394 | + | if bridgeScript != nil { | |
| 1395 | + | C.webkit_user_content_manager_add_script(w.native.ucm, bridgeScript) | |
| 1396 | + | C.webkit_user_script_unref(bridgeScript) | |
| 1397 | + | log.Printf("[webkit] Page-world toast bridge injected") | |
| 1398 | + | } | |
| 1399 | + | ||
| 1400 | + | // If an API token is provided, inject a fetch wrapper in the isolated GUI world | |
| 1401 | + | if cfg.APIToken != "" { | |
| 1402 | + | wrapper := "(() => { try {" + | |
| 1403 | + | "const TOKEN='" + cfg.APIToken + "';" + | |
| 1404 | + | "const appendToken=(u)=>{ try { let url; try { url = new URL(u); } catch(_) { url = new URL(u, 'dumb://homepage'); } if(url.protocol==='dumb:' && url.pathname.startsWith('/api')){ url.searchParams.set('token', TOKEN); return url.toString(); } } catch(_){} return u; };" + | |
| 1405 | + | "const _fetch=window.fetch.bind(window); window.fetch=(input, init)=>{ try { if(typeof input==='string'){ input=appendToken(input); } else if(input && input.url){ const nu=appendToken(input.url); if(nu!==input.url){ input=new Request(nu, input); } } } catch(_){} return _fetch(input, init); };" + | |
| 1406 | + | "} catch(e) { console.warn('[dumber] api token wrapper failed', e); } })();" | |
| 1407 | + | cWrap := C.CString(wrapper) | |
| 1408 | + | defer C.free(unsafe.Pointer(cWrap)) | |
| 1409 | + | world := C.CString("dumber-gui") | |
| 1410 | + | defer C.free(unsafe.Pointer(world)) | |
| 1411 | + | wrapScript := C.webkit_user_script_new_for_world((*C.gchar)(cWrap), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil) | |
| 1412 | + | if wrapScript != nil { | |
| 1413 | + | C.webkit_user_content_manager_add_script(w.native.ucm, wrapScript) | |
| 1414 | + | C.webkit_user_script_unref(wrapScript) | |
| 1415 | + | log.Printf("[webkit] API token fetch wrapper injected in isolated GUI world") | |
| 1416 | + | } | |
| 1417 | + | } | |
| 1297 | 1418 | } else { | |
| 1298 | 1419 | log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err) | |
| 1299 | 1420 | } | |
| @@ -212,6 +212,18 @@ static void register_uri_scheme_handler(WebKitWebContext* context, const char* s | |||||
| 212 | if (!context || !scheme) return; | 212 | if (!context || !scheme) return; | ||
| 213 | webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL); | 213 | webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL); | ||
| 214 | } | 214 | } | ||
| 215 | + | static void register_uri_scheme_security(WebKitWebContext* context, const char* scheme) { | |||
| 216 | + | if (!context || !scheme) return; | |||
| 217 | + | WebKitSecurityManager* sm = webkit_web_context_get_security_manager(context); | |||
| 218 | + | if (!sm) return; | |||
| 219 | + | ||||
| 220 | + | // Mark as secure to avoid mixed-content warnings when requested by https pages | |||
| 221 | + | webkit_security_manager_register_uri_scheme_as_secure(sm, scheme); | |||
| 222 | + | // Treat as local scheme | |||
| 223 | + | webkit_security_manager_register_uri_scheme_as_local(sm, scheme); | |||
| 224 | + | // Allow cross-origin requests to this scheme (bypass CORS header checks for this scheme) | |||
| 225 | + | webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme); | |||
| 226 | + | } | |||
| 215 | 227 | ||||
| 216 | // Script message handler wiring for WebKitGTK 6 | 228 | // Script message handler wiring for WebKitGTK 6 | ||
| 217 | // Forward declaration for callback used in g_signal_connect_data | 229 | // Forward declaration for callback used in g_signal_connect_data | ||
| @@ -238,6 +250,17 @@ static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* | |||||
| 238 | return TRUE; | 250 | return TRUE; | ||
| 239 | } | 251 | } | ||
| 240 | 252 | ||||
| 253 | + | // Register a script message handler for a specific JavaScript world | |||
| 254 | + | static gboolean register_ucm_handler_world(WebKitUserContentManager* ucm, const char* name, const char* world_name, unsigned long id) { | |||
| 255 | + | if (!ucm || !name || !world_name) return FALSE; | |||
| 256 | + | webkit_user_content_manager_register_script_message_handler(ucm, name, world_name); | |||
| 257 | + | gchar* signal = g_strdup_printf("script-message-received::%s", name); | |||
| 258 | + | if (!signal) return FALSE; | |||
| 259 | + | g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0); | |||
| 260 | + | g_free(signal); | |||
| 261 | + | return TRUE; | |||
| 262 | + | } | |||
| 263 | + | ||||
| 241 | // ----- Conditional helpers for settings across WebKit versions ----- | 264 | // ----- Conditional helpers for settings across WebKit versions ----- | ||
| 242 | #ifndef WEBKIT_CHECK_VERSION | 265 | #ifndef WEBKIT_CHECK_VERSION | ||
| 243 | #define WEBKIT_CHECK_VERSION(major,minor,micro) (0) | 266 | #define WEBKIT_CHECK_VERSION(major,minor,micro) (0) | ||
| @@ -842,6 +865,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 842 | { | 865 | { | ||
| 843 | sch := C.CString("dumb") | 866 | sch := C.CString("dumb") | ||
| 844 | C.register_uri_scheme_handler(ctx, sch) | 867 | C.register_uri_scheme_handler(ctx, sch) | ||
| 868 | + | C.register_uri_scheme_security(ctx, sch) | |||
| 845 | C.free(unsafe.Pointer(sch)) | 869 | C.free(unsafe.Pointer(sch)) | ||
| 846 | } | 870 | } | ||
| 847 | // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session | 871 | // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session | ||
| @@ -950,14 +974,14 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 950 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | 974 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | ||
| 951 | // Find in page (Ctrl/Cmd+F): use new keyboard service bridge | 975 | // Find in page (Ctrl/Cmd+F): use new keyboard service bridge | ||
| 952 | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | 976 | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | ||
| 953 | - | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')") | 977 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))") |
| 954 | }) | 978 | }) | ||
| 955 | // Navigation with Alt+Arrow keys: use new keyboard service bridge | 979 | // Navigation with Alt+Arrow keys: use new keyboard service bridge | ||
| 956 | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | 980 | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | ||
| 957 | - | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowleft')") | 981 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowleft'}}))") |
| 958 | }) | 982 | }) | ||
| 959 | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | 983 | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | ||
| 960 | - | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowright')") | 984 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowright'}}))") |
| 961 | }) | 985 | }) | ||
| 962 | // Update window title when page title changes | 986 | // Update window title when page title changes | ||
| 963 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | 987 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | ||
| @@ -1246,7 +1270,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 1246 | if w == nil || w.native == nil || w.native.ucm == nil { | 1270 | if w == nil || w.native == nil || w.native.ucm == nil { | ||
| 1247 | return | 1271 | return | ||
| 1248 | } | 1272 | } | ||
| 1249 | - | // Register handler "dumber" | 1273 | + | // Register handler "dumber" for main world |
| 1250 | cname := C.CString("dumber") | 1274 | cname := C.CString("dumber") | ||
| 1251 | ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id)) | 1275 | ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id)) | ||
| 1252 | if ok == 0 { | 1276 | if ok == 0 { | ||
| @@ -1254,6 +1278,16 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 1254 | } | 1278 | } | ||
| 1255 | C.free(unsafe.Pointer(cname)) | 1279 | C.free(unsafe.Pointer(cname)) | ||
| 1256 | 1280 | ||||
| 1281 | + | // Also register handler for the isolated GUI world so GUI can post messages to native | |||
| 1282 | + | cworld := C.CString("dumber-gui") | |||
| 1283 | + | cname2 := C.CString("dumber") | |||
| 1284 | + | ok2 := C.register_ucm_handler_world(w.native.ucm, cname2, cworld, C.ulong(w.id)) | |||
| 1285 | + | if ok2 == 0 { | |||
| 1286 | + | log.Printf("[webkit] Failed to register UCM handler for isolated world 'dumber-gui'") | |||
| 1287 | + | } | |||
| 1288 | + | C.free(unsafe.Pointer(cworld)) | |||
| 1289 | + | C.free(unsafe.Pointer(cname2)) | |||
| 1290 | + | ||||
| 1257 | // Inject color-scheme preference script at document-start to inform sites of system theme | 1291 | // Inject color-scheme preference script at document-start to inform sites of system theme | ||
| 1258 | preferDark := C.gtk_prefers_dark() != 0 | 1292 | preferDark := C.gtk_prefers_dark() != 0 | ||
| 1259 | if preferDark { | 1293 | if preferDark { | ||
| @@ -1285,15 +1319,102 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 1285 | cGui := C.CString(guiScript) | 1319 | cGui := C.CString(guiScript) | ||
| 1286 | defer C.free(unsafe.Pointer(cGui)) | 1320 | defer C.free(unsafe.Pointer(cGui)) | ||
| 1287 | 1321 | ||||
| 1288 | - | log.Printf("[webkit] Creating GUI user script for document-start injection in main world") | 1322 | + | log.Printf("[webkit] Creating GUI user script for document-start injection in isolated world 'dumber-gui'") |
| 1289 | - | guiUserScript := C.webkit_user_script_new((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | 1323 | + | world := C.CString("dumber-gui") |
| 1324 | + | defer C.free(unsafe.Pointer(world)) | |||
| 1325 | + | guiUserScript := C.webkit_user_script_new_for_world((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil) | |||
| 1290 | if guiUserScript != nil { | 1326 | if guiUserScript != nil { | ||
| 1291 | C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript) | 1327 | C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript) | ||
| 1292 | C.webkit_user_script_unref(guiUserScript) | 1328 | C.webkit_user_script_unref(guiUserScript) | ||
| 1293 | - | log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in main world") | 1329 | + | log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in isolated world") |
| 1294 | } else { | 1330 | } else { | ||
| 1295 | log.Printf("[webkit] ERROR: Failed to create GUI user script") | 1331 | log.Printf("[webkit] ERROR: Failed to create GUI user script") | ||
| 1296 | } | 1332 | } | ||
| 1333 | + | ||||
| 1334 | + | // Inject a unified page-world bridge to dispatch standard events and expose unified API | |||
| 1335 | + | bridge := `(() => { try { | |||
| 1336 | + | if (window.__dumber_page_bridge_installed) return; | |||
| 1337 | + | window.__dumber_page_bridge_installed = true; | |||
| 1338 | + | // Ensure unified API object | |||
| 1339 | + | window.__dumber = window.__dumber || {}; | |||
| 1340 | + | // Toast namespace | |||
| 1341 | + | window.__dumber.toast = window.__dumber.toast || { | |||
| 1342 | + | show: function(message, duration, type) { | |||
| 1343 | + | try { | |||
| 1344 | + | document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } })); | |||
| 1345 | + | // Legacy compatibility | |||
| 1346 | + | document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } })); | |||
| 1347 | + | } catch(e) { /* ignore */ } | |||
| 1348 | + | }, | |||
| 1349 | + | zoom: function(level) { | |||
| 1350 | + | try { | |||
| 1351 | + | document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } })); | |||
| 1352 | + | // Legacy compatibility | |||
| 1353 | + | document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } })); | |||
| 1354 | + | } catch(e) { /* ignore */ } | |||
| 1355 | + | } | |||
| 1356 | + | }; | |||
| 1357 | + | // Back-compat global helpers | |||
| 1358 | + | window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); }; | |||
| 1359 | + | window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); }; | |||
| 1360 | + | ||||
| 1361 | + | // Omnibox suggestions bridge (with queue before ready) | |||
| 1362 | + | let __omniboxQueue = []; | |||
| 1363 | + | let __omniboxReady = false; | |||
| 1364 | + | function __omniboxDispatch(suggestions) { | |||
| 1365 | + | try { | |||
| 1366 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } })); | |||
| 1367 | + | // Legacy compatibility event name | |||
| 1368 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } })); | |||
| 1369 | + | } catch (e) { /* ignore */ } | |||
| 1370 | + | } | |||
| 1371 | + | window.__dumber_omnibox_suggestions = function(suggestions) { | |||
| 1372 | + | if (__omniboxReady) { | |||
| 1373 | + | __omniboxDispatch(suggestions); | |||
| 1374 | + | } else { | |||
| 1375 | + | try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ } | |||
| 1376 | + | } | |||
| 1377 | + | }; | |||
| 1378 | + | document.addEventListener('dumber:omnibox-ready', function() { | |||
| 1379 | + | __omniboxReady = true; | |||
| 1380 | + | if (__omniboxQueue && __omniboxQueue.length) { | |||
| 1381 | + | const items = __omniboxQueue.slice(); | |||
| 1382 | + | __omniboxQueue.length = 0; | |||
| 1383 | + | for (const s of items) __omniboxDispatch(s); | |||
| 1384 | + | } | |||
| 1385 | + | }); | |||
| 1386 | + | // Unified omnibox API for page-world callers | |||
| 1387 | + | window.__dumber.omnibox = window.__dumber.omnibox || { | |||
| 1388 | + | suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); } | |||
| 1389 | + | }; | |||
| 1390 | + | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | |||
| 1391 | + | cBridge := C.CString(bridge) | |||
| 1392 | + | defer C.free(unsafe.Pointer(cBridge)) | |||
| 1393 | + | bridgeScript := C.webkit_user_script_new((*C.gchar)(cBridge), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |||
| 1394 | + | if bridgeScript != nil { | |||
| 1395 | + | C.webkit_user_content_manager_add_script(w.native.ucm, bridgeScript) | |||
| 1396 | + | C.webkit_user_script_unref(bridgeScript) | |||
| 1397 | + | log.Printf("[webkit] Page-world toast bridge injected") | |||
| 1398 | + | } | |||
| 1399 | + | ||||
| 1400 | + | // If an API token is provided, inject a fetch wrapper in the isolated GUI world | |||
| 1401 | + | if cfg.APIToken != "" { | |||
| 1402 | + | wrapper := "(() => { try {" + | |||
| 1403 | + | "const TOKEN='" + cfg.APIToken + "';" + | |||
| 1404 | + | "const appendToken=(u)=>{ try { let url; try { url = new URL(u); } catch(_) { url = new URL(u, 'dumb://homepage'); } if(url.protocol==='dumb:' && url.pathname.startsWith('/api')){ url.searchParams.set('token', TOKEN); return url.toString(); } } catch(_){} return u; };" + | |||
| 1405 | + | "const _fetch=window.fetch.bind(window); window.fetch=(input, init)=>{ try { if(typeof input==='string'){ input=appendToken(input); } else if(input && input.url){ const nu=appendToken(input.url); if(nu!==input.url){ input=new Request(nu, input); } } } catch(_){} return _fetch(input, init); };" + | |||
| 1406 | + | "} catch(e) { console.warn('[dumber] api token wrapper failed', e); } })();" | |||
| 1407 | + | cWrap := C.CString(wrapper) | |||
| 1408 | + | defer C.free(unsafe.Pointer(cWrap)) | |||
| 1409 | + | world := C.CString("dumber-gui") | |||
| 1410 | + | defer C.free(unsafe.Pointer(world)) | |||
| 1411 | + | wrapScript := C.webkit_user_script_new_for_world((*C.gchar)(cWrap), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil) | |||
| 1412 | + | if wrapScript != nil { | |||
| 1413 | + | C.webkit_user_content_manager_add_script(w.native.ucm, wrapScript) | |||
| 1414 | + | C.webkit_user_script_unref(wrapScript) | |||
| 1415 | + | log.Printf("[webkit] API token fetch wrapper injected in isolated GUI world") | |||
| 1416 | + | } | |||
| 1417 | + | } | |||
| 1297 | } else { | 1418 | } else { | ||
| 1298 | log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err) | 1419 | log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err) | ||
| 1299 | } | 1420 | } | ||