detached 2e2d702

fix(flatpak): ensure favicon cache directory exists before saving

commit
2e2d70290b122183584dfb4fd19097584cbd0513
author
bnema · · 2026-01-06 18:31 UTC
committer
bnema · · 2026-01-06 19:04 UTC
parent
5bc9fcf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +48 -44
M internal/infrastructure/config/xdg.go +1 -0
@@ -185,6 +185,7 @@ func EnsureDirectories() error {
185185 dirs.ConfigHome,
186186 dirs.DataHome,
187187 dirs.StateHome,
188+ dirs.CacheHome,
188189 }
189190
190191 for _, dir := range directories {
@@ -185,6 +185,7 @@ func EnsureDirectories() error {
185 dirs.ConfigHome,185 dirs.ConfigHome,
186 dirs.DataHome,186 dirs.DataHome,
187 dirs.StateHome,187 dirs.StateHome,
188+ dirs.CacheHome,
188 }189 }
189 190
190 for _, dir := range directories {191 for _, dir := range directories {
M internal/infrastructure/favicon/cache.go +9 -0
@@ -218,6 +218,15 @@ func (c *Cache) Close() {
218218 })
219219 }
220220
221+// EnsureDir ensures the cache directory exists on disk.
222+// Call this before using DiskPathPNG with external save functions.
223+func (c *Cache) EnsureDir() error {
224+ if c.diskDir == "" {
225+ return nil
226+ }
227+ return os.MkdirAll(c.diskDir, diskCacheDirPerm)
228+}
229+
221230 // Clear removes all entries from the in-memory cache.
222231 func (c *Cache) Clear() {
223232 c.mu.Lock()
@@ -218,6 +218,15 @@ func (c *Cache) Close() {
218 })218 })
219 }219 }
220 220
221+// EnsureDir ensures the cache directory exists on disk.
222+// Call this before using DiskPathPNG with external save functions.
223+func (c *Cache) EnsureDir() error {
224+ if c.diskDir == "" {
225+ return nil
226+ }
227+ return os.MkdirAll(c.diskDir, diskCacheDirPerm)
228+}
229+
221 // Clear removes all entries from the in-memory cache.230 // Clear removes all entries from the in-memory cache.
222 func (c *Cache) Clear() {231 func (c *Cache) Clear() {
223 c.mu.Lock()232 c.mu.Lock()
M internal/infrastructure/favicon/service.go +6 -0
@@ -185,3 +185,9 @@ func (s *Service) EnsureInternalFaviconPNG(pngData []byte, size int) string {
185185 s.cache.WritePNGSized(InternalDomain, pngData, size)
186186 return s.DiskPathPNGSized(InternalDomain, size)
187187 }
188+
189+// EnsureCacheDir ensures the favicon cache directory exists.
190+// Call this before using DiskPathPNG with external save functions like GTK's SaveToPng.
191+func (s *Service) EnsureCacheDir() error {
192+ return s.cache.EnsureDir()
193+}
@@ -185,3 +185,9 @@ func (s *Service) EnsureInternalFaviconPNG(pngData []byte, size int) string {
185 s.cache.WritePNGSized(InternalDomain, pngData, size)185 s.cache.WritePNGSized(InternalDomain, pngData, size)
186 return s.DiskPathPNGSized(InternalDomain, size)186 return s.DiskPathPNGSized(InternalDomain, size)
187 }187 }
188+
189+// EnsureCacheDir ensures the favicon cache directory exists.
190+// Call this before using DiskPathPNG with external save functions like GTK's SaveToPng.
191+func (s *Service) EnsureCacheDir() error {
192+ return s.cache.EnsureDir()
193+}
M internal/ui/adapter/favicon.go +32 -44
@@ -166,34 +166,11 @@ func (a *FaviconAdapter) StoreFromWebKit(ctx context.Context, pageURL string, te
166166 return
167167 }
168168
169- log := logging.FromContext(ctx)
170-
171169 // Store in texture cache
172170 a.setTexture(domain, texture)
173171
174- // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread
175- // since GTK texture operations need to happen there
176- pngPath := a.service.DiskPathPNG(domain)
177- if pngPath != "" && !a.service.HasPNGOnDisk(domain) {
178- if ok := texture.SaveToPng(pngPath); !ok {
179- log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG")
180- } else {
181- log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG")
182-
183- // Create normalized sized copy for dmenu/fuzzel (async)
184- // The original PNG is now on disk, so this should succeed
185- go func() {
186- if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil {
187- log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG after save")
188- } else {
189- log.Debug().Str("domain", domain).Msg("created sized PNG after save")
190- }
191- }()
192- }
193- }
194-
195- // Ensure disk cache is populated (async, in background)
196- go a.service.EnsureDiskCache(ctx, domain)
172+ // Export as PNG and ensure disk cache
173+ a.saveFaviconToDisk(ctx, domain, texture)
197174 }
198175
199176 // StoreFromWebKitWithOrigin stores a favicon for both current URL and original URL.
@@ -205,34 +182,45 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin(
205182
206183 // Also store under original URL domain if different
207184 if originURL != "" && originURL != currentURL {
208- log := logging.FromContext(ctx)
209185 originDomain := domainurl.ExtractDomain(originURL)
210186 currentDomain := domainurl.ExtractDomain(currentURL)
211187 if originDomain != "" && originDomain != currentDomain {
212188 a.setTexture(originDomain, texture)
189+ a.saveFaviconToDisk(ctx, originDomain, texture)
190+ }
191+ }
192+}
193+
194+// saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated.
195+func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) {
196+ log := logging.FromContext(ctx)
197+
198+ // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread
199+ // since GTK texture operations need to happen there
200+ pngPath := a.service.DiskPathPNG(domain)
201+ if pngPath != "" && !a.service.HasPNGOnDisk(domain) {
202+ // Ensure cache directory exists before SaveToPng (GTK won't create it)
203+ if err := a.service.EnsureCacheDir(); err != nil {
204+ log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir")
205+ } else if ok := texture.SaveToPng(pngPath); !ok {
206+ log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG")
207+ } else {
208+ log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG")
213209
214- // Export PNG for origin domain too
215- pngPath := a.service.DiskPathPNG(originDomain)
216- if pngPath != "" && !a.service.HasPNGOnDisk(originDomain) {
217- if ok := texture.SaveToPng(pngPath); !ok {
218- log.Warn().Str("domain", originDomain).Str("path", pngPath).Msg("failed to save origin favicon PNG")
210+ // Create normalized sized copy for dmenu/fuzzel (async)
211+ // The original PNG is now on disk, so this should succeed
212+ go func() {
213+ if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil {
214+ log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG")
219215 } else {
220- log.Debug().Str("domain", originDomain).Str("path", pngPath).Msg("saved origin favicon PNG")
221-
222- // Create normalized sized copy for dmenu/fuzzel (async)
223- go func() {
224- if err := a.service.EnsureSizedPNG(ctx, originDomain, favicon.NormalizedIconSize); err != nil {
225- log.Warn().Err(err).Str("domain", originDomain).Msg("failed to create sized PNG for origin")
226- } else {
227- log.Debug().Str("domain", originDomain).Msg("created sized PNG for origin")
228- }
229- }()
216+ log.Debug().Str("domain", domain).Msg("created sized PNG")
230217 }
231- }
232-
233- go a.service.EnsureDiskCache(ctx, originDomain)
218+ }()
234219 }
235220 }
221+
222+ // Ensure disk cache is populated (async, in background)
223+ go a.service.EnsureDiskCache(ctx, domain)
236224 }
237225
238226 // PreloadFromCache attempts to load a favicon from cache without fetching.
@@ -166,34 +166,11 @@ func (a *FaviconAdapter) StoreFromWebKit(ctx context.Context, pageURL string, te
166 return166 return
167 }167 }
168 168
169- log := logging.FromContext(ctx)
170-
171 // Store in texture cache169 // Store in texture cache
172 a.setTexture(domain, texture)170 a.setTexture(domain, texture)
173 171
174- // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread172+ // Export as PNG and ensure disk cache
175- // since GTK texture operations need to happen there173+ a.saveFaviconToDisk(ctx, domain, texture)
176- pngPath := a.service.DiskPathPNG(domain)
177- if pngPath != "" && !a.service.HasPNGOnDisk(domain) {
178- if ok := texture.SaveToPng(pngPath); !ok {
179- log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG")
180- } else {
181- log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG")
182-
183- // Create normalized sized copy for dmenu/fuzzel (async)
184- // The original PNG is now on disk, so this should succeed
185- go func() {
186- if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil {
187- log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG after save")
188- } else {
189- log.Debug().Str("domain", domain).Msg("created sized PNG after save")
190- }
191- }()
192- }
193- }
194-
195- // Ensure disk cache is populated (async, in background)
196- go a.service.EnsureDiskCache(ctx, domain)
197 }174 }
198 175
199 // StoreFromWebKitWithOrigin stores a favicon for both current URL and original URL.176 // StoreFromWebKitWithOrigin stores a favicon for both current URL and original URL.
@@ -205,34 +182,45 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin(
205 182
206 // Also store under original URL domain if different183 // Also store under original URL domain if different
207 if originURL != "" && originURL != currentURL {184 if originURL != "" && originURL != currentURL {
208- log := logging.FromContext(ctx)
209 originDomain := domainurl.ExtractDomain(originURL)185 originDomain := domainurl.ExtractDomain(originURL)
210 currentDomain := domainurl.ExtractDomain(currentURL)186 currentDomain := domainurl.ExtractDomain(currentURL)
211 if originDomain != "" && originDomain != currentDomain {187 if originDomain != "" && originDomain != currentDomain {
212 a.setTexture(originDomain, texture)188 a.setTexture(originDomain, texture)
189+ a.saveFaviconToDisk(ctx, originDomain, texture)
190+ }
191+ }
192+}
193+
194+// saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated.
195+func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) {
196+ log := logging.FromContext(ctx)
197+
198+ // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread
199+ // since GTK texture operations need to happen there
200+ pngPath := a.service.DiskPathPNG(domain)
201+ if pngPath != "" && !a.service.HasPNGOnDisk(domain) {
202+ // Ensure cache directory exists before SaveToPng (GTK won't create it)
203+ if err := a.service.EnsureCacheDir(); err != nil {
204+ log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir")
205+ } else if ok := texture.SaveToPng(pngPath); !ok {
206+ log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG")
207+ } else {
208+ log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG")
213 209
214- // Export PNG for origin domain too210+ // Create normalized sized copy for dmenu/fuzzel (async)
215- pngPath := a.service.DiskPathPNG(originDomain)211+ // The original PNG is now on disk, so this should succeed
216- if pngPath != "" && !a.service.HasPNGOnDisk(originDomain) {212+ go func() {
217- if ok := texture.SaveToPng(pngPath); !ok {213+ if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil {
218- log.Warn().Str("domain", originDomain).Str("path", pngPath).Msg("failed to save origin favicon PNG")214+ log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG")
219 } else {215 } else {
220- log.Debug().Str("domain", originDomain).Str("path", pngPath).Msg("saved origin favicon PNG")216+ log.Debug().Str("domain", domain).Msg("created sized PNG")
221-
222- // Create normalized sized copy for dmenu/fuzzel (async)
223- go func() {
224- if err := a.service.EnsureSizedPNG(ctx, originDomain, favicon.NormalizedIconSize); err != nil {
225- log.Warn().Err(err).Str("domain", originDomain).Msg("failed to create sized PNG for origin")
226- } else {
227- log.Debug().Str("domain", originDomain).Msg("created sized PNG for origin")
228- }
229- }()
230 }217 }
231- }218+ }()
232-
233- go a.service.EnsureDiskCache(ctx, originDomain)
234 }219 }
235 }220 }
221+
222+ // Ensure disk cache is populated (async, in background)
223+ go a.service.EnsureDiskCache(ctx, domain)
236 }224 }
237 225
238 // PreloadFromCache attempts to load a favicon from cache without fetching.226 // PreloadFromCache attempts to load a favicon from cache without fetching.

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