types.go RawHistoryFolder
1package cache
2
3import (
4 "sync"
5 "time"
6 "unsafe"
7
8 "github.com/bnema/dumber/internal/config"
9)
10
11// DmenuFuzzyCache provides high-performance fuzzy search for dmenu history.
12type DmenuFuzzyCache struct {
13 // Pre-computed search indices
14 trigramIndex map[string][]uint32 // 3-char sequences -> entry IDs
15 prefixTrie *Trie // Compressed trie for prefix matching
16 sortedIndex []uint32 // Pre-sorted entry indices by score (highest first)
17
18 // Compact history storage
19 entries []CompactEntry // Minimal memory footprint
20
21 // Metadata
22 version uint32 // Cache format version
23 lastModified int64 // Unix timestamp
24 entryCount uint32 // Number of entries
25
26 // Concurrency control
27 mu sync.RWMutex
28 data unsafe.Pointer // Atomic pointer to cache data
29}
30
31// CompactEntry represents a history entry optimized for memory usage.
32type CompactEntry struct {
33 URL string // Original URL
34 Title string // Page title
35 VisitCount uint16 // Max 65535 visits (sufficient for most use cases)
36 LastVisit uint32 // Days since Unix epoch (saves 4 bytes vs time.Time)
37 Score uint16 // Pre-computed base score (0-65535)
38}
39
40// FuzzyResult contains the results of a fuzzy search query.
41type FuzzyResult struct {
42 Matches []FuzzyMatch // Matching entries
43 QueryTime time.Duration // Time taken to execute query
44}
45
46// FuzzyMatch represents a single fuzzy search match.
47type FuzzyMatch struct {
48 Entry *CompactEntry // Matched entry
49 Score float64 // Final fuzzy match score (0.0-1.0)
50 URLScore float64 // URL similarity score
51 TitleScore float64 // Title similarity score
52 RecencyScore float64 // Recency boost score
53 VisitScore float64 // Visit count boost score
54 MatchType MatchType // Type of match found
55}
56
57// MatchType indicates how the query matched the entry.
58type MatchType uint8
59
60const (
61 MatchTypeExact MatchType = iota // Exact substring match
62 MatchTypePrefix // Prefix match
63 MatchTypeFuzzy // Fuzzy similarity match
64 MatchTypeTrigram // Trigram-based match
65)
66
67// CacheConfig holds configuration for the fuzzy cache.
68type CacheConfig struct {
69 // File paths
70 CacheFile string // Path to binary cache file
71
72 // Performance tuning
73 MaxEntries uint32 // Maximum entries to cache
74 TrigramThreshold uint32 // Minimum trigram matches required
75 ScoreThreshold float64 // Minimum score for results
76 MaxResults int // Maximum results to return
77
78 // Scoring weights
79 URLWeight float64 // Weight for URL matches
80 TitleWeight float64 // Weight for title matches
81 RecencyWeight float64 // Weight for recency
82 VisitWeight float64 // Weight for visit count
83
84 // Cache behavior
85 TTL time.Duration // Cache time-to-live
86 WarmupEnabled bool // Enable background cache warming
87}
88
89// DefaultCacheConfig returns sensible default configuration.
90func DefaultCacheConfig() *CacheConfig {
91 return &CacheConfig{
92 MaxEntries: 10000,
93 TrigramThreshold: 2,
94 ScoreThreshold: 0.2,
95 MaxResults: 50,
96 URLWeight: 0.4,
97 TitleWeight: 0.3,
98 RecencyWeight: 0.2,
99 VisitWeight: 0.1,
100 TTL: 30 * time.Minute,
101 WarmupEnabled: true,
102 }
103}
104
105// GetStateDir returns the state directory using the config package.
106func (c *CacheConfig) GetStateDir() (string, error) {
107 return config.GetStateDir()
108}
109
110// Trie represents a compressed prefix tree for fast prefix matching.
111type Trie struct {
112 Root *TrieNode
113}
114
115// TrieNode represents a node in the prefix trie.
116type TrieNode struct {
117 Children map[rune]*TrieNode // Child nodes
118 Entries []uint32 // Entry IDs that end at this node
119 IsEnd bool // Marks end of a prefix
120}
121
122// Reset clears the FuzzyResult for reuse.
123func (r *FuzzyResult) Reset() {
124 r.Matches = r.Matches[:0]
125 r.QueryTime = 0
126}
127
128// String returns a string representation of the match type.
129func (mt MatchType) String() string {
130 switch mt {
131 case MatchTypeExact:
132 return "exact"
133 case MatchTypePrefix:
134 return "prefix"
135 case MatchTypeFuzzy:
136 return "fuzzy"
137 case MatchTypeTrigram:
138 return "trigram"
139 default:
140 return "unknown"
141 }
142}
143
144// DaysFromTime converts a time.Time to days since Unix epoch.
145func DaysFromTime(t time.Time) uint32 {
146 return uint32(t.Unix() / 86400) // 86400 seconds per day
147}
148
149// TimeFromDays converts days since Unix epoch back to time.Time.
150func TimeFromDays(days uint32) time.Time {
151 return time.Unix(int64(days)*86400, 0)
152}
153
154// NewCompactEntry creates a CompactEntry from database values.
155func NewCompactEntry(url, title string, visitCount int64, lastVisit time.Time) CompactEntry {
156 entry := CompactEntry{
157 URL: url,
158 Title: title,
159 }
160
161 // Clamp visit count to uint16 max
162 if visitCount > 65535 {
163 entry.VisitCount = 65535
164 } else if visitCount < 0 {
165 entry.VisitCount = 0
166 } else {
167 entry.VisitCount = uint16(visitCount)
168 }
169
170 entry.LastVisit = DaysFromTime(lastVisit)
171
172 // Pre-compute a base score for sorting
173 entry.Score = entry.calculateBaseScore()
174
175 return entry
176}
177
178// calculateBaseScore computes a base score for the entry based on visits and recency.
179func (e *CompactEntry) calculateBaseScore() uint16 {
180 visitScore := float64(e.VisitCount) / 65535.0 // Normalize to 0-1
181
182 now := time.Now()
183 lastVisit := TimeFromDays(e.LastVisit)
184 daysSince := now.Sub(lastVisit).Hours() / 24
185 recencyScore := 1.0 / (1.0 + daysSince/30.0) // Decay over 30 days
186
187 // Weighted combination
188 baseScore := (visitScore*0.3 + recencyScore*0.7) * 65535.0
189
190 if baseScore > 65535 {
191 return 65535
192 }
193 return uint16(baseScore)
194}
195
196// Display returns a formatted string for dmenu display.
197func (e *CompactEntry) Display() string {
198 if e.Title != "" && e.Title != e.URL {
199 return e.Title
200 }
201 return e.URL
202}