v0.10.0 0f4e9eb

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

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