v0.8.0 0fa6785

webview_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
7#cgo CFLAGS: -I/usr/include/webkitgtk-6.0
8#include <stdlib.h>
9#include <string.h>
10#include <gtk/gtk.h>
11#include <webkit/webkit.h>
12#include <glib-object.h>
13#include <gdk/gdk.h>
14#include <jsc/jsc.h>
15#include <glib.h>
16#include <gio/gio.h>
17#include "webview_callbacks.h"
18
19static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
20
21// Forward declaration
22extern void goQuitMainLoop();
23
24// Connect window close signal to quit main loop
25static void connect_destroy_quit(GtkWidget* w) {
26    if (!w) return;
27    g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL);
28}
29static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
30// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
31
32// Forward declare helpers used below
33static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
34
35// TLS error handling forward declarations
36extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
37static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
38                                               const char *failing_uri,
39                                               GTlsCertificate *certificate,
40                                               GTlsCertificateFlags errors,
41                                               gpointer user_data);
42
43// Handle create signal for _blank links - redirect to current window
44static WebKitWebView* on_create_new_window(WebKitWebView* web_view,
45                                           WebKitNavigationAction* navigation_action,
46                                           gpointer user_data) {
47    (void)user_data;
48
49    // Get the request from navigation action
50    WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action);
51    if (request) {
52        const gchar* uri = webkit_uri_request_get_uri(request);
53        if (uri) {
54            // Load in current window instead of creating new one
55            webkit_web_view_load_uri(web_view, uri);
56        }
57    }
58
59    // Return NULL to prevent new window creation
60    return NULL;
61}
62static void connect_tls_error_handler(WebKitWebView* wv);
63
64
65// Dialog callback structure for modern AlertDialog async handling
66typedef struct {
67    int response;
68    gboolean completed;
69    GMainLoop *loop;
70} AlertDialogResponse;
71
72static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data);
73static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
74static char* extract_certificate_info(GTlsCertificate* certificate);
75
76// Setup video acceleration environment variables
77static void setup_video_acceleration(const char* driver_name, int enable_all, int legacy) {
78    if (legacy) {
79        // Legacy mode: force old VA-API plugins, disable modern ones
80        setenv("WEBKIT_GST_ENABLE_LEGACY_VAAPI", "1", 1);
81        // Don't set other env vars in legacy mode to avoid conflicts
82        return;
83    }
84
85    // Modern mode (default): use improved VA-API handling
86    if (driver_name && strlen(driver_name) > 0) {
87        if (strcmp(driver_name, "nvidia") == 0) {
88            // NVIDIA uses different env vars for hardware video acceleration
89            setenv("VDPAU_DRIVER", "nvidia", 1);
90            setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
91        } else if (strcmp(driver_name, "vdpau") == 0) {
92            // Explicit VDPAU backend (for NVIDIA via VA-API)
93            setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
94        } else {
95            // AMD/Intel use VA-API directly
96            setenv("LIBVA_DRIVER_NAME", driver_name, 1);
97        }
98    }
99
100    if (enable_all) {
101        setenv("GST_VAAPI_ALL_DRIVERS", "1", 1);
102    }
103}
104
105// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
106static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm, WebKitMemoryPressureSettings* pressure_settings) {
107    WebKitUserContentManager* u = webkit_user_content_manager_new();
108    if (!u) return NULL;
109    // WebKitGTK 6: create NetworkSession with data/cache directories
110    WebKitNetworkSession* sess = webkit_network_session_new(
111        data_dir ? data_dir : "",
112        cache_dir ? cache_dir : "");
113    if (!sess) { g_object_unref(u); return NULL; }
114    // Apply memory pressure settings to the session when provided
115    if (pressure_settings) {
116        // WebKitGTK 6.0 API: setter expects only the settings pointer
117        webkit_network_session_set_memory_pressure_settings(pressure_settings);
118    }
119    // Configure cookie persistence and policy (no third-party by default)
120    WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
121    if (cm) {
122        if (cookie_path && cookie_path[0]) {
123            webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
124        }
125        // 2 => no-third-party (default), 0 => always, 1 => never
126        maybe_set_cookie_policy(cm, 2);
127    }
128    // Persist credentials (HTTP auth, etc.)
129    webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
130    // Configure TLS errors to emit signals instead of failing silently
131    webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
132    GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
133        "user-content-manager", u,
134        "network-session", sess,
135        NULL));
136    // WebView holds refs to provided objects; drop our temporary refs
137    g_object_unref(sess);
138    // Set WebView background to black (easier on eyes when pages are loading)
139    GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
140    webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
141
142    // Handle create signal to redirect _blank links to current window
143    g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL);
144
145    // Connect TLS error handler
146    connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
147
148    if (out_ucm) { *out_ucm = u; }
149    return w;
150}
151
152static gboolean gtk_prefers_dark() {
153    // Method 1: Check GNOME desktop interface color-scheme (primary method)
154    GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
155    if (desktop_settings) {
156        gchar* color_scheme = g_settings_get_string(desktop_settings, "color-scheme");
157        if (color_scheme) {
158            gboolean prefer_dark = (g_strcmp0(color_scheme, "prefer-dark") == 0);
159            g_free(color_scheme);
160            g_object_unref(desktop_settings);
161            if (prefer_dark) return TRUE;
162        }
163        g_object_unref(desktop_settings);
164    }
165
166    // Method 2: Check GTK theme name for dark variants
167    GtkSettings* settings = gtk_settings_get_default();
168    if (settings) {
169        gchar* theme_name = NULL;
170        g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
171        if (theme_name) {
172            gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
173                               strstr(theme_name, "-Dark") != NULL);
174            g_free(theme_name);
175            if (is_dark) return TRUE;
176        }
177
178        // Method 3: Check gtk-application-prefer-dark-theme (fallback)
179        gboolean prefer = FALSE;
180        g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
181        if (prefer) return TRUE;
182    }
183
184    return FALSE;
185}
186
187// Note: preferred color scheme is handled via a user script injection to support
188// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
189
190extern void goOnUcmMessage(unsigned long id, char* json);
191
192// Helper functions to connect signals using static callbacks
193static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
194    if (!widget) return;
195    g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
196}
197
198static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
199    if (!widget) return;
200    g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
201}
202
203static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
204    if (!widget) return;
205    g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
206}
207
208static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
209    if (!settings) return;
210    g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
211}
212
213static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
214    if (!context || !scheme) return;
215    webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
216}
217static void register_uri_scheme_security(WebKitWebContext* context, const char* scheme) {
218	if (!context || !scheme) return;
219	WebKitSecurityManager* sm = webkit_web_context_get_security_manager(context);
220	if (!sm) return;
221
222	// Mark as secure to avoid mixed-content warnings when requested by https pages
223	webkit_security_manager_register_uri_scheme_as_secure(sm, scheme);
224	// Treat as local scheme
225	webkit_security_manager_register_uri_scheme_as_local(sm, scheme);
226	// Allow cross-origin requests to this scheme (bypass CORS header checks for this scheme)
227	webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);
228}
229
230// Script message handler wiring for WebKitGTK 6
231// Forward declaration for callback used in g_signal_connect_data
232// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
233static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
234// Uses WebKitScriptMessage and script-message-received::NAME signal.
235static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
236    (void)ucm;
237    if (!val) return;
238    gchar* s = jsc_value_to_string(val);
239    if (!s) return;
240    goOnUcmMessage((unsigned long)user_data, s);
241    g_free(s);
242}
243
244static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
245    if (!ucm || !name) return FALSE;
246    // WebKitGTK 6 signature: (ucm, name, world_name)
247    webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
248    gchar* signal = g_strdup_printf("script-message-received::%s", name);
249    if (!signal) return FALSE;
250    g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
251    g_free(signal);
252    return TRUE;
253}
254
255// Register a script message handler for a specific JavaScript world
256static gboolean register_ucm_handler_world(WebKitUserContentManager* ucm, const char* name, const char* world_name, unsigned long id) {
257	if (!ucm || !name || !world_name) return FALSE;
258	webkit_user_content_manager_register_script_message_handler(ucm, name, world_name);
259	gchar* signal = g_strdup_printf("script-message-received::%s", name);
260	if (!signal) return FALSE;
261	g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
262	g_free(signal);
263	return TRUE;
264}
265
266// ----- Conditional helpers for settings across WebKit versions -----
267#ifndef WEBKIT_CHECK_VERSION
268#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
269#endif
270
271static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
272#if WEBKIT_CHECK_VERSION(2,40,0)
273    // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
274    if (!settings) return;
275    if (mode == 1) {
276        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
277    } else if (mode == 2) {
278        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
279    } else {
280        // Leave default policy (auto) without forcing a value to avoid referencing
281        // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
282    }
283#else
284    (void)settings; (void)mode;
285#endif
286}
287
288static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
289#if WEBKIT_CHECK_VERSION(2,36,0)
290    if (!settings) return;
291    webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
292#else
293    (void)settings; (void)enable;
294#endif
295}
296
297static void maybe_set_webgl(WebKitSettings* settings, int enable) {
298#if WEBKIT_CHECK_VERSION(2,20,0)
299    if (!settings) return;
300    webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
301#else
302    (void)settings; (void)enable;
303#endif
304}
305
306static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
307#if WEBKIT_CHECK_VERSION(2,20,0)
308    if (!settings) return;
309    webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
310#else
311    (void)settings; (void)enable;
312#endif
313}
314
315static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
316#if WEBKIT_CHECK_VERSION(2,20,0)
317    if (!settings) return;
318    webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
319#else
320    (void)settings; (void)require;
321#endif
322}
323
324// Enable two-finger swipe back/forward navigation gestures when supported
325static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
326#if WEBKIT_CHECK_VERSION(2,38,0)
327    if (!settings) return;
328    webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
329#else
330    (void)settings; (void)enable;
331#endif
332}
333
334// Set cookie accept policy with compile-time compatibility across WebKit versions
335static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
336    if (!cm) return;
337#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
338    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
339    if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
340    else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
341    webkit_cookie_manager_set_accept_policy(cm, p);
342#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
343    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
344    if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
345    else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
346    webkit_cookie_manager_set_accept_policy(cm, p);
347#else
348    (void)policy; // Not supported in this header version
349#endif
350}
351
352// Memory pressure settings helpers (version-guarded)
353static WebKitMemoryPressureSettings* create_memory_pressure_settings(
354    unsigned int memory_limit_mb,
355    double conservative_threshold,
356    double strict_threshold,
357    double kill_threshold,
358    double poll_interval) {
359    // If headers don't expose memory pressure APIs, return NULL to signal unsupported
360#if WEBKIT_CHECK_VERSION(2,44,0)
361    WebKitMemoryPressureSettings* settings = webkit_memory_pressure_settings_new();
362    if (!settings) return NULL;
363
364    if (memory_limit_mb > 0) {
365        webkit_memory_pressure_settings_set_memory_limit(settings, (guint)(memory_limit_mb * 1024 * 1024));
366    }
367
368    webkit_memory_pressure_settings_set_conservative_threshold(settings, conservative_threshold);
369    webkit_memory_pressure_settings_set_strict_threshold(settings, strict_threshold);
370
371    if (kill_threshold > 0) {
372        webkit_memory_pressure_settings_set_kill_threshold(settings, kill_threshold);
373    }
374
375    webkit_memory_pressure_settings_set_poll_interval(settings, poll_interval);
376
377    return settings;
378#else
379    (void)memory_limit_mb; (void)conservative_threshold; (void)strict_threshold; (void)kill_threshold; (void)poll_interval;
380    return NULL;
381#endif
382}
383
384// Cache model configuration
385static void maybe_set_cache_model(WebKitWebContext* context, int model) {
386    if (!context) return;
387    WebKitCacheModel cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; // default
388
389    switch (model) {
390        case 0: cache_model = WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER; break;
391        case 1: cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; break;
392        case 2:
393#ifdef WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER
394            cache_model = WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER;
395#else
396            cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER;
397#endif
398            break;
399    }
400
401    webkit_web_context_set_cache_model(context, cache_model);
402}
403
404// Additional settings for memory optimization
405static void maybe_set_page_cache(WebKitSettings* settings, int enable) {
406#if WEBKIT_CHECK_VERSION(2,6,0)
407    if (!settings) return;
408    webkit_settings_set_enable_page_cache(settings, enable ? TRUE : FALSE);
409#else
410    (void)settings; (void)enable;
411#endif
412}
413
414// Offline Web Application Cache is deprecated in modern WebKit; keep as no-op to avoid warnings.
415static void maybe_set_offline_app_cache(WebKitSettings* settings, int enable) {
416    (void)settings; (void)enable;
417}
418
419// Apply memory pressure settings to context if available
420static void maybe_set_memory_pressure_settings(WebKitWebContext* context, WebKitMemoryPressureSettings* settings) {
421    // No-op wrapper kept for compatibility; settings applied to NetworkSession instead.
422    (void)context; (void)settings;
423}
424
425// Trigger JavaScript GC if supported by headers
426static void maybe_collect_js(WebKitWebContext* context) {
427    // Disabled by default due to API availability variance across 6.0 builds.
428    (void)context;
429}
430
431// TLS error handling implementation
432static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
433                                               const char *failing_uri,
434                                               GTlsCertificate *certificate,
435                                               GTlsCertificateFlags errors,
436                                               gpointer user_data) {
437    (void)user_data;
438
439    // Extract host from URI for the Go callback
440    const char* host = failing_uri;
441    char* host_copy = NULL;
442
443    if (strstr(failing_uri, "://")) {
444        host = strstr(failing_uri, "://") + 3;
445        char* slash = strchr(host, '/');
446        if (slash) {
447            // Create a temporary null-terminated host string
448            size_t host_len = slash - host;
449            host_copy = malloc(host_len + 1);
450            if (host_copy) {
451                strncpy(host_copy, host, host_len);
452                host_copy[host_len] = '\0';
453                host = host_copy;
454            }
455        }
456    }
457
458    // Extract certificate information
459    char* cert_info = extract_certificate_info(certificate);
460
461    gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
462
463    // If user accepted, allow the certificate for this host
464    if (should_proceed && certificate) {
465        printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
466        fflush(stdout);
467        // Get the network session from the web view
468        WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
469        if (session) {
470            webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
471            printf("[dumber] Certificate exception added for host: %s\n", host);
472            fflush(stdout);
473
474            // Try loading the URL again after adding the exception
475            webkit_web_view_load_uri(web_view, failing_uri);
476            printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
477            fflush(stdout);
478        }
479    }
480
481    // Free the certificate info string
482    if (cert_info) {
483        g_free(cert_info);
484    }
485
486    if (host_copy) {
487        free(host_copy);
488    }
489
490    return should_proceed;
491}
492
493// Extract certificate information for display
494static char* extract_certificate_info(GTlsCertificate* certificate) {
495    if (!certificate) {
496        return g_strdup("Certificate information not available");
497    }
498
499    printf("[dumber] Extracting certificate information...\n");
500    fflush(stdout);
501
502    GString* info = g_string_new("Certificate Information:\n");
503
504    // Try to get certificate properties - but these might not be available in all GTK versions
505    gchar* subject = NULL;
506    gchar* issuer = NULL;
507    GDateTime* not_valid_before = NULL;
508    GDateTime* not_valid_after = NULL;
509
510    // Try property access but handle failures gracefully
511    g_object_get(certificate,
512                 "subject-name", &subject,
513                 "issuer-name", &issuer,
514                 "not-valid-before", &not_valid_before,
515                 "not-valid-after", &not_valid_after,
516                 NULL);
517
518    if (subject) {
519        g_string_append_printf(info, "Subject: %s\n", subject);
520        g_free(subject);
521        printf("[dumber] Found subject info\n");
522    } else {
523        g_string_append(info, "Subject: Information not available\n");
524    }
525
526    if (issuer) {
527        g_string_append_printf(info, "Issued by: %s\n", issuer);
528        g_free(issuer);
529        printf("[dumber] Found issuer info\n");
530    } else {
531        g_string_append(info, "Issued by: Information not available\n");
532    }
533
534    if (not_valid_before) {
535        gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
536        g_string_append_printf(info, "Valid from: %s\n", date_str);
537        g_free(date_str);
538        g_date_time_unref(not_valid_before);
539    } else {
540        g_string_append(info, "Valid from: Information not available\n");
541    }
542
543    if (not_valid_after) {
544        gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
545        g_string_append_printf(info, "Valid until: %s\n", date_str);
546        g_free(date_str);
547        g_date_time_unref(not_valid_after);
548    } else {
549        g_string_append(info, "Valid until: Information not available\n");
550    }
551
552    char* result = g_string_free(info, FALSE);
553    printf("[dumber] Certificate info: %s\n", result);
554    fflush(stdout);
555    return result;
556}
557
558static void connect_tls_error_handler(WebKitWebView* wv) {
559    if (!wv) return;
560    g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
561                     G_CALLBACK(on_load_failed_with_tls_errors), NULL);
562}
563
564// Modern AlertDialog async callback
565static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data) {
566    AlertDialogResponse *data = (AlertDialogResponse*)user_data;
567    GError *error = NULL;
568    data->response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
569    if (error) {
570        printf("[dumber] AlertDialog error: %s\n", error->message);
571        g_error_free(error);
572        data->response = 0;
573    }
574    data->completed = TRUE;
575    const char *response_names[] = {"GO_BACK", "PROCEED_ONCE", "ALWAYS_ACCEPT"};
576    const char *response_name = (data->response >= 0 && data->response <= 2) ?
577                               response_names[data->response] : "UNKNOWN";
578    printf("[dumber] TLS dialog response: %s (button_index=%d)\n", response_name, data->response);
579    fflush(stdout);
580    if (data->loop) {
581        g_main_loop_quit(data->loop);
582    }
583}
584
585// Show TLS warning dialog synchronously using modern GtkAlertDialog API
586static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
587    printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
588    printf("[dumber] Error message: %s\n", error_msg);
589    fflush(stdout);
590
591    // Create the modern AlertDialog
592    GtkAlertDialog *dialog = gtk_alert_dialog_new("Certificate Error for %s", hostname);
593
594    // Set modal behavior and error details
595    gtk_alert_dialog_set_modal(dialog, TRUE);
596    gtk_alert_dialog_set_detail(dialog, error_msg);
597
598    // Configure buttons (Go Back = 0, Proceed Once = 1, Always Accept = 2)
599    const char *buttons[] = {"Go Back", "Proceed Once (Unsafe)", "Always Accept This Site", NULL};
600    gtk_alert_dialog_set_buttons(dialog, buttons);
601    gtk_alert_dialog_set_default_button(dialog, 0);
602    gtk_alert_dialog_set_cancel_button(dialog, 0);
603
604    // Set up response data with main loop for sync behavior
605    AlertDialogResponse response_data = {0, FALSE, g_main_loop_new(NULL, FALSE)};
606
607    // Start async operation
608    gtk_alert_dialog_choose(dialog, parent, NULL,
609                           on_alert_dialog_choose_done, &response_data);
610
611    // Run event loop until dialog completes
612    g_main_loop_run(response_data.loop);
613    g_main_loop_unref(response_data.loop);
614
615    // Clean up
616    g_object_unref(dialog);
617
618    // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept
619    return response_data.response;
620}
621*/
622import "C"
623
624import (
625	"context"
626	"crypto/sha256"
627	"database/sql"
628	"errors"
629	"fmt"
630	"log"
631	"os"
632	"path/filepath"
633	"runtime"
634	"strings"
635	"time"
636	"unsafe"
637
638	"github.com/bnema/dumber/internal/config"
639	"github.com/bnema/dumber/internal/db"
640	"github.com/bnema/dumber/internal/logging"
641	_ "github.com/ncruces/go-sqlite3"
642)
643
644// FilterManager interface for content blocking integration
645type FilterManager interface {
646	GetNetworkFilters() ([]byte, error)
647	GetCosmeticScript() string
648	GetCosmeticScriptForDomain(domain string) string
649}
650
651// Helper function to convert bool to int for C interop
652func boolToInt(b bool) int {
653	if b {
654		return 1
655	}
656	return 0
657}
658
659// getCertificateHash generates a SHA256 hash of the certificate info for storage
660func getCertificateHash(certificateInfo string) string {
661	hash := sha256.Sum256([]byte(certificateInfo))
662	return fmt.Sprintf("%x", hash)
663}
664
665// getDBConnection opens a database connection and returns queries
666func getDBConnection() (*db.Queries, *sql.DB, error) {
667	cfg := config.Get()
668	if cfg == nil {
669		return nil, nil, fmt.Errorf("config not available")
670	}
671
672	dbPath := cfg.Database.Path
673	if dbPath == "" {
674		var err error
675		dbPath, err = config.GetDatabaseFile()
676		if err != nil {
677			return nil, nil, fmt.Errorf("failed to get database path: %w", err)
678		}
679	}
680
681	database, err := sql.Open("sqlite3", dbPath)
682	if err != nil {
683		return nil, nil, fmt.Errorf("failed to open database: %w", err)
684	}
685
686	queries := db.New(database)
687	return queries, database, nil
688}
689
690// checkStoredCertificateDecision checks if user has previously decided on this certificate
691func checkStoredCertificateDecision(hostname, certificateInfo string) (string, bool) {
692	queries, database, err := getDBConnection()
693	if err != nil {
694		log.Printf("[tls] Failed to connect to database: %v", err)
695		return "", false
696	}
697	defer database.Close()
698
699	certHash := getCertificateHash(certificateInfo)
700	ctx := context.Background()
701
702	validation, err := queries.GetCertificateValidation(ctx, hostname, certHash)
703	if err != nil {
704		if err == sql.ErrNoRows {
705			return "", false // No previous decision
706		}
707		log.Printf("[tls] Failed to query certificate validation: %v", err)
708		return "", false
709	}
710
711	log.Printf("[tls] Found stored certificate decision for %s: %s", hostname, validation.UserDecision)
712	return validation.UserDecision, true
713}
714
715// storeCertificateDecision stores the user's decision about a certificate
716func storeCertificateDecision(hostname, certificateInfo, decision string, permanent bool) error {
717	queries, database, err := getDBConnection()
718	if err != nil {
719		return fmt.Errorf("failed to connect to database: %w", err)
720	}
721	defer database.Close()
722
723	certHash := getCertificateHash(certificateInfo)
724	ctx := context.Background()
725
726	var expiresAt sql.NullTime
727	if !permanent {
728		// For temporary decisions, expire after 24 hours
729		expiresAt = sql.NullTime{
730			Time:  time.Now().Add(24 * time.Hour),
731			Valid: true,
732		}
733	}
734
735	err = queries.StoreCertificateValidation(ctx, hostname, certHash, decision, expiresAt)
736	if err != nil {
737		return fmt.Errorf("failed to store certificate validation: %w", err)
738	}
739
740	log.Printf("[tls] Stored certificate decision for %s: %s (permanent: %t)", hostname, decision, permanent)
741	return nil
742}
743
744type nativeView struct {
745	win  *C.GtkWidget
746	view *C.GtkWidget
747	wv   *C.WebKitWebView
748	ucm  *C.WebKitUserContentManager
749}
750
751type memoryStats struct {
752	pageLoadCount          int
753	lastGCTime             time.Time
754	memoryPressureSettings *C.WebKitMemoryPressureSettings
755}
756
757// WebView represents a browser view powered by WebKit2GTK.
758type WebView struct {
759	config        *Config
760	zoom          float64
761	url           string
762	destroyed     bool
763	native        *nativeView
764	window        *Window
765	id            uintptr
766	msgHandler    func(payload string)
767	titleHandler  func(title string)
768	uriHandler    func(uri string)
769	zoomHandler   func(level float64)
770	memStats      *memoryStats
771	gcTicker      *time.Ticker
772	gcDone        chan struct{}
773	tlsExceptions map[string]bool // host -> whether user allowed certificate exception
774}
775
776// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
777func NewWebView(cfg *Config) (*WebView, error) {
778	log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
779	if C.gtk_init_check() == 0 {
780		return nil, errors.New("failed to initialize GTK")
781	}
782
783	if cfg == nil {
784		cfg = &Config{}
785	}
786
787	// Setup video acceleration environment variables before creating WebView
788	if cfg.VideoAcceleration.EnableVAAPI {
789		driverName := C.CString(cfg.VideoAcceleration.VAAPIDriverName)
790		enableAll := 0
791		if cfg.VideoAcceleration.EnableAllDrivers {
792			enableAll = 1
793		}
794		legacy := 0
795		if cfg.VideoAcceleration.LegacyVAAPI {
796			legacy = 1
797		}
798		C.setup_video_acceleration(driverName, C.int(enableAll), C.int(legacy))
799		C.free(unsafe.Pointer(driverName))
800	}
801
802	// Prepare persistent website data/caches
803	dataDir := cfg.DataDir
804	cacheDir := cfg.CacheDir
805	if dataDir == "" {
806		dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data")
807	}
808	if cacheDir == "" {
809		cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache")
810	}
811	_ = os.MkdirAll(dataDir, 0o755)
812	_ = os.MkdirAll(cacheDir, 0o755)
813
814	cData := C.CString(dataDir)
815	cCache := C.CString(cacheDir)
816	cookieFile := filepath.Join(dataDir, "cookies.db")
817	cCookie := C.CString(cookieFile)
818	defer C.free(unsafe.Pointer(cData))
819	defer C.free(unsafe.Pointer(cCache))
820	defer C.free(unsafe.Pointer(cCookie))
821	// Use default WebContext to register the custom URI scheme handler; the
822	// WebView itself is created with a WebKitNetworkSession for persistence.
823	ctx := C.webkit_web_context_get_default()
824
825	// Apply cache model to WebContext
826	// Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
827	cacheModel := 1
828	if cfg.Memory.CacheModel != "" {
829		switch cfg.Memory.CacheModel {
830		case CacheModelWebBrowser:
831			cacheModel = 1
832		case CacheModelPrimaryWebBrowser:
833			cacheModel = 2
834		default: // CacheModelDocumentViewer or unset
835			cacheModel = 0
836		}
837	}
838	C.maybe_set_cache_model(ctx, C.int(cacheModel))
839
840	// Configure memory pressure settings if specified
841	var pressureSettings *C.WebKitMemoryPressureSettings
842	// Only configure memory pressure if explicitly requested via config
843	if cfg.Memory.MemoryLimitMB > 0 || cfg.Memory.ConservativeThreshold > 0 || cfg.Memory.StrictThreshold > 0 || cfg.Memory.KillThreshold > 0 || cfg.Memory.PollIntervalSeconds > 0 {
844		memLimitMB := cfg.Memory.MemoryLimitMB
845		conservativeThreshold := cfg.Memory.ConservativeThreshold
846		strictThreshold := cfg.Memory.StrictThreshold
847		killThreshold := cfg.Memory.KillThreshold
848		pollInterval := cfg.Memory.PollIntervalSeconds
849
850		pressureSettings = C.create_memory_pressure_settings(
851			C.uint(memLimitMB),
852			C.double(conservativeThreshold),
853			C.double(strictThreshold),
854			C.double(killThreshold),
855			C.double(pollInterval))
856
857		if pressureSettings != nil {
858			C.maybe_set_memory_pressure_settings(ctx, pressureSettings)
859			if cfg.Memory.EnableMemoryMonitoring {
860				log.Printf("[webkit] Memory pressure settings applied: limit=%dMB, conservative=%.2f, strict=%.2f, poll=%.1fs",
861					memLimitMB, conservativeThreshold, strictThreshold, pollInterval)
862			}
863		}
864	}
865
866	// Register custom URI scheme handler for "dumb://"
867	{
868		sch := C.CString("dumb")
869		C.register_uri_scheme_handler(ctx, sch)
870		C.register_uri_scheme_security(ctx, sch)
871		C.free(unsafe.Pointer(sch))
872	}
873	// Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
874
875	// Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
876	var createdUcm *C.WebKitUserContentManager
877	viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm, pressureSettings)
878	if viewWidget == nil {
879		return nil, errors.New("failed to create WebKitWebView")
880	}
881
882	// Create a top-level window to host the view
883	win := C.new_window()
884	if win == nil {
885		return nil, errors.New("failed to create GtkWindow")
886	}
887
888	// Pack view widget into the window
889	// GTK4: containers removed; use gtk_window_set_child
890	C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
891	C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
892	C.connect_destroy_quit(win)
893
894	v := &WebView{
895		config: cfg,
896		zoom:   1.0,
897		native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
898		window: &Window{win: win},
899		memStats: &memoryStats{
900			pageLoadCount:          0,
901			lastGCTime:             time.Now(),
902			memoryPressureSettings: pressureSettings,
903		},
904		gcDone:        make(chan struct{}),
905		tlsExceptions: make(map[string]bool),
906	}
907	// Assign an ID for accelerator dispatch
908	v.id = nextViewID()
909	registerView(v.id, v)
910
911	// Register with global memory manager if monitoring is enabled
912	if cfg.Memory.EnableMemoryMonitoring {
913		if globalMemoryManager == nil {
914			InitializeGlobalMemoryManager(
915				cfg.Memory.EnableMemoryMonitoring,
916				cfg.Memory.ProcessRecycleThreshold,
917				30*time.Second, // Default monitoring interval
918			)
919		}
920		if globalMemoryManager != nil {
921			globalMemoryManager.RegisterWebView(v)
922		}
923	}
924	// Attach GTK4 input controllers (keyboard, mouse)
925	AttachKeyboardControllers(v)
926
927	// Watch for GTK theme changes and propagate to page at runtime
928	if settings := C.gtk_settings_get_default(); settings != nil {
929		C.connect_theme_changed_with_id(settings, C.ulong(v.id))
930	}
931
932	// Native zoom shortcuts (independent of app services)
933	// Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
934	_ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
935		nz := v.zoom
936		if nz <= 0 {
937			nz = 1.0
938		}
939		nz *= 1.1
940		if nz < 0.25 {
941			nz = 0.25
942		}
943		if nz > 5.0 {
944			nz = 5.0
945		}
946		_ = v.SetZoom(nz)
947	})
948	_ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
949		nz := v.zoom
950		if nz <= 0 {
951			nz = 1.0
952		}
953		nz *= 1.1
954		if nz < 0.25 {
955			nz = 0.25
956		}
957		if nz > 5.0 {
958			nz = 5.0
959		}
960		_ = v.SetZoom(nz)
961	})
962	_ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
963		nz := v.zoom
964		if nz <= 0 {
965			nz = 1.0
966		}
967		nz /= 1.1
968		if nz < 0.25 {
969			nz = 0.25
970		}
971		if nz > 5.0 {
972			nz = 5.0
973		}
974		_ = v.SetZoom(nz)
975	})
976	_ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
977	// Find in page (Ctrl/Cmd+F): use new keyboard service bridge
978	_ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() {
979		_ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))")
980	})
981	// Navigation with Alt+Arrow keys: use new keyboard service bridge
982	_ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() {
983		_ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowleft'}}))")
984	})
985	_ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() {
986		_ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowright'}}))")
987	})
988	// Update window title when page title changes
989	C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
990	// Also dispatch title change to Go with view id
991	C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
992	// Notify URI changes to Go to keep current URL in sync
993	C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
994	// Apply hardware acceleration and related settings based on cfg.Rendering
995	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
996		// Hardware acceleration policy (guarded by version)
997		switch cfg.Rendering.Mode {
998		case "gpu":
999			C.maybe_set_hw_policy(settings, 1)
1000			C.maybe_set_webgl(settings, 1)
1001			C.maybe_set_canvas_accel(settings, 1)
1002		case "cpu":
1003			C.maybe_set_hw_policy(settings, 2)
1004			C.maybe_set_webgl(settings, 0)
1005			C.maybe_set_canvas_accel(settings, 0)
1006		default: // auto
1007			C.maybe_set_hw_policy(settings, 0)
1008			C.maybe_set_webgl(settings, 1)
1009			C.maybe_set_canvas_accel(settings, 1)
1010		}
1011		// Optional compositing indicators for debugging (guarded by version)
1012		if cfg.Rendering.DebugGPU {
1013			C.maybe_set_draw_indicators(settings, 1)
1014		}
1015		// Reduce media pipeline churn by requiring a user gesture for playback
1016		C.maybe_set_media_user_gesture(settings, 1)
1017		// Enable trackpad back/forward gestures when available
1018		C.maybe_set_back_forward_gestures(settings, 1)
1019
1020		// Keep page cache enabled by default for performance
1021		enablePageCache := true
1022		if cfg.Memory.EnablePageCache {
1023			enablePageCache = true
1024		}
1025		C.maybe_set_page_cache(settings, C.int(boolToInt(enablePageCache)))
1026
1027		// Offline app cache is deprecated; keep as no-op
1028		C.maybe_set_offline_app_cache(settings, C.int(0))
1029
1030		if cfg.Memory.EnableMemoryMonitoring {
1031			log.Printf("[webkit] Memory settings applied: page_cache=%v", enablePageCache)
1032		}
1033	}
1034
1035	if cfg.ZoomDefault > 0 {
1036		v.zoom = cfg.ZoomDefault
1037		C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
1038	}
1039	if cfg.EnableDeveloperExtras {
1040		settings := C.webkit_web_view_get_settings(v.native.wv)
1041		if settings != nil {
1042			C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
1043		}
1044	}
1045
1046	// Apply custom User-Agent for codec negotiation
1047	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1048		if cfg.CodecPreferences.CustomUserAgent != "" {
1049			cUserAgent := C.CString(cfg.CodecPreferences.CustomUserAgent)
1050			C.webkit_settings_set_user_agent(settings, (*C.gchar)(cUserAgent))
1051			C.free(unsafe.Pointer(cUserAgent))
1052			log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)
1053		} else if cfg.CodecPreferences.ForceAV1 {
1054			// Use modern Chrome UA that signals AV1 support
1055			av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
1056			cAV1UA := C.CString(av1UA)
1057			C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))
1058			C.free(unsafe.Pointer(cAV1UA))
1059			log.Printf("[webkit] Set AV1-optimized User-Agent")
1060		}
1061	}
1062
1063	// Apply default fonts if provided in cfg
1064	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1065		if cfg.DefaultSansFont != "" {
1066			csans := C.CString(cfg.DefaultSansFont)
1067			C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
1068			C.free(unsafe.Pointer(csans))
1069			// Also set default font family to sans
1070			csans2 := C.CString(cfg.DefaultSansFont)
1071			C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
1072			C.free(unsafe.Pointer(csans2))
1073		}
1074		if cfg.DefaultSerifFont != "" {
1075			cserif := C.CString(cfg.DefaultSerifFont)
1076			C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
1077			C.free(unsafe.Pointer(cserif))
1078		}
1079		if cfg.DefaultMonospaceFont != "" {
1080			cmono := C.CString(cfg.DefaultMonospaceFont)
1081			C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
1082			C.free(unsafe.Pointer(cmono))
1083		}
1084		if cfg.DefaultFontSize > 0 {
1085			C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
1086		}
1087	}
1088	if cfg.InitialURL != "" {
1089		_ = v.LoadURL(cfg.InitialURL)
1090	}
1091	// Ensure native resources are released if user forgets to call Destroy
1092	runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
1093	log.Printf("[webkit] WebView created (CGO)")
1094
1095	// Initialize UCM scripts and handlers
1096	if v.native != nil && v.native.ucm != nil {
1097		v.enableUserContentManager(cfg)
1098	}
1099
1100	// Start periodic JavaScript garbage collection if configured
1101	if cfg.Memory.EnableGCInterval > 0 {
1102		v.startPeriodicGC(time.Duration(cfg.Memory.EnableGCInterval) * time.Second)
1103		if cfg.Memory.EnableMemoryMonitoring {
1104			log.Printf("[webkit] Periodic GC enabled: interval=%ds", cfg.Memory.EnableGCInterval)
1105		}
1106	}
1107
1108	return v, nil
1109}
1110
1111func (w *WebView) LoadURL(rawURL string) error {
1112	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1113		return ErrNotImplemented
1114	}
1115
1116	// Track page loads for process recycling
1117	if w.memStats != nil {
1118		w.memStats.pageLoadCount++
1119
1120		// Check if we need to recycle this WebView
1121		if w.config.Memory.ProcessRecycleThreshold > 0 &&
1122			w.memStats.pageLoadCount >= w.config.Memory.ProcessRecycleThreshold {
1123			if w.config.Memory.EnableMemoryMonitoring {
1124				log.Printf("[webkit] Process recycle threshold reached (%d pages), consider creating new WebView",
1125					w.memStats.pageLoadCount)
1126			}
1127		}
1128	}
1129
1130	w.url = rawURL
1131	curl := C.CString(rawURL)
1132	defer C.free(unsafe.Pointer(curl))
1133	C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
1134	log.Printf("[webkit] LoadURL: %s", rawURL)
1135	return nil
1136}
1137
1138func (w *WebView) Show() error {
1139	if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
1140		return ErrNotImplemented
1141	}
1142	C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
1143	C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
1144	C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1145	log.Printf("[webkit] Show window")
1146	return nil
1147}
1148
1149func (w *WebView) Hide() error {
1150	if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
1151		return ErrNotImplemented
1152	}
1153	C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
1154	C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
1155	return nil
1156}
1157
1158func (w *WebView) Destroy() error {
1159	if w == nil || w.native == nil || w.native.win == nil {
1160		return ErrNotImplemented
1161	}
1162
1163	// Stop periodic GC if running
1164	w.stopPeriodicGC()
1165
1166	// Clean up memory pressure settings
1167	if w.memStats != nil && w.memStats.memoryPressureSettings != nil {
1168		// WebKit will clean up the memory pressure settings when context is destroyed
1169		w.memStats.memoryPressureSettings = nil
1170	}
1171
1172	// Unregister from memory manager
1173	if globalMemoryManager != nil {
1174		globalMemoryManager.UnregisterWebView(w.id)
1175	}
1176
1177	// GTK4: destroy windows via gtk_window_destroy
1178	C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1179	w.destroyed = true
1180	unregisterView(w.id)
1181	log.Printf("[webkit] Destroy window")
1182	return nil
1183}
1184
1185// Window returns the associated native window.
1186func (w *WebView) Window() *Window { return w.window }
1187
1188// GetCurrentURL returns the current URI from WebKit.
1189func (w *WebView) GetCurrentURL() string {
1190	if w == nil || w.native == nil || w.native.wv == nil {
1191		return ""
1192	}
1193	uri := C.webkit_web_view_get_uri(w.native.wv)
1194	if uri == nil {
1195		return ""
1196	}
1197	return C.GoString((*C.char)(unsafe.Pointer(uri)))
1198}
1199
1200func (w *WebView) GoBack() error {
1201	if w == nil || w.native == nil || w.native.wv == nil {
1202		return ErrNotImplemented
1203	}
1204	C.webkit_web_view_go_back(w.native.wv)
1205	return nil
1206}
1207
1208func (w *WebView) GoForward() error {
1209	if w == nil || w.native == nil || w.native.wv == nil {
1210		return ErrNotImplemented
1211	}
1212	C.webkit_web_view_go_forward(w.native.wv)
1213	return nil
1214}
1215
1216func (w *WebView) Reload() error {
1217	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1218		return ErrNotImplemented
1219	}
1220	C.webkit_web_view_reload(w.native.wv)
1221	log.Printf("[webkit] Reload page")
1222	return nil
1223}
1224
1225func (w *WebView) ReloadBypassCache() error {
1226	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1227		return ErrNotImplemented
1228	}
1229	C.webkit_web_view_reload_bypass_cache(w.native.wv)
1230	log.Printf("[webkit] Hard reload page (bypass cache)")
1231	return nil
1232}
1233
1234// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
1235func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
1236
1237func (w *WebView) dispatchScriptMessage(payload string) {
1238	if w != nil && w.msgHandler != nil {
1239		w.msgHandler(payload)
1240	}
1241}
1242
1243// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
1244func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
1245
1246func (w *WebView) dispatchTitleChanged(title string) {
1247	if w != nil && w.titleHandler != nil {
1248		w.titleHandler(title)
1249	}
1250}
1251
1252// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
1253func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
1254
1255func (w *WebView) dispatchURIChanged(uri string) {
1256	if w != nil && w.uriHandler != nil {
1257		w.uriHandler(uri)
1258	}
1259}
1260
1261// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
1262func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
1263
1264func (w *WebView) dispatchZoomChanged(level float64) {
1265	if w != nil && w.zoomHandler != nil {
1266		w.zoomHandler(level)
1267	}
1268}
1269
1270// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
1271func (w *WebView) enableUserContentManager(cfg *Config) {
1272	if w == nil || w.native == nil || w.native.ucm == nil {
1273		return
1274	}
1275	// Register handler "dumber" for main world
1276	cname := C.CString("dumber")
1277	ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
1278	if ok == 0 {
1279		log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
1280	}
1281	C.free(unsafe.Pointer(cname))
1282
1283	// Also register handler for the isolated GUI world so GUI can post messages to native
1284	cworld := C.CString("dumber-gui")
1285	cname2 := C.CString("dumber")
1286	ok2 := C.register_ucm_handler_world(w.native.ucm, cname2, cworld, C.ulong(w.id))
1287	if ok2 == 0 {
1288		log.Printf("[webkit] Failed to register UCM handler for isolated world 'dumber-gui'")
1289	}
1290	C.free(unsafe.Pointer(cworld))
1291	C.free(unsafe.Pointer(cname2))
1292
1293	// Inject color-scheme preference script at document-start to inform sites of system theme
1294	preferDark := C.gtk_prefers_dark() != 0
1295	if preferDark {
1296		log.Printf("[theme] GTK prefers: dark")
1297	} else {
1298		log.Printf("[theme] GTK prefers: light")
1299	}
1300	// Load color scheme script from compiled GUI assets instead of inline
1301	var schemeJS string
1302	if cfg != nil && cfg.Assets != nil {
1303		if schemeBytes, err := cfg.Assets.ReadFile("assets/gui/color-scheme.js"); err == nil {
1304			baseScript := string(schemeBytes)
1305			// Inject the GTK preference as a parameter
1306			if preferDark {
1307				schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = true; %s", baseScript)
1308			} else {
1309				schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = false; %s", baseScript)
1310			}
1311		} else {
1312			log.Printf("[webkit] Warning: Failed to load color-scheme.js from assets, falling back to basic theme detection: %v", err)
1313			// Simple fallback if the compiled script is not available
1314			if preferDark {
1315				schemeJS = `console.log('[dumber] GTK detected color mode: dark'); document.documentElement.classList.add('dark');`
1316			} else {
1317				schemeJS = `console.log('[dumber] GTK detected color mode: light'); document.documentElement.classList.remove('dark');`
1318			}
1319		}
1320	}
1321	cScheme := C.CString(schemeJS)
1322	defer C.free(unsafe.Pointer(cScheme))
1323	schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1324	if schemeScript != nil {
1325		C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
1326		C.webkit_user_script_unref(schemeScript)
1327	}
1328
1329	// Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
1330	if cfg != nil && cfg.Assets != nil {
1331		log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
1332		if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil {
1333			guiScript := string(guiBytes)
1334			log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes))
1335
1336			cGui := C.CString(guiScript)
1337			defer C.free(unsafe.Pointer(cGui))
1338
1339			log.Printf("[webkit] Creating GUI user script for document-start injection in isolated world 'dumber-gui'")
1340			world := C.CString("dumber-gui")
1341			defer C.free(unsafe.Pointer(world))
1342			guiUserScript := C.webkit_user_script_new_for_world((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil)
1343			if guiUserScript != nil {
1344				C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
1345				C.webkit_user_script_unref(guiUserScript)
1346				log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in isolated world")
1347			} else {
1348				log.Printf("[webkit] ERROR: Failed to create GUI user script")
1349			}
1350
1351			// Inject a unified page-world bridge to dispatch standard events and expose unified API
1352			bridge := `(() => { try {
1353              if (window.__dumber_page_bridge_installed) return; 
1354              window.__dumber_page_bridge_installed = true;
1355              
1356              // Theme setter function for GTK theme integration
1357              window.__dumber_setTheme = (theme) => {
1358                window.__dumber_initial_theme = theme;
1359                console.log('[dumber] Setting theme to:', theme);
1360                if (theme === 'dark') {
1361                  document.documentElement.classList.add('dark');
1362                } else {
1363                  document.documentElement.classList.remove('dark');
1364                }
1365              };
1366              
1367              // Ensure unified API object
1368              window.__dumber = window.__dumber || {};
1369              // Toast namespace
1370              window.__dumber.toast = window.__dumber.toast || {
1371                show: function(message, duration, type) {
1372                  try {
1373                    document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } }));
1374                    // Legacy compatibility
1375                    document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } }));
1376                  } catch(e) { /* ignore */ }
1377                },
1378                zoom: function(level) {
1379                  try {
1380                    document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } }));
1381                    // Legacy compatibility
1382                    document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } }));
1383                  } catch(e) { /* ignore */ }
1384                }
1385              };
1386              // Back-compat global helpers
1387              window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); };
1388              window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); };
1389
1390              // Omnibox suggestions bridge (with queue before ready)
1391              let __omniboxQueue = [];
1392              let __omniboxReady = false;
1393              function __omniboxDispatch(suggestions) {
1394                try {
1395                  document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } }));
1396                  // Legacy compatibility event name
1397                  document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } }));
1398                } catch (e) { /* ignore */ }
1399              }
1400              window.__dumber_omnibox_suggestions = function(suggestions) {
1401                if (__omniboxReady) {
1402                  __omniboxDispatch(suggestions);
1403                } else {
1404                  try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ }
1405                }
1406              };
1407              document.addEventListener('dumber:omnibox-ready', function() {
1408                __omniboxReady = true;
1409                if (__omniboxQueue && __omniboxQueue.length) {
1410                  const items = __omniboxQueue.slice();
1411                  __omniboxQueue.length = 0;
1412                  for (const s of items) __omniboxDispatch(s);
1413                }
1414              });
1415              // Unified omnibox API for page-world callers
1416              window.__dumber.omnibox = window.__dumber.omnibox || {
1417                suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); }
1418              };
1419            } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();`
1420			cBridge := C.CString(bridge)
1421			defer C.free(unsafe.Pointer(cBridge))
1422			bridgeScript := C.webkit_user_script_new((*C.gchar)(cBridge), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1423			if bridgeScript != nil {
1424				C.webkit_user_content_manager_add_script(w.native.ucm, bridgeScript)
1425				C.webkit_user_script_unref(bridgeScript)
1426				log.Printf("[webkit] Page-world toast bridge injected")
1427			}
1428
1429			// If an API token is provided, inject a fetch wrapper in the isolated GUI world
1430			if cfg.APIToken != "" {
1431				wrapper := "(() => { try {" +
1432					"const TOKEN='" + cfg.APIToken + "';" +
1433					"const appendToken=(u)=>{ try { let url; try { url = new URL(u); } catch(_) { url = new URL(u, 'dumb://homepage'); } if(url.protocol==='dumb:' && url.pathname.startsWith('/api')){ url.searchParams.set('token', TOKEN); return url.toString(); } } catch(_){} return u; };" +
1434					"const _fetch=window.fetch.bind(window); window.fetch=(input, init)=>{ try { if(typeof input==='string'){ input=appendToken(input); } else if(input && input.url){ const nu=appendToken(input.url); if(nu!==input.url){ input=new Request(nu, input); } } } catch(_){} return _fetch(input, init); };" +
1435					"} catch(e) { console.warn('[dumber] api token wrapper failed', e); } })();"
1436				cWrap := C.CString(wrapper)
1437				defer C.free(unsafe.Pointer(cWrap))
1438				world := C.CString("dumber-gui")
1439				defer C.free(unsafe.Pointer(world))
1440				wrapScript := C.webkit_user_script_new_for_world((*C.gchar)(cWrap), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, (*C.gchar)(world), nil, nil)
1441				if wrapScript != nil {
1442					C.webkit_user_content_manager_add_script(w.native.ucm, wrapScript)
1443					C.webkit_user_script_unref(wrapScript)
1444					log.Printf("[webkit] API token fetch wrapper injected in isolated GUI world")
1445				}
1446			}
1447		} else {
1448			log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
1449		}
1450
1451		// Add GUI CSS styles as user stylesheet using WebKit native API
1452		if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil {
1453			cssContent := string(cssBytes)
1454			cCss := C.CString(cssContent)
1455			defer C.free(unsafe.Pointer(cCss))
1456
1457			styleSheet := C.webkit_user_style_sheet_new(
1458				(*C.gchar)(cCss),
1459				C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1460				C.WEBKIT_USER_STYLE_LEVEL_AUTHOR,
1461				nil, // allow_list
1462				nil, // block_list
1463			)
1464			if styleSheet != nil {
1465				C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet)
1466				C.webkit_user_style_sheet_unref(styleSheet)
1467			} else {
1468				log.Printf("[webkit] ERROR: Failed to create GUI stylesheet")
1469			}
1470		} else {
1471			log.Printf("[webkit] ERROR: Failed to load GUI CSS from assets: %v", err)
1472		}
1473	}
1474
1475	// Inject Wails runtime fetch interceptor for homepage bridging
1476	wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();`
1477	cWails := C.CString(wailsBridge)
1478	defer C.free(unsafe.Pointer(cWails))
1479	wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1480	if wailsScript != nil {
1481		C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
1482		C.webkit_user_script_unref(wailsScript)
1483	}
1484
1485	// Inject codec control script if codec preferences are configured
1486	if cfg != nil && (cfg.CodecPreferences.ForceAV1 || len(cfg.CodecPreferences.BlockedCodecs) > 0 || len(cfg.CodecPreferences.PreferredCodecs) > 0) {
1487		codecScript := GenerateCodecControlScript(cfg.CodecPreferences)
1488		cCodec := C.CString(codecScript)
1489		defer C.free(unsafe.Pointer(cCodec))
1490		codecUserScript := C.webkit_user_script_new(
1491			(*C.gchar)(cCodec),
1492			C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
1493			C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
1494			nil, nil)
1495		if codecUserScript != nil {
1496			C.webkit_user_content_manager_add_script(w.native.ucm, codecUserScript)
1497			C.webkit_user_script_unref(codecUserScript)
1498			log.Printf("[webkit] Codec control script injected")
1499		}
1500	}
1501
1502	// No JS fallback bridge — native UCM is active
1503}
1504
1505// startPeriodicGC starts a ticker to trigger JavaScript garbage collection periodically
1506func (w *WebView) startPeriodicGC(interval time.Duration) {
1507	if w == nil || w.gcTicker != nil {
1508		return // Already running or invalid
1509	}
1510
1511	w.gcTicker = time.NewTicker(interval)
1512
1513	go func() {
1514		defer func() {
1515			if w.gcTicker != nil {
1516				w.gcTicker.Stop()
1517				w.gcTicker = nil
1518			}
1519		}()
1520
1521		for {
1522			select {
1523			case <-w.gcTicker.C:
1524				w.triggerGC()
1525			case <-w.gcDone:
1526				return
1527			}
1528		}
1529	}()
1530}
1531
1532// stopPeriodicGC stops the periodic garbage collection
1533func (w *WebView) stopPeriodicGC() {
1534	if w == nil {
1535		return
1536	}
1537
1538	if w.gcDone != nil {
1539		close(w.gcDone)
1540		w.gcDone = nil
1541	}
1542
1543	if w.gcTicker != nil {
1544		w.gcTicker.Stop()
1545		w.gcTicker = nil
1546	}
1547}
1548
1549// triggerGC manually triggers JavaScript garbage collection
1550func (w *WebView) triggerGC() {
1551	if w == nil || w.memStats == nil {
1552		return
1553	}
1554
1555	// Get default WebContext and trigger GC
1556	ctx := C.webkit_web_context_get_default()
1557	if ctx != nil {
1558		C.maybe_collect_js(ctx)
1559		w.memStats.lastGCTime = time.Now()
1560
1561		if w.config.Memory.EnableMemoryMonitoring {
1562			log.Printf("[webkit] JavaScript GC triggered (page loads: %d)", w.memStats.pageLoadCount)
1563		}
1564	}
1565}
1566
1567// GetMemoryStats returns current memory statistics for this WebView
1568func (w *WebView) GetMemoryStats() map[string]interface{} {
1569	if w == nil || w.memStats == nil {
1570		return nil
1571	}
1572
1573	return map[string]interface{}{
1574		"page_load_count":              w.memStats.pageLoadCount,
1575		"last_gc_time":                 w.memStats.lastGCTime,
1576		"has_memory_pressure_settings": w.memStats.memoryPressureSettings != nil,
1577	}
1578}
1579
1580// TriggerMemoryCleanup manually triggers garbage collection and memory cleanup
1581func (w *WebView) TriggerMemoryCleanup() {
1582	if w == nil {
1583		return
1584	}
1585
1586	w.triggerGC()
1587
1588	if w.config.Memory.EnableMemoryMonitoring {
1589		log.Printf("[webkit] Manual memory cleanup triggered")
1590	}
1591}
1592
1593//export goHandleTLSError
1594func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
1595	uri := C.GoString(failingURI)
1596	hostname := C.GoString(host)
1597	certificateInfo := C.GoString(certInfo)
1598
1599	log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
1600
1601	// Find the WebView that triggered this error
1602	// For now, we'll get the most recently created view
1603	// In a more sophisticated implementation, we'd pass the view ID through user_data
1604	var webView *WebView
1605	regMu.RLock()
1606	for _, v := range viewByID {
1607		if v != nil {
1608			webView = v
1609			break
1610		}
1611	}
1612	regMu.RUnlock()
1613
1614	if webView == nil {
1615		log.Printf("[tls] No WebView found to handle TLS error")
1616		return C.FALSE // Don't proceed
1617	}
1618
1619	// Check if we've already allowed this host
1620	if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
1621		log.Printf("[tls] Certificate exception already granted for %s", hostname)
1622		return C.TRUE // Proceed with the load
1623	}
1624
1625	// Show warning dialog and get user decision
1626	if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
1627		webView.tlsExceptions[hostname] = true
1628		log.Printf("[tls] User accepted certificate exception for %s", hostname)
1629		return C.TRUE // Proceed with the load
1630	}
1631
1632	log.Printf("[tls] User rejected certificate for %s", hostname)
1633	return C.FALSE // Don't proceed
1634}
1635
1636// showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
1637func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
1638	if w == nil || w.native == nil || w.native.win == nil {
1639		log.Printf("[tls] Cannot show dialog: WebView or window is nil")
1640		return false
1641	}
1642
1643	// Check if we have a stored decision for this certificate
1644	if storedDecision, hasStored := checkStoredCertificateDecision(hostname, certificateInfo); hasStored {
1645		switch storedDecision {
1646		case "accepted":
1647			log.Printf("[tls] Using stored ACCEPT decision for %s", hostname)
1648			return true
1649		case "rejected":
1650			log.Printf("[tls] Using stored REJECT decision for %s", hostname)
1651			return false
1652		default:
1653			log.Printf("[tls] Unknown stored decision '%s' for %s, showing dialog", storedDecision, hostname)
1654		}
1655	}
1656
1657	log.Printf("[tls] Showing certificate error dialog for %s", hostname)
1658
1659	// Format the error message with certificate information
1660	errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
1661
1662	// Convert strings to C strings
1663	cHostname := C.CString(hostname)
1664	defer C.free(unsafe.Pointer(cHostname))
1665
1666	cErrorMsg := C.CString(errorMsg)
1667	defer C.free(unsafe.Pointer(cErrorMsg))
1668
1669	// Show the dialog and get user response (0=Go Back, 1=Proceed Once, 2=Always Accept)
1670	result := C.show_tls_warning_dialog_sync(
1671		(*C.GtkWindow)(unsafe.Pointer(w.native.win)),
1672		cHostname,
1673		cErrorMsg,
1674	)
1675
1676	responseCode := int(result)
1677
1678	switch responseCode {
1679	case 0: // Go Back
1680		log.Printf("[tls] User chose to GO BACK for %s", hostname)
1681		return false
1682
1683	case 1: // Proceed Once
1684		log.Printf("[tls] User chose to PROCEED ONCE for %s", hostname)
1685		// Store temporary decision (expires in 24 hours)
1686		if err := storeCertificateDecision(hostname, certificateInfo, "accepted", false); err != nil {
1687			log.Printf("[tls] Warning: Failed to store temporary certificate decision: %v", err)
1688		}
1689		return true
1690
1691	case 2: // Always Accept
1692		log.Printf("[tls] User chose to ALWAYS ACCEPT for %s", hostname)
1693		// Store permanent decision
1694		if err := storeCertificateDecision(hostname, certificateInfo, "accepted", true); err != nil {
1695			log.Printf("[tls] Warning: Failed to store permanent certificate decision: %v", err)
1696		}
1697		return true
1698
1699	default:
1700		log.Printf("[tls] Unknown response code %d for %s, defaulting to reject", responseCode, hostname)
1701		return false
1702	}
1703}
1704
1705// formatTLSErrorMessage creates a detailed error message based on the error flags
1706func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
1707	msg := "Website: " + hostname + "\n\n"
1708
1709	// Add certificate information
1710	if certificateInfo != "" {
1711		msg += "Certificate Details:\n" + certificateInfo + "\n"
1712	}
1713
1714	msg += "Security Issues:\n"
1715	// Decode GTlsCertificateFlags (common values)
1716	if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
1717		msg += "• Certificate authority is not trusted\n"
1718	}
1719	if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
1720		msg += "• Certificate does not match the website identity\n"
1721	}
1722	if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
1723		msg += "• Certificate is not yet valid\n"
1724	}
1725	if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
1726		msg += "• Certificate has expired\n"
1727	}
1728	if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
1729		msg += "• Certificate has been revoked\n"
1730	}
1731	if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
1732		msg += "• Certificate uses weak cryptography\n"
1733	}
1734
1735	msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
1736
1737	return msg
1738}
1739
1740// InitializeContentBlocking initializes WebKit content blocking with filter manager
1741func (w *WebView) InitializeContentBlocking(filterManager FilterManager) error {
1742	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1743		return ErrWebViewNotInitialized
1744	}
1745
1746	// Load filters asynchronously with proper error recovery
1747	go func() {
1748		defer func() {
1749			if r := recover(); r != nil {
1750				logging.Error(fmt.Sprintf("[webkit] Content blocking initialization panic: %v", r))
1751			}
1752		}()
1753
1754		// Wait longer for WebView to be ready before applying filters on first load
1755		// This prevents interference with WebKit's preconnect operations
1756		time.Sleep(1500 * time.Millisecond)
1757
1758		// Retry mechanism with exponential backoff
1759		maxRetries := 5
1760		baseDelay := 200 * time.Millisecond
1761
1762		// Apply network filters with error recovery
1763		for i := 0; i < maxRetries; i++ {
1764			if w.destroyed {
1765				logging.Debug("[webkit] WebView destroyed, aborting filter loading")
1766				return
1767			}
1768
1769			if filters, err := filterManager.GetNetworkFilters(); err == nil {
1770				if len(filters) > 10 && string(filters) != "null" {
1771					logging.Info("[webkit] Loading network filters from filter manager (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
1772					if err := w.ApplyContentFilters(filters, "network-filters"); err != nil {
1773						logging.Error("Failed to apply network filters (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
1774						if i == maxRetries-1 {
1775							logging.Warn("[webkit] Network filtering disabled due to errors")
1776							break
1777						}
1778						time.Sleep(baseDelay * time.Duration(1<<uint(i))) // Exponential backoff
1779						continue
1780					}
1781					break
1782				}
1783			} else if i == maxRetries-1 {
1784				logging.Error("Failed to get network filters after retries: " + err.Error())
1785			}
1786		}
1787
1788		// Apply cosmetic filtering with error recovery
1789		time.Sleep(200 * time.Millisecond) // Additional delay to ensure network setup is complete
1790
1791		for i := 0; i < maxRetries; i++ {
1792			if w.destroyed {
1793				logging.Debug("[webkit] WebView destroyed, aborting cosmetic filter loading")
1794				return
1795			}
1796
1797			script := filterManager.GetCosmeticScript()
1798			if len(script) > 0 {
1799				logging.Info("[webkit] Loading cosmetic script from filter manager (" + fmt.Sprintf("%d", len(script)) + " chars)")
1800				if err := w.InjectCosmeticFilter(script); err != nil {
1801					logging.Error("Failed to inject cosmetic filter (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
1802					if i == maxRetries-1 {
1803						logging.Warn("[webkit] Cosmetic filtering disabled due to errors")
1804					} else {
1805						time.Sleep(baseDelay * time.Duration(1<<uint(i)))
1806						continue
1807					}
1808				}
1809				break
1810			}
1811		}
1812
1813		logging.Info("Content blocking initialization completed")
1814	}()
1815
1816	return nil
1817}
1818
1819// OnNavigate sets up domain-specific cosmetic filtering on navigation
1820func (w *WebView) OnNavigate(url string, filterManager FilterManager) {
1821	if w == nil || w.destroyed {
1822		return
1823	}
1824
1825	domain := extractDomain(url)
1826	if domain == "" {
1827		return
1828	}
1829
1830	// Get domain-specific cosmetic rules
1831	script := filterManager.GetCosmeticScriptForDomain(domain)
1832	if script != "" {
1833		// Inject domain-specific cosmetic rules
1834		logging.Info("[webkit] Applying domain-specific cosmetic rules for: " + domain + " (" + fmt.Sprintf("%d", len(script)) + " chars)")
1835		w.InjectScript(fmt.Sprintf(
1836			"if (typeof window.__dumber_cosmetic_init === 'function') { %s }",
1837			script,
1838		))
1839	} else {
1840		logging.Debug("[webkit] No domain-specific cosmetic rules found for: " + domain)
1841	}
1842}
1843
1844// UpdateContentFilters updates the content filters dynamically
1845func (w *WebView) UpdateContentFilters(filterManager FilterManager) error {
1846	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1847		return ErrWebViewNotInitialized
1848	}
1849
1850	// Clear existing filters first
1851	if err := w.ClearAllFilters(); err != nil {
1852		return fmt.Errorf("failed to clear existing filters: %w", err)
1853	}
1854
1855	// Apply new filters
1856	if filters, err := filterManager.GetNetworkFilters(); err == nil {
1857		logging.Info("[webkit] Updating network filters (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
1858		if err := w.ApplyContentFilters(filters, "network-filters-updated"); err != nil {
1859			return fmt.Errorf("failed to apply updated network filters: %w", err)
1860		}
1861	}
1862
1863	// Update cosmetic filtering
1864	script := filterManager.GetCosmeticScript()
1865	logging.Info("[webkit] Updating cosmetic script (" + fmt.Sprintf("%d", len(script)) + " chars)")
1866	if err := w.InjectCosmeticFilter(script); err != nil {
1867		return fmt.Errorf("failed to inject updated cosmetic filter: %w", err)
1868	}
1869
1870	logging.Info("Content filters updated successfully")
1871	return nil
1872}
1873
1874// extractDomain extracts the domain from a URL
1875func extractDomain(url string) string {
1876	if url == "" {
1877		return ""
1878	}
1879
1880	// Simple domain extraction - remove protocol and path
1881	start := strings.Index(url, "://")
1882	if start != -1 {
1883		url = url[start+3:]
1884	}
1885
1886	end := strings.Index(url, "/")
1887	if end != -1 {
1888		url = url[:end]
1889	}
1890
1891	// Remove port if present
1892	if portIdx := strings.Index(url, ":"); portIdx != -1 {
1893		url = url[:portIdx]
1894	}
1895
1896	return url
1897}

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