main 5041353

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 is the legacy favicon cache adapter kept for callers that have not yet
19// been migrated to the application favicon usecase. It must not own refresh or
20// remote-fetch policy; new code should use the usecase plus BlobStore/Fetcher.
21type Service struct {
22	cache *Cache
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{cache: NewCache(cacheDir)}
29}
30
31func (s *Service) Cache() *Cache {
32	return s.cache
33}
34
35// Get returns favicon bytes for a domain from the legacy cache only.
36func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) {
37	if domain == "" {
38		return nil, nil
39	}
40	log := logging.FromContext(ctx)
41	log.Debug().Str("domain", domain).Msg("favicon: Service.Get begin")
42
43	// Internal domain returns the app logo
44	if domain == InternalDomain {
45		log.Debug().Str("domain", domain).Msg("favicon: Service.Get internal domain")
46		return assets.LogoSVG, nil
47	}
48
49	if data, ok := s.cache.Get(ctx, domain); ok {
50		log.Debug().
51			Str("domain", domain).
52			Int("bytes", len(data)).
53			Msg("favicon: Service.Get cache hit")
54		return data, nil
55	}
56	log.Debug().Str("domain", domain).Msg("favicon: Service.Get cache miss")
57	return nil, nil
58}
59
60// IsInternalURL checks if a URL uses the internal dumb:// scheme.
61func IsInternalURL(url string) bool {
62	return strings.HasPrefix(url, InternalScheme)
63}
64
65// GetLogoBytes returns the raw SVG bytes of the app logo.
66// Used for internal pages that need the logo as a favicon.
67func GetLogoBytes() []byte {
68	return assets.LogoSVG
69}
70
71// GetCached returns favicon bytes only if already cached (no external fetch).
72func (s *Service) GetCached(ctx context.Context, domain string) ([]byte, bool) {
73	data, ok := s.cache.Get(ctx, domain)
74	logging.FromContext(ctx).Debug().
75		Str("domain", domain).
76		Bool("hit", ok).
77		Int("bytes", len(data)).
78		Msg("favicon: Service.GetCached")
79	return data, ok
80}
81
82// Store saves favicon bytes for a domain to cache.
83func (s *Service) Store(ctx context.Context, domain string, data []byte) error {
84	s.cache.Set(ctx, domain, data)
85	return nil
86}
87
88// DiskPath returns the filesystem path where a domain's favicon is cached (ICO).
89func (s *Service) DiskPath(domain string) string {
90	return s.cache.DiskPath(domain)
91}
92
93// DiskPathPNG returns the filesystem path for PNG favicon.
94func (s *Service) DiskPathPNG(domain string) string {
95	return s.cache.DiskPathPNG(domain)
96}
97
98// HasPNGOnDisk checks if a PNG favicon exists on disk for the domain.
99func (s *Service) HasPNGOnDisk(domain string) bool {
100	return s.cache.HasPNGOnDisk(domain)
101}
102
103// WritePNG writes raw PNG data to disk for a domain.
104func (s *Service) WritePNG(domain string, pngData []byte) {
105	s.cache.WritePNG(domain, pngData)
106}
107
108// Close shuts down background workers and releases resources.
109func (s *Service) Close() {
110	s.cache.Close()
111}
112
113// EnsureDiskCache is a legacy no-op. Disk population is handled by explicit
114// texture/blob writes after EnsureCacheDir; remote refresh policy belongs to the application usecase.
115func (*Service) EnsureDiskCache(ctx context.Context, domain string) {
116	logging.FromContext(ctx).Debug().Str("domain", domain).Msg("favicon: legacy EnsureDiskCache skipped")
117}
118
119// DiskPathPNGSized returns the filesystem path for a sized PNG favicon.
120func (s *Service) DiskPathPNGSized(domain string, size int) string {
121	return s.cache.DiskPathPNGSized(domain, size)
122}
123
124// HasPNGSizedOnDisk checks if a sized PNG favicon exists on disk.
125func (s *Service) HasPNGSizedOnDisk(domain string, size int) bool {
126	return s.cache.HasPNGSizedOnDisk(domain, size)
127}
128
129// EnsureSizedPNG creates or overwrites a resized PNG from the original PNG.
130// This is a legacy adapter method; new code should use the favicon usecase.
131func (s *Service) EnsureSizedPNG(ctx context.Context, domain string, size int) error {
132	if domain == "" {
133		return nil
134	}
135	log := logging.FromContext(ctx)
136	log.Debug().
137		Str("domain", domain).
138		Int("size", size).
139		Msg("favicon: EnsureSizedPNG begin")
140
141	srcPath := s.DiskPathPNG(domain)
142	if srcPath == "" || !s.HasPNGOnDisk(domain) {
143		return fmt.Errorf("source PNG not found for domain %s", domain)
144	}
145
146	dstPath := s.DiskPathPNGSized(domain, size)
147	if dstPath == "" {
148		return fmt.Errorf("cannot determine destination path for domain %s", domain)
149	}
150
151	log.Debug().Str("domain", domain).Int("size", size).Msg("creating sized favicon")
152
153	err := ResizePNG(srcPath, dstPath, size)
154	log.Debug().
155		Err(err).
156		Str("domain", domain).
157		Int("size", size).
158		Msg("favicon: EnsureSizedPNG end")
159	return err
160}
161
162// EnsureInternalFaviconPNG ensures the internal app logo PNG exists on disk.
163// Returns the path to the sized PNG file. Used by dmenu to get a filesystem
164// path to the internal favicon for rofi/fuzzel display.
165func (s *Service) EnsureInternalFaviconPNG(pngData []byte, size int) string {
166	if s.HasPNGSizedOnDisk(InternalDomain, size) {
167		return s.DiskPathPNGSized(InternalDomain, size)
168	}
169
170	s.cache.WritePNGSized(InternalDomain, pngData, size)
171	return s.DiskPathPNGSized(InternalDomain, size)
172}
173
174// EnsureCacheDir ensures the favicon cache directory exists.
175// Call this before using DiskPathPNG with external save functions like GTK's SaveToPng.
176func (s *Service) EnsureCacheDir() error {
177	return s.cache.EnsureDir()
178}

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