v0.4.0 6203a22

binary_format.go RawHistoryFolder

1package cache
2
3import (
4	"encoding/binary"
5	"fmt"
6	"os"
7	"syscall"
8	"unsafe"
9)
10
11// Binary cache format layout:
12// Header:    [magic:4][version:4][entryCount:4][indexOffset:8][lastModified:8]
13// Entries:   [url_len:2][url:N][title_len:2][title:N][visit:2][lastVisit:4][score:2]
14// Index:     [trigramCount:4][trigram:3][count:4][ids:4*count]...
15
16const (
17	CacheMagic   = 0x44554d42 // "DUMB" in little-endian
18	CacheVersion = 1
19	HeaderSize   = 28 // Size of binary header
20)
21
22// BinaryHeader represents the cache file header.
23type BinaryHeader struct {
24	Magic        uint32 // Magic number for format identification
25	Version      uint32 // Cache format version
26	EntryCount   uint32 // Number of entries in cache
27	IndexOffset  uint64 // Offset to trigram index section
28	LastModified int64  // Unix timestamp of last modification
29}
30
31// SaveToBinary writes the cache to a binary file using memory mapping for performance.
32func (c *DmenuFuzzyCache) SaveToBinary(filename string) error {
33	c.mu.RLock()
34	defer c.mu.RUnlock()
35
36	// Calculate total file size
37	totalSize := c.calculateFileSize()
38
39	// Create the file with the exact size needed
40	file, err := os.Create(filename)
41	if err != nil {
42		return fmt.Errorf("failed to create cache file: %w", err)
43	}
44	defer file.Close()
45
46	// Pre-allocate file space
47	if err := file.Truncate(int64(totalSize)); err != nil {
48		return fmt.Errorf("failed to allocate file space: %w", err)
49	}
50
51	// Memory map the file for fast writing
52	data, err := syscall.Mmap(int(file.Fd()), 0, int(totalSize), syscall.PROT_WRITE, syscall.MAP_SHARED)
53	if err != nil {
54		return fmt.Errorf("failed to mmap file: %w", err)
55	}
56	defer syscall.Munmap(data)
57
58	// Write data using unsafe pointer operations for maximum speed
59	offset := 0
60
61	// Write header
62	offset = c.writeHeader(data, offset, totalSize)
63
64	// Write entries
65	entriesEndOffset := c.writeEntries(data, offset)
66
67	// Write trigram index
68	trigramEndOffset := c.writeTrigramIndex(data, entriesEndOffset)
69
70	// Write sorted index
71	c.writeSortedIndex(data, trigramEndOffset)
72
73	// Force sync to disk (Linux-specific, skip on error)
74	// syscall.Msync may not be available on all platforms
75	// _ = syscall.Msync(data, syscall.MS_SYNC)
76
77	return nil
78}
79
80// LoadFromBinary loads cache from a binary file using memory mapping.
81func (c *DmenuFuzzyCache) LoadFromBinary(filename string) error {
82	file, err := os.Open(filename)
83	if err != nil {
84		return fmt.Errorf("failed to open cache file: %w", err)
85	}
86	defer file.Close()
87
88	// Get file size
89	stat, err := file.Stat()
90	if err != nil {
91		return fmt.Errorf("failed to stat cache file: %w", err)
92	}
93	fileSize := int(stat.Size())
94
95	if fileSize < HeaderSize {
96		return fmt.Errorf("cache file too small: %d bytes", fileSize)
97	}
98
99	// Memory map the file for fast reading
100	data, err := syscall.Mmap(int(file.Fd()), 0, fileSize, syscall.PROT_READ, syscall.MAP_SHARED)
101	if err != nil {
102		return fmt.Errorf("failed to mmap cache file: %w", err)
103	}
104	// Note: We don't defer Munmap here as we want to keep the mapping alive
105	// The mapping will be cleaned up when the process exits
106
107	// Parse header
108	header, err := c.parseHeader(data)
109	if err != nil {
110		syscall.Munmap(data)
111		return fmt.Errorf("failed to parse header: %w", err)
112	}
113
114	c.mu.Lock()
115	defer c.mu.Unlock()
116
117	// Parse entries
118	entries, err := c.parseEntries(data, HeaderSize, header.EntryCount)
119	if err != nil {
120		syscall.Munmap(data)
121		return fmt.Errorf("failed to parse entries: %w", err)
122	}
123
124	// Parse trigram index
125	trigramIndex, trigramEndOffset, err := c.parseTrigramIndex(data, int(header.IndexOffset))
126	if err != nil {
127		syscall.Munmap(data)
128		return fmt.Errorf("failed to parse trigram index: %w", err)
129	}
130
131	// Parse sorted index
132	sortedIndex, err := c.parseSortedIndex(data, trigramEndOffset)
133	if err != nil {
134		syscall.Munmap(data)
135		return fmt.Errorf("failed to parse sorted index: %w", err)
136	}
137
138	// Update cache state
139	c.entries = entries
140	c.trigramIndex = trigramIndex
141	c.sortedIndex = sortedIndex
142	c.entryCount = header.EntryCount
143	c.lastModified = header.LastModified
144	c.version = header.Version
145
146	return nil
147}
148
149// calculateFileSize estimates the total file size needed.
150func (c *DmenuFuzzyCache) calculateFileSize() int {
151	size := HeaderSize
152
153	// Calculate entries section size
154	for _, entry := range c.entries {
155		size += 2 + len(entry.URL)   // url_len + url
156		size += 2 + len(entry.Title) // title_len + title
157		size += 2 + 4 + 2            // visit + lastVisit + score
158	}
159
160	// Calculate trigram index size
161	size += 4 // trigram count
162	for trigram, ids := range c.trigramIndex {
163		size += len(trigram) + 4 + len(ids)*4 // trigram + count + ids
164	}
165
166	// Calculate sorted index size
167	size += 4 + len(c.sortedIndex)*4 // count + indices
168
169	return size
170}
171
172// writeHeader writes the binary header.
173func (c *DmenuFuzzyCache) writeHeader(data []byte, offset int, totalSize int) int {
174	// Calculate index offset (header + all entries)
175	indexOffset := HeaderSize
176	for _, entry := range c.entries {
177		indexOffset += 2 + len(entry.URL) + 2 + len(entry.Title) + 8
178	}
179
180	header := BinaryHeader{
181		Magic:        CacheMagic,
182		Version:      CacheVersion,
183		EntryCount:   uint32(len(c.entries)),
184		IndexOffset:  uint64(indexOffset),
185		LastModified: c.lastModified,
186	}
187
188	// Write header using unsafe for maximum performance
189	*(*BinaryHeader)(unsafe.Pointer(&data[offset])) = header
190	return offset + HeaderSize
191}
192
193// writeEntries writes all cache entries in binary format.
194func (c *DmenuFuzzyCache) writeEntries(data []byte, offset int) int {
195	for _, entry := range c.entries {
196		// Write URL
197		urlLen := uint16(len(entry.URL))
198		*(*uint16)(unsafe.Pointer(&data[offset])) = urlLen
199		offset += 2
200		copy(data[offset:], entry.URL)
201		offset += len(entry.URL)
202
203		// Write Title
204		titleLen := uint16(len(entry.Title))
205		*(*uint16)(unsafe.Pointer(&data[offset])) = titleLen
206		offset += 2
207		copy(data[offset:], entry.Title)
208		offset += len(entry.Title)
209
210		// Write metadata
211		*(*uint16)(unsafe.Pointer(&data[offset])) = entry.VisitCount
212		offset += 2
213		*(*uint32)(unsafe.Pointer(&data[offset])) = entry.LastVisit
214		offset += 4
215		*(*uint16)(unsafe.Pointer(&data[offset])) = entry.Score
216		offset += 2
217	}
218	return offset
219}
220
221// writeTrigramIndex writes the trigram index in binary format.
222func (c *DmenuFuzzyCache) writeTrigramIndex(data []byte, offset int) int {
223	// Write trigram count
224	trigramCount := uint32(len(c.trigramIndex))
225	*(*uint32)(unsafe.Pointer(&data[offset])) = trigramCount
226	offset += 4
227
228	// Write trigrams and their entry lists
229	for trigram, ids := range c.trigramIndex {
230		// Write trigram (exactly 3 bytes)
231		copy(data[offset:offset+3], trigram)
232		offset += 3
233
234		// Write entry count
235		idCount := uint32(len(ids))
236		*(*uint32)(unsafe.Pointer(&data[offset])) = idCount
237		offset += 4
238
239		// Write entry IDs
240		for _, id := range ids {
241			*(*uint32)(unsafe.Pointer(&data[offset])) = id
242			offset += 4
243		}
244	}
245	return offset
246}
247
248// writeSortedIndex writes the sorted index in binary format.
249func (c *DmenuFuzzyCache) writeSortedIndex(data []byte, offset int) int {
250	// Write sorted index count
251	indexCount := uint32(len(c.sortedIndex))
252	*(*uint32)(unsafe.Pointer(&data[offset])) = indexCount
253	offset += 4
254
255	// Write sorted indices
256	for _, id := range c.sortedIndex {
257		*(*uint32)(unsafe.Pointer(&data[offset])) = id
258		offset += 4
259	}
260	return offset
261}
262
263// parseHeader parses the binary header from mapped data.
264func (c *DmenuFuzzyCache) parseHeader(data []byte) (*BinaryHeader, error) {
265	if len(data) < HeaderSize {
266		return nil, fmt.Errorf("insufficient data for header")
267	}
268
269	header := (*BinaryHeader)(unsafe.Pointer(&data[0]))
270
271	if header.Magic != CacheMagic {
272		return nil, fmt.Errorf("invalid magic number: 0x%x", header.Magic)
273	}
274
275	if header.Version != CacheVersion {
276		return nil, fmt.Errorf("unsupported cache version: %d", header.Version)
277	}
278
279	return header, nil
280}
281
282// parseEntries parses cache entries from binary data.
283func (c *DmenuFuzzyCache) parseEntries(data []byte, offset int, count uint32) ([]CompactEntry, error) {
284	entries := make([]CompactEntry, 0, count)
285
286	for i := uint32(0); i < count; i++ {
287		if offset+2 > len(data) {
288			return nil, fmt.Errorf("insufficient data for URL length at entry %d", i)
289		}
290
291		// Read URL
292		urlLen := *(*uint16)(unsafe.Pointer(&data[offset]))
293		offset += 2
294		if offset+int(urlLen) > len(data) {
295			return nil, fmt.Errorf("insufficient data for URL at entry %d", i)
296		}
297		url := string(data[offset : offset+int(urlLen)])
298		offset += int(urlLen)
299
300		// Read Title
301		if offset+2 > len(data) {
302			return nil, fmt.Errorf("insufficient data for title length at entry %d", i)
303		}
304		titleLen := *(*uint16)(unsafe.Pointer(&data[offset]))
305		offset += 2
306		if offset+int(titleLen) > len(data) {
307			return nil, fmt.Errorf("insufficient data for title at entry %d", i)
308		}
309		title := string(data[offset : offset+int(titleLen)])
310		offset += int(titleLen)
311
312		// Read metadata
313		if offset+8 > len(data) {
314			return nil, fmt.Errorf("insufficient data for metadata at entry %d", i)
315		}
316		visitCount := *(*uint16)(unsafe.Pointer(&data[offset]))
317		offset += 2
318		lastVisit := *(*uint32)(unsafe.Pointer(&data[offset]))
319		offset += 4
320		score := *(*uint16)(unsafe.Pointer(&data[offset]))
321		offset += 2
322
323		entries = append(entries, CompactEntry{
324			URL:        url,
325			Title:      title,
326			VisitCount: visitCount,
327			LastVisit:  lastVisit,
328			Score:      score,
329		})
330	}
331
332	return entries, nil
333}
334
335// parseTrigramIndex parses the trigram index from binary data.
336func (c *DmenuFuzzyCache) parseTrigramIndex(data []byte, offset int) (map[string][]uint32, int, error) {
337	if offset+4 > len(data) {
338		return nil, 0, fmt.Errorf("insufficient data for trigram count")
339	}
340
341	trigramCount := *(*uint32)(unsafe.Pointer(&data[offset]))
342	offset += 4
343
344	trigramIndex := make(map[string][]uint32, trigramCount)
345
346	for i := uint32(0); i < trigramCount; i++ {
347		// Read trigram
348		if offset+3 > len(data) {
349			return nil, 0, fmt.Errorf("insufficient data for trigram %d", i)
350		}
351		trigram := string(data[offset : offset+3])
352		offset += 3
353
354		// Read entry count
355		if offset+4 > len(data) {
356			return nil, 0, fmt.Errorf("insufficient data for entry count in trigram %d", i)
357		}
358		idCount := *(*uint32)(unsafe.Pointer(&data[offset]))
359		offset += 4
360
361		// Read entry IDs
362		if offset+int(idCount)*4 > len(data) {
363			return nil, 0, fmt.Errorf("insufficient data for entry IDs in trigram %d", i)
364		}
365
366		ids := make([]uint32, idCount)
367		for j := uint32(0); j < idCount; j++ {
368			ids[j] = *(*uint32)(unsafe.Pointer(&data[offset]))
369			offset += 4
370		}
371
372		trigramIndex[trigram] = ids
373	}
374
375	return trigramIndex, offset, nil
376}
377
378// parseSortedIndex parses the sorted index from binary data.
379func (c *DmenuFuzzyCache) parseSortedIndex(data []byte, offset int) ([]uint32, error) {
380	if offset+4 > len(data) {
381		return nil, fmt.Errorf("insufficient data for sorted index count")
382	}
383
384	indexCount := *(*uint32)(unsafe.Pointer(&data[offset]))
385	offset += 4
386
387	if offset+int(indexCount)*4 > len(data) {
388		return nil, fmt.Errorf("insufficient data for sorted index entries")
389	}
390
391	sortedIndex := make([]uint32, indexCount)
392	for i := uint32(0); i < indexCount; i++ {
393		sortedIndex[i] = *(*uint32)(unsafe.Pointer(&data[offset]))
394		offset += 4
395	}
396
397	return sortedIndex, nil
398}
399
400// IsValidCacheFile checks if a file is a valid cache file without fully loading it.
401func IsValidCacheFile(filename string) bool {
402	file, err := os.Open(filename)
403	if err != nil {
404		return false
405	}
406	defer file.Close()
407
408	var header BinaryHeader
409	err = binary.Read(file, binary.LittleEndian, &header)
410	if err != nil {
411		return false
412	}
413
414	return header.Magic == CacheMagic && header.Version == CacheVersion
415}

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