v0.9.0 636c44c

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 <stdint.h>
11#include <gtk/gtk.h>
12#include <webkit/webkit.h>
13#include <glib-object.h>
14#include <gdk/gdk.h>
15#include <jsc/jsc.h>
16#include <glib.h>
17#include <gio/gio.h>
18#include "webview_callbacks.h"
19
20static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
21
22// Forward declaration
23extern void goQuitMainLoop();
24extern void goInvokeHandle(uintptr_t handle);
25extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type, char* frame_name, gboolean is_user_gesture, guint modifiers, guint mouse_button);
26extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height);
27extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event);
28extern void goHandleWebViewClose(unsigned long id);
29extern void goCreateNewPanel(unsigned long id, char* uri);
30static gboolean invoke_handle_cb(gpointer data) {
31    goInvokeHandle((uintptr_t)data);
32    return G_SOURCE_REMOVE;
33}
34
35static void schedule_on_main_thread(uintptr_t handle) {
36    g_idle_add_full(G_PRIORITY_DEFAULT, invoke_handle_cb, (gpointer)handle, NULL);
37}
38
39// Connect window close signal to quit main loop
40static void connect_destroy_quit(GtkWidget* w) {
41    if (!w) return;
42    g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL);
43}
44static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
45// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
46
47// Forward declare helpers used below
48static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
49
50// TLS error handling forward declarations
51extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
52
53// Console message handling forward declarations
54extern void goHandleConsoleMessage(char* message);
55
56static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
57                                               const char *failing_uri,
58                                               GTlsCertificate *certificate,
59                                               GTlsCertificateFlags errors,
60                                               gpointer user_data);
61
62
63// Handle load_changed signal for OAuth completion detection
64static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) {
65    const char* uri = webkit_web_view_get_uri(web_view);
66    unsigned long view_id = (unsigned long)user_data;
67
68    // Only process WEBKIT_LOAD_FINISHED events
69    if (load_event == WEBKIT_LOAD_FINISHED && uri) {
70        printf("[webkit-load] Load finished for URI: %s\n", uri);
71        goHandleLoadChanged(view_id, (char*)uri, load_event);
72    }
73}
74
75// Handle close signal when JavaScript calls window.close()
76static void on_webview_close(WebKitWebView* web_view, gpointer user_data) {
77    unsigned long view_id = (unsigned long)user_data;
78    printf("[webkit-close] WebView close signal from view_id: %lu\n", view_id);
79    goHandleWebViewClose(view_id);
80}
81
82
83// Handle decide-policy signal for new window requests - more robust than create signal
84static gboolean on_decide_policy(WebKitWebView *web_view,
85                                 WebKitPolicyDecision *decision,
86                                 WebKitPolicyDecisionType type,
87                                 gpointer user_data) {
88    // Debug: log all policy decisions
89    const char* type_names[] = {"NAVIGATION_ACTION", "NEW_WINDOW_ACTION", "RESPONSE"};
90    const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN";
91    printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type);
92
93    if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) {
94        WebKitNavigationPolicyDecision *nav_decision =
95            WEBKIT_NAVIGATION_POLICY_DECISION(decision);
96        WebKitNavigationAction *action =
97            webkit_navigation_policy_decision_get_navigation_action(nav_decision);
98        WebKitURIRequest *request =
99            webkit_navigation_action_get_request(action);
100        const char* uri = webkit_uri_request_get_uri(request);
101
102        // Get navigation type to understand how popup was triggered
103        WebKitNavigationType nav_type = webkit_navigation_action_get_navigation_type(action);
104
105        // Get frame name to distinguish _blank from other popups
106        const char* frame_name = webkit_navigation_action_get_frame_name(action);
107
108        // Get additional properties to better distinguish _blank from popups
109        gboolean is_user_gesture = webkit_navigation_action_is_user_gesture(action);
110        guint modifiers = webkit_navigation_action_get_modifiers(action);
111        guint mouse_button = webkit_navigation_action_get_mouse_button(action);
112
113        // Call Go handler for popup creation
114        unsigned long view_id = (unsigned long)user_data;
115        gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type, (char*)frame_name,
116                                                 is_user_gesture, modifiers, mouse_button);
117
118        if (handled) {
119            webkit_policy_decision_ignore(decision);
120        } else {
121            webkit_policy_decision_use(decision);
122        }
123        return TRUE;
124    }
125    return FALSE;
126}
127
128static void connect_tls_error_handler(WebKitWebView* wv);
129
130
131// Dialog callback structure for modern AlertDialog async handling
132typedef struct {
133    int response;
134    gboolean completed;
135    GMainLoop *loop;
136} AlertDialogResponse;
137
138static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data);
139static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
140static char* extract_certificate_info(GTlsCertificate* certificate);
141
142// Setup video acceleration environment variables
143static void setup_video_acceleration(const char* driver_name, int enable_all, int legacy) {
144    if (legacy) {
145        // Legacy mode: force old VA-API plugins, disable modern ones
146        setenv("WEBKIT_GST_ENABLE_LEGACY_VAAPI", "1", 1);
147        // Don't set other env vars in legacy mode to avoid conflicts
148        return;
149    }
150
151    // Modern mode (default): use improved VA-API handling
152    if (driver_name && strlen(driver_name) > 0) {
153        if (strcmp(driver_name, "nvidia") == 0) {
154            // NVIDIA uses different env vars for hardware video acceleration
155            setenv("VDPAU_DRIVER", "nvidia", 1);
156            setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
157        } else if (strcmp(driver_name, "vdpau") == 0) {
158            // Explicit VDPAU backend (for NVIDIA via VA-API)
159            setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
160        } else {
161            // AMD/Intel use VA-API directly
162            setenv("LIBVA_DRIVER_NAME", driver_name, 1);
163        }
164    }
165
166    if (enable_all) {
167        setenv("GST_VAAPI_ALL_DRIVERS", "1", 1);
168    }
169}
170
171// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
172static 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) {
173    WebKitUserContentManager* u = webkit_user_content_manager_new();
174    if (!u) return NULL;
175    // WebKitGTK 6: create NetworkSession with data/cache directories
176    WebKitNetworkSession* sess = webkit_network_session_new(
177        data_dir ? data_dir : "",
178        cache_dir ? cache_dir : "");
179    if (!sess) { g_object_unref(u); return NULL; }
180    // Apply memory pressure settings to the session when provided
181    if (pressure_settings) {
182        // WebKitGTK 6.0 API: setter expects only the settings pointer
183        webkit_network_session_set_memory_pressure_settings(pressure_settings);
184    }
185    // Configure cookie persistence and policy (no third-party by default)
186    WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
187    if (cm) {
188        if (cookie_path && cookie_path[0]) {
189            webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
190        }
191        // 2 => no-third-party (default), 0 => always, 1 => never
192        maybe_set_cookie_policy(cm, 2);
193    }
194    // Persist credentials (HTTP auth, etc.)
195    webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
196    // Configure TLS errors to emit signals instead of failing silently
197    webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
198    GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
199        "user-content-manager", u,
200        "network-session", sess,
201        NULL));
202    // WebView holds refs to provided objects; drop our temporary refs
203    g_object_unref(sess);
204    // Set WebView background to black (easier on eyes when pages are loading)
205    GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
206    webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
207
208    // Connect TLS error handler
209    connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
210
211    if (out_ucm) { *out_ucm = u; }
212    return w;
213}
214
215// Create a new WebView related to an existing one for process sharing
216static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserContentManager** out_ucm) {
217    if (!related_view) {
218        // Fallback to regular WebView creation - we need to provide the required parameters
219        return new_webview_with_ucm_and_session(NULL, NULL, NULL, out_ucm, NULL);
220    }
221
222    // Get the network session and user content manager from the related view
223    WebKitNetworkSession* related_session = webkit_web_view_get_network_session(related_view);
224    WebKitUserContentManager* related_ucm = webkit_web_view_get_user_content_manager(related_view);
225
226    // Create content manager if needed
227    WebKitUserContentManager* u = related_ucm;
228    if (!u) {
229        u = webkit_user_content_manager_new();
230    } else {
231        g_object_ref(u); // Add ref since we'll pass it to new WebView
232    }
233
234    // Create WebView with related view for process sharing (network session is inherited)
235    GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
236        "user-content-manager", u,
237        "related-view", related_view,
238        NULL));
239
240    // Connect TLS error handler
241    connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
242
243    // WebView holds refs to provided objects; drop our temporary refs
244    g_object_unref(u);
245    if (out_ucm) { *out_ucm = u; }
246    return w;
247}
248
249static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) {
250    if (!web_view) return;
251    // Create signal is now handled in window_handler_cgo.go
252    // Use ConnectCreateSignal from Go to connect the signal
253    printf("[webkit-debug] Policy handler connected (create signal handled separately)\n");
254}
255
256// Connect close signal to a WebView for popup auto-close functionality
257static void connect_close_signal(WebKitWebView* web_view, unsigned long id) {
258    if (!web_view) return;
259    g_signal_connect_data(G_OBJECT(web_view), "close", G_CALLBACK(on_webview_close), (gpointer)id, NULL, 0);
260    printf("[webkit-connect] Connected close signal to WebView with id: %lu\n", id);
261}
262
263static gboolean gtk_prefers_dark() {
264    // Method 1: Check GNOME desktop interface color-scheme (primary method)
265    GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
266    if (desktop_settings) {
267        gchar* color_scheme = g_settings_get_string(desktop_settings, "color-scheme");
268        if (color_scheme) {
269            gboolean prefer_dark = (g_strcmp0(color_scheme, "prefer-dark") == 0);
270            g_free(color_scheme);
271            g_object_unref(desktop_settings);
272            if (prefer_dark) return TRUE;
273        }
274        g_object_unref(desktop_settings);
275    }
276
277    // Method 2: Check GTK theme name for dark variants
278    GtkSettings* settings = gtk_settings_get_default();
279    if (settings) {
280        gchar* theme_name = NULL;
281        g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
282        if (theme_name) {
283            gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
284                               strstr(theme_name, "-Dark") != NULL);
285            g_free(theme_name);
286            if (is_dark) return TRUE;
287        }
288
289        // Method 3: Check gtk-application-prefer-dark-theme (fallback)
290        gboolean prefer = FALSE;
291        g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
292        if (prefer) return TRUE;
293    }
294
295    return FALSE;
296}
297
298// Note: preferred color scheme is handled via a user script injection to support
299// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
300
301extern void goOnUcmMessage(unsigned long id, char* json);
302
303// Helper functions to connect signals using static callbacks
304static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
305    if (!widget) return;
306    g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
307}
308
309static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
310    if (!widget) return;
311    g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
312}
313
314static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
315    if (!widget) return;
316    g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
317}
318
319static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
320    if (!settings) return;
321    g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
322}
323
324static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
325    if (!context || !scheme) return;
326    webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
327}
328static void register_uri_scheme_security(WebKitWebContext* context, const char* scheme) {
329	if (!context || !scheme) return;
330	WebKitSecurityManager* sm = webkit_web_context_get_security_manager(context);
331	if (!sm) return;
332
333	// Mark as secure to avoid mixed-content warnings when requested by https pages
334	webkit_security_manager_register_uri_scheme_as_secure(sm, scheme);
335	// Treat as local scheme
336	webkit_security_manager_register_uri_scheme_as_local(sm, scheme);
337	// Allow cross-origin requests to this scheme (bypass CORS header checks for this scheme)
338	webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme);
339}
340
341// Script message handler wiring for WebKitGTK 6
342// Forward declaration for callback used in g_signal_connect_data
343// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
344static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
345// Uses WebKitScriptMessage and script-message-received::NAME signal.
346static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
347    (void)ucm;
348    if (!val) return;
349    gchar* s = jsc_value_to_string(val);
350    if (!s) return;
351    goOnUcmMessage((unsigned long)user_data, s);
352    g_free(s);
353}
354
355static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
356    if (!ucm || !name) return FALSE;
357    // WebKitGTK 6 signature: (ucm, name, world_name)
358    webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
359    gchar* signal = g_strdup_printf("script-message-received::%s", name);
360    if (!signal) return FALSE;
361    g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
362    g_free(signal);
363    return TRUE;
364}
365
366// Register a script message handler for a specific JavaScript world
367static gboolean register_ucm_handler_world(WebKitUserContentManager* ucm, const char* name, const char* world_name, unsigned long id) {
368	if (!ucm || !name || !world_name) return FALSE;
369	webkit_user_content_manager_register_script_message_handler(ucm, name, world_name);
370	gchar* signal = g_strdup_printf("script-message-received::%s", name);
371	if (!signal) return FALSE;
372	g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
373	g_free(signal);
374	return TRUE;
375}
376
377// ----- Conditional helpers for settings across WebKit versions -----
378#ifndef WEBKIT_CHECK_VERSION
379#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
380#endif
381
382static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
383#if WEBKIT_CHECK_VERSION(2,40,0)
384    // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
385    if (!settings) return;
386    if (mode == 1) {
387        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
388    } else if (mode == 2) {
389        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
390    } else {
391        // Leave default policy (auto) without forcing a value to avoid referencing
392        // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
393    }
394#else
395    (void)settings; (void)mode;
396#endif
397}
398
399static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
400#if WEBKIT_CHECK_VERSION(2,36,0)
401    if (!settings) return;
402    webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
403#else
404    (void)settings; (void)enable;
405#endif
406}
407
408static void maybe_set_webgl(WebKitSettings* settings, int enable) {
409#if WEBKIT_CHECK_VERSION(2,20,0)
410    if (!settings) return;
411    webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
412#else
413    (void)settings; (void)enable;
414#endif
415}
416
417static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
418#if WEBKIT_CHECK_VERSION(2,20,0)
419    if (!settings) return;
420    webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
421#else
422    (void)settings; (void)enable;
423#endif
424}
425
426static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
427#if WEBKIT_CHECK_VERSION(2,20,0)
428    if (!settings) return;
429    webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
430#else
431    (void)settings; (void)require;
432#endif
433}
434
435static void maybe_set_smooth_scrolling(WebKitSettings* settings, int enable) {
436#if WEBKIT_CHECK_VERSION(2,10,0)
437    if (!settings) return;
438    webkit_settings_set_enable_smooth_scrolling(settings, enable ? TRUE : FALSE);
439#else
440    (void)settings; (void)enable;
441#endif
442}
443
444// Enable two-finger swipe back/forward navigation gestures when supported
445static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
446#if WEBKIT_CHECK_VERSION(2,38,0)
447    if (!settings) return;
448    webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
449#else
450    (void)settings; (void)enable;
451#endif
452}
453
454// Set cookie accept policy with compile-time compatibility across WebKit versions
455static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
456    if (!cm) return;
457#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
458    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
459    if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
460    else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
461    webkit_cookie_manager_set_accept_policy(cm, p);
462#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
463    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
464    if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
465    else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
466    webkit_cookie_manager_set_accept_policy(cm, p);
467#else
468    (void)policy; // Not supported in this header version
469#endif
470}
471
472// Memory pressure settings helpers (version-guarded)
473static WebKitMemoryPressureSettings* create_memory_pressure_settings(
474    unsigned int memory_limit_mb,
475    double conservative_threshold,
476    double strict_threshold,
477    double kill_threshold,
478    double poll_interval) {
479    // If headers don't expose memory pressure APIs, return NULL to signal unsupported
480#if WEBKIT_CHECK_VERSION(2,44,0)
481    WebKitMemoryPressureSettings* settings = webkit_memory_pressure_settings_new();
482    if (!settings) return NULL;
483
484    if (memory_limit_mb > 0) {
485        webkit_memory_pressure_settings_set_memory_limit(settings, (guint)(memory_limit_mb * 1024 * 1024));
486    }
487
488    webkit_memory_pressure_settings_set_conservative_threshold(settings, conservative_threshold);
489    webkit_memory_pressure_settings_set_strict_threshold(settings, strict_threshold);
490
491    if (kill_threshold > 0) {
492        webkit_memory_pressure_settings_set_kill_threshold(settings, kill_threshold);
493    }
494
495    webkit_memory_pressure_settings_set_poll_interval(settings, poll_interval);
496
497    return settings;
498#else
499    (void)memory_limit_mb; (void)conservative_threshold; (void)strict_threshold; (void)kill_threshold; (void)poll_interval;
500    return NULL;
501#endif
502}
503
504// Cache model configuration
505static void maybe_set_cache_model(WebKitWebContext* context, int model) {
506    if (!context) return;
507    WebKitCacheModel cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; // default
508
509    switch (model) {
510        case 0: cache_model = WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER; break;
511        case 1: cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; break;
512        case 2:
513#ifdef WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER
514            cache_model = WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER;
515#else
516            cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER;
517#endif
518            break;
519    }
520
521    webkit_web_context_set_cache_model(context, cache_model);
522}
523
524// Additional settings for memory optimization
525static void maybe_set_page_cache(WebKitSettings* settings, int enable) {
526#if WEBKIT_CHECK_VERSION(2,6,0)
527    if (!settings) return;
528    webkit_settings_set_enable_page_cache(settings, enable ? TRUE : FALSE);
529#else
530    (void)settings; (void)enable;
531#endif
532}
533
534// Offline Web Application Cache is deprecated in modern WebKit; keep as no-op to avoid warnings.
535static void maybe_set_offline_app_cache(WebKitSettings* settings, int enable) {
536    (void)settings; (void)enable;
537}
538
539// Apply memory pressure settings to context if available
540static void maybe_set_memory_pressure_settings(WebKitWebContext* context, WebKitMemoryPressureSettings* settings) {
541    // No-op wrapper kept for compatibility; settings applied to NetworkSession instead.
542    (void)context; (void)settings;
543}
544
545// Trigger JavaScript GC if supported by headers
546static void maybe_collect_js(WebKitWebContext* context) {
547    // Disabled by default due to API availability variance across 6.0 builds.
548    (void)context;
549}
550
551// TLS error handling implementation
552static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
553                                               const char *failing_uri,
554                                               GTlsCertificate *certificate,
555                                               GTlsCertificateFlags errors,
556                                               gpointer user_data) {
557    (void)user_data;
558
559    // Extract host from URI for the Go callback
560    const char* host = failing_uri;
561    char* host_copy = NULL;
562
563    if (strstr(failing_uri, "://")) {
564        host = strstr(failing_uri, "://") + 3;
565        char* slash = strchr(host, '/');
566        if (slash) {
567            // Create a temporary null-terminated host string
568            size_t host_len = slash - host;
569            host_copy = malloc(host_len + 1);
570            if (host_copy) {
571                strncpy(host_copy, host, host_len);
572                host_copy[host_len] = '\0';
573                host = host_copy;
574            }
575        }
576    }
577
578    // Extract certificate information
579    char* cert_info = extract_certificate_info(certificate);
580
581    gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
582
583    // If user accepted, allow the certificate for this host
584    if (should_proceed && certificate) {
585        printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
586        fflush(stdout);
587        // Get the network session from the web view
588        WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
589        if (session) {
590            webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
591            printf("[dumber] Certificate exception added for host: %s\n", host);
592            fflush(stdout);
593
594            // Try loading the URL again after adding the exception
595            webkit_web_view_load_uri(web_view, failing_uri);
596            printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
597            fflush(stdout);
598        }
599    }
600
601    // Free the certificate info string
602    if (cert_info) {
603        g_free(cert_info);
604    }
605
606    if (host_copy) {
607        free(host_copy);
608    }
609
610    return should_proceed;
611}
612
613// Extract certificate information for display
614static char* extract_certificate_info(GTlsCertificate* certificate) {
615    if (!certificate) {
616        return g_strdup("Certificate information not available");
617    }
618
619    printf("[dumber] Extracting certificate information...\n");
620    fflush(stdout);
621
622    GString* info = g_string_new("Certificate Information:\n");
623
624    // Try to get certificate properties - but these might not be available in all GTK versions
625    gchar* subject = NULL;
626    gchar* issuer = NULL;
627    GDateTime* not_valid_before = NULL;
628    GDateTime* not_valid_after = NULL;
629
630    // Try property access but handle failures gracefully
631    g_object_get(certificate,
632                 "subject-name", &subject,
633                 "issuer-name", &issuer,
634                 "not-valid-before", &not_valid_before,
635                 "not-valid-after", &not_valid_after,
636                 NULL);
637
638    if (subject) {
639        g_string_append_printf(info, "Subject: %s\n", subject);
640        g_free(subject);
641        printf("[dumber] Found subject info\n");
642    } else {
643        g_string_append(info, "Subject: Information not available\n");
644    }
645
646    if (issuer) {
647        g_string_append_printf(info, "Issued by: %s\n", issuer);
648        g_free(issuer);
649        printf("[dumber] Found issuer info\n");
650    } else {
651        g_string_append(info, "Issued by: Information not available\n");
652    }
653
654    if (not_valid_before) {
655        gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
656        g_string_append_printf(info, "Valid from: %s\n", date_str);
657        g_free(date_str);
658        g_date_time_unref(not_valid_before);
659    } else {
660        g_string_append(info, "Valid from: Information not available\n");
661    }
662
663    if (not_valid_after) {
664        gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
665        g_string_append_printf(info, "Valid until: %s\n", date_str);
666        g_free(date_str);
667        g_date_time_unref(not_valid_after);
668    } else {
669        g_string_append(info, "Valid until: Information not available\n");
670    }
671
672    char* result = g_string_free(info, FALSE);
673    printf("[dumber] Certificate info: %s\n", result);
674    fflush(stdout);
675    return result;
676}
677
678static void connect_tls_error_handler(WebKitWebView* wv) {
679    if (!wv) return;
680    g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
681                     G_CALLBACK(on_load_failed_with_tls_errors), NULL);
682}
683
684// Modern AlertDialog async callback
685static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data) {
686    AlertDialogResponse *data = (AlertDialogResponse*)user_data;
687    GError *error = NULL;
688    data->response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
689    if (error) {
690        printf("[dumber] AlertDialog error: %s\n", error->message);
691        g_error_free(error);
692        data->response = 0;
693    }
694    data->completed = TRUE;
695    const char *response_names[] = {"GO_BACK", "PROCEED_ONCE", "ALWAYS_ACCEPT"};
696    const char *response_name = (data->response >= 0 && data->response <= 2) ?
697                               response_names[data->response] : "UNKNOWN";
698    printf("[dumber] TLS dialog response: %s (button_index=%d)\n", response_name, data->response);
699    fflush(stdout);
700    if (data->loop) {
701        g_main_loop_quit(data->loop);
702    }
703}
704
705// Show TLS warning dialog synchronously using modern GtkAlertDialog API
706static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
707    printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
708    printf("[dumber] Error message: %s\n", error_msg);
709    fflush(stdout);
710
711    // Create the modern AlertDialog
712    GtkAlertDialog *dialog = gtk_alert_dialog_new("Certificate Error for %s", hostname);
713
714    // Set modal behavior and error details
715    gtk_alert_dialog_set_modal(dialog, TRUE);
716    gtk_alert_dialog_set_detail(dialog, error_msg);
717
718    // Configure buttons (Go Back = 0, Proceed Once = 1, Always Accept = 2)
719    const char *buttons[] = {"Go Back", "Proceed Once (Unsafe)", "Always Accept This Site", NULL};
720    gtk_alert_dialog_set_buttons(dialog, buttons);
721    gtk_alert_dialog_set_default_button(dialog, 0);
722    gtk_alert_dialog_set_cancel_button(dialog, 0);
723
724    // Set up response data with main loop for sync behavior
725    AlertDialogResponse response_data = {0, FALSE, g_main_loop_new(NULL, FALSE)};
726
727    // Start async operation
728    gtk_alert_dialog_choose(dialog, parent, NULL,
729                           on_alert_dialog_choose_done, &response_data);
730
731    // Run event loop until dialog completes
732    g_main_loop_run(response_data.loop);
733    g_main_loop_unref(response_data.loop);
734
735    // Clean up
736    g_object_unref(dialog);
737
738    // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept
739    return response_data.response;
740}
741
742*/
743import "C"
744
745import (
746	"context"
747	"crypto/sha256"
748	"database/sql"
749	"encoding/json"
750	"errors"
751	"fmt"
752	"log"
753	"math"
754	"os"
755	"path/filepath"
756	"runtime"
757	"runtime/cgo"
758	"strconv"
759	"strings"
760	"sync"
761	"time"
762	"unsafe"
763
764	"github.com/bnema/dumber/internal/config"
765	"github.com/bnema/dumber/internal/db"
766	"github.com/bnema/dumber/internal/logging"
767	_ "github.com/ncruces/go-sqlite3"
768)
769
770// FilterManager interface for content blocking integration
771type FilterManager interface {
772	GetNetworkFilters() ([]byte, error)
773	GetCosmeticScript() string
774	GetCosmeticScriptForDomain(domain string) string
775}
776
777// domBridgeTemplate removed - now loading compiled main-world.min.js
778
779var registerSchemeOnce sync.Once
780
781func registerDumbScheme(ctx *C.WebKitWebContext) {
782	if ctx == nil {
783		return
784	}
785
786	registerSchemeOnce.Do(func() {
787		sch := C.CString("dumb")
788		defer C.free(unsafe.Pointer(sch))
789		C.register_uri_scheme_handler(ctx, sch)
790		C.register_uri_scheme_security(ctx, sch)
791	})
792}
793
794// Helper function to convert bool to int for C interop
795func boolToInt(b bool) int {
796	if b {
797		return 1
798	}
799	return 0
800}
801
802//export goInvokeHandle
803func goInvokeHandle(handle C.uintptr_t) {
804	h := cgo.Handle(handle)
805	if fn, ok := h.Value().(func()); ok {
806		fn()
807	}
808	h.Delete()
809}
810
811// getCertificateHash generates a SHA256 hash of the certificate info for storage
812func getCertificateHash(certificateInfo string) string {
813	hash := sha256.Sum256([]byte(certificateInfo))
814	return fmt.Sprintf("%x", hash)
815}
816
817// getDBConnection opens a database connection and returns queries
818func getDBConnection() (*db.Queries, *sql.DB, error) {
819	cfg := config.Get()
820	if cfg == nil {
821		return nil, nil, fmt.Errorf("config not available")
822	}
823
824	dbPath := cfg.Database.Path
825	if dbPath == "" {
826		var err error
827		dbPath, err = config.GetDatabaseFile()
828		if err != nil {
829			return nil, nil, fmt.Errorf("failed to get database path: %w", err)
830		}
831	}
832
833	database, err := sql.Open("sqlite3", dbPath)
834	if err != nil {
835		return nil, nil, fmt.Errorf("failed to open database: %w", err)
836	}
837
838	queries := db.New(database)
839	return queries, database, nil
840}
841
842// checkStoredCertificateDecision checks if user has previously decided on this certificate
843func checkStoredCertificateDecision(hostname, certificateInfo string) (string, bool) {
844	queries, database, err := getDBConnection()
845	if err != nil {
846		log.Printf("[tls] Failed to connect to database: %v", err)
847		return "", false
848	}
849	defer database.Close()
850
851	certHash := getCertificateHash(certificateInfo)
852	ctx := context.Background()
853
854	validation, err := queries.GetCertificateValidation(ctx, hostname, certHash)
855	if err != nil {
856		if err == sql.ErrNoRows {
857			return "", false // No previous decision
858		}
859		log.Printf("[tls] Failed to query certificate validation: %v", err)
860		return "", false
861	}
862
863	log.Printf("[tls] Found stored certificate decision for %s: %s", hostname, validation.UserDecision)
864	return validation.UserDecision, true
865}
866
867// storeCertificateDecision stores the user's decision about a certificate
868func storeCertificateDecision(hostname, certificateInfo, decision string, permanent bool) error {
869	queries, database, err := getDBConnection()
870	if err != nil {
871		return fmt.Errorf("failed to connect to database: %w", err)
872	}
873	defer database.Close()
874
875	certHash := getCertificateHash(certificateInfo)
876	ctx := context.Background()
877
878	var expiresAt sql.NullTime
879	if !permanent {
880		// For temporary decisions, expire after 24 hours
881		expiresAt = sql.NullTime{
882			Time:  time.Now().Add(24 * time.Hour),
883			Valid: true,
884		}
885	}
886
887	err = queries.StoreCertificateValidation(ctx, hostname, certHash, decision, expiresAt)
888	if err != nil {
889		return fmt.Errorf("failed to store certificate validation: %w", err)
890	}
891
892	log.Printf("[tls] Stored certificate decision for %s: %s (permanent: %t)", hostname, decision, permanent)
893	return nil
894}
895
896type nativeView struct {
897	win       *C.GtkWidget
898	container *C.GtkWidget
899	view      *C.GtkWidget
900	wv        *C.WebKitWebView
901	ucm       *C.WebKitUserContentManager
902}
903
904type memoryStats struct {
905	pageLoadCount          int
906	lastGCTime             time.Time
907	memoryPressureSettings *C.WebKitMemoryPressureSettings
908}
909
910// WebView represents a browser view powered by WebKit2GTK.
911type WebView struct {
912	config          *Config
913	zoom            float64
914	url             string
915	destroyed       bool
916	native          *nativeView
917	window          *Window
918	id              uintptr
919	msgHandler      func(payload string)
920	titleHandler    func(title string)
921	uriHandler      func(uri string)
922	zoomHandler     func(level float64)
923	popupHandler    func(string) *WebView
924	closeHandler    func()
925	memStats        *memoryStats
926	gcTicker        *time.Ticker
927	gcDone          chan struct{}
928	tlsExceptions   map[string]bool // host -> whether user allowed certificate exception
929	useDomZoom      bool
930	domZoomSeed     float64
931	domBridgeScript *C.WebKitUserScript
932	wasReparented   bool // Track if this WebView has been reparented
933
934	// Window type detection and relationships
935	windowType         WindowType
936	windowFeatures     *WindowFeatures
937	windowTypeCallback func(WindowType, *WindowFeatures)
938	parentWebView      *WebView
939	isRelated          bool
940	isActive           bool // Track if this WebView is currently active/focused
941}
942
943// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
944func NewWebView(cfg *Config) (*WebView, error) {
945	log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
946	if C.gtk_init_check() == 0 {
947		return nil, errors.New("failed to initialize GTK")
948	}
949
950	if cfg == nil {
951		cfg = &Config{}
952	}
953
954	// Setup video acceleration environment variables before creating WebView
955	if cfg.VideoAcceleration.EnableVAAPI {
956		driverName := C.CString(cfg.VideoAcceleration.VAAPIDriverName)
957		enableAll := 0
958		if cfg.VideoAcceleration.EnableAllDrivers {
959			enableAll = 1
960		}
961		legacy := 0
962		if cfg.VideoAcceleration.LegacyVAAPI {
963			legacy = 1
964		}
965		C.setup_video_acceleration(driverName, C.int(enableAll), C.int(legacy))
966		C.free(unsafe.Pointer(driverName))
967	}
968
969	// Prepare persistent website data/caches
970	dataDir := cfg.DataDir
971	cacheDir := cfg.CacheDir
972	if dataDir == "" {
973		dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data")
974	}
975	if cacheDir == "" {
976		cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache")
977	}
978	_ = os.MkdirAll(dataDir, 0o755)
979	_ = os.MkdirAll(cacheDir, 0o755)
980
981	cData := C.CString(dataDir)
982	cCache := C.CString(cacheDir)
983	cookieFile := filepath.Join(dataDir, "cookies.db")
984	cCookie := C.CString(cookieFile)
985	defer C.free(unsafe.Pointer(cData))
986	defer C.free(unsafe.Pointer(cCache))
987	defer C.free(unsafe.Pointer(cCookie))
988	// Use default WebContext to register the custom URI scheme handler; the
989	// WebView itself is created with a WebKitNetworkSession for persistence.
990	ctx := C.webkit_web_context_get_default()
991
992	// Apply cache model to WebContext
993	// Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
994	cacheModel := 1
995	if cfg.Memory.CacheModel != "" {
996		switch cfg.Memory.CacheModel {
997		case CacheModelWebBrowser:
998			cacheModel = 1
999		case CacheModelPrimaryWebBrowser:
1000			cacheModel = 2
1001		default: // CacheModelDocumentViewer or unset
1002			cacheModel = 0
1003		}
1004	}
1005	C.maybe_set_cache_model(ctx, C.int(cacheModel))
1006
1007	// Configure memory pressure settings if specified
1008	var pressureSettings *C.WebKitMemoryPressureSettings
1009	// Only configure memory pressure if explicitly requested via config
1010	if cfg.Memory.MemoryLimitMB > 0 || cfg.Memory.ConservativeThreshold > 0 || cfg.Memory.StrictThreshold > 0 || cfg.Memory.KillThreshold > 0 || cfg.Memory.PollIntervalSeconds > 0 {
1011		memLimitMB := cfg.Memory.MemoryLimitMB
1012		conservativeThreshold := cfg.Memory.ConservativeThreshold
1013		strictThreshold := cfg.Memory.StrictThreshold
1014		killThreshold := cfg.Memory.KillThreshold
1015		pollInterval := cfg.Memory.PollIntervalSeconds
1016
1017		pressureSettings = C.create_memory_pressure_settings(
1018			C.uint(memLimitMB),
1019			C.double(conservativeThreshold),
1020			C.double(strictThreshold),
1021			C.double(killThreshold),
1022			C.double(pollInterval))
1023
1024		if pressureSettings != nil {
1025			C.maybe_set_memory_pressure_settings(ctx, pressureSettings)
1026			if cfg.Memory.EnableMemoryMonitoring {
1027				log.Printf("[webkit] Memory pressure settings applied: limit=%dMB, conservative=%.2f, strict=%.2f, poll=%.1fs",
1028					memLimitMB, conservativeThreshold, strictThreshold, pollInterval)
1029			}
1030		}
1031	}
1032
1033	registerDumbScheme(ctx)
1034	// Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
1035
1036	// Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
1037	var createdUcm *C.WebKitUserContentManager
1038	viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm, pressureSettings)
1039	if viewWidget == nil {
1040		return nil, errors.New("failed to create WebKitWebView")
1041	}
1042
1043	var win *C.GtkWidget
1044	var window *Window
1045	container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0)
1046	if container == nil {
1047		return nil, errors.New("failed to create GtkBox container")
1048	}
1049	// Hold a strong reference so the WebView keeps the container alive across reparenting.
1050	C.g_object_ref_sink(C.gpointer(container))
1051	C.gtk_widget_set_hexpand(container, C.gboolean(1))
1052	C.gtk_widget_set_vexpand(container, C.gboolean(1))
1053	C.gtk_widget_set_hexpand(viewWidget, C.gboolean(1))
1054	C.gtk_widget_set_vexpand(viewWidget, C.gboolean(1))
1055	C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget)
1056
1057	// Only create window if requested
1058	if cfg.CreateWindow {
1059		// Create a top-level window to host the view
1060		win = C.new_window()
1061		if win == nil {
1062			return nil, errors.New("failed to create GtkWindow")
1063		}
1064
1065		// Pack view widget into the window
1066		// GTK4: containers removed; use gtk_window_set_child
1067		C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container)
1068		C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
1069		C.connect_destroy_quit(win)
1070		window = &Window{win: win}
1071	}
1072
1073	seed := 1.0
1074	if cfg.ZoomDefault > 0 {
1075		seed = cfg.ZoomDefault
1076	}
1077
1078	v := &WebView{
1079		config:      cfg,
1080		zoom:        1.0,
1081		useDomZoom:  cfg.UseDomZoom,
1082		domZoomSeed: seed,
1083		native:      &nativeView{win: win, container: container, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
1084		window:      window,
1085		memStats: &memoryStats{
1086			pageLoadCount:          0,
1087			lastGCTime:             time.Now(),
1088			memoryPressureSettings: pressureSettings,
1089		},
1090		gcDone:        make(chan struct{}),
1091		tlsExceptions: make(map[string]bool),
1092	}
1093	// Assign an ID for accelerator dispatch
1094	v.id = nextViewID()
1095	registerView(v.id, v)
1096	C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1097	ConnectCreateSignal(v) // Connect create signal for window.open support
1098
1099	// Register with global memory manager if monitoring is enabled
1100	if cfg.Memory.EnableMemoryMonitoring {
1101		if globalMemoryManager == nil {
1102			InitializeGlobalMemoryManager(
1103				cfg.Memory.EnableMemoryMonitoring,
1104				cfg.Memory.ProcessRecycleThreshold,
1105				30*time.Second, // Default monitoring interval
1106			)
1107		}
1108		if globalMemoryManager != nil {
1109			globalMemoryManager.RegisterWebView(v)
1110		}
1111	}
1112	// Attach GTK4 input controllers (keyboard, mouse)
1113	AttachKeyboardControllers(v)
1114
1115	// Watch for GTK theme changes and propagate to page at runtime
1116	if settings := C.gtk_settings_get_default(); settings != nil {
1117		C.connect_theme_changed_with_id(settings, C.ulong(v.id))
1118	}
1119
1120	// Zoom shortcuts are now handled by application-level workspace shortcuts
1121	// in workspace_manager.go to ensure proper active pane targeting
1122	// Find in page (Ctrl/Cmd+F): handled by window-level shortcuts in window_shortcuts.go
1123	// Navigation with Ctrl/Cmd + Arrow keys: forward to keyboard service bridge
1124	_ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowLeft", func() {
1125		_ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowleft'}}))")
1126	})
1127	_ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowRight", func() {
1128		_ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowright'}}))")
1129	})
1130	// Update window title when page title changes
1131	C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
1132	// Also dispatch title change to Go with view id
1133	C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
1134	// Notify URI changes to Go to keep current URL in sync
1135	C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
1136	// Apply hardware acceleration and related settings based on cfg.Rendering
1137	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1138		// Hardware acceleration policy (guarded by version)
1139		switch cfg.Rendering.Mode {
1140		case "gpu":
1141			log.Printf("[webkit] Applying GPU rendering mode")
1142			C.maybe_set_hw_policy(settings, 1)
1143			C.maybe_set_webgl(settings, 1)
1144			C.maybe_set_canvas_accel(settings, 1)
1145		case "cpu":
1146			log.Printf("[webkit] Applying CPU rendering mode")
1147			C.maybe_set_hw_policy(settings, 2)
1148			C.maybe_set_webgl(settings, 0)
1149			C.maybe_set_canvas_accel(settings, 0)
1150		default: // auto
1151			log.Printf("[webkit] Applying AUTO rendering mode - detecting actual backend...")
1152			C.maybe_set_hw_policy(settings, 0)
1153			C.maybe_set_webgl(settings, 1)
1154			C.maybe_set_canvas_accel(settings, 1)
1155			// Detect actual rendering backend after applying auto settings
1156			v.detectAndLogRenderingBackend()
1157		}
1158		// Optional compositing indicators for debugging (guarded by version)
1159		if cfg.Rendering.DebugGPU {
1160			C.maybe_set_draw_indicators(settings, 1)
1161		}
1162		// Reduce media pipeline churn by requiring a user gesture for playback
1163		C.maybe_set_media_user_gesture(settings, 1)
1164		// Enable trackpad back/forward gestures when available
1165		C.maybe_set_back_forward_gestures(settings, 1)
1166		// Smooth scrolling for wheel/pinch gestures when supported
1167		C.maybe_set_smooth_scrolling(settings, 1)
1168
1169		// Keep page cache enabled by default for performance
1170		enablePageCache := true
1171		if cfg.Memory.EnablePageCache {
1172			enablePageCache = true
1173		}
1174		C.maybe_set_page_cache(settings, C.int(boolToInt(enablePageCache)))
1175
1176		// Offline app cache is deprecated; keep as no-op
1177		C.maybe_set_offline_app_cache(settings, C.int(0))
1178
1179		if cfg.Memory.EnableMemoryMonitoring {
1180			log.Printf("[webkit] Memory settings applied: page_cache=%v", enablePageCache)
1181		}
1182	}
1183
1184	if cfg.ZoomDefault > 0 {
1185		v.zoom = cfg.ZoomDefault
1186		C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
1187	}
1188	if cfg.EnableDeveloperExtras {
1189		settings := C.webkit_web_view_get_settings(v.native.wv)
1190		if settings != nil {
1191			C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
1192		}
1193	}
1194
1195	// Enable JavaScript popup windows for workspace popup handling
1196	settings := C.webkit_web_view_get_settings(v.native.wv)
1197	if settings != nil {
1198		C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1))
1199		log.Printf("[webkit] Enabled JavaScript popup windows")
1200	}
1201
1202	// Apply custom User-Agent for codec negotiation
1203	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1204		if cfg.CodecPreferences.CustomUserAgent != "" {
1205			cUserAgent := C.CString(cfg.CodecPreferences.CustomUserAgent)
1206			C.webkit_settings_set_user_agent(settings, (*C.gchar)(cUserAgent))
1207			C.free(unsafe.Pointer(cUserAgent))
1208			log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)
1209		} else if cfg.CodecPreferences.ForceAV1 {
1210			// Use modern Chrome UA that signals AV1 support
1211			av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
1212			cAV1UA := C.CString(av1UA)
1213			C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))
1214			C.free(unsafe.Pointer(cAV1UA))
1215			log.Printf("[webkit] Set AV1-optimized User-Agent")
1216		}
1217	}
1218
1219	// Apply default fonts if provided in cfg
1220	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1221		if cfg.DefaultSansFont != "" {
1222			csans := C.CString(cfg.DefaultSansFont)
1223			C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
1224			C.free(unsafe.Pointer(csans))
1225			// Also set default font family to sans
1226			csans2 := C.CString(cfg.DefaultSansFont)
1227			C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
1228			C.free(unsafe.Pointer(csans2))
1229		}
1230		if cfg.DefaultSerifFont != "" {
1231			cserif := C.CString(cfg.DefaultSerifFont)
1232			C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
1233			C.free(unsafe.Pointer(cserif))
1234		}
1235		if cfg.DefaultMonospaceFont != "" {
1236			cmono := C.CString(cfg.DefaultMonospaceFont)
1237			C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
1238			C.free(unsafe.Pointer(cmono))
1239		}
1240		if cfg.DefaultFontSize > 0 {
1241			C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
1242		}
1243	}
1244	if cfg.InitialURL != "" {
1245		_ = v.LoadURL(cfg.InitialURL)
1246	}
1247	// Ensure native resources are released if user forgets to call Destroy
1248	runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
1249	log.Printf("[webkit] WebView created (CGO)")
1250
1251	// Initialize UCM scripts and handlers
1252	if v.native != nil && v.native.ucm != nil {
1253		v.enableUserContentManager(cfg)
1254	}
1255
1256	// Start periodic JavaScript garbage collection if configured
1257	if cfg.Memory.EnableGCInterval > 0 {
1258		v.startPeriodicGC(time.Duration(cfg.Memory.EnableGCInterval) * time.Second)
1259		if cfg.Memory.EnableMemoryMonitoring {
1260			log.Printf("[webkit] Periodic GC enabled: interval=%ds", cfg.Memory.EnableGCInterval)
1261		}
1262	}
1263
1264	// Setup console message logging
1265	v.setupConsoleLogging()
1266
1267	return v, nil
1268}
1269
1270// setupConsoleLogging enables console message capture for the WebView
1271func (v *WebView) setupConsoleLogging() {
1272	if v.native == nil || v.native.wv == nil {
1273		return
1274	}
1275
1276	// Check if console capture is enabled in config
1277	cfg := config.Get()
1278	if !cfg.Logging.CaptureConsole {
1279		// Disable console messages to stdout (always, to keep backend clean)
1280		if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1281			C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1282		}
1283		return
1284	}
1285
1286	// If console capture is enabled, disable stdout and enable our capture
1287	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1288		C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1289		log.Printf("[webkit] Console message capture enabled - redirecting to console.log file")
1290	}
1291
1292	// TODO: Implement JavaScript injection for console capture
1293	// This will inject code to intercept console.log/error/warn and send to Go
1294}
1295
1296// setupGlobalConsoleCapture sets up a global stdout filter to capture console messages
1297func setupGlobalConsoleCapture() {
1298	// This should be called once globally, not per WebView
1299	logging.StartConsoleCapture()
1300}
1301
1302// injectConsoleCapture injects JavaScript to capture console messages
1303func (v *WebView) injectConsoleCapture() {
1304	consoleScript := `
1305(function() {
1306	const originalLog = console.log;
1307	const originalWarn = console.warn;
1308	const originalError = console.error;
1309	const originalInfo = console.info;
1310	const originalDebug = console.debug;
1311	
1312	function sendToGo(level, args) {
1313		const message = args.map(arg => 
1314			typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1315		).join(' ');
1316		
1317		const formattedMessage = '[' + level + '] ' + window.location.href + ' ' + message;
1318		
1319		// Send to Go via user script message
1320		if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumberBridge) {
1321			window.webkit.messageHandlers.dumberBridge.postMessage({
1322				type: 'console',
1323				data: formattedMessage
1324			});
1325		}
1326	}
1327	
1328	console.log = function(...args) {
1329		originalLog.apply(console, args);
1330		sendToGo('LOG', args);
1331	};
1332	
1333	console.warn = function(...args) {
1334		originalWarn.apply(console, args);
1335		sendToGo('WARN', args);
1336	};
1337	
1338	console.error = function(...args) {
1339		originalError.apply(console, args);
1340		sendToGo('ERROR', args);
1341	};
1342	
1343	console.info = function(...args) {
1344		originalInfo.apply(console, args);
1345		sendToGo('INFO', args);
1346	};
1347	
1348	console.debug = function(...args) {
1349		originalDebug.apply(console, args);
1350		sendToGo('DEBUG', args);
1351	};
1352})();
1353`
1354	_ = v.InjectScript(consoleScript)
1355}
1356
1357// handleConsoleMessage processes console messages from JavaScript
1358func (v *WebView) handleConsoleMessage(payload string) {
1359	// Parse the message payload to check if it's a console message
1360	var msg struct {
1361		Type string `json:"type"`
1362		Data string `json:"data"`
1363	}
1364
1365	if err := json.Unmarshal([]byte(payload), &msg); err != nil {
1366		return
1367	}
1368
1369	if msg.Type == "console" {
1370		// Send to logging system with [CONSOLE] tag
1371		logging.CaptureWebKitLog(msg.Data)
1372	}
1373}
1374
1375// startConsoleCapture starts capturing stdout to filter console messages
1376func (v *WebView) startConsoleCapture() {
1377	// This is a simplified approach - we'll rely on the existing OutputCapture
1378	// mechanism in internal/logging/capture.go to handle stdout capture
1379	// and filter for console messages in the log processing
1380}
1381
1382// NewWebViewWithRelated creates a new WebView related to an existing one for process sharing
1383func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) {
1384	if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil {
1385		// Fallback to regular WebView creation
1386		return NewWebView(cfg)
1387	}
1388
1389	log.Printf("[webkit] Creating related WebView for process sharing (CGO)")
1390	if C.gtk_init_check() == 0 {
1391		return nil, errors.New("failed to initialize GTK")
1392	}
1393
1394	if cfg == nil {
1395		cfg = &Config{}
1396	}
1397
1398	// Create WebView related to the existing one
1399	var createdUcm *C.WebKitUserContentManager
1400	viewWidget := C.new_webview_related(relatedView.native.wv, &createdUcm)
1401	if viewWidget == nil {
1402		return nil, errors.New("failed to create related WebKitWebView")
1403	}
1404
1405	var win *C.GtkWidget
1406	var window *Window
1407	container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0)
1408	if container == nil {
1409		return nil, errors.New("failed to create GtkBox container")
1410	}
1411
1412	C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget)
1413
1414	// Make the WebView widget visible and expandable for popup rendering
1415	C.gtk_widget_set_visible(viewWidget, C.gboolean(1))
1416	C.gtk_widget_set_visible(container, C.gboolean(1))
1417	C.gtk_widget_set_hexpand(viewWidget, C.gboolean(1))
1418	C.gtk_widget_set_vexpand(viewWidget, C.gboolean(1))
1419	C.gtk_widget_set_hexpand(container, C.gboolean(1))
1420	C.gtk_widget_set_vexpand(container, C.gboolean(1))
1421	// Remove any size constraints from popup WebView to let it fill the pane
1422	C.gtk_widget_set_size_request(viewWidget, -1, -1)
1423	C.gtk_widget_set_size_request(container, -1, -1)
1424
1425	if cfg.CreateWindow {
1426		win = C.new_window()
1427		if win == nil {
1428			return nil, errors.New("failed to create native window")
1429		}
1430		C.connect_destroy_quit(win)
1431		C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container)
1432		window = &Window{win: win}
1433	} else {
1434		window = &Window{}
1435	}
1436
1437	v := &WebView{
1438		config: cfg,
1439		window: window,
1440		zoom:   cfg.ZoomDefault,
1441		native: &nativeView{
1442			win:       win,
1443			container: container,
1444			view:      viewWidget,
1445			wv:        (*C.WebKitWebView)(unsafe.Pointer(viewWidget)),
1446			ucm:       createdUcm,
1447		},
1448		useDomZoom:    cfg.UseDomZoom,
1449		domZoomSeed:   cfg.ZoomDefault,
1450		gcDone:        make(chan struct{}),
1451		tlsExceptions: make(map[string]bool),
1452	}
1453
1454	// Enable JavaScript popup windows for workspace popup handling
1455	settings := C.webkit_web_view_get_settings(v.native.wv)
1456	if settings != nil {
1457		C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1))
1458		log.Printf("[webkit] Enabled JavaScript popup windows for related WebView")
1459	}
1460
1461	// Assign an ID for accelerator dispatch
1462	v.id = nextViewID()
1463	registerView(v.id, v)
1464	C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1465	ConnectCreateSignal(v) // Connect create signal for window.open support
1466
1467	// Register with memory manager if enabled
1468	if cfg.Memory.EnableMemoryMonitoring {
1469		if globalMemoryManager == nil {
1470			InitializeGlobalMemoryManager(
1471				cfg.Memory.EnableMemoryMonitoring,
1472				cfg.Memory.ProcessRecycleThreshold,
1473				30*time.Second,
1474			)
1475		}
1476		if globalMemoryManager != nil {
1477			globalMemoryManager.RegisterWebView(v)
1478		}
1479	}
1480
1481	if cfg.ZoomDefault <= 0 {
1482		v.zoom = 1.0
1483	}
1484	v.domZoomSeed = v.zoom
1485
1486	// Initialize UCM scripts and handlers (same as NewWebView)
1487	if v.native != nil && v.native.ucm != nil {
1488		v.enableUserContentManager(cfg)
1489	}
1490
1491	return v, nil
1492}
1493
1494func (w *WebView) LoadURL(rawURL string) error {
1495	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1496		return ErrNotImplemented
1497	}
1498
1499	// Track page loads for process recycling
1500	if w.memStats != nil {
1501		w.memStats.pageLoadCount++
1502
1503		// Check if we need to recycle this WebView
1504		if w.config.Memory.ProcessRecycleThreshold > 0 &&
1505			w.memStats.pageLoadCount >= w.config.Memory.ProcessRecycleThreshold {
1506			if w.config.Memory.EnableMemoryMonitoring {
1507				log.Printf("[webkit] Process recycle threshold reached (%d pages), consider creating new WebView",
1508					w.memStats.pageLoadCount)
1509			}
1510		}
1511	}
1512
1513	w.url = rawURL
1514	curl := C.CString(rawURL)
1515	defer C.free(unsafe.Pointer(curl))
1516	C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
1517	log.Printf("[webkit] LoadURL: %s", rawURL)
1518	return nil
1519}
1520
1521func (w *WebView) Show() error {
1522	if w == nil || w.destroyed || w.native == nil {
1523		return ErrNotImplemented
1524	}
1525	if w.native.container != nil {
1526		C.gtk_widget_set_visible(w.native.container, C.gboolean(1))
1527	}
1528	if w.native.view != nil {
1529		C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
1530	}
1531	if w.native.win == nil {
1532		return ErrNotImplemented
1533	}
1534	C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
1535	C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1536	log.Printf("[webkit] Show window")
1537	return nil
1538}
1539
1540func (w *WebView) Hide() error {
1541	if w == nil || w.destroyed || w.native == nil {
1542		return ErrNotImplemented
1543	}
1544	if w.native.container != nil {
1545		C.gtk_widget_set_visible(w.native.container, C.gboolean(0))
1546	}
1547	if w.native.view != nil {
1548		C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
1549	}
1550	if w.native.win == nil {
1551		return ErrNotImplemented
1552	}
1553	C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
1554	return nil
1555}
1556
1557func (w *WebView) Destroy() error {
1558	if w == nil || w.native == nil {
1559		return ErrNotImplemented
1560	}
1561
1562	// Stop periodic GC if running
1563	w.stopPeriodicGC()
1564
1565	// Clean up memory pressure settings
1566	if w.memStats != nil && w.memStats.memoryPressureSettings != nil {
1567		// WebKit will clean up the memory pressure settings when context is destroyed
1568		w.memStats.memoryPressureSettings = nil
1569	}
1570
1571	// Unregister from memory manager
1572	if globalMemoryManager != nil {
1573		globalMemoryManager.UnregisterWebView(w.id)
1574	}
1575
1576	if w.native != nil && w.native.ucm != nil && w.domBridgeScript != nil {
1577		C.webkit_user_content_manager_remove_script(w.native.ucm, w.domBridgeScript)
1578		C.webkit_user_script_unref(w.domBridgeScript)
1579		w.domBridgeScript = nil
1580	}
1581
1582	// GTK4: destroy windows via gtk_window_destroy if we created one
1583	if w.native.win != nil {
1584		C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1585		w.native.win = nil
1586		if w.window != nil {
1587			w.window.win = nil
1588		}
1589		log.Printf("[webkit] Destroy window")
1590	}
1591
1592	w.releaseNativeWidgets()
1593	w.destroyed = true
1594	unregisterView(w.id)
1595	return nil
1596}
1597
1598func (w *WebView) releaseNativeWidgets() {
1599	if w == nil || w.native == nil {
1600		return
1601	}
1602	if w.native.container != nil {
1603		C.g_object_unref(C.gpointer(w.native.container))
1604		w.native.container = nil
1605	}
1606}
1607
1608// Window returns the associated native window.
1609func (w *WebView) Window() *Window { return w.window }
1610
1611// GetCurrentURL returns the current URI from WebKit.
1612func (w *WebView) GetCurrentURL() string {
1613	if w == nil || w.native == nil || w.native.wv == nil {
1614		return ""
1615	}
1616	uri := C.webkit_web_view_get_uri(w.native.wv)
1617	if uri == nil {
1618		return ""
1619	}
1620	return C.GoString((*C.char)(unsafe.Pointer(uri)))
1621}
1622
1623func (w *WebView) GoBack() error {
1624	if w == nil || w.native == nil || w.native.wv == nil {
1625		return ErrNotImplemented
1626	}
1627	C.webkit_web_view_go_back(w.native.wv)
1628	return nil
1629}
1630
1631func (w *WebView) GoForward() error {
1632	if w == nil || w.native == nil || w.native.wv == nil {
1633		return ErrNotImplemented
1634	}
1635	C.webkit_web_view_go_forward(w.native.wv)
1636	return nil
1637}
1638
1639func (w *WebView) Reload() error {
1640	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1641		return ErrNotImplemented
1642	}
1643	C.webkit_web_view_reload(w.native.wv)
1644	log.Printf("[webkit] Reload page")
1645	return nil
1646}
1647
1648func (w *WebView) ReloadBypassCache() error {
1649	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1650		return ErrNotImplemented
1651	}
1652	C.webkit_web_view_reload_bypass_cache(w.native.wv)
1653	log.Printf("[webkit] Hard reload page (bypass cache)")
1654	return nil
1655}
1656
1657// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
1658func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
1659
1660func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandler = cb }
1661
1662func (w *WebView) RegisterCloseHandler(cb func()) {
1663	w.closeHandler = cb
1664	// Also connect the CGO close signal to ensure window.close() is detected
1665	if w.native != nil && w.native.wv != nil {
1666		C.connect_close_signal(w.native.wv, C.ulong(w.id))
1667	}
1668}
1669
1670func (w *WebView) dispatchScriptMessage(payload string) {
1671	if w != nil && w.msgHandler != nil {
1672		w.msgHandler(payload)
1673	}
1674}
1675
1676func (w *WebView) dispatchPopupRequest(uri string) *WebView {
1677	if w != nil && w.popupHandler != nil {
1678		return w.popupHandler(uri)
1679	}
1680	return nil
1681}
1682
1683// CreateRelatedView creates a WebView that shares process/context with this view
1684func (w *WebView) CreateRelatedView() *WebView {
1685	if w == nil || w.native == nil || w.native.wv == nil {
1686		return nil
1687	}
1688	// Build a minimal config inheriting from current view
1689	cfg := &Config{
1690		Assets:                w.config.Assets,
1691		ZoomDefault:           w.config.ZoomDefault,
1692		EnableDeveloperExtras: w.config.EnableDeveloperExtras,
1693		DataDir:               w.config.DataDir,
1694		CacheDir:              w.config.CacheDir,
1695		APIToken:              w.config.APIToken,
1696		DefaultSansFont:       w.config.DefaultSansFont,
1697		DefaultSerifFont:      w.config.DefaultSerifFont,
1698		DefaultMonospaceFont:  w.config.DefaultMonospaceFont,
1699		DefaultFontSize:       w.config.DefaultFontSize,
1700		Rendering:             w.config.Rendering,
1701		UseDomZoom:            w.config.UseDomZoom,
1702		VideoAcceleration:     w.config.VideoAcceleration,
1703		Memory:                w.config.Memory,
1704		CodecPreferences:      w.config.CodecPreferences,
1705		CreateWindow:          false,
1706	}
1707	related, err := NewWebViewWithRelated(cfg, w)
1708	if err != nil {
1709		log.Printf("[webkit] CreateRelatedView failed: %v", err)
1710		return nil
1711	}
1712	return related
1713}
1714
1715// OnWindowTypeDetected registers a callback invoked when window type is detected
1716func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) {
1717	if w == nil {
1718		return
1719	}
1720	w.windowTypeCallback = callback
1721}
1722
1723// GetNativePointer returns the native WebView pointer for CGO callbacks
1724func (w *WebView) GetNativePointer() unsafe.Pointer {
1725	if w != nil && w.native != nil && w.native.wv != nil {
1726		return unsafe.Pointer(w.native.wv)
1727	}
1728	return nil
1729}
1730
1731// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
1732func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
1733
1734func (w *WebView) dispatchTitleChanged(title string) {
1735	if w != nil && w.titleHandler != nil {
1736		w.titleHandler(title)
1737	}
1738}
1739
1740// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
1741func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
1742
1743func (w *WebView) dispatchURIChanged(uri string) {
1744	if w != nil && w.uriHandler != nil {
1745		w.uriHandler(uri)
1746	}
1747}
1748
1749// RunOnMainThread schedules fn to execute on the GTK main thread.
1750func (w *WebView) RunOnMainThread(fn func()) {
1751	if w == nil || fn == nil {
1752		return
1753	}
1754	h := cgo.NewHandle(fn)
1755	C.schedule_on_main_thread(C.uintptr_t(h))
1756}
1757
1758// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
1759func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
1760
1761func (w *WebView) dispatchZoomChanged(level float64) {
1762	if w != nil && w.zoomHandler != nil {
1763		w.zoomHandler(level)
1764	}
1765}
1766
1767// Widget returns the underlying GtkWidget pointer for the WebView.
1768func (w *WebView) Widget() uintptr {
1769	if w == nil || w.native == nil || w.native.view == nil {
1770		return 0
1771	}
1772	return uintptr(unsafe.Pointer(w.native.view))
1773}
1774
1775// RootWidget returns the container widget that should be reparented for pane management.
1776func (w *WebView) RootWidget() uintptr {
1777	if w == nil || w.native == nil || w.native.container == nil {
1778		return 0
1779	}
1780	return uintptr(unsafe.Pointer(w.native.container))
1781}
1782
1783// DestroyWindow destroys the toplevel window hosting this WebView, if any.
1784func (w *WebView) DestroyWindow() {
1785	if w == nil || w.native == nil || w.native.win == nil {
1786		return
1787	}
1788	C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1789	w.native.win = nil
1790	if w.window != nil {
1791		w.window.win = nil
1792	}
1793}
1794
1795// PrepareForReparenting prepares the WebView widget for being moved to a new parent
1796func (w *WebView) PrepareForReparenting() {
1797	if w == nil || w.native == nil || w.native.container == nil {
1798		return
1799	}
1800
1801	// Mark that this view is being reparented
1802	w.wasReparented = true
1803
1804	// Ensure the widget is visible and properly sized for reparenting
1805	C.gtk_widget_set_visible(w.native.container, C.gboolean(1))
1806	C.gtk_widget_set_size_request(w.native.container, 100, 100)
1807	C.gtk_widget_queue_draw(w.native.container)
1808}
1809
1810// RefreshAfterReparenting refreshes the WebView after it has been reparented
1811func (w *WebView) RefreshAfterReparenting() {
1812	if w == nil || w.native == nil || w.native.wv == nil {
1813		return
1814	}
1815
1816	if w.wasReparented {
1817		widgetHandle := w.RootWidget()
1818		parentHandle := WidgetGetParent(widgetHandle)
1819		if parentHandle == 0 {
1820			log.Printf("[workspace] RefreshAfterReparenting deferred: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url)
1821			return
1822		}
1823		log.Printf("[workspace] RefreshAfterReparenting: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url)
1824		// Force WebKit to refresh its rendering context by triggering a reload
1825		// This ensures the rendering context is properly reinitialized after reparenting
1826		if w.url != "" {
1827			w.LoadURL(w.url) // Reload current URL to refresh rendering context
1828		}
1829		w.wasReparented = false
1830	}
1831}
1832
1833// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
1834func (w *WebView) enableUserContentManager(cfg *Config) {
1835	if w == nil || w.native == nil || w.native.ucm == nil {
1836		return
1837	}
1838	// Register handler "dumber" for main world
1839	cname := C.CString("dumber")
1840	ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
1841	if ok == 0 {
1842		log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
1843	}
1844	C.free(unsafe.Pointer(cname))
1845
1846	// Also register handler for the isolated GUI world so GUI can post messages to native
1847	cworld := C.CString("dumber-gui")
1848	cname2 := C.CString("dumber")
1849	ok2 := C.register_ucm_handler_world(w.native.ucm, cname2, cworld, C.ulong(w.id))
1850	if ok2 == 0 {
1851		log.Printf("[webkit] Failed to register UCM handler for isolated world 'dumber-gui'")
1852	}
1853	C.free(unsafe.Pointer(cworld))
1854	C.free(unsafe.Pointer(cname2))
1855
1856	// Inject color-scheme preference script at document-start to inform sites of system theme
1857	preferDark := C.gtk_prefers_dark() != 0
1858	if preferDark {
1859		log.Printf("[theme] GTK prefers: dark")
1860	} else {
1861		log.Printf("[theme] GTK prefers: light")
1862	}
1863	// Load color scheme script from compiled GUI assets instead of inline
1864	var schemeJS string
1865	if cfg != nil && cfg.Assets != nil {
1866		if schemeBytes, err := cfg.Assets.ReadFile("assets/gui/color-scheme.js"); err == nil {
1867			baseScript := string(schemeBytes)
1868			// Inject the GTK preference as a parameter
1869			if preferDark {
1870				schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = true; %s", baseScript)
1871			} else {
1872				schemeJS = fmt.Sprintf("window.__dumber_gtk_prefers_dark = false; %s", baseScript)
1873			}
1874		} else {
1875			log.Printf("[webkit] Warning: Failed to load color-scheme.js from assets, falling back to basic theme detection: %v", err)
1876			// Simple fallback if the compiled script is not available
1877			if preferDark {
1878				schemeJS = `console.log('[dumber] GTK detected color mode: dark'); document.documentElement.classList.add('dark');`
1879			} else {
1880				schemeJS = `console.log('[dumber] GTK detected color mode: light'); document.documentElement.classList.remove('dark');`
1881			}
1882		}
1883	}
1884	cScheme := C.CString(schemeJS)
1885	defer C.free(unsafe.Pointer(cScheme))
1886	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)
1887	if schemeScript != nil {
1888		C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
1889		C.webkit_user_script_unref(schemeScript)
1890	}
1891
1892	// Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
1893	if cfg != nil && cfg.Assets != nil {
1894		log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
1895		if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil {
1896			guiScript := string(guiBytes)
1897			log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes))
1898
1899			cGui := C.CString(guiScript)
1900			defer C.free(unsafe.Pointer(cGui))
1901
1902			log.Printf("[webkit] Creating GUI user script for document-start injection in isolated world 'dumber-gui'")
1903			world := C.CString("dumber-gui")
1904			defer C.free(unsafe.Pointer(world))
1905			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)
1906			if guiUserScript != nil {
1907				C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript)
1908				C.webkit_user_script_unref(guiUserScript)
1909				log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in isolated world")
1910			} else {
1911				log.Printf("[webkit] ERROR: Failed to create GUI user script")
1912			}
1913
1914			// Inject a unified page-world bridge to dispatch standard events and expose unified API
1915			w.installDomBridgeScript()
1916
1917			// If an API token is provided, inject a fetch wrapper in the isolated GUI world
1918			if cfg.APIToken != "" {
1919				wrapper := "(() => { try {" +
1920					"const TOKEN='" + cfg.APIToken + "';" +
1921					"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; };" +
1922					"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); };" +
1923					"} catch(e) { console.warn('[dumber] api token wrapper failed', e); } })();"
1924				cWrap := C.CString(wrapper)
1925				defer C.free(unsafe.Pointer(cWrap))
1926				world := C.CString("dumber-gui")
1927				defer C.free(unsafe.Pointer(world))
1928				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)
1929				if wrapScript != nil {
1930					C.webkit_user_content_manager_add_script(w.native.ucm, wrapScript)
1931					C.webkit_user_script_unref(wrapScript)
1932					log.Printf("[webkit] API token fetch wrapper injected in isolated GUI world")
1933				}
1934			}
1935		} else {
1936			log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err)
1937		}
1938
1939		// Add GUI CSS styles as user stylesheet when a legacy bundle is present.
1940		// Modern builds inline Tailwind output inside gui.min.js, so this file is optional.
1941		if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil {
1942			cssContent := string(cssBytes)
1943			cCss := C.CString(cssContent)
1944			defer C.free(unsafe.Pointer(cCss))
1945
1946			styleSheet := C.webkit_user_style_sheet_new(
1947				(*C.gchar)(cCss),
1948				C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1949				C.WEBKIT_USER_STYLE_LEVEL_AUTHOR,
1950				nil, // allow_list
1951				nil, // block_list
1952			)
1953			if styleSheet != nil {
1954				C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet)
1955				C.webkit_user_style_sheet_unref(styleSheet)
1956			} else {
1957				log.Printf("[webkit] ERROR: Failed to create GUI stylesheet")
1958			}
1959		} else if errors.Is(err, os.ErrNotExist) {
1960			log.Printf("[webkit] GUI stylesheet not found; assuming inline Tailwind bundle and skipping separate injection")
1961		} else {
1962			log.Printf("[webkit] Warning: Failed to load GUI CSS from assets, skipping separate stylesheet injection: %v", err)
1963		}
1964	}
1965
1966	// Inject Wails runtime fetch interceptor for homepage bridging
1967	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(_){} })();`
1968	cWails := C.CString(wailsBridge)
1969	defer C.free(unsafe.Pointer(cWails))
1970	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)
1971	if wailsScript != nil {
1972		C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
1973		C.webkit_user_script_unref(wailsScript)
1974	}
1975
1976	// Inject codec control script if codec preferences are configured
1977	if cfg != nil && (cfg.CodecPreferences.ForceAV1 || len(cfg.CodecPreferences.BlockedCodecs) > 0 || len(cfg.CodecPreferences.PreferredCodecs) > 0) {
1978		codecScript := GenerateCodecControlScript(cfg.CodecPreferences)
1979		cCodec := C.CString(codecScript)
1980		defer C.free(unsafe.Pointer(cCodec))
1981		codecUserScript := C.webkit_user_script_new(
1982			(*C.gchar)(cCodec),
1983			C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
1984			C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
1985			nil, nil)
1986		if codecUserScript != nil {
1987			C.webkit_user_content_manager_add_script(w.native.ucm, codecUserScript)
1988			C.webkit_user_script_unref(codecUserScript)
1989			log.Printf("[webkit] Codec control script injected")
1990		}
1991	}
1992
1993	// No JS fallback bridge — native UCM is active
1994}
1995
1996func clampDomZoom(level float64) float64 {
1997	if level < 0.25 {
1998		return 0.25
1999	}
2000	if level > 5.0 {
2001		return 5.0
2002	}
2003	return level
2004}
2005
2006func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w *WebView) string {
2007	if cfg == nil || cfg.Assets == nil {
2008		log.Printf("[webkit] Config or assets not available for main-world script")
2009		return ""
2010	}
2011
2012	// Load the compiled main-world script
2013	scriptBytes, err := cfg.Assets.ReadFile("assets/gui/main-world.min.js")
2014	if err != nil {
2015		log.Printf("[webkit] Failed to load main-world script: %v", err)
2016		return ""
2017	}
2018
2019	// Replace the zoom placeholder with the actual value
2020	zoom := clampDomZoom(initialZoom)
2021	zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64)
2022	script := strings.ReplaceAll(string(scriptBytes), "__DOM_ZOOM_DEFAULT__", zoomStr)
2023
2024	// Inject WebView ID and active state
2025	webViewIdStr := strconv.FormatUint(uint64(webViewId), 10)
2026	script = strings.ReplaceAll(script, "__WEBVIEW_ID__", webViewIdStr)
2027	// Default to active for the initial WebView, since it will be focused immediately
2028	// For single WebView scenarios, assume it should be active
2029	activeState := "true"
2030	log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState)
2031	script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)
2032
2033	log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
2034	return script
2035}
2036
2037func (w *WebView) UsesDomZoom() bool {
2038	return w != nil && w.useDomZoom
2039}
2040
2041func (w *WebView) installDomBridgeScript() {
2042	if w == nil || w.native == nil || w.native.ucm == nil {
2043		return
2044	}
2045
2046	w.domZoomSeed = clampDomZoom(w.domZoomSeed)
2047
2048	if w.domBridgeScript != nil {
2049		C.webkit_user_content_manager_remove_script(w.native.ucm, w.domBridgeScript)
2050		C.webkit_user_script_unref(w.domBridgeScript)
2051		w.domBridgeScript = nil
2052	}
2053
2054	source := buildDomBridgeScript(w.config, w.domZoomSeed, w.id, w)
2055	cBridge := C.CString(source)
2056	defer C.free(unsafe.Pointer(cBridge))
2057
2058	script := 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)
2059	if script == nil {
2060		log.Printf("[webkit] ERROR: Failed to create GUI user script")
2061		return
2062	}
2063
2064	C.webkit_user_content_manager_add_script(w.native.ucm, script)
2065	w.domBridgeScript = script
2066	log.Printf("[webkit] Page-world toast bridge injected (dom zoom seed=%.2f)", w.domZoomSeed)
2067}
2068
2069// SeedDomZoom updates the document-start bridge so the upcoming page paints with the saved DOM zoom level.
2070func (w *WebView) SeedDomZoom(level float64) {
2071	if w == nil || w.destroyed || !w.useDomZoom {
2072		return
2073	}
2074
2075	seed := clampDomZoom(level)
2076	if math.Abs(w.domZoomSeed-seed) < 1e-6 {
2077		return
2078	}
2079	w.domZoomSeed = seed
2080	// Instead of re-injecting the entire script, just update the zoom level
2081	w.RunOnMainThread(func() {
2082		zoomStr := strconv.FormatFloat(seed, 'f', -1, 64)
2083		script := fmt.Sprintf("if (window.__dumber_applyDomZoom) { window.__dumber_applyDomZoom(%s); }", zoomStr)
2084		w.InjectScript(script)
2085	})
2086}
2087
2088// startPeriodicGC starts a ticker to trigger JavaScript garbage collection periodically
2089func (w *WebView) startPeriodicGC(interval time.Duration) {
2090	if w == nil || w.gcTicker != nil {
2091		return // Already running or invalid
2092	}
2093
2094	w.gcTicker = time.NewTicker(interval)
2095
2096	go func() {
2097		defer func() {
2098			if w.gcTicker != nil {
2099				w.gcTicker.Stop()
2100				w.gcTicker = nil
2101			}
2102		}()
2103
2104		for {
2105			select {
2106			case <-w.gcTicker.C:
2107				w.triggerGC()
2108			case <-w.gcDone:
2109				return
2110			}
2111		}
2112	}()
2113}
2114
2115// stopPeriodicGC stops the periodic garbage collection
2116func (w *WebView) stopPeriodicGC() {
2117	if w == nil {
2118		return
2119	}
2120
2121	if w.gcDone != nil {
2122		close(w.gcDone)
2123		w.gcDone = nil
2124	}
2125
2126	if w.gcTicker != nil {
2127		w.gcTicker.Stop()
2128		w.gcTicker = nil
2129	}
2130}
2131
2132// triggerGC manually triggers JavaScript garbage collection
2133func (w *WebView) triggerGC() {
2134	if w == nil || w.memStats == nil {
2135		return
2136	}
2137
2138	// Get default WebContext and trigger GC
2139	ctx := C.webkit_web_context_get_default()
2140	if ctx != nil {
2141		C.maybe_collect_js(ctx)
2142		w.memStats.lastGCTime = time.Now()
2143
2144		if w.config.Memory.EnableMemoryMonitoring {
2145			log.Printf("[webkit] JavaScript GC triggered (page loads: %d)", w.memStats.pageLoadCount)
2146		}
2147	}
2148}
2149
2150// GetMemoryStats returns current memory statistics for this WebView
2151func (w *WebView) GetMemoryStats() map[string]interface{} {
2152	if w == nil || w.memStats == nil {
2153		return nil
2154	}
2155
2156	return map[string]interface{}{
2157		"page_load_count":              w.memStats.pageLoadCount,
2158		"last_gc_time":                 w.memStats.lastGCTime,
2159		"has_memory_pressure_settings": w.memStats.memoryPressureSettings != nil,
2160	}
2161}
2162
2163// TriggerMemoryCleanup manually triggers garbage collection and memory cleanup
2164func (w *WebView) TriggerMemoryCleanup() {
2165	if w == nil {
2166		return
2167	}
2168
2169	w.triggerGC()
2170
2171	if w.config.Memory.EnableMemoryMonitoring {
2172		log.Printf("[webkit] Manual memory cleanup triggered")
2173	}
2174}
2175
2176//export goHandleTLSError
2177func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
2178	uri := C.GoString(failingURI)
2179	hostname := C.GoString(host)
2180	certificateInfo := C.GoString(certInfo)
2181
2182	log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
2183
2184	// Find the WebView that triggered this error
2185	// For now, we'll get the most recently created view
2186	// In a more sophisticated implementation, we'd pass the view ID through user_data
2187	var webView *WebView
2188	regMu.RLock()
2189	for _, v := range viewByID {
2190		if v != nil {
2191			webView = v
2192			break
2193		}
2194	}
2195	regMu.RUnlock()
2196
2197	if webView == nil {
2198		log.Printf("[tls] No WebView found to handle TLS error")
2199		return C.FALSE // Don't proceed
2200	}
2201
2202	// Check if we've already allowed this host
2203	if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
2204		log.Printf("[tls] Certificate exception already granted for %s", hostname)
2205		return C.TRUE // Proceed with the load
2206	}
2207
2208	// Show warning dialog and get user decision
2209	if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
2210		webView.tlsExceptions[hostname] = true
2211		log.Printf("[tls] User accepted certificate exception for %s", hostname)
2212		return C.TRUE // Proceed with the load
2213	}
2214
2215	log.Printf("[tls] User rejected certificate for %s", hostname)
2216	return C.FALSE // Don't proceed
2217}
2218
2219//export goHandleConsoleMessage
2220//export goHandleConsoleMessage
2221func goHandleConsoleMessage(message *C.char) {
2222	messageText := C.GoString(message)
2223	logging.CaptureWebKitLog(messageText)
2224}
2225
2226// showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
2227func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
2228	if w == nil || w.native == nil || w.native.win == nil {
2229		log.Printf("[tls] Cannot show dialog: WebView or window is nil")
2230		return false
2231	}
2232
2233	// Check if we have a stored decision for this certificate
2234	if storedDecision, hasStored := checkStoredCertificateDecision(hostname, certificateInfo); hasStored {
2235		switch storedDecision {
2236		case "accepted":
2237			log.Printf("[tls] Using stored ACCEPT decision for %s", hostname)
2238			return true
2239		case "rejected":
2240			log.Printf("[tls] Using stored REJECT decision for %s", hostname)
2241			return false
2242		default:
2243			log.Printf("[tls] Unknown stored decision '%s' for %s, showing dialog", storedDecision, hostname)
2244		}
2245	}
2246
2247	log.Printf("[tls] Showing certificate error dialog for %s", hostname)
2248
2249	// Format the error message with certificate information
2250	errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
2251
2252	// Convert strings to C strings
2253	cHostname := C.CString(hostname)
2254	defer C.free(unsafe.Pointer(cHostname))
2255
2256	cErrorMsg := C.CString(errorMsg)
2257	defer C.free(unsafe.Pointer(cErrorMsg))
2258
2259	// Show the dialog and get user response (0=Go Back, 1=Proceed Once, 2=Always Accept)
2260	result := C.show_tls_warning_dialog_sync(
2261		(*C.GtkWindow)(unsafe.Pointer(w.native.win)),
2262		cHostname,
2263		cErrorMsg,
2264	)
2265
2266	responseCode := int(result)
2267
2268	switch responseCode {
2269	case 0: // Go Back
2270		log.Printf("[tls] User chose to GO BACK for %s", hostname)
2271		return false
2272
2273	case 1: // Proceed Once
2274		log.Printf("[tls] User chose to PROCEED ONCE for %s", hostname)
2275		// Store temporary decision (expires in 24 hours)
2276		if err := storeCertificateDecision(hostname, certificateInfo, "accepted", false); err != nil {
2277			log.Printf("[tls] Warning: Failed to store temporary certificate decision: %v", err)
2278		}
2279		return true
2280
2281	case 2: // Always Accept
2282		log.Printf("[tls] User chose to ALWAYS ACCEPT for %s", hostname)
2283		// Store permanent decision
2284		if err := storeCertificateDecision(hostname, certificateInfo, "accepted", true); err != nil {
2285			log.Printf("[tls] Warning: Failed to store permanent certificate decision: %v", err)
2286		}
2287		return true
2288
2289	default:
2290		log.Printf("[tls] Unknown response code %d for %s, defaulting to reject", responseCode, hostname)
2291		return false
2292	}
2293}
2294
2295// formatTLSErrorMessage creates a detailed error message based on the error flags
2296func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
2297	msg := "Website: " + hostname + "\n\n"
2298
2299	// Add certificate information
2300	if certificateInfo != "" {
2301		msg += "Certificate Details:\n" + certificateInfo + "\n"
2302	}
2303
2304	msg += "Security Issues:\n"
2305	// Decode GTlsCertificateFlags (common values)
2306	if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
2307		msg += "• Certificate authority is not trusted\n"
2308	}
2309	if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
2310		msg += "• Certificate does not match the website identity\n"
2311	}
2312	if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
2313		msg += "• Certificate is not yet valid\n"
2314	}
2315	if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
2316		msg += "• Certificate has expired\n"
2317	}
2318	if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
2319		msg += "• Certificate has been revoked\n"
2320	}
2321	if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
2322		msg += "• Certificate uses weak cryptography\n"
2323	}
2324
2325	msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
2326
2327	return msg
2328}
2329
2330// InitializeContentBlocking initializes WebKit content blocking with filter manager
2331func (w *WebView) InitializeContentBlocking(filterManager FilterManager) error {
2332	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
2333		return ErrWebViewNotInitialized
2334	}
2335
2336	// Load filters asynchronously with proper error recovery
2337	go func() {
2338		defer func() {
2339			if r := recover(); r != nil {
2340				logging.Error(fmt.Sprintf("[webkit] Content blocking initialization panic: %v", r))
2341			}
2342		}()
2343
2344		// Wait longer for WebView to be ready before applying filters on first load
2345		// This prevents interference with WebKit's preconnect operations
2346		time.Sleep(1500 * time.Millisecond)
2347
2348		// Retry mechanism with exponential backoff
2349		maxRetries := 5
2350		baseDelay := 200 * time.Millisecond
2351
2352		// Apply network filters with error recovery
2353		for i := 0; i < maxRetries; i++ {
2354			if w.destroyed {
2355				logging.Debug("[webkit] WebView destroyed, aborting filter loading")
2356				return
2357			}
2358
2359			if filters, err := filterManager.GetNetworkFilters(); err == nil {
2360				if len(filters) > 10 && string(filters) != "null" {
2361					logging.Info("[webkit] Loading network filters from filter manager (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
2362					if err := w.ApplyContentFilters(filters, "network-filters"); err != nil {
2363						logging.Error("Failed to apply network filters (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
2364						if i == maxRetries-1 {
2365							logging.Warn("[webkit] Network filtering disabled due to errors")
2366							break
2367						}
2368						time.Sleep(baseDelay * time.Duration(1<<uint(i))) // Exponential backoff
2369						continue
2370					}
2371					break
2372				}
2373			} else if i == maxRetries-1 {
2374				logging.Error("Failed to get network filters after retries: " + err.Error())
2375			}
2376		}
2377
2378		// Apply cosmetic filtering with error recovery
2379		time.Sleep(200 * time.Millisecond) // Additional delay to ensure network setup is complete
2380
2381		for i := 0; i < maxRetries; i++ {
2382			if w.destroyed {
2383				logging.Debug("[webkit] WebView destroyed, aborting cosmetic filter loading")
2384				return
2385			}
2386
2387			script := filterManager.GetCosmeticScript()
2388			if len(script) > 0 {
2389				logging.Info("[webkit] Loading cosmetic script from filter manager (" + fmt.Sprintf("%d", len(script)) + " chars)")
2390				if err := w.InjectCosmeticFilter(script); err != nil {
2391					logging.Error("Failed to inject cosmetic filter (retry " + fmt.Sprintf("%d", i+1) + "): " + err.Error())
2392					if i == maxRetries-1 {
2393						logging.Warn("[webkit] Cosmetic filtering disabled due to errors")
2394					} else {
2395						time.Sleep(baseDelay * time.Duration(1<<uint(i)))
2396						continue
2397					}
2398				}
2399				break
2400			}
2401		}
2402
2403		logging.Info("Content blocking initialization completed")
2404	}()
2405
2406	return nil
2407}
2408
2409// OnNavigate sets up domain-specific cosmetic filtering on navigation
2410func (w *WebView) OnNavigate(url string, filterManager FilterManager) {
2411	if w == nil || w.destroyed {
2412		return
2413	}
2414
2415	domain := extractDomain(url)
2416	if domain == "" {
2417		return
2418	}
2419
2420	// Get domain-specific cosmetic rules
2421	script := filterManager.GetCosmeticScriptForDomain(domain)
2422	if script != "" {
2423		// Inject domain-specific cosmetic rules
2424		logging.Info("[webkit] Applying domain-specific cosmetic rules for: " + domain + " (" + fmt.Sprintf("%d", len(script)) + " chars)")
2425		w.InjectScript(fmt.Sprintf(
2426			"if (typeof window.__dumber_cosmetic_init === 'function') { %s }",
2427			script,
2428		))
2429	} else {
2430		logging.Debug("[webkit] No domain-specific cosmetic rules found for: " + domain)
2431	}
2432}
2433
2434// UpdateContentFilters updates the content filters dynamically
2435func (w *WebView) UpdateContentFilters(filterManager FilterManager) error {
2436	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
2437		return ErrWebViewNotInitialized
2438	}
2439
2440	// Clear existing filters first
2441	if err := w.ClearAllFilters(); err != nil {
2442		return fmt.Errorf("failed to clear existing filters: %w", err)
2443	}
2444
2445	// Apply new filters
2446	if filters, err := filterManager.GetNetworkFilters(); err == nil {
2447		logging.Info("[webkit] Updating network filters (" + fmt.Sprintf("%d", len(filters)) + " bytes)")
2448		if err := w.ApplyContentFilters(filters, "network-filters-updated"); err != nil {
2449			return fmt.Errorf("failed to apply updated network filters: %w", err)
2450		}
2451	}
2452
2453	// Update cosmetic filtering
2454	script := filterManager.GetCosmeticScript()
2455	logging.Info("[webkit] Updating cosmetic script (" + fmt.Sprintf("%d", len(script)) + " chars)")
2456	if err := w.InjectCosmeticFilter(script); err != nil {
2457		return fmt.Errorf("failed to inject updated cosmetic filter: %w", err)
2458	}
2459
2460	logging.Info("Content filters updated successfully")
2461	return nil
2462}
2463
2464// extractDomain extracts the domain from a URL
2465func extractDomain(url string) string {
2466	if url == "" {
2467		return ""
2468	}
2469
2470	// Simple domain extraction - remove protocol and path
2471	start := strings.Index(url, "://")
2472	if start != -1 {
2473		url = url[start+3:]
2474	}
2475
2476	end := strings.Index(url, "/")
2477	if end != -1 {
2478		url = url[:end]
2479	}
2480
2481	// Remove port if present
2482	if portIdx := strings.Index(url, ":"); portIdx != -1 {
2483		url = url[:portIdx]
2484	}
2485
2486	return url
2487}
2488
2489// detectAndLogRenderingBackend detects and logs the actual rendering backend being used
2490func (w *WebView) detectAndLogRenderingBackend() {
2491	if w == nil {
2492		return
2493	}
2494
2495	// Use a small delay to let WebKit initialize
2496	go func() {
2497		time.Sleep(500 * time.Millisecond)
2498
2499		// Inject a script to detect rendering capabilities
2500		detectionScript := `
2501			(() => {
2502				try {
2503					const canvas = document.createElement('canvas');
2504					const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
2505					const info = {
2506						webgl_available: !!gl,
2507						renderer: gl ? gl.getParameter(gl.RENDERER) : 'N/A',
2508						vendor: gl ? gl.getParameter(gl.VENDOR) : 'N/A',
2509						version: gl ? gl.getParameter(gl.VERSION) : 'N/A',
2510						shading_language_version: gl ? gl.getParameter(gl.SHADING_LANGUAGE_VERSION) : 'N/A',
2511						max_texture_size: gl ? gl.getParameter(gl.MAX_TEXTURE_SIZE) : 0,
2512						hardware_accelerated: false
2513					};
2514					
2515					// Check if renderer indicates hardware acceleration
2516					if (gl && info.renderer) {
2517						const renderer = info.renderer.toLowerCase();
2518						// Look for GPU indicators
2519						info.hardware_accelerated = (
2520							renderer.includes('nvidia') ||
2521							renderer.includes('amd') ||
2522							renderer.includes('intel') ||
2523							renderer.includes('radeon') ||
2524							renderer.includes('geforce') ||
2525							renderer.includes('quadro') ||
2526							(renderer.includes('mesa') && !renderer.includes('software')) ||
2527							!renderer.includes('software')
2528						);
2529					}
2530					
2531					window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({
2532						type: 'rendering_backend_detection',
2533						data: info
2534					}));
2535				} catch (e) {
2536					console.error('[dumber] Rendering backend detection failed:', e);
2537				}
2538			})();
2539		`
2540
2541		w.InjectScript(detectionScript)
2542	}()
2543}
2544
2545// SetWindowFeatures sets the window features for this WebView
2546func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
2547	w.windowFeatures = features
2548}
2549
2550// SetActive sets whether this WebView is currently active/focused
2551func (w *WebView) SetActive(active bool) {
2552	if w == nil || w.destroyed {
2553		return
2554	}
2555
2556	if w.isActive != active {
2557		w.isActive = active
2558		log.Printf("[webkit] WebView %d active state changed to: %v", w.id, active)
2559
2560		// Update the active state in the JavaScript context
2561		activeStr := "false"
2562		if active {
2563			activeStr = "true"
2564		}
2565		script := fmt.Sprintf("window.__dumber_is_active = %s;", activeStr)
2566		w.InjectScript(script)
2567	}
2568}
2569
2570// IsActive returns whether this WebView is currently active/focused
2571func (w *WebView) IsActive() bool {
2572	return w != nil && w.isActive
2573}
2574
2575// ID returns the unique identifier for this WebView as a string
2576func (w *WebView) ID() string {
2577	if w == nil {
2578		return ""
2579	}
2580	return strconv.FormatUint(uint64(w.id), 10)
2581}

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