v0.23.2 bfd5eb4

service.go RawHistoryFolder

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

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