v0.21.0 73bf644

service.go RawHistoryFolder

1package favicon
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/dumber/internal/logging"
8)
9
10// Service implements the domain FaviconService interface.
11// It coordinates between the cache and fetcher components.
12type Service struct {
13	cache   *Cache
14	fetcher *Fetcher
15}
16
17// NewService creates a new favicon service.
18// cacheDir is the directory for disk caching; empty string disables disk caching.
19func NewService(cacheDir string) *Service {
20	return &Service{
21		cache:   NewCache(cacheDir),
22		fetcher: NewFetcher(),
23	}
24}
25
26// Get returns favicon bytes for a domain.
27// Checks memory cache, then disk cache, then fetches from external API.
28func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) {
29	if domain == "" {
30		return nil, nil
31	}
32
33	// Check cache first (memory + disk)
34	if data, ok := s.cache.Get(domain); ok {
35		return data, nil
36	}
37
38	// Fetch from external API
39	data, err := s.fetcher.Fetch(ctx, domain)
40	if err != nil {
41		return nil, err
42	}
43
44	// Store in cache if we got data
45	if len(data) > 0 {
46		s.cache.Set(domain, data)
47	}
48
49	return data, nil
50}
51
52// GetCached returns favicon bytes only if already cached (no external fetch).
53func (s *Service) GetCached(domain string) ([]byte, bool) {
54	return s.cache.Get(domain)
55}
56
57// Store saves favicon bytes for a domain to cache.
58func (s *Service) Store(domain string, data []byte) error {
59	s.cache.Set(domain, data)
60	return nil
61}
62
63// DiskPath returns the filesystem path where a domain's favicon is cached (ICO).
64func (s *Service) DiskPath(domain string) string {
65	return s.cache.DiskPath(domain)
66}
67
68// DiskPathPNG returns the filesystem path for PNG favicon.
69func (s *Service) DiskPathPNG(domain string) string {
70	return s.cache.DiskPathPNG(domain)
71}
72
73// HasPNGOnDisk checks if a PNG favicon exists on disk for the domain.
74func (s *Service) HasPNGOnDisk(domain string) bool {
75	return s.cache.HasPNGOnDisk(domain)
76}
77
78// WritePNG writes raw PNG data to disk for a domain.
79func (s *Service) WritePNG(domain string, pngData []byte) {
80	s.cache.WritePNG(domain, pngData)
81}
82
83// Close shuts down background workers and releases resources.
84func (s *Service) Close() {
85	s.cache.Close()
86}
87
88// EnsureDiskCache fetches favicon from external API and saves to disk if not already cached.
89// This is used when we receive a texture from WebKit but want to ensure disk persistence.
90func (s *Service) EnsureDiskCache(ctx context.Context, domain string) {
91	if domain == "" {
92		return
93	}
94
95	// Already on disk?
96	if s.cache.HasOnDisk(domain) {
97		return
98	}
99
100	log := logging.FromContext(ctx)
101
102	// Fetch and store
103	data, err := s.fetcher.Fetch(ctx, domain)
104	if err != nil {
105		log.Debug().Err(err).Str("domain", domain).Msg("failed to fetch favicon for disk cache")
106		return
107	}
108
109	if len(data) > 0 {
110		s.cache.Set(domain, data)
111	}
112}
113
114// DiskPathPNGSized returns the filesystem path for a sized PNG favicon.
115func (s *Service) DiskPathPNGSized(domain string, size int) string {
116	return s.cache.DiskPathPNGSized(domain, size)
117}
118
119// HasPNGSizedOnDisk checks if a sized PNG favicon exists on disk.
120func (s *Service) HasPNGSizedOnDisk(domain string, size int) bool {
121	return s.cache.HasPNGSizedOnDisk(domain, size)
122}
123
124// EnsureSizedPNG creates a resized PNG from the original if it doesn't exist.
125// This is used to generate normalized icons for dmenu/fuzzel.
126func (s *Service) EnsureSizedPNG(ctx context.Context, domain string, size int) error {
127	if domain == "" {
128		return nil
129	}
130
131	// Already exists?
132	if s.HasPNGSizedOnDisk(domain, size) {
133		return nil
134	}
135
136	// Check source PNG exists
137	srcPath := s.DiskPathPNG(domain)
138	if srcPath == "" || !s.HasPNGOnDisk(domain) {
139		return fmt.Errorf("source PNG not found for domain %s", domain)
140	}
141
142	dstPath := s.DiskPathPNGSized(domain, size)
143	if dstPath == "" {
144		return fmt.Errorf("cannot determine destination path for domain %s", domain)
145	}
146
147	log := logging.FromContext(ctx)
148	log.Debug().Str("domain", domain).Int("size", size).Msg("creating sized favicon")
149
150	return ResizePNG(srcPath, dstPath, size)
151}

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