detached 147e404

feat(favicon): implement WebKit native favicon detection system

- Add WebKit's built-in favicon database integration for automatic favicon detection
- Support all favicon formats (PNG, SVG, ICO) without external dependencies
- Implement binary data caching with direct PNG storage
- Add favicon URI change handlers for comprehensive coverage
- Enhance favicon cache with URL-based path resolution
- Replace legacy favicon fetching with native WebKit callbacks
commit
147e40474890557387cfa387ebb136afee771af6
author
bamen · · 2025-09-23 16:20 UTC
committer
bamen · · 2025-09-23 16:07 UTC
parent
75a1db0
signature
unsigned
5 files · +250 -49
M internal/cache/favicon.go +32 -0
@@ -73,6 +73,38 @@ func (fc *FaviconCache) CacheAsync(faviconURL string) {
7373 }()
7474 }
7575
76+// CachePNGData caches PNG favicon data directly for a URL
77+func (fc *FaviconCache) CachePNGData(pageURL string, pngData []byte) error {
78+ if pageURL == "" || len(pngData) == 0 {
79+ return fmt.Errorf("invalid input: empty URL or data")
80+ }
81+
82+ filename := fc.getFilename(pageURL)
83+ cachedPath := filepath.Join(fc.cacheDir, filename)
84+
85+ // Write PNG data directly to cache
86+ return os.WriteFile(cachedPath, pngData, 0644)
87+}
88+
89+// GetCachedPathForURL returns the cached path for a page URL (not favicon URL)
90+func (fc *FaviconCache) GetCachedPathForURL(pageURL string) string {
91+ if pageURL == "" {
92+ return ""
93+ }
94+
95+ filename := fc.getFilename(pageURL)
96+ cachedPath := filepath.Join(fc.cacheDir, filename)
97+
98+ // Check if file exists and is not too old (7 days)
99+ if info, err := os.Stat(cachedPath); err == nil {
100+ if time.Since(info.ModTime()) < 7*24*time.Hour {
101+ return cachedPath
102+ }
103+ }
104+
105+ return ""
106+}
107+
76108 // getFilename generates a safe filename for a favicon URL - always PNG for fuzzel compatibility
77109 func (fc *FaviconCache) getFilename(faviconURL string) string {
78110 hash := fmt.Sprintf("%x", md5.Sum([]byte(faviconURL)))
@@ -73,6 +73,38 @@ func (fc *FaviconCache) CacheAsync(faviconURL string) {
73 }()73 }()
74 }74 }
75 75
76+// CachePNGData caches PNG favicon data directly for a URL
77+func (fc *FaviconCache) CachePNGData(pageURL string, pngData []byte) error {
78+ if pageURL == "" || len(pngData) == 0 {
79+ return fmt.Errorf("invalid input: empty URL or data")
80+ }
81+
82+ filename := fc.getFilename(pageURL)
83+ cachedPath := filepath.Join(fc.cacheDir, filename)
84+
85+ // Write PNG data directly to cache
86+ return os.WriteFile(cachedPath, pngData, 0644)
87+}
88+
89+// GetCachedPathForURL returns the cached path for a page URL (not favicon URL)
90+func (fc *FaviconCache) GetCachedPathForURL(pageURL string) string {
91+ if pageURL == "" {
92+ return ""
93+ }
94+
95+ filename := fc.getFilename(pageURL)
96+ cachedPath := filepath.Join(fc.cacheDir, filename)
97+
98+ // Check if file exists and is not too old (7 days)
99+ if info, err := os.Stat(cachedPath); err == nil {
100+ if time.Since(info.ModTime()) < 7*24*time.Hour {
101+ return cachedPath
102+ }
103+ }
104+
105+ return ""
106+}
107+
76 // getFilename generates a safe filename for a favicon URL - always PNG for fuzzel compatibility108 // getFilename generates a safe filename for a favicon URL - always PNG for fuzzel compatibility
77 func (fc *FaviconCache) getFilename(faviconURL string) string {109 func (fc *FaviconCache) getFilename(faviconURL string) string {
78 hash := fmt.Sprintf("%x", md5.Sum([]byte(faviconURL)))110 hash := fmt.Sprintf("%x", md5.Sum([]byte(faviconURL)))
M internal/services/browser.go +73 -12
@@ -3,6 +3,7 @@ package services
33
44 import (
55 "context"
6+ "crypto/md5"
67 "database/sql"
78 "embed"
89 "errors"
@@ -90,6 +91,19 @@ func (s *BrowserService) SetWindowTitleUpdater(updater WindowTitleUpdater) {
9091 // AttachWebView connects a native WebKit WebView to this service for integration.
9192 func (s *BrowserService) AttachWebView(view *webkit.WebView) {
9293 s.webView = view
94+
95+ // Register legacy favicon change handler (binary data - fallback)
96+ view.RegisterFaviconChangedHandler(func(data []byte) {
97+ if currentURL := view.GetCurrentURL(); currentURL != "" {
98+ log.Printf("[favicon] Favicon detected for %s, data size: %d bytes", currentURL, len(data))
99+ s.handleFaviconChanged(currentURL, data)
100+ }
101+ })
102+
103+ // Register favicon URI change handler (WebKit native)
104+ view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) {
105+ s.handleFaviconURIChanged(pageURI, faviconURI)
106+ })
93107 }
94108
95109 // LoadGUIBundle loads the unified GUI bundle from assets
@@ -189,8 +203,7 @@ func (s *BrowserService) Navigate(ctx context.Context, url string) (*NavigationR
189203 fmt.Printf("Failed to record history: %v\n", err)
190204 }
191205
192- // Update favicon asynchronously after navigation
193- go s.updateFavicon(ctx, url)
206+ // Favicon detection is now handled automatically by WebKit's native favicon database
194207
195208 return &NavigationResult{
196209 URL: url,
@@ -576,11 +589,22 @@ func zoomKeyFromURL(raw string) string {
576589 // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages.
577590 func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) }
578591
579-// updateFavicon fetches and stores favicon URL for a given page URL
580-func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) {
592+
593+// handleFaviconChanged processes favicon data received from WebKit (legacy binary data method)
594+func (s *BrowserService) handleFaviconChanged(pageURL string, pngData []byte) {
595+ log.Printf("[favicon] Processing favicon for %s, data size: %d bytes", pageURL, len(pngData))
596+
597+ if len(pngData) == 0 {
598+ log.Printf("[favicon] No favicon data for %s", pageURL)
599+ return
600+ }
601+
602+ ctx := context.Background()
603+
604+ // Parse URL for validation
581605 parsedURL, err := neturl.Parse(pageURL)
582606 if err != nil {
583- return // Silently fail for invalid URLs
607+ return
584608 }
585609
586610 // Skip favicon update for localhost, file://, or special schemes
@@ -591,18 +615,55 @@ func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) {
591615 return
592616 }
593617
594- // Standard favicon location
595- faviconURL := fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host)
618+ // Cache the PNG data directly
619+ if faviconCache, err := cache.NewFaviconCache(); err == nil {
620+ if err := faviconCache.CachePNGData(pageURL, pngData); err != nil {
621+ // Log error but don't fail
622+ log.Printf("[favicon] Failed to cache favicon for %s: %v", pageURL, err)
623+ } else {
624+ log.Printf("[favicon] Successfully cached favicon for %s (%d bytes)", pageURL, len(pngData))
625+ }
626+ } else {
627+ log.Printf("[favicon] Failed to create favicon cache: %v", err)
628+ }
596629
597- // Update in database
598- faviconNullString := sql.NullString{String: faviconURL, Valid: true}
630+ // Store a reference to the cached file in the database
631+ // We use a consistent path format for the favicon URL
632+ faviconCacheURL := fmt.Sprintf("dumber://favicon/%x.png", md5.Sum([]byte(pageURL)))
633+ faviconNullString := sql.NullString{String: faviconCacheURL, Valid: true}
599634 if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {
600635 // Silently fail - favicon is not critical
636+ log.Printf("[browser] Failed to update favicon URL in database for %s: %v", pageURL, err)
637+ }
638+}
639+
640+// handleFaviconURIChanged processes favicon URI changes from WebKit's native favicon database
641+func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI string) {
642+ log.Printf("[favicon] Processing favicon URI for %s: %s", pageURL, faviconURI)
643+
644+ ctx := context.Background()
645+
646+ // Parse URL for validation
647+ parsedURL, err := neturl.Parse(pageURL)
648+ if err != nil {
649+ log.Printf("[favicon] Invalid page URL: %s", pageURL)
601650 return
602651 }
603652
604- // Also cache the favicon for dmenu use
605- if faviconCache, err := cache.NewFaviconCache(); err == nil {
606- faviconCache.CacheAsync(faviconURL)
653+ // Skip favicon update for localhost, file://, or special schemes
654+ if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
655+ return
656+ }
657+ if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" {
658+ return
659+ }
660+
661+ // Store the favicon URI directly in the database
662+ // The frontend will handle SVG vs PNG/ICO detection
663+ faviconNullString := sql.NullString{String: faviconURI, Valid: true}
664+ if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {
665+ log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err)
666+ } else {
667+ log.Printf("[browser] Successfully stored favicon URI for %s: %s", pageURL, faviconURI)
607668 }
608669 }
@@ -3,6 +3,7 @@ package services
3 3
4 import (4 import (
5 "context"5 "context"
6+ "crypto/md5"
6 "database/sql"7 "database/sql"
7 "embed"8 "embed"
8 "errors"9 "errors"
@@ -90,6 +91,19 @@ func (s *BrowserService) SetWindowTitleUpdater(updater WindowTitleUpdater) {
90 // AttachWebView connects a native WebKit WebView to this service for integration.91 // AttachWebView connects a native WebKit WebView to this service for integration.
91 func (s *BrowserService) AttachWebView(view *webkit.WebView) {92 func (s *BrowserService) AttachWebView(view *webkit.WebView) {
92 s.webView = view93 s.webView = view
94+
95+ // Register legacy favicon change handler (binary data - fallback)
96+ view.RegisterFaviconChangedHandler(func(data []byte) {
97+ if currentURL := view.GetCurrentURL(); currentURL != "" {
98+ log.Printf("[favicon] Favicon detected for %s, data size: %d bytes", currentURL, len(data))
99+ s.handleFaviconChanged(currentURL, data)
100+ }
101+ })
102+
103+ // Register favicon URI change handler (WebKit native)
104+ view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) {
105+ s.handleFaviconURIChanged(pageURI, faviconURI)
106+ })
93 }107 }
94 108
95 // LoadGUIBundle loads the unified GUI bundle from assets109 // LoadGUIBundle loads the unified GUI bundle from assets
@@ -189,8 +203,7 @@ func (s *BrowserService) Navigate(ctx context.Context, url string) (*NavigationR
189 fmt.Printf("Failed to record history: %v\n", err)203 fmt.Printf("Failed to record history: %v\n", err)
190 }204 }
191 205
192- // Update favicon asynchronously after navigation206+ // Favicon detection is now handled automatically by WebKit's native favicon database
193- go s.updateFavicon(ctx, url)
194 207
195 return &NavigationResult{208 return &NavigationResult{
196 URL: url,209 URL: url,
@@ -576,11 +589,22 @@ func zoomKeyFromURL(raw string) string {
576 // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages.589 // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages.
577 func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) }590 func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) }
578 591
579-// updateFavicon fetches and stores favicon URL for a given page URL592+
580-func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) {593+// handleFaviconChanged processes favicon data received from WebKit (legacy binary data method)
594+func (s *BrowserService) handleFaviconChanged(pageURL string, pngData []byte) {
595+ log.Printf("[favicon] Processing favicon for %s, data size: %d bytes", pageURL, len(pngData))
596+
597+ if len(pngData) == 0 {
598+ log.Printf("[favicon] No favicon data for %s", pageURL)
599+ return
600+ }
601+
602+ ctx := context.Background()
603+
604+ // Parse URL for validation
581 parsedURL, err := neturl.Parse(pageURL)605 parsedURL, err := neturl.Parse(pageURL)
582 if err != nil {606 if err != nil {
583- return // Silently fail for invalid URLs607+ return
584 }608 }
585 609
586 // Skip favicon update for localhost, file://, or special schemes610 // Skip favicon update for localhost, file://, or special schemes
@@ -591,18 +615,55 @@ func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) {
591 return615 return
592 }616 }
593 617
594- // Standard favicon location618+ // Cache the PNG data directly
595- faviconURL := fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host)619+ if faviconCache, err := cache.NewFaviconCache(); err == nil {
620+ if err := faviconCache.CachePNGData(pageURL, pngData); err != nil {
621+ // Log error but don't fail
622+ log.Printf("[favicon] Failed to cache favicon for %s: %v", pageURL, err)
623+ } else {
624+ log.Printf("[favicon] Successfully cached favicon for %s (%d bytes)", pageURL, len(pngData))
625+ }
626+ } else {
627+ log.Printf("[favicon] Failed to create favicon cache: %v", err)
628+ }
596 629
597- // Update in database630+ // Store a reference to the cached file in the database
598- faviconNullString := sql.NullString{String: faviconURL, Valid: true}631+ // We use a consistent path format for the favicon URL
632+ faviconCacheURL := fmt.Sprintf("dumber://favicon/%x.png", md5.Sum([]byte(pageURL)))
633+ faviconNullString := sql.NullString{String: faviconCacheURL, Valid: true}
599 if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {634 if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {
600 // Silently fail - favicon is not critical635 // Silently fail - favicon is not critical
636+ log.Printf("[browser] Failed to update favicon URL in database for %s: %v", pageURL, err)
637+ }
638+}
639+
640+// handleFaviconURIChanged processes favicon URI changes from WebKit's native favicon database
641+func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI string) {
642+ log.Printf("[favicon] Processing favicon URI for %s: %s", pageURL, faviconURI)
643+
644+ ctx := context.Background()
645+
646+ // Parse URL for validation
647+ parsedURL, err := neturl.Parse(pageURL)
648+ if err != nil {
649+ log.Printf("[favicon] Invalid page URL: %s", pageURL)
601 return650 return
602 }651 }
603 652
604- // Also cache the favicon for dmenu use653+ // Skip favicon update for localhost, file://, or special schemes
605- if faviconCache, err := cache.NewFaviconCache(); err == nil {654+ if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
606- faviconCache.CacheAsync(faviconURL)655+ return
656+ }
657+ if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" {
658+ return
659+ }
660+
661+ // Store the favicon URI directly in the database
662+ // The frontend will handle SVG vs PNG/ICO detection
663+ faviconNullString := sql.NullString{String: faviconURI, Valid: true}
664+ if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {
665+ log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err)
666+ } else {
667+ log.Printf("[browser] Successfully stored favicon URI for %s: %s", pageURL, faviconURI)
607 }668 }
608 }669 }
M pkg/webkit/webview.go +37 -16
@@ -21,22 +21,24 @@ func nextStubViewID() uintptr {
2121 // WebView represents a browser view powered by WebKit2GTK.
2222 // Methods are currently stubs returning ErrNotImplemented to satisfy TDD ordering.
2323 type WebView struct {
24- config *Config
25- visible bool
26- zoom float64
27- url string
28- destroyed bool
29- window *Window
30- msgHandler func(payload string)
31- titleHandler func(title string)
32- uriHandler func(uri string)
33- zoomHandler func(level float64)
34- popupHandler func(string) *WebView
35- closeHandler func()
36- useDomZoom bool
37- domZoomSeed float64
38- container uintptr
39- id uintptr // WebView unique identifier
24+ config *Config
25+ visible bool
26+ zoom float64
27+ url string
28+ destroyed bool
29+ window *Window
30+ msgHandler func(payload string)
31+ titleHandler func(title string)
32+ uriHandler func(uri string)
33+ faviconHandler func(data []byte)
34+ faviconURIHandler func(pageURI, faviconURI string)
35+ zoomHandler func(level float64)
36+ popupHandler func(string) *WebView
37+ closeHandler func()
38+ useDomZoom bool
39+ domZoomSeed float64
40+ container uintptr
41+ id uintptr // WebView unique identifier
4042
4143 // Window type fields (no-op in stub)
4244 windowType WindowType
@@ -112,6 +114,11 @@ func (w *WebView) Destroy() error {
112114 return nil
113115 }
114116
117+// IsDestroyed returns whether this WebView has been destroyed
118+func (w *WebView) IsDestroyed() bool {
119+ return w != nil && w.destroyed
120+}
121+
115122 // Window returns the associated native window wrapper (non-nil).
116123 func (w *WebView) Window() *Window { return w.window }
117124
@@ -182,6 +189,20 @@ func (w *WebView) dispatchURIChanged(uri string) { //nolint:unused // Called fro
182189 }
183190 }
184191
192+// RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes.
193+func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) { w.faviconHandler = cb }
194+
195+// RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI.
196+func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) {
197+ w.faviconURIHandler = cb
198+}
199+
200+func (w *WebView) dispatchFaviconChanged(data []byte) { //nolint:unused // Called from CGO WebKit callbacks
201+ if w != nil && w.faviconHandler != nil {
202+ w.faviconHandler(data)
203+ }
204+}
205+
185206 // RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
186207 func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
187208
@@ -21,22 +21,24 @@ func nextStubViewID() uintptr {
21 // WebView represents a browser view powered by WebKit2GTK.21 // WebView represents a browser view powered by WebKit2GTK.
22 // Methods are currently stubs returning ErrNotImplemented to satisfy TDD ordering.22 // Methods are currently stubs returning ErrNotImplemented to satisfy TDD ordering.
23 type WebView struct {23 type WebView struct {
24- config *Config24+ config *Config
25- visible bool25+ visible bool
26- zoom float6426+ zoom float64
27- url string27+ url string
28- destroyed bool28+ destroyed bool
29- window *Window29+ window *Window
30- msgHandler func(payload string)30+ msgHandler func(payload string)
31- titleHandler func(title string)31+ titleHandler func(title string)
32- uriHandler func(uri string)32+ uriHandler func(uri string)
33- zoomHandler func(level float64)33+ faviconHandler func(data []byte)
34- popupHandler func(string) *WebView34+ faviconURIHandler func(pageURI, faviconURI string)
35- closeHandler func()35+ zoomHandler func(level float64)
36- useDomZoom bool36+ popupHandler func(string) *WebView
37- domZoomSeed float6437+ closeHandler func()
38- container uintptr38+ useDomZoom bool
39- id uintptr // WebView unique identifier39+ domZoomSeed float64
40+ container uintptr
41+ id uintptr // WebView unique identifier
40 42
41 // Window type fields (no-op in stub)43 // Window type fields (no-op in stub)
42 windowType WindowType44 windowType WindowType
@@ -112,6 +114,11 @@ func (w *WebView) Destroy() error {
112 return nil114 return nil
113 }115 }
114 116
117+// IsDestroyed returns whether this WebView has been destroyed
118+func (w *WebView) IsDestroyed() bool {
119+ return w != nil && w.destroyed
120+}
121+
115 // Window returns the associated native window wrapper (non-nil).122 // Window returns the associated native window wrapper (non-nil).
116 func (w *WebView) Window() *Window { return w.window }123 func (w *WebView) Window() *Window { return w.window }
117 124
@@ -182,6 +189,20 @@ func (w *WebView) dispatchURIChanged(uri string) { //nolint:unused // Called fro
182 }189 }
183 }190 }
184 191
192+// RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes.
193+func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) { w.faviconHandler = cb }
194+
195+// RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI.
196+func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) {
197+ w.faviconURIHandler = cb
198+}
199+
200+func (w *WebView) dispatchFaviconChanged(data []byte) { //nolint:unused // Called from CGO WebKit callbacks
201+ if w != nil && w.faviconHandler != nil {
202+ w.faviconHandler(data)
203+ }
204+}
205+
185 // RegisterZoomChangedHandler registers a callback invoked when zoom level changes.206 // RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
186 func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }207 func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
187 208
M pkg/webkit/webview_callbacks.h +16 -0
@@ -9,6 +9,7 @@
99 extern void goOnTitleChanged(unsigned long id, char* title);
1010 extern void goOnURIChanged(unsigned long id, char* uri);
1111 extern void goOnThemeChanged(unsigned long id, int prefer_dark);
12+extern void goOnFaviconURIChanged(unsigned long id, char* page_uri, char* favicon_uri);
1213 extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
1314 extern void goQuitMainLoop();
1415
@@ -62,6 +63,21 @@ static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_
6263 }
6364 }
6465
66+static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) {
67+ (void)database;
68+ printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n",
69+ page_uri ? page_uri : "NULL", favicon_uri ? favicon_uri : "NULL", (unsigned long)user_data);
70+
71+ if (!favicon_uri || !page_uri) {
72+ printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data);
73+ return;
74+ }
75+
76+ // Pass the page URI and favicon URI to Go
77+ // WebKit manages the actual favicon data - we just need to store the URI mapping
78+ goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri);
79+}
80+
6581 // GTK4 close-request signal handler
6682 static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) {
6783 (void)window; (void)user_data;
@@ -9,6 +9,7 @@
9 extern void goOnTitleChanged(unsigned long id, char* title);9 extern void goOnTitleChanged(unsigned long id, char* title);
10 extern void goOnURIChanged(unsigned long id, char* uri);10 extern void goOnURIChanged(unsigned long id, char* uri);
11 extern void goOnThemeChanged(unsigned long id, int prefer_dark);11 extern void goOnThemeChanged(unsigned long id, int prefer_dark);
12+extern void goOnFaviconURIChanged(unsigned long id, char* page_uri, char* favicon_uri);
12 extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);13 extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
13 extern void goQuitMainLoop();14 extern void goQuitMainLoop();
14 15
@@ -62,6 +63,21 @@ static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_
62 }63 }
63 }64 }
64 65
66+static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) {
67+ (void)database;
68+ printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n",
69+ page_uri ? page_uri : "NULL", favicon_uri ? favicon_uri : "NULL", (unsigned long)user_data);
70+
71+ if (!favicon_uri || !page_uri) {
72+ printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data);
73+ return;
74+ }
75+
76+ // Pass the page URI and favicon URI to Go
77+ // WebKit manages the actual favicon data - we just need to store the URI mapping
78+ goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri);
79+}
80+
65 // GTK4 close-request signal handler81 // GTK4 close-request signal handler
66 static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) {82 static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) {
67 (void)window; (void)user_data;83 (void)window; (void)user_data;
M pkg/webkit/webview_cgo.go +92 -21
@@ -336,6 +336,28 @@ static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
336336 g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
337337 }
338338
339+static void connect_favicon_changed_with_id(GtkWidget* widget, unsigned long id) {
340+ if (!widget) return;
341+ WebKitWebView* web_view = WEBKIT_WEB_VIEW(widget);
342+ WebKitNetworkSession* network_session = webkit_web_view_get_network_session(web_view);
343+ if (network_session) {
344+ WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(network_session);
345+ if (data_manager) {
346+ WebKitFaviconDatabase* favicon_db = webkit_website_data_manager_get_favicon_database(data_manager);
347+ if (favicon_db) {
348+ g_signal_connect_data(G_OBJECT(favicon_db), "favicon-changed", G_CALLBACK(on_favicon_changed), (gpointer)id, NULL, 0);
349+ printf("[favicon-c] Connected favicon-changed signal for WebView ID %lu\n", id);
350+ } else {
351+ printf("[favicon-c] Failed to get favicon database\n");
352+ }
353+ } else {
354+ printf("[favicon-c] Failed to get website data manager for favicon signal\n");
355+ }
356+ } else {
357+ printf("[favicon-c] Failed to get network session\n");
358+ }
359+}
360+
339361 static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
340362 if (!settings) return;
341363 g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
@@ -358,6 +380,17 @@ static void register_uri_scheme_security(WebKitWebContext* context, const char*
358380 webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);
359381 }
360382
383+static void enable_favicon_database(WebKitNetworkSession* session) {
384+ if (!session) return;
385+ WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(session);
386+ if (data_manager) {
387+ webkit_website_data_manager_set_favicons_enabled(data_manager, TRUE);
388+ printf("[favicon-c] Favicon database enabled\n");
389+ } else {
390+ printf("[favicon-c] Failed to get website data manager\n");
391+ }
392+}
393+
361394 // Script message handler wiring for WebKitGTK 6
362395 // Forward declaration for callback used in g_signal_connect_data
363396 // WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
@@ -929,27 +962,29 @@ type memoryStats struct {
929962
930963 // WebView represents a browser view powered by WebKit2GTK.
931964 type WebView struct {
932- config *Config
933- zoom float64
934- url string
935- destroyed bool
936- native *nativeView
937- window *Window
938- id uintptr
939- msgHandler func(payload string)
940- titleHandler func(title string)
941- uriHandler func(uri string)
942- zoomHandler func(level float64)
943- popupHandler func(string) *WebView
944- closeHandler func()
945- memStats *memoryStats
946- gcTicker *time.Ticker
947- gcDone chan struct{}
948- tlsExceptions map[string]bool // host -> whether user allowed certificate exception
949- useDomZoom bool
950- domZoomSeed float64
951- domBridgeScript *C.WebKitUserScript
952- wasReparented bool // Track if this WebView has been reparented
965+ config *Config
966+ zoom float64
967+ url string
968+ destroyed bool
969+ native *nativeView
970+ window *Window
971+ id uintptr
972+ msgHandler func(payload string)
973+ titleHandler func(title string)
974+ uriHandler func(uri string)
975+ faviconHandler func(data []byte)
976+ faviconURIHandler func(pageURI, faviconURI string)
977+ zoomHandler func(level float64)
978+ popupHandler func(string) *WebView
979+ closeHandler func()
980+ memStats *memoryStats
981+ gcTicker *time.Ticker
982+ gcDone chan struct{}
983+ tlsExceptions map[string]bool // host -> whether user allowed certificate exception
984+ useDomZoom bool
985+ domZoomSeed float64
986+ domBridgeScript *C.WebKitUserScript
987+ wasReparented bool // Track if this WebView has been reparented
953988
954989 // Window type detection and relationships
955990 windowType WindowType
@@ -1009,6 +1044,8 @@ func NewWebView(cfg *Config) (*WebView, error) {
10091044 // WebView itself is created with a WebKitNetworkSession for persistence.
10101045 ctx := C.webkit_web_context_get_default()
10111046
1047+ // Enable favicon database - we'll do this per WebView when created since we need NetworkSession
1048+
10121049 // Apply cache model to WebContext
10131050 // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
10141051 cacheModel := 1
@@ -1156,6 +1193,13 @@ func NewWebView(cfg *Config) (*WebView, error) {
11561193 C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
11571194 // Notify URI changes to Go to keep current URL in sync
11581195 C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
1196+ // Enable favicon database for this WebView's NetworkSession
1197+ networkSession := C.webkit_web_view_get_network_session(v.native.wv)
1198+ C.enable_favicon_database(networkSession)
1199+
1200+ // Notify favicon changes to Go for caching
1201+ log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id)
1202+ C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id))
11591203 // Apply hardware acceleration and related settings based on cfg.Rendering
11601204 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
11611205 // Hardware acceleration policy (guarded by version)
@@ -1785,6 +1829,33 @@ func (w *WebView) dispatchURIChanged(uri string) {
17851829 }
17861830 }
17871831
1832+// RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes.
1833+func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) {
1834+ if w != nil {
1835+ w.faviconHandler = cb
1836+ }
1837+}
1838+
1839+// RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI.
1840+func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) {
1841+ if w != nil {
1842+ w.faviconURIHandler = cb
1843+ }
1844+}
1845+
1846+func (w *WebView) dispatchFaviconChanged(data []byte) {
1847+ if w != nil && w.faviconHandler != nil {
1848+ w.faviconHandler(data)
1849+ }
1850+}
1851+
1852+func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) {
1853+ log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI)
1854+ if w != nil && w.faviconURIHandler != nil {
1855+ w.faviconURIHandler(pageURI, faviconURI)
1856+ }
1857+}
1858+
17881859 // RunOnMainThread schedules fn to execute on the GTK main thread.
17891860 func (w *WebView) RunOnMainThread(fn func()) {
17901861 if w == nil || fn == nil {
@@ -336,6 +336,28 @@ static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
336 g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);336 g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
337 }337 }
338 338
339+static void connect_favicon_changed_with_id(GtkWidget* widget, unsigned long id) {
340+ if (!widget) return;
341+ WebKitWebView* web_view = WEBKIT_WEB_VIEW(widget);
342+ WebKitNetworkSession* network_session = webkit_web_view_get_network_session(web_view);
343+ if (network_session) {
344+ WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(network_session);
345+ if (data_manager) {
346+ WebKitFaviconDatabase* favicon_db = webkit_website_data_manager_get_favicon_database(data_manager);
347+ if (favicon_db) {
348+ g_signal_connect_data(G_OBJECT(favicon_db), "favicon-changed", G_CALLBACK(on_favicon_changed), (gpointer)id, NULL, 0);
349+ printf("[favicon-c] Connected favicon-changed signal for WebView ID %lu\n", id);
350+ } else {
351+ printf("[favicon-c] Failed to get favicon database\n");
352+ }
353+ } else {
354+ printf("[favicon-c] Failed to get website data manager for favicon signal\n");
355+ }
356+ } else {
357+ printf("[favicon-c] Failed to get network session\n");
358+ }
359+}
360+
339 static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {361 static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
340 if (!settings) return;362 if (!settings) return;
341 g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);363 g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
@@ -358,6 +380,17 @@ static void register_uri_scheme_security(WebKitWebContext* context, const char*
358 webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);380 webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);
359 }381 }
360 382
383+static void enable_favicon_database(WebKitNetworkSession* session) {
384+ if (!session) return;
385+ WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(session);
386+ if (data_manager) {
387+ webkit_website_data_manager_set_favicons_enabled(data_manager, TRUE);
388+ printf("[favicon-c] Favicon database enabled\n");
389+ } else {
390+ printf("[favicon-c] Failed to get website data manager\n");
391+ }
392+}
393+
361 // Script message handler wiring for WebKitGTK 6394 // Script message handler wiring for WebKitGTK 6
362 // Forward declaration for callback used in g_signal_connect_data395 // Forward declaration for callback used in g_signal_connect_data
363 // WebKitGTK 6 delivers JSCValue* for script-message-received::NAME396 // WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
@@ -929,27 +962,29 @@ type memoryStats struct {
929 962
930 // WebView represents a browser view powered by WebKit2GTK.963 // WebView represents a browser view powered by WebKit2GTK.
931 type WebView struct {964 type WebView struct {
932- config *Config965+ config *Config
933- zoom float64966+ zoom float64
934- url string967+ url string
935- destroyed bool968+ destroyed bool
936- native *nativeView969+ native *nativeView
937- window *Window970+ window *Window
938- id uintptr971+ id uintptr
939- msgHandler func(payload string)972+ msgHandler func(payload string)
940- titleHandler func(title string)973+ titleHandler func(title string)
941- uriHandler func(uri string)974+ uriHandler func(uri string)
942- zoomHandler func(level float64)975+ faviconHandler func(data []byte)
943- popupHandler func(string) *WebView976+ faviconURIHandler func(pageURI, faviconURI string)
944- closeHandler func()977+ zoomHandler func(level float64)
945- memStats *memoryStats978+ popupHandler func(string) *WebView
946- gcTicker *time.Ticker979+ closeHandler func()
947- gcDone chan struct{}980+ memStats *memoryStats
948- tlsExceptions map[string]bool // host -> whether user allowed certificate exception981+ gcTicker *time.Ticker
949- useDomZoom bool982+ gcDone chan struct{}
950- domZoomSeed float64983+ tlsExceptions map[string]bool // host -> whether user allowed certificate exception
951- domBridgeScript *C.WebKitUserScript984+ useDomZoom bool
952- wasReparented bool // Track if this WebView has been reparented985+ domZoomSeed float64
986+ domBridgeScript *C.WebKitUserScript
987+ wasReparented bool // Track if this WebView has been reparented
953 988
954 // Window type detection and relationships989 // Window type detection and relationships
955 windowType WindowType990 windowType WindowType
@@ -1009,6 +1044,8 @@ func NewWebView(cfg *Config) (*WebView, error) {
1009 // WebView itself is created with a WebKitNetworkSession for persistence.1044 // WebView itself is created with a WebKitNetworkSession for persistence.
1010 ctx := C.webkit_web_context_get_default()1045 ctx := C.webkit_web_context_get_default()
1011 1046
1047+ // Enable favicon database - we'll do this per WebView when created since we need NetworkSession
1048+
1012 // Apply cache model to WebContext1049 // Apply cache model to WebContext
1013 // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled1050 // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
1014 cacheModel := 11051 cacheModel := 1
@@ -1156,6 +1193,13 @@ func NewWebView(cfg *Config) (*WebView, error) {
1156 C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))1193 C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
1157 // Notify URI changes to Go to keep current URL in sync1194 // Notify URI changes to Go to keep current URL in sync
1158 C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))1195 C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
1196+ // Enable favicon database for this WebView's NetworkSession
1197+ networkSession := C.webkit_web_view_get_network_session(v.native.wv)
1198+ C.enable_favicon_database(networkSession)
1199+
1200+ // Notify favicon changes to Go for caching
1201+ log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id)
1202+ C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id))
1159 // Apply hardware acceleration and related settings based on cfg.Rendering1203 // Apply hardware acceleration and related settings based on cfg.Rendering
1160 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {1204 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1161 // Hardware acceleration policy (guarded by version)1205 // Hardware acceleration policy (guarded by version)
@@ -1785,6 +1829,33 @@ func (w *WebView) dispatchURIChanged(uri string) {
1785 }1829 }
1786 }1830 }
1787 1831
1832+// RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes.
1833+func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) {
1834+ if w != nil {
1835+ w.faviconHandler = cb
1836+ }
1837+}
1838+
1839+// RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI.
1840+func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) {
1841+ if w != nil {
1842+ w.faviconURIHandler = cb
1843+ }
1844+}
1845+
1846+func (w *WebView) dispatchFaviconChanged(data []byte) {
1847+ if w != nil && w.faviconHandler != nil {
1848+ w.faviconHandler(data)
1849+ }
1850+}
1851+
1852+func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) {
1853+ log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI)
1854+ if w != nil && w.faviconURIHandler != nil {
1855+ w.faviconURIHandler(pageURI, faviconURI)
1856+ }
1857+}
1858+
1788 // RunOnMainThread schedules fn to execute on the GTK main thread.1859 // RunOnMainThread schedules fn to execute on the GTK main thread.
1789 func (w *WebView) RunOnMainThread(fn func()) {1860 func (w *WebView) RunOnMainThread(fn func()) {
1790 if w == nil || fn == nil {1861 if w == nil || fn == nil {

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