detached d923696

feat(messaging): expand webkit message bridge with unified handler system

commit
d92369678c298f7f280ab8d66c2288b8f41be4e3
author
bamen · · 2025-09-16 08:13 UTC
parent
6ad5700
signature
unsigned
3 files · +413 -36
M internal/app/browser/webview.go +1 -0
@@ -60,6 +60,7 @@ func (app *BrowserApp) createWebView() error {
6060 EnableDeveloperExtras: true,
6161 DataDir: webkitData,
6262 CacheDir: webkitCache,
63+ APIToken: app.config.APISecurity.Token,
6364 DefaultSansFont: app.config.Appearance.SansFont,
6465 DefaultSerifFont: app.config.Appearance.SerifFont,
6566 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 (
55 "encoding/json"
66 "log"
77 neturl "net/url"
8+ "sort"
9+ "strconv"
10+ "strings"
811
912 "github.com/bnema/dumber/internal/app/constants"
1013 "github.com/bnema/dumber/pkg/webkit"
@@ -21,11 +24,16 @@ type Handler struct {
2124
2225 // Message represents a script message from the WebView
2326 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"`
2937 // Wails fetch bridge
3038 ID string `json:"id"`
3139 Payload json.RawMessage `json:"payload"`
@@ -41,20 +49,32 @@ func NewHandler(parserService *services.ParserService, browserService *services.
4149
4250 // Handle processes incoming script messages
4351 func (h *Handler) Handle(payload string) {
52+ log.Printf("[DEBUG] Received message: %s", payload)
4453 var msg Message
4554 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
55+ log.Printf("[ERROR] Failed to unmarshal message: %v", err)
4656 return
4757 }
58+ log.Printf("[DEBUG] Parsed message type: %s", msg.Type)
4859
4960 switch msg.Type {
5061 case "navigate":
5162 h.handleNavigation(msg)
5263 case "query":
64+ log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit)
5365 h.handleQuery(msg)
5466 case "wails":
5567 h.handleWailsBridge(msg)
5668 case "theme":
5769 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)
5878 }
5979 }
6080
@@ -80,48 +100,149 @@ func (h *Handler) handleNavigation(msg Message) {
80100 }
81101 }
82102
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
84105 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+ }
85115 ctx := context.Background()
116+ q := msg.Q
117+ if q == "" {
118+ q = ""
119+ }
86120 limit := msg.Limit
87- if limit <= 0 || limit > 25 {
121+ if limit <= 0 {
88122 limit = 10
89123 }
90- entries, err := h.browserService.SearchHistory(ctx, msg.Q, limit)
91- if err != nil {
92- return
93- }
94124
95- // Map to lightweight items for frontend
96- type item struct {
125+ // Build suggestions: shortcuts first, then history search
126+ type suggestion struct {
97127 URL string `json:"url"`
98- Favicon string `json:"favicon"`
128+ Favicon string `json:"favicon,omitempty"`
99129 }
100130
101131 buildFavicon := func(raw string) string {
102- u, err := h.parserService.ParseInput(ctx, raw)
103- if err != nil || u.URL == "" {
132+ if raw == "" {
104133 return ""
105134 }
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 == "" {
108153 return ""
109154 }
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)
113176 }
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")
115196 }
116197
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+ }
120231 }
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+ }
125246 }
126247 }
127248
@@ -182,3 +303,137 @@ func (h *Handler) handleTheme(msg Message) {
182303 h.lastTheme = msg.Value
183304 }
184305 }
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 WebView25 // 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 bridge37 // 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 messages50 // 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 Message53 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 return56 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 frontend103+// (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.Limit120 limit := msg.Limit
87- if limit <= 0 || limit > 25 {121+ if limit <= 0 {
88 limit = 10122 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 frontend125+ // 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.Scheme155+ 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 page233+ // 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.Value303 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
212212 if (!context || !scheme) return;
213213 webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
214214 }
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+}
215227
216228 // Script message handler wiring for WebKitGTK 6
217229 // Forward declaration for callback used in g_signal_connect_data
@@ -238,6 +250,17 @@ static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char*
238250 return TRUE;
239251 }
240252
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+
241264 // ----- Conditional helpers for settings across WebKit versions -----
242265 #ifndef WEBKIT_CHECK_VERSION
243266 #define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
@@ -842,6 +865,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
842865 {
843866 sch := C.CString("dumb")
844867 C.register_uri_scheme_handler(ctx, sch)
868+ C.register_uri_scheme_security(ctx, sch)
845869 C.free(unsafe.Pointer(sch))
846870 }
847871 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
@@ -950,14 +974,14 @@ func NewWebView(cfg *Config) (*WebView, error) {
950974 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
951975 // Find in page (Ctrl/Cmd+F): use new keyboard service bridge
952976 _ = 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'}}))")
954978 })
955979 // Navigation with Alt+Arrow keys: use new keyboard service bridge
956980 _ = 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'}}))")
958982 })
959983 _ = 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'}}))")
961985 })
962986 // Update window title when page title changes
963987 C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
@@ -1246,7 +1270,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
12461270 if w == nil || w.native == nil || w.native.ucm == nil {
12471271 return
12481272 }
1249- // Register handler "dumber"
1273+ // Register handler "dumber" for main world
12501274 cname := C.CString("dumber")
12511275 ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
12521276 if ok == 0 {
@@ -1254,6 +1278,16 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
12541278 }
12551279 C.free(unsafe.Pointer(cname))
12561280
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+
12571291 // Inject color-scheme preference script at document-start to inform sites of system theme
12581292 preferDark := C.gtk_prefers_dark() != 0
12591293 if preferDark {
@@ -1285,15 +1319,102 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
12851319 cGui := C.CString(guiScript)
12861320 defer C.free(unsafe.Pointer(cGui))
12871321
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)
12901326 if guiUserScript != nil {
12911327 C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
12921328 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")
12941330 } else {
12951331 log.Printf("[webkit] ERROR: Failed to create GUI user script")
12961332 }
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+ }
12971418 } else {
12981419 log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
12991420 }
@@ -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 6228 // Script message handler wiring for WebKitGTK 6
217 // Forward declaration for callback used in g_signal_connect_data229 // 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_VERSION265 #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_session871 // 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 bridge975 // 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 bridge979 // 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 changes986 // 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 return1271 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 theme1291 // Inject color-scheme preference script at document-start to inform sites of system theme
1258 preferDark := C.gtk_prefers_dark() != 01292 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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close