v0.27.1 c78ce83

store.go RawHistoryFolder

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

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