v0.33.1 9577fee

store.go RawHistoryFolder

1package filtering
2
3import (
4	"context"
5	"fmt"
6	"slices"
7	"sync"
8
9	"github.com/bnema/dumber/internal/logging"
10	"github.com/bnema/puregotk/v4/gio"
11	"github.com/bnema/puregotk/v4/webkit"
12)
13
14// Store wraps webkit.UserContentFilterStore with Go-friendly async operations.
15type Store struct {
16	inner *webkit.UserContentFilterStore
17	path  string
18	mu    sync.Mutex
19}
20
21// NewStore creates a new Store at the given path.
22// The path is where WebKit will store compiled filter bytecode.
23func NewStore(storagePath string) *Store {
24	inner := webkit.NewUserContentFilterStore(storagePath)
25	if inner == nil {
26		return nil
27	}
28	return &Store{
29		inner: inner,
30		path:  storagePath,
31	}
32}
33
34// Path returns the storage path for compiled filters.
35func (s *Store) Path() string {
36	return s.path
37}
38
39// CompileResult holds the result of a filter compilation.
40type CompileResult struct {
41	Filter *webkit.UserContentFilter
42	Err    error
43}
44
45// Compile compiles a JSON filter file and stores it with the given identifier.
46// This is an async operation that may take several seconds for large filter sets.
47// The JSON file must be in Safari Content Blocker format.
48func (s *Store) Compile(ctx context.Context, identifier, jsonPath string) (*webkit.UserContentFilter, error) {
49	log := logging.FromContext(ctx).With().
50		Str("component", "filter-store").
51		Str("identifier", identifier).
52		Str("json_path", jsonPath).
53		Logger()
54
55	s.mu.Lock()
56	defer s.mu.Unlock()
57
58	// Create GFile from path
59	file := gio.FileNewForPath(jsonPath)
60	if file == nil {
61		return nil, fmt.Errorf("failed to create GFile for path: %s", jsonPath)
62	}
63
64	log.Debug().Msg("starting filter compilation")
65
66	// Use a channel to receive the async result
67	resultCh := make(chan CompileResult, 1)
68
69	// Create async callback
70	cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
71		if resPtr == 0 {
72			resultCh <- CompileResult{Err: fmt.Errorf("nil async result")}
73			return
74		}
75
76		res := &gio.AsyncResultBase{Ptr: resPtr}
77		filter, err := s.inner.SaveFromFileFinish(res)
78		resultCh <- CompileResult{Filter: filter, Err: err}
79	})
80
81	// Start async compilation
82	s.inner.SaveFromFile(identifier, file, nil, &cb, 0)
83
84	// Wait for result or context cancellation
85	select {
86	case result := <-resultCh:
87		if result.Err != nil {
88			log.Error().Err(result.Err).Msg("filter compilation failed")
89			return nil, result.Err
90		}
91		log.Info().Msg("filter compilation completed")
92		return result.Filter, nil
93	case <-ctx.Done():
94		log.Warn().Msg("filter compilation canceled")
95		return nil, ctx.Err()
96	}
97}
98
99// LoadResult holds the result of loading a compiled filter.
100type LoadResult struct {
101	Filter *webkit.UserContentFilter
102	Err    error
103}
104
105// Load loads a previously compiled filter by its identifier.
106// This is fast (~50ms) as it loads pre-compiled bytecode.
107func (s *Store) Load(ctx context.Context, identifier string) (*webkit.UserContentFilter, error) {
108	log := logging.FromContext(ctx).With().
109		Str("component", "filter-store").
110		Str("identifier", identifier).
111		Logger()
112
113	s.mu.Lock()
114	defer s.mu.Unlock()
115
116	log.Debug().Msg("loading compiled filter")
117
118	// Use a channel to receive the async result
119	resultCh := make(chan LoadResult, 1)
120
121	// Create async callback
122	cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
123		if resPtr == 0 {
124			resultCh <- LoadResult{Err: fmt.Errorf("nil async result")}
125			return
126		}
127
128		res := &gio.AsyncResultBase{Ptr: resPtr}
129		filter, err := s.inner.LoadFinish(res)
130		resultCh <- LoadResult{Filter: filter, Err: err}
131	})
132
133	// Start async load
134	s.inner.Load(identifier, nil, &cb, 0)
135
136	// Wait for result or context cancellation
137	select {
138	case result := <-resultCh:
139		if result.Err != nil {
140			log.Debug().Err(result.Err).Msg("filter load failed (may not exist)")
141			return nil, result.Err
142		}
143		log.Debug().Msg("filter loaded successfully")
144		return result.Filter, nil
145	case <-ctx.Done():
146		log.Warn().Msg("filter load canceled")
147		return nil, ctx.Err()
148	}
149}
150
151// FetchIdentifiersResult holds the result of fetching filter identifiers.
152type FetchIdentifiersResult struct {
153	Identifiers []string
154	Err         error
155}
156
157// FetchIdentifiers returns all stored filter identifiers.
158func (s *Store) FetchIdentifiers(ctx context.Context) ([]string, error) {
159	log := logging.FromContext(ctx).With().
160		Str("component", "filter-store").
161		Logger()
162
163	s.mu.Lock()
164	defer s.mu.Unlock()
165
166	log.Debug().Msg("fetching filter identifiers")
167
168	// Use a channel to receive the async result
169	resultCh := make(chan FetchIdentifiersResult, 1)
170
171	// Create async callback
172	cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
173		if resPtr == 0 {
174			resultCh <- FetchIdentifiersResult{Err: fmt.Errorf("nil async result")}
175			return
176		}
177
178		res := &gio.AsyncResultBase{Ptr: resPtr}
179		identifiers := s.inner.FetchIdentifiersFinish(res)
180		resultCh <- FetchIdentifiersResult{Identifiers: identifiers}
181	})
182
183	// Start async fetch
184	s.inner.FetchIdentifiers(nil, &cb, 0)
185
186	// Wait for result or context cancellation
187	select {
188	case result := <-resultCh:
189		if result.Err != nil {
190			log.Error().Err(result.Err).Msg("failed to fetch identifiers")
191			return nil, result.Err
192		}
193		log.Debug().Int("count", len(result.Identifiers)).Msg("fetched filter identifiers")
194		return result.Identifiers, nil
195	case <-ctx.Done():
196		log.Warn().Msg("fetch identifiers canceled")
197		return nil, ctx.Err()
198	}
199}
200
201// Remove removes a compiled filter by its identifier.
202func (s *Store) Remove(ctx context.Context, identifier string) error {
203	log := logging.FromContext(ctx).With().
204		Str("component", "filter-store").
205		Str("identifier", identifier).
206		Logger()
207
208	s.mu.Lock()
209	defer s.mu.Unlock()
210
211	log.Debug().Msg("removing filter")
212
213	// Use a channel to receive the async result
214	resultCh := make(chan error, 1)
215
216	// Create async callback
217	cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
218		if resPtr == 0 {
219			resultCh <- fmt.Errorf("nil async result")
220			return
221		}
222
223		res := &gio.AsyncResultBase{Ptr: resPtr}
224		_, err := s.inner.RemoveFinish(res)
225		resultCh <- err
226	})
227
228	// Start async remove
229	s.inner.Remove(identifier, nil, &cb, 0)
230
231	// Wait for result or context cancellation
232	select {
233	case err := <-resultCh:
234		if err != nil {
235			log.Error().Err(err).Msg("failed to remove filter")
236			return err
237		}
238		log.Info().Msg("filter removed")
239		return nil
240	case <-ctx.Done():
241		log.Warn().Msg("remove filter canceled")
242		return ctx.Err()
243	}
244}
245
246// HasCompiledFilter checks if a compiled filter exists for the given identifier.
247func (s *Store) HasCompiledFilter(ctx context.Context, identifier string) bool {
248	identifiers, err := s.FetchIdentifiers(ctx)
249	if err != nil {
250		return false
251	}
252	return slices.Contains(identifiers, identifier)
253}

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