v0.20.1 d9a391f

cache.go RawHistoryFolder

1package favicon
2
3import (
4	"os"
5	"path/filepath"
6	"sync"
7
8	domainurl "github.com/bnema/dumber/internal/domain/url"
9)
10
11const (
12	// diskWriteBufferSize defines the capacity of the favicon write channel.
13	diskWriteBufferSize = 100
14	// File permissions for favicon cache.
15	diskCacheDirPerm  = 0750
16	diskCacheFilePerm = 0600
17)
18
19// diskWrite represents a favicon to be written to disk asynchronously.
20type diskWrite struct {
21	domain string
22	data   []byte
23}
24
25// Cache provides multi-tier favicon caching (memory + disk).
26type Cache struct {
27	memCache  map[string][]byte
28	diskDir   string
29	writeChan chan diskWrite
30	closeOnce sync.Once
31	mu        sync.RWMutex
32}
33
34// NewCache creates a new favicon cache.
35// If diskDir is empty, only in-memory caching is used.
36func NewCache(diskDir string) *Cache {
37	c := &Cache{
38		memCache:  make(map[string][]byte),
39		diskDir:   diskDir,
40		writeChan: make(chan diskWrite, diskWriteBufferSize),
41	}
42
43	// Start background writer goroutine if disk caching is enabled
44	if diskDir != "" {
45		go c.diskWriter()
46	}
47
48	return c
49}
50
51// Get retrieves favicon bytes for a domain.
52// Checks memory cache first, then disk cache.
53// Returns the bytes and true if found, nil and false otherwise.
54func (c *Cache) Get(domain string) ([]byte, bool) {
55	if domain == "" {
56		return nil, false
57	}
58
59	// Check memory cache first
60	c.mu.RLock()
61	data, ok := c.memCache[domain]
62	c.mu.RUnlock()
63	if ok {
64		return data, true
65	}
66
67	// Check disk cache
68	data = c.loadFromDisk(domain)
69	if data != nil {
70		// Populate memory cache
71		c.mu.Lock()
72		c.memCache[domain] = data
73		c.mu.Unlock()
74		return data, true
75	}
76
77	return nil, false
78}
79
80// Set stores favicon bytes for a domain.
81// Writes to memory cache immediately and queues async disk write.
82func (c *Cache) Set(domain string, data []byte) {
83	if domain == "" || len(data) == 0 {
84		return
85	}
86
87	// Write to memory cache
88	c.mu.Lock()
89	c.memCache[domain] = data
90	c.mu.Unlock()
91
92	// Queue async disk write
93	c.queueDiskWrite(domain, data)
94}
95
96// DiskPath returns the filesystem path for a domain's cached favicon (ICO format).
97// Returns empty string if disk caching is disabled or domain is empty.
98func (c *Cache) DiskPath(domain string) string {
99	if c.diskDir == "" || domain == "" {
100		return ""
101	}
102	filename := domainurl.SanitizeDomainForFilename(domain)
103	return filepath.Join(c.diskDir, filename)
104}
105
106// DiskPathPNG returns the filesystem path for a domain's PNG favicon.
107// PNG format is required by external tools like rofi/fuzzel.
108// Returns empty string if disk caching is disabled or domain is empty.
109func (c *Cache) DiskPathPNG(domain string) string {
110	if c.diskDir == "" || domain == "" {
111		return ""
112	}
113	filename := domainurl.SanitizeDomainForPNG(domain)
114	return filepath.Join(c.diskDir, filename)
115}
116
117// HasPNGOnDisk checks if a PNG favicon exists on disk for the given domain.
118func (c *Cache) HasPNGOnDisk(domain string) bool {
119	path := c.DiskPathPNG(domain)
120	if path == "" {
121		return false
122	}
123	_, err := os.Stat(path)
124	return err == nil
125}
126
127// DiskPathPNGSized returns the filesystem path for a sized PNG favicon.
128// Used for normalized favicon export (e.g., 32x32) for CLI tools like rofi/fuzzel.
129// Returns empty string if disk caching is disabled or domain is empty.
130func (c *Cache) DiskPathPNGSized(domain string, size int) string {
131	if c.diskDir == "" || domain == "" {
132		return ""
133	}
134	filename := domainurl.SanitizeDomainForPNGSized(domain, size)
135	return filepath.Join(c.diskDir, filename)
136}
137
138// HasPNGSizedOnDisk checks if a sized PNG favicon exists on disk for the given domain.
139func (c *Cache) HasPNGSizedOnDisk(domain string, size int) bool {
140	path := c.DiskPathPNGSized(domain, size)
141	if path == "" {
142		return false
143	}
144	_, err := os.Stat(path)
145	return err == nil
146}
147
148// WritePNG writes raw PNG data to disk for a domain.
149// Used by UI layer to export WebKit textures for CLI tools.
150func (c *Cache) WritePNG(domain string, pngData []byte) {
151	if c.diskDir == "" || len(pngData) == 0 || domain == "" {
152		return
153	}
154
155	// Ensure directory exists
156	if err := os.MkdirAll(c.diskDir, diskCacheDirPerm); err != nil {
157		return
158	}
159
160	filename := domainurl.SanitizeDomainForPNG(domain)
161	finalPath := filepath.Join(c.diskDir, filename)
162	tempPath := finalPath + ".tmp"
163
164	// Write to temp file
165	if err := os.WriteFile(tempPath, pngData, diskCacheFilePerm); err != nil {
166		return
167	}
168
169	// Atomic rename
170	if err := os.Rename(tempPath, finalPath); err != nil {
171		_ = os.Remove(tempPath)
172	}
173}
174
175// HasOnDisk checks if a favicon exists on disk for the given domain.
176func (c *Cache) HasOnDisk(domain string) bool {
177	path := c.DiskPath(domain)
178	if path == "" {
179		return false
180	}
181	_, err := os.Stat(path)
182	return err == nil
183}
184
185// Close shuts down the background writer goroutine.
186func (c *Cache) Close() {
187	c.closeOnce.Do(func() {
188		if c.writeChan != nil {
189			close(c.writeChan)
190		}
191	})
192}
193
194// Clear removes all entries from the in-memory cache.
195func (c *Cache) Clear() {
196	c.mu.Lock()
197	c.memCache = make(map[string][]byte)
198	c.mu.Unlock()
199}
200
201// Size returns the number of entries in the in-memory cache.
202func (c *Cache) Size() int {
203	c.mu.RLock()
204	size := len(c.memCache)
205	c.mu.RUnlock()
206	return size
207}
208
209// loadFromDisk attempts to load favicon bytes from disk cache.
210func (c *Cache) loadFromDisk(domain string) []byte {
211	path := c.DiskPath(domain)
212	if path == "" {
213		return nil
214	}
215
216	data, err := os.ReadFile(path)
217	if err != nil {
218		return nil
219	}
220
221	if len(data) == 0 {
222		return nil
223	}
224
225	return data
226}
227
228// writeToDisk atomically writes favicon data to disk.
229func (c *Cache) writeToDisk(domain string, data []byte) {
230	if c.diskDir == "" || len(data) == 0 {
231		return
232	}
233
234	// Ensure directory exists
235	if err := os.MkdirAll(c.diskDir, diskCacheDirPerm); err != nil {
236		return
237	}
238
239	filename := domainurl.SanitizeDomainForFilename(domain)
240	finalPath := filepath.Join(c.diskDir, filename)
241	tempPath := finalPath + ".tmp"
242
243	// Write to temp file
244	if err := os.WriteFile(tempPath, data, diskCacheFilePerm); err != nil {
245		return
246	}
247
248	// Atomic rename
249	if err := os.Rename(tempPath, finalPath); err != nil {
250		_ = os.Remove(tempPath)
251	}
252}
253
254// queueDiskWrite sends favicon data to be written asynchronously.
255func (c *Cache) queueDiskWrite(domain string, data []byte) {
256	if c.diskDir == "" {
257		return
258	}
259	select {
260	case c.writeChan <- diskWrite{domain: domain, data: data}:
261		// queued successfully
262	default:
263		// channel full, skip write
264	}
265}
266
267// diskWriter processes async write requests.
268func (c *Cache) diskWriter() {
269	for write := range c.writeChan {
270		c.writeToDisk(write.domain, write.data)
271	}
272}

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