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", ¬_valid_before,
716 "not-valid-after", ¬_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}