detached 8305e33

fix(favicon): address PR review cache and href issues

commit
8305e33e4f895eeefe1b702eff85aaacd3fde1cc
author
brice · · 2026-06-07 18:39 UTC
parent
8a0ce09
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +51 -4
M internal/infrastructure/cef/favicon_discovery.go +6 -2
@@ -87,8 +87,12 @@ func faviconRel(rel string) bool {
8787 }
8888
8989 func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) {
90- icon, err := url.Parse(strings.TrimSpace(rawIconURL))
91- if err != nil || rawIconURL == "" {
90+ trimmed := strings.TrimSpace(rawIconURL)
91+ if trimmed == "" {
92+ return "", false
93+ }
94+ icon, err := url.Parse(trimmed)
95+ if err != nil {
9296 return "", false
9397 }
9498 if icon.IsAbs() {
@@ -87,8 +87,12 @@ func faviconRel(rel string) bool {
87 }87 }
88 88
89 func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) {89 func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) {
90- icon, err := url.Parse(strings.TrimSpace(rawIconURL))90+ trimmed := strings.TrimSpace(rawIconURL)
91- if err != nil || rawIconURL == "" {91+ if trimmed == "" {
92+ return "", false
93+ }
94+ icon, err := url.Parse(trimmed)
95+ if err != nil {
92 return "", false96 return "", false
93 }97 }
94 if icon.IsAbs() {98 if icon.IsAbs() {
M internal/infrastructure/cef/favicon_discovery_test.go +5 -0
@@ -45,6 +45,11 @@ func TestFaviconDiscoveryIgnoresUnsafeIconURLs(t *testing.T) {
4545 require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got)
4646 }
4747
48+func TestFaviconDiscoveryIgnoresWhitespaceOnlyHrefs(t *testing.T) {
49+ got := DiscoverFaviconCandidates("https://example.com/path", `<link rel="icon" href=" ">`)
50+ require.Equal(t, []string{"https://example.com/favicon.ico"}, got)
51+}
52+
4853 func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) {
4954 prevDecode := decodeCEFStringList
5055 decodeCEFStringList = func(list purecef.StringList) []string {
@@ -45,6 +45,11 @@ func TestFaviconDiscoveryIgnoresUnsafeIconURLs(t *testing.T) {
45 require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got)45 require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got)
46 }46 }
47 47
48+func TestFaviconDiscoveryIgnoresWhitespaceOnlyHrefs(t *testing.T) {
49+ got := DiscoverFaviconCandidates("https://example.com/path", `<link rel="icon" href=" ">`)
50+ require.Equal(t, []string{"https://example.com/favicon.ico"}, got)
51+}
52+
48 func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) {53 func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) {
49 prevDecode := decodeCEFStringList54 prevDecode := decodeCEFStringList
50 decodeCEFStringList = func(list purecef.StringList) []string {55 decodeCEFStringList = func(list purecef.StringList) []string {
M internal/ui/adapter/favicon.go +21 -2
@@ -234,7 +234,7 @@ func (a *FaviconAdapter) resolveTexture(ctx context.Context, pageURL string, sch
234234 }
235235 texture := a.textureFromBytesOnGTK(ctx, resolved.Bytes)
236236 if texture != nil {
237- a.setTexture(string(resolved.Key), texture)
237+ a.setResolvedTexture(pageURL, resolved.Key, texture)
238238 }
239239 return texture
240240 }
@@ -512,10 +512,19 @@ func (a *FaviconAdapter) Close() {
512512 }
513513 }
514514
515-// Invalidate clears one resolved-key texture cache entry.
515+// Invalidate clears one resolved-key texture cache entry and any domain aliases
516+// pointing at the same texture.
516517 func (a *FaviconAdapter) Invalidate(_ context.Context, key favicon.Key) error {
517518 a.mu.Lock()
519+ texture := a.textureCache[string(key)]
518520 delete(a.textureCache, string(key))
521+ if texture != nil {
522+ for cacheKey, cached := range a.textureCache {
523+ if cached == texture {
524+ delete(a.textureCache, cacheKey)
525+ }
526+ }
527+ }
519528 a.mu.Unlock()
520529 return nil
521530 }
@@ -535,6 +544,16 @@ func (a *FaviconAdapter) Size() int {
535544 return size
536545 }
537546
547+// setResolvedTexture stores resolver results under both the resolved favicon key
548+// and the current page domain. The domain alias keeps UI lookups fast while
549+// Invalidate removes aliases sharing the same texture during the migration path.
550+func (a *FaviconAdapter) setResolvedTexture(pageURL string, key favicon.Key, texture *gdk.Texture) {
551+ a.setTexture(string(key), texture)
552+ if domain := domainurl.ExtractDomain(pageURL); domain != "" {
553+ a.setTexture(domain, texture)
554+ }
555+}
556+
538557 // setTexture stores a texture in the cache.
539558 func (a *FaviconAdapter) setTexture(domain string, texture *gdk.Texture) {
540559 if domain == "" || texture == nil {
@@ -234,7 +234,7 @@ func (a *FaviconAdapter) resolveTexture(ctx context.Context, pageURL string, sch
234 }234 }
235 texture := a.textureFromBytesOnGTK(ctx, resolved.Bytes)235 texture := a.textureFromBytesOnGTK(ctx, resolved.Bytes)
236 if texture != nil {236 if texture != nil {
237- a.setTexture(string(resolved.Key), texture)237+ a.setResolvedTexture(pageURL, resolved.Key, texture)
238 }238 }
239 return texture239 return texture
240 }240 }
@@ -512,10 +512,19 @@ func (a *FaviconAdapter) Close() {
512 }512 }
513 }513 }
514 514
515-// Invalidate clears one resolved-key texture cache entry.515+// Invalidate clears one resolved-key texture cache entry and any domain aliases
516+// pointing at the same texture.
516 func (a *FaviconAdapter) Invalidate(_ context.Context, key favicon.Key) error {517 func (a *FaviconAdapter) Invalidate(_ context.Context, key favicon.Key) error {
517 a.mu.Lock()518 a.mu.Lock()
519+ texture := a.textureCache[string(key)]
518 delete(a.textureCache, string(key))520 delete(a.textureCache, string(key))
521+ if texture != nil {
522+ for cacheKey, cached := range a.textureCache {
523+ if cached == texture {
524+ delete(a.textureCache, cacheKey)
525+ }
526+ }
527+ }
519 a.mu.Unlock()528 a.mu.Unlock()
520 return nil529 return nil
521 }530 }
@@ -535,6 +544,16 @@ func (a *FaviconAdapter) Size() int {
535 return size544 return size
536 }545 }
537 546
547+// setResolvedTexture stores resolver results under both the resolved favicon key
548+// and the current page domain. The domain alias keeps UI lookups fast while
549+// Invalidate removes aliases sharing the same texture during the migration path.
550+func (a *FaviconAdapter) setResolvedTexture(pageURL string, key favicon.Key, texture *gdk.Texture) {
551+ a.setTexture(string(key), texture)
552+ if domain := domainurl.ExtractDomain(pageURL); domain != "" {
553+ a.setTexture(domain, texture)
554+ }
555+}
556+
538 // setTexture stores a texture in the cache.557 // setTexture stores a texture in the cache.
539 func (a *FaviconAdapter) setTexture(domain string, texture *gdk.Texture) {558 func (a *FaviconAdapter) setTexture(domain string, texture *gdk.Texture) {
540 if domain == "" || texture == nil {559 if domain == "" || texture == nil {
M internal/ui/adapter/favicon_test.go +19 -0
@@ -1,12 +1,31 @@
11 package adapter
22
33 import (
4+ "context"
45 "sync/atomic"
56 "testing"
67
8+ "github.com/bnema/dumber/internal/domain/favicon"
9+ "github.com/bnema/puregotk/v4/gdk"
710 "github.com/rs/zerolog"
811 )
912
13+func TestFaviconAdapterInvalidateRemovesResolvedTextureAliases(t *testing.T) {
14+ adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{})
15+ texture := &gdk.Texture{}
16+ adapter.setResolvedTexture("https://docs.example.com/page", favicon.Key("example.com"), texture)
17+
18+ if adapter.GetTexture("example.com") == nil || adapter.GetTexture("docs.example.com") == nil {
19+ t.Fatal("expected both resolved key and domain alias to be cached")
20+ }
21+ if err := adapter.Invalidate(context.Background(), "example.com"); err != nil {
22+ t.Fatal(err)
23+ }
24+ if adapter.GetTexture("example.com") != nil || adapter.GetTexture("docs.example.com") != nil {
25+ t.Fatal("expected invalidation to remove resolved key and alias")
26+ }
27+}
28+
1029 func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) {
1130 adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{})
1231
@@ -1,12 +1,31 @@
1 package adapter1 package adapter
2 2
3 import (3 import (
4+ "context"
4 "sync/atomic"5 "sync/atomic"
5 "testing"6 "testing"
6 7
8+ "github.com/bnema/dumber/internal/domain/favicon"
9+ "github.com/bnema/puregotk/v4/gdk"
7 "github.com/rs/zerolog"10 "github.com/rs/zerolog"
8 )11 )
9 12
13+func TestFaviconAdapterInvalidateRemovesResolvedTextureAliases(t *testing.T) {
14+ adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{})
15+ texture := &gdk.Texture{}
16+ adapter.setResolvedTexture("https://docs.example.com/page", favicon.Key("example.com"), texture)
17+
18+ if adapter.GetTexture("example.com") == nil || adapter.GetTexture("docs.example.com") == nil {
19+ t.Fatal("expected both resolved key and domain alias to be cached")
20+ }
21+ if err := adapter.Invalidate(context.Background(), "example.com"); err != nil {
22+ t.Fatal(err)
23+ }
24+ if adapter.GetTexture("example.com") != nil || adapter.GetTexture("docs.example.com") != nil {
25+ t.Fatal("expected invalidation to remove resolved key and alias")
26+ }
27+}
28+
10 func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) {29 func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) {
11 adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{})30 adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{})
12 31

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