v0.8.0 0fa6785

filtering_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: webkitgtk-6.0 gtk4
7#cgo CFLAGS: -I/usr/include/webkitgtk-6.0
8#include <webkit/webkit.h>
9#include <stdlib.h>
10
11// Global filter store instance
12static WebKitUserContentFilterStore *filter_store = NULL;
13
14// External Go functions
15extern char* get_webkit_filter_store_path();
16extern void log_filter_success(char* identifier);
17extern void log_filter_error(char* error_msg, char* identifier);
18
19// Callback for save operation completion
20static void filter_save_callback(GObject *source_object,
21                                 GAsyncResult *result,
22                                 gpointer user_data) {
23    WebKitUserContentFilterStore *store = WEBKIT_USER_CONTENT_FILTER_STORE(source_object);
24    WebKitUserContentManager *manager = WEBKIT_USER_CONTENT_MANAGER(user_data);
25    GError *error = NULL;
26
27    WebKitUserContentFilter *filter = webkit_user_content_filter_store_save_finish(store, result, &error);
28
29    if (filter && !error) {
30        webkit_user_content_manager_add_filter(manager, filter);
31        const char *id = webkit_user_content_filter_get_identifier(filter);
32        log_filter_success((char*)id);
33        webkit_user_content_filter_unref(filter);
34    }
35
36    if (error) {
37        char *msg = g_strdup(error->message);
38        log_filter_error(msg, "unknown");
39        g_free(msg);
40        g_error_free(error);
41    }
42}
43
44// Initialize filter store if not already done
45static void ensure_filter_store() {
46    if (filter_store == NULL) {
47        // Get XDG-compliant path from Go (with tmp fallback)
48        char *store_path = get_webkit_filter_store_path();
49        filter_store = webkit_user_content_filter_store_new(store_path);
50        free(store_path);  // Free the C string returned by Go
51    }
52}
53
54static void apply_content_filter(WebKitUserContentManager *manager,
55                                 const char *filter_json,
56                                 const char *identifier) {
57    ensure_filter_store();
58
59    GBytes *bytes = g_bytes_new(filter_json, strlen(filter_json));
60
61    // Save filter to store asynchronously - it will be applied in the callback
62    webkit_user_content_filter_store_save(filter_store,
63                                          identifier,
64                                          bytes,
65                                          NULL,
66                                          filter_save_callback,
67                                          manager);
68
69    g_bytes_unref(bytes);
70}
71
72static void inject_cosmetic_script(WebKitUserContentManager *manager,
73                                   const char *script_source) {
74    // Use isolated script world to prevent interference with page JavaScript
75    WebKitUserScript *script = webkit_user_script_new_for_world(
76        script_source,
77        WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
78        WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END, // Changed from DOCUMENT_START to avoid preconnect interference
79        "dumber-cosmetic", // Isolated world name
80        NULL, NULL
81    );
82
83    webkit_user_content_manager_add_script(manager, script);
84    webkit_user_script_unref(script);
85}
86
87static void clear_all_filters(WebKitUserContentManager *manager) {
88    webkit_user_content_manager_remove_all_filters(manager);
89}
90
91static void clear_all_scripts(WebKitUserContentManager *manager) {
92    webkit_user_content_manager_remove_all_scripts(manager);
93}
94*/
95import "C"
96
97import (
98	"encoding/json"
99	"fmt"
100	"os"
101	"path/filepath"
102	"unsafe"
103
104	"github.com/bnema/dumber/internal/config"
105	"github.com/bnema/dumber/internal/logging"
106)
107
108//export get_webkit_filter_store_path
109func get_webkit_filter_store_path() *C.char {
110	// Try XDG directories first
111	if filterCacheDir, err := config.GetFilterCacheDir(); err == nil {
112		webkitFilterDir := filepath.Join(filterCacheDir, "webkit-store")
113		if err := os.MkdirAll(webkitFilterDir, 0755); err == nil {
114			return C.CString(webkitFilterDir)
115		}
116	}
117
118	// Fallback to tmp if XDG fails
119	tmpDir := os.TempDir()
120	fallbackDir := filepath.Join(tmpDir, "dumber-filters")
121	if err := os.MkdirAll(fallbackDir, 0755); err == nil {
122		logging.Warn("[webkit] Using fallback tmp directory for filter store: " + fallbackDir)
123		return C.CString(fallbackDir)
124	}
125
126	// Last resort - return tmp dir
127	logging.Error("[webkit] Failed to create filter store directory, using tmp")
128	return C.CString(tmpDir)
129}
130
131//export log_filter_success
132func log_filter_success(identifier *C.char) {
133	id := C.GoString(identifier)
134	logging.Info("[webkit] Network filter applied successfully: " + id)
135}
136
137//export log_filter_error
138func log_filter_error(errorMsg *C.char, identifier *C.char) {
139	msg := C.GoString(errorMsg)
140	id := C.GoString(identifier)
141	logging.Error("[webkit] Failed to apply network filter '" + id + "': " + msg)
142}
143
144// ApplyContentFilters applies WebKit native content filters with error recovery
145func (w *WebView) ApplyContentFilters(filters []byte, identifier string) error {
146	// Check WebView state with debug support
147	if !w.CheckWebViewState() {
148		w.LogDebugInfo()
149		return ErrWebViewNotInitialized
150	}
151
152	// Validate input
153	if len(filters) == 0 || identifier == "" {
154		return fmt.Errorf("invalid filter parameters: empty filters or identifier")
155	}
156
157	// Check if filters are valid JSON
158	if !json.Valid(filters) {
159		return fmt.Errorf("invalid filter JSON for identifier: %s", identifier)
160	}
161
162	defer func() {
163		if r := recover(); r != nil {
164			logging.Error(fmt.Sprintf("[webkit] Panic in ApplyContentFilters: %v", r))
165		}
166	}()
167
168	manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
169	if manager == nil {
170		return ErrContentManagerNotFound
171	}
172
173	cFilters := C.CString(string(filters))
174	cID := C.CString(identifier)
175	defer C.free(unsafe.Pointer(cFilters))
176	defer C.free(unsafe.Pointer(cID))
177
178	logging.Info("[webkit] Applying network filters: " + identifier + " (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
179
180	// Apply filter with error checking
181	C.apply_content_filter(manager, cFilters, cID)
182	return nil
183}
184
185// InjectCosmeticFilter injects JavaScript for cosmetic filtering with error recovery
186func (w *WebView) InjectCosmeticFilter(script string) error {
187	// Check WebView state with debug support
188	if !w.CheckWebViewState() {
189		w.LogDebugInfo()
190		return ErrWebViewNotInitialized
191	}
192
193	// Validate input
194	if len(script) == 0 {
195		return fmt.Errorf("empty cosmetic filter script")
196	}
197
198	defer func() {
199		if r := recover(); r != nil {
200			logging.Error(fmt.Sprintf("[webkit] Panic in InjectCosmeticFilter: %v", r))
201		}
202	}()
203
204	manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
205	if manager == nil {
206		return ErrContentManagerNotFound
207	}
208
209	cScript := C.CString(script)
210	defer C.free(unsafe.Pointer(cScript))
211
212	logging.Info("[webkit] Injecting cosmetic filter script (" + fmt.Sprintf("%d", len(script)) + " chars)")
213
214	// Inject script with error recovery
215	C.inject_cosmetic_script(manager, cScript)
216	return nil
217}
218
219// ClearAllFilters removes all content filters
220func (w *WebView) ClearAllFilters() error {
221	if w.native == nil || w.native.wv == nil {
222		return ErrWebViewNotInitialized
223	}
224
225	manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
226	if manager == nil {
227		return ErrContentManagerNotFound
228	}
229
230	logging.Info("[webkit] Clearing all content filters")
231	C.clear_all_filters(manager)
232	return nil
233}
234
235// ClearAllScripts removes all injected scripts
236func (w *WebView) ClearAllScripts() error {
237	if w.native == nil || w.native.wv == nil {
238		return ErrWebViewNotInitialized
239	}
240
241	manager := C.webkit_web_view_get_user_content_manager(w.native.wv)
242	if manager == nil {
243		return ErrContentManagerNotFound
244	}
245
246	logging.Info("[webkit] Clearing all injected scripts")
247	C.clear_all_scripts(manager)
248	return nil
249}

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