v0.11.0 dc50343

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

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