v0.5.0 f906adb

webview_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
7#include <stdlib.h>
8#include <string.h>
9#include <gtk/gtk.h>
10#include <webkit/webkit.h>
11#include <glib-object.h>
12#include <gdk/gdk.h>
13#include <jsc/jsc.h>
14#include <glib.h>
15#include <gio/gio.h>
16#include "webview_callbacks.h"
17
18static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
19
20// Forward declaration
21extern void goQuitMainLoop();
22
23// Connect window close signal to quit main loop
24static void connect_destroy_quit(GtkWidget* w) {
25    if (!w) return;
26    g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL);
27}
28static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
29// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
30
31// Forward declare helpers used below
32static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
33
34// TLS error handling forward declarations
35extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
36static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
37                                               const char *failing_uri,
38                                               GTlsCertificate *certificate,
39                                               GTlsCertificateFlags errors,
40                                               gpointer user_data);
41static void connect_tls_error_handler(WebKitWebView* wv);
42
43// Dialog callback structure for TLS warnings
44typedef struct {
45    gboolean user_accepted;
46    gboolean dialog_completed;
47} TLSDialogData;
48
49static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data);
50static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
51static char* extract_certificate_info(GTlsCertificate* certificate);
52
53// Setup video acceleration environment variables
54static void setup_video_acceleration(const char* driver_name, int enable_all, int legacy) {
55    if (legacy) {
56        // Legacy mode: force old VA-API plugins, disable modern ones
57        setenv("WEBKIT_GST_ENABLE_LEGACY_VAAPI", "1", 1);
58        // Don't set other env vars in legacy mode to avoid conflicts
59        return;
60    }
61    
62    // Modern mode (default): use improved VA-API handling
63    if (driver_name && strlen(driver_name) > 0) {
64        if (strcmp(driver_name, "nvidia") == 0) {
65            // NVIDIA uses different env vars for hardware video acceleration
66            setenv("VDPAU_DRIVER", "nvidia", 1);
67            setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
68        } else if (strcmp(driver_name, "vdpau") == 0) {
69            // Explicit VDPAU backend (for NVIDIA via VA-API)
70            setenv("LIBVA_DRIVER_NAME", "vdpau", 1);
71        } else {
72            // AMD/Intel use VA-API directly
73            setenv("LIBVA_DRIVER_NAME", driver_name, 1);
74        }
75    }
76    
77    if (enable_all) {
78        setenv("GST_VAAPI_ALL_DRIVERS", "1", 1);
79    }
80}
81
82// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
83static 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) {
84    WebKitUserContentManager* u = webkit_user_content_manager_new();
85    if (!u) return NULL;
86    // WebKitGTK 6: create NetworkSession with data/cache directories
87    WebKitNetworkSession* sess = webkit_network_session_new(
88        data_dir ? data_dir : "",
89        cache_dir ? cache_dir : "");
90    if (!sess) { g_object_unref(u); return NULL; }
91    // Apply memory pressure settings to the session when provided
92    if (pressure_settings) {
93        // WebKitGTK 6.0 API: setter expects only the settings pointer
94        webkit_network_session_set_memory_pressure_settings(pressure_settings);
95    }
96    // Configure cookie persistence and policy (no third-party by default)
97    WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
98    if (cm) {
99        if (cookie_path && cookie_path[0]) {
100            webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
101        }
102        // 2 => no-third-party (default), 0 => always, 1 => never
103        maybe_set_cookie_policy(cm, 2);
104    }
105    // Persist credentials (HTTP auth, etc.)
106    webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
107    // Configure TLS errors to emit signals instead of failing silently
108    webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
109    GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
110        "user-content-manager", u,
111        "network-session", sess,
112        NULL));
113    // WebView holds refs to provided objects; drop our temporary refs
114    g_object_unref(sess);
115    // Set WebView background to black (easier on eyes when pages are loading)
116    GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
117    webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
118    
119    // Connect TLS error handler
120    connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
121    if (out_ucm) { *out_ucm = u; }
122    return w;
123}
124
125static gboolean gtk_prefers_dark() {
126    // Method 1: Check GNOME desktop interface color-scheme (primary method)
127    GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
128    if (desktop_settings) {
129        gchar* color_scheme = g_settings_get_string(desktop_settings, "color-scheme");
130        if (color_scheme) {
131            gboolean prefer_dark = (g_strcmp0(color_scheme, "prefer-dark") == 0);
132            g_free(color_scheme);
133            g_object_unref(desktop_settings);
134            if (prefer_dark) return TRUE;
135        }
136        g_object_unref(desktop_settings);
137    }
138
139    // Method 2: Check GTK theme name for dark variants
140    GtkSettings* settings = gtk_settings_get_default();
141    if (settings) {
142        gchar* theme_name = NULL;
143        g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
144        if (theme_name) {
145            gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
146                               strstr(theme_name, "-Dark") != NULL);
147            g_free(theme_name);
148            if (is_dark) return TRUE;
149        }
150
151        // Method 3: Check gtk-application-prefer-dark-theme (fallback)
152        gboolean prefer = FALSE;
153        g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
154        if (prefer) return TRUE;
155    }
156
157    return FALSE;
158}
159
160// Note: preferred color scheme is handled via a user script injection to support
161// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
162
163extern void goOnUcmMessage(unsigned long id, char* json);
164
165// Helper functions to connect signals using static callbacks
166static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
167    if (!widget) return;
168    g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
169}
170
171static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
172    if (!widget) return;
173    g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
174}
175
176static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
177    if (!widget) return;
178    g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
179}
180
181static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
182    if (!settings) return;
183    g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
184}
185
186static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
187    if (!context || !scheme) return;
188    webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
189}
190
191// Script message handler wiring for WebKitGTK 6
192// Forward declaration for callback used in g_signal_connect_data
193// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
194static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
195// Uses WebKitScriptMessage and script-message-received::NAME signal.
196static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
197    (void)ucm;
198    if (!val) return;
199    gchar* s = jsc_value_to_string(val);
200    if (!s) return;
201    goOnUcmMessage((unsigned long)user_data, s);
202    g_free(s);
203}
204
205static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
206    if (!ucm || !name) return FALSE;
207    // WebKitGTK 6 signature: (ucm, name, world_name)
208    webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
209    gchar* signal = g_strdup_printf("script-message-received::%s", name);
210    if (!signal) return FALSE;
211    g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
212    g_free(signal);
213    return TRUE;
214}
215
216// ----- Conditional helpers for settings across WebKit versions -----
217#ifndef WEBKIT_CHECK_VERSION
218#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
219#endif
220
221static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
222#if WEBKIT_CHECK_VERSION(2,40,0)
223    // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
224    if (!settings) return;
225    if (mode == 1) {
226        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
227    } else if (mode == 2) {
228        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
229    } else {
230        // Leave default policy (auto) without forcing a value to avoid referencing
231        // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
232    }
233#else
234    (void)settings; (void)mode;
235#endif
236}
237
238static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
239#if WEBKIT_CHECK_VERSION(2,36,0)
240    if (!settings) return;
241    webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
242#else
243    (void)settings; (void)enable;
244#endif
245}
246
247static void maybe_set_webgl(WebKitSettings* settings, int enable) {
248#if WEBKIT_CHECK_VERSION(2,20,0)
249    if (!settings) return;
250    webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
251#else
252    (void)settings; (void)enable;
253#endif
254}
255
256static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
257#if WEBKIT_CHECK_VERSION(2,20,0)
258    if (!settings) return;
259    webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
260#else
261    (void)settings; (void)enable;
262#endif
263}
264
265static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
266#if WEBKIT_CHECK_VERSION(2,20,0)
267    if (!settings) return;
268    webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
269#else
270    (void)settings; (void)require;
271#endif
272}
273
274// Enable two-finger swipe back/forward navigation gestures when supported
275static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
276#if WEBKIT_CHECK_VERSION(2,38,0)
277    if (!settings) return;
278    webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
279#else
280    (void)settings; (void)enable;
281#endif
282}
283
284// Set cookie accept policy with compile-time compatibility across WebKit versions
285static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
286    if (!cm) return;
287#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
288    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
289    if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
290    else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
291    webkit_cookie_manager_set_accept_policy(cm, p);
292#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
293    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
294    if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
295    else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
296    webkit_cookie_manager_set_accept_policy(cm, p);
297#else
298    (void)policy; // Not supported in this header version
299#endif
300}
301
302// Memory pressure settings helpers (version-guarded)
303static WebKitMemoryPressureSettings* create_memory_pressure_settings(
304    unsigned int memory_limit_mb,
305    double conservative_threshold,
306    double strict_threshold,
307    double kill_threshold,
308    double poll_interval) {
309    // If headers don't expose memory pressure APIs, return NULL to signal unsupported
310#if WEBKIT_CHECK_VERSION(2,44,0)
311    WebKitMemoryPressureSettings* settings = webkit_memory_pressure_settings_new();
312    if (!settings) return NULL;
313
314    if (memory_limit_mb > 0) {
315        webkit_memory_pressure_settings_set_memory_limit(settings, (guint)(memory_limit_mb * 1024 * 1024));
316    }
317
318    webkit_memory_pressure_settings_set_conservative_threshold(settings, conservative_threshold);
319    webkit_memory_pressure_settings_set_strict_threshold(settings, strict_threshold);
320
321    if (kill_threshold > 0) {
322        webkit_memory_pressure_settings_set_kill_threshold(settings, kill_threshold);
323    }
324
325    webkit_memory_pressure_settings_set_poll_interval(settings, poll_interval);
326
327    return settings;
328#else
329    (void)memory_limit_mb; (void)conservative_threshold; (void)strict_threshold; (void)kill_threshold; (void)poll_interval;
330    return NULL;
331#endif
332}
333
334// Cache model configuration
335static void maybe_set_cache_model(WebKitWebContext* context, int model) {
336    if (!context) return;
337    WebKitCacheModel cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; // default
338
339    switch (model) {
340        case 0: cache_model = WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER; break;
341        case 1: cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; break;
342        case 2:
343#ifdef WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER
344            cache_model = WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER;
345#else
346            cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER;
347#endif
348            break;
349    }
350
351    webkit_web_context_set_cache_model(context, cache_model);
352}
353
354// Additional settings for memory optimization
355static void maybe_set_page_cache(WebKitSettings* settings, int enable) {
356#if WEBKIT_CHECK_VERSION(2,6,0)
357    if (!settings) return;
358    webkit_settings_set_enable_page_cache(settings, enable ? TRUE : FALSE);
359#else
360    (void)settings; (void)enable;
361#endif
362}
363
364// Offline Web Application Cache is deprecated in modern WebKit; keep as no-op to avoid warnings.
365static void maybe_set_offline_app_cache(WebKitSettings* settings, int enable) {
366    (void)settings; (void)enable;
367}
368
369// Apply memory pressure settings to context if available
370static void maybe_set_memory_pressure_settings(WebKitWebContext* context, WebKitMemoryPressureSettings* settings) {
371    // No-op wrapper kept for compatibility; settings applied to NetworkSession instead.
372    (void)context; (void)settings;
373}
374
375// Trigger JavaScript GC if supported by headers
376static void maybe_collect_js(WebKitWebContext* context) {
377    // Disabled by default due to API availability variance across 6.0 builds.
378    (void)context;
379}
380
381// TLS error handling implementation
382static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
383                                               const char *failing_uri,
384                                               GTlsCertificate *certificate,
385                                               GTlsCertificateFlags errors,
386                                               gpointer user_data) {
387    (void)user_data;
388
389    // Extract host from URI for the Go callback
390    const char* host = failing_uri;
391    char* host_copy = NULL;
392    
393    if (strstr(failing_uri, "://")) {
394        host = strstr(failing_uri, "://") + 3;
395        char* slash = strchr(host, '/');
396        if (slash) {
397            // Create a temporary null-terminated host string
398            size_t host_len = slash - host;
399            host_copy = malloc(host_len + 1);
400            if (host_copy) {
401                strncpy(host_copy, host, host_len);
402                host_copy[host_len] = '\0';
403                host = host_copy;
404            }
405        }
406    }
407
408    // Extract certificate information
409    char* cert_info = extract_certificate_info(certificate);
410    
411    gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
412
413    // If user accepted, allow the certificate for this host
414    if (should_proceed && certificate) {
415        printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
416        fflush(stdout);
417        // Get the network session from the web view
418        WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
419        if (session) {
420            webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
421            printf("[dumber] Certificate exception added for host: %s\n", host);
422            fflush(stdout);
423            
424            // Try loading the URL again after adding the exception
425            webkit_web_view_load_uri(web_view, failing_uri);
426            printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
427            fflush(stdout);
428        }
429    }
430    
431    // Free the certificate info string
432    if (cert_info) {
433        g_free(cert_info);
434    }
435
436    if (host_copy) {
437        free(host_copy);
438    }
439
440    return should_proceed;
441}
442
443// Extract certificate information for display
444static char* extract_certificate_info(GTlsCertificate* certificate) {
445    if (!certificate) {
446        return g_strdup("Certificate information not available");
447    }
448    
449    printf("[dumber] Extracting certificate information...\n");
450    fflush(stdout);
451    
452    GString* info = g_string_new("Certificate Information:\n");
453    
454    // Try to get certificate properties - but these might not be available in all GTK versions
455    gchar* subject = NULL;
456    gchar* issuer = NULL;
457    GDateTime* not_valid_before = NULL;
458    GDateTime* not_valid_after = NULL;
459    
460    // Try property access but handle failures gracefully
461    g_object_get(certificate,
462                 "subject-name", &subject,
463                 "issuer-name", &issuer,
464                 "not-valid-before", &not_valid_before,
465                 "not-valid-after", &not_valid_after,
466                 NULL);
467    
468    if (subject) {
469        g_string_append_printf(info, "Subject: %s\n", subject);
470        g_free(subject);
471        printf("[dumber] Found subject info\n");
472    } else {
473        g_string_append(info, "Subject: Information not available\n");
474    }
475    
476    if (issuer) {
477        g_string_append_printf(info, "Issued by: %s\n", issuer);
478        g_free(issuer);
479        printf("[dumber] Found issuer info\n");
480    } else {
481        g_string_append(info, "Issued by: Information not available\n");
482    }
483    
484    if (not_valid_before) {
485        gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
486        g_string_append_printf(info, "Valid from: %s\n", date_str);
487        g_free(date_str);
488        g_date_time_unref(not_valid_before);
489    } else {
490        g_string_append(info, "Valid from: Information not available\n");
491    }
492    
493    if (not_valid_after) {
494        gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
495        g_string_append_printf(info, "Valid until: %s\n", date_str);
496        g_free(date_str);
497        g_date_time_unref(not_valid_after);
498    } else {
499        g_string_append(info, "Valid until: Information not available\n");
500    }
501    
502    char* result = g_string_free(info, FALSE);
503    printf("[dumber] Certificate info: %s\n", result);
504    fflush(stdout);
505    return result;
506}
507
508static void connect_tls_error_handler(WebKitWebView* wv) {
509    if (!wv) return;
510    g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
511                     G_CALLBACK(on_load_failed_with_tls_errors), NULL);
512}
513
514// TLS dialog response callback
515static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data) {
516    TLSDialogData *data = (TLSDialogData*)user_data;
517    data->user_accepted = (response_id == GTK_RESPONSE_ACCEPT);
518    data->dialog_completed = TRUE;
519    printf("[dumber] TLS dialog response: %s (response_id=%d)\n", 
520           data->user_accepted ? "ACCEPTED" : "REJECTED", response_id);
521    fflush(stdout);
522    gtk_window_destroy(GTK_WINDOW(dialog));
523}
524
525// Show TLS warning dialog synchronously using GTK4 patterns
526static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
527    printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
528    printf("[dumber] Error message: %s\n", error_msg);
529    fflush(stdout);
530    
531    // Create the dialog
532    GtkWidget *dialog = gtk_message_dialog_new(
533        parent,
534        GTK_DIALOG_MODAL,
535        GTK_MESSAGE_WARNING,
536        GTK_BUTTONS_NONE,
537        "Certificate Error for %s", hostname
538    );
539
540    // Set secondary text with error details
541    gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_msg);
542
543    // Add buttons
544    gtk_dialog_add_button(GTK_DIALOG(dialog), "Go Back", GTK_RESPONSE_CANCEL);
545    GtkWidget *proceed_btn = gtk_dialog_add_button(GTK_DIALOG(dialog), "Proceed Anyway (Unsafe)", GTK_RESPONSE_ACCEPT);
546
547    // Style the proceed button as destructive (red)
548    gtk_widget_add_css_class(proceed_btn, "destructive-action");
549
550    // Set default response to cancel (safer option)
551    gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
552
553    // Set up dialog data
554    TLSDialogData dialog_data = { FALSE, FALSE };
555
556    // Connect response signal
557    g_signal_connect(dialog, "response", G_CALLBACK(on_tls_dialog_response), &dialog_data);
558
559    // Show the dialog
560    gtk_window_present(GTK_WINDOW(dialog));
561
562    // Run a nested main loop until dialog is completed
563    while (!dialog_data.dialog_completed) {
564        g_main_context_iteration(NULL, TRUE);
565    }
566
567    return dialog_data.user_accepted;
568}
569*/
570import "C"
571
572import (
573	"errors"
574	"log"
575	"os"
576	"path/filepath"
577	"runtime"
578	"time"
579	"unsafe"
580)
581
582// Helper function to convert bool to int for C interop
583func boolToInt(b bool) int {
584	if b {
585		return 1
586	}
587	return 0
588}
589
590type nativeView struct {
591	win  *C.GtkWidget
592	view *C.GtkWidget
593	wv   *C.WebKitWebView
594	ucm  *C.WebKitUserContentManager
595}
596
597type memoryStats struct {
598	pageLoadCount          int
599	lastGCTime             time.Time
600	memoryPressureSettings *C.WebKitMemoryPressureSettings
601}
602
603// WebView represents a browser view powered by WebKit2GTK.
604type WebView struct {
605	config        *Config
606	zoom          float64
607	url           string
608	destroyed     bool
609	native        *nativeView
610	window        *Window
611	id            uintptr
612	msgHandler    func(payload string)
613	titleHandler  func(title string)
614	uriHandler    func(uri string)
615	zoomHandler   func(level float64)
616	memStats      *memoryStats
617	gcTicker      *time.Ticker
618	gcDone        chan struct{}
619	tlsExceptions map[string]bool // host -> whether user allowed certificate exception
620}
621
622// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
623func NewWebView(cfg *Config) (*WebView, error) {
624	log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
625	if C.gtk_init_check() == 0 {
626		return nil, errors.New("failed to initialize GTK")
627	}
628
629	if cfg == nil {
630		cfg = &Config{}
631	}
632
633	// Setup video acceleration environment variables before creating WebView
634	if cfg.VideoAcceleration.EnableVAAPI {
635		driverName := C.CString(cfg.VideoAcceleration.VAAPIDriverName)
636		enableAll := 0
637		if cfg.VideoAcceleration.EnableAllDrivers {
638			enableAll = 1
639		}
640		legacy := 0
641		if cfg.VideoAcceleration.LegacyVAAPI {
642			legacy = 1
643		}
644		C.setup_video_acceleration(driverName, C.int(enableAll), C.int(legacy))
645		C.free(unsafe.Pointer(driverName))
646	}
647
648	// Prepare persistent website data/caches
649	dataDir := cfg.DataDir
650	cacheDir := cfg.CacheDir
651	if dataDir == "" {
652		dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data")
653	}
654	if cacheDir == "" {
655		cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache")
656	}
657	_ = os.MkdirAll(dataDir, 0o755)
658	_ = os.MkdirAll(cacheDir, 0o755)
659
660	cData := C.CString(dataDir)
661	cCache := C.CString(cacheDir)
662	cookieFile := filepath.Join(dataDir, "cookies.db")
663	cCookie := C.CString(cookieFile)
664	defer C.free(unsafe.Pointer(cData))
665	defer C.free(unsafe.Pointer(cCache))
666	defer C.free(unsafe.Pointer(cCookie))
667	// Use default WebContext to register the custom URI scheme handler; the
668	// WebView itself is created with a WebKitNetworkSession for persistence.
669	ctx := C.webkit_web_context_get_default()
670
671	// Apply cache model to WebContext
672	// Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
673	cacheModel := 1
674	if cfg.Memory.CacheModel != "" {
675		switch cfg.Memory.CacheModel {
676		case CacheModelWebBrowser:
677			cacheModel = 1
678		case CacheModelPrimaryWebBrowser:
679			cacheModel = 2
680		default: // CacheModelDocumentViewer or unset
681			cacheModel = 0
682		}
683	}
684	C.maybe_set_cache_model(ctx, C.int(cacheModel))
685
686	// Configure memory pressure settings if specified
687	var pressureSettings *C.WebKitMemoryPressureSettings
688	// Only configure memory pressure if explicitly requested via config
689	if cfg.Memory.MemoryLimitMB > 0 || cfg.Memory.ConservativeThreshold > 0 || cfg.Memory.StrictThreshold > 0 || cfg.Memory.KillThreshold > 0 || cfg.Memory.PollIntervalSeconds > 0 {
690		memLimitMB := cfg.Memory.MemoryLimitMB
691		conservativeThreshold := cfg.Memory.ConservativeThreshold
692		strictThreshold := cfg.Memory.StrictThreshold
693		killThreshold := cfg.Memory.KillThreshold
694		pollInterval := cfg.Memory.PollIntervalSeconds
695
696		pressureSettings = C.create_memory_pressure_settings(
697			C.uint(memLimitMB),
698			C.double(conservativeThreshold),
699			C.double(strictThreshold),
700			C.double(killThreshold),
701			C.double(pollInterval))
702
703		if pressureSettings != nil {
704			C.maybe_set_memory_pressure_settings(ctx, pressureSettings)
705			if cfg.Memory.EnableMemoryMonitoring {
706				log.Printf("[webkit] Memory pressure settings applied: limit=%dMB, conservative=%.2f, strict=%.2f, poll=%.1fs",
707					memLimitMB, conservativeThreshold, strictThreshold, pollInterval)
708			}
709		}
710	}
711
712	// Register custom URI scheme handler for "dumb://"
713	{
714		sch := C.CString("dumb")
715		C.register_uri_scheme_handler(ctx, sch)
716		C.free(unsafe.Pointer(sch))
717	}
718	// Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
719
720	// Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
721	var createdUcm *C.WebKitUserContentManager
722	viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm, pressureSettings)
723	if viewWidget == nil {
724		return nil, errors.New("failed to create WebKitWebView")
725	}
726
727	// Create a top-level window to host the view
728	win := C.new_window()
729	if win == nil {
730		return nil, errors.New("failed to create GtkWindow")
731	}
732
733	// Pack view widget into the window
734	// GTK4: containers removed; use gtk_window_set_child
735	C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
736	C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
737	C.connect_destroy_quit(win)
738
739	v := &WebView{
740		config: cfg,
741		zoom:   1.0,
742		native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
743		window: &Window{win: win},
744		memStats: &memoryStats{
745			pageLoadCount:          0,
746			lastGCTime:             time.Now(),
747			memoryPressureSettings: pressureSettings,
748		},
749		gcDone:        make(chan struct{}),
750		tlsExceptions: make(map[string]bool),
751	}
752	// Assign an ID for accelerator dispatch
753	v.id = nextViewID()
754	registerView(v.id, v)
755
756	// Register with global memory manager if monitoring is enabled
757	if cfg.Memory.EnableMemoryMonitoring {
758		if globalMemoryManager == nil {
759			InitializeGlobalMemoryManager(
760				cfg.Memory.EnableMemoryMonitoring,
761				cfg.Memory.ProcessRecycleThreshold,
762				30*time.Second, // Default monitoring interval
763			)
764		}
765		if globalMemoryManager != nil {
766			globalMemoryManager.RegisterWebView(v)
767		}
768	}
769	// Attach GTK4 input controllers (keyboard, mouse)
770	AttachKeyboardControllers(v)
771
772	// Watch for GTK theme changes and propagate to page at runtime
773	if settings := C.gtk_settings_get_default(); settings != nil {
774		C.connect_theme_changed_with_id(settings, C.ulong(v.id))
775	}
776
777	// Native zoom shortcuts (independent of app services)
778	// Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
779	_ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
780		nz := v.zoom
781		if nz <= 0 {
782			nz = 1.0
783		}
784		nz *= 1.1
785		if nz < 0.25 {
786			nz = 0.25
787		}
788		if nz > 5.0 {
789			nz = 5.0
790		}
791		_ = v.SetZoom(nz)
792	})
793	_ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
794		nz := v.zoom
795		if nz <= 0 {
796			nz = 1.0
797		}
798		nz *= 1.1
799		if nz < 0.25 {
800			nz = 0.25
801		}
802		if nz > 5.0 {
803			nz = 5.0
804		}
805		_ = v.SetZoom(nz)
806	})
807	_ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
808		nz := v.zoom
809		if nz <= 0 {
810			nz = 1.0
811		}
812		nz /= 1.1
813		if nz < 0.25 {
814			nz = 0.25
815		}
816		if nz > 5.0 {
817			nz = 5.0
818		}
819		_ = v.SetZoom(nz)
820	})
821	_ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
822	// Find in page (Ctrl/Cmd+F) reuses omnibox component in find mode
823	_ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { _ = v.OpenFind("") })
824	// Navigation with Alt+Arrow keys
825	_ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
826	_ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
827	// Update window title when page title changes
828	C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
829	// Also dispatch title change to Go with view id
830	C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
831	// Notify URI changes to Go to keep current URL in sync
832	C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
833	// Apply hardware acceleration and related settings based on cfg.Rendering
834	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
835		// Hardware acceleration policy (guarded by version)
836		switch cfg.Rendering.Mode {
837		case "gpu":
838			C.maybe_set_hw_policy(settings, 1)
839			C.maybe_set_webgl(settings, 1)
840			C.maybe_set_canvas_accel(settings, 1)
841		case "cpu":
842			C.maybe_set_hw_policy(settings, 2)
843			C.maybe_set_webgl(settings, 0)
844			C.maybe_set_canvas_accel(settings, 0)
845		default: // auto
846			C.maybe_set_hw_policy(settings, 0)
847			C.maybe_set_webgl(settings, 1)
848			C.maybe_set_canvas_accel(settings, 1)
849		}
850		// Optional compositing indicators for debugging (guarded by version)
851		if cfg.Rendering.DebugGPU {
852			C.maybe_set_draw_indicators(settings, 1)
853		}
854		// Reduce media pipeline churn by requiring a user gesture for playback
855		C.maybe_set_media_user_gesture(settings, 1)
856		// Enable trackpad back/forward gestures when available
857		C.maybe_set_back_forward_gestures(settings, 1)
858
859		// Keep page cache enabled by default for performance
860		enablePageCache := true
861		if cfg.Memory.EnablePageCache {
862			enablePageCache = true
863		}
864		C.maybe_set_page_cache(settings, C.int(boolToInt(enablePageCache)))
865
866		// Offline app cache is deprecated; keep as no-op
867		C.maybe_set_offline_app_cache(settings, C.int(0))
868
869		if cfg.Memory.EnableMemoryMonitoring {
870			log.Printf("[webkit] Memory settings applied: page_cache=%v", enablePageCache)
871		}
872	}
873
874	if cfg.ZoomDefault > 0 {
875		v.zoom = cfg.ZoomDefault
876		C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
877	}
878	if cfg.EnableDeveloperExtras {
879		settings := C.webkit_web_view_get_settings(v.native.wv)
880		if settings != nil {
881			C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
882		}
883	}
884	// Apply default fonts if provided in cfg
885	if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
886		if cfg.DefaultSansFont != "" {
887			csans := C.CString(cfg.DefaultSansFont)
888			C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
889			C.free(unsafe.Pointer(csans))
890			// Also set default font family to sans
891			csans2 := C.CString(cfg.DefaultSansFont)
892			C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
893			C.free(unsafe.Pointer(csans2))
894		}
895		if cfg.DefaultSerifFont != "" {
896			cserif := C.CString(cfg.DefaultSerifFont)
897			C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
898			C.free(unsafe.Pointer(cserif))
899		}
900		if cfg.DefaultMonospaceFont != "" {
901			cmono := C.CString(cfg.DefaultMonospaceFont)
902			C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
903			C.free(unsafe.Pointer(cmono))
904		}
905		if cfg.DefaultFontSize > 0 {
906			C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
907		}
908	}
909	if cfg.InitialURL != "" {
910		_ = v.LoadURL(cfg.InitialURL)
911	}
912	// Ensure native resources are released if user forgets to call Destroy
913	runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
914	log.Printf("[webkit] WebView created (CGO)")
915
916	// Initialize UCM scripts and handlers
917	if v.native != nil && v.native.ucm != nil {
918		v.enableUserContentManager()
919	}
920
921	// Start periodic JavaScript garbage collection if configured
922	if cfg.Memory.EnableGCInterval > 0 {
923		v.startPeriodicGC(time.Duration(cfg.Memory.EnableGCInterval) * time.Second)
924		if cfg.Memory.EnableMemoryMonitoring {
925			log.Printf("[webkit] Periodic GC enabled: interval=%ds", cfg.Memory.EnableGCInterval)
926		}
927	}
928
929	return v, nil
930}
931
932func (w *WebView) LoadURL(rawURL string) error {
933	if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
934		return ErrNotImplemented
935	}
936
937	// Track page loads for process recycling
938	if w.memStats != nil {
939		w.memStats.pageLoadCount++
940
941		// Check if we need to recycle this WebView
942		if w.config.Memory.ProcessRecycleThreshold > 0 &&
943			w.memStats.pageLoadCount >= w.config.Memory.ProcessRecycleThreshold {
944			if w.config.Memory.EnableMemoryMonitoring {
945				log.Printf("[webkit] Process recycle threshold reached (%d pages), consider creating new WebView",
946					w.memStats.pageLoadCount)
947			}
948		}
949	}
950
951	w.url = rawURL
952	curl := C.CString(rawURL)
953	defer C.free(unsafe.Pointer(curl))
954	C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
955	log.Printf("[webkit] LoadURL: %s", rawURL)
956	return nil
957}
958
959func (w *WebView) Show() error {
960	if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
961		return ErrNotImplemented
962	}
963	C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
964	C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
965	C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
966	log.Printf("[webkit] Show window")
967	return nil
968}
969
970func (w *WebView) Hide() error {
971	if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
972		return ErrNotImplemented
973	}
974	C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
975	C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
976	return nil
977}
978
979func (w *WebView) Destroy() error {
980	if w == nil || w.native == nil || w.native.win == nil {
981		return ErrNotImplemented
982	}
983
984	// Stop periodic GC if running
985	w.stopPeriodicGC()
986
987	// Clean up memory pressure settings
988	if w.memStats != nil && w.memStats.memoryPressureSettings != nil {
989		// WebKit will clean up the memory pressure settings when context is destroyed
990		w.memStats.memoryPressureSettings = nil
991	}
992
993	// Unregister from memory manager
994	if globalMemoryManager != nil {
995		globalMemoryManager.UnregisterWebView(w.id)
996	}
997
998	// GTK4: destroy windows via gtk_window_destroy
999	C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1000	w.destroyed = true
1001	unregisterView(w.id)
1002	log.Printf("[webkit] Destroy window")
1003	return nil
1004}
1005
1006// Window returns the associated native window.
1007func (w *WebView) Window() *Window { return w.window }
1008
1009// GetCurrentURL returns the current URI from WebKit.
1010func (w *WebView) GetCurrentURL() string {
1011	if w == nil || w.native == nil || w.native.wv == nil {
1012		return ""
1013	}
1014	uri := C.webkit_web_view_get_uri(w.native.wv)
1015	if uri == nil {
1016		return ""
1017	}
1018	return C.GoString((*C.char)(unsafe.Pointer(uri)))
1019}
1020
1021func (w *WebView) GoBack() error {
1022	if w == nil || w.native == nil || w.native.wv == nil {
1023		return ErrNotImplemented
1024	}
1025	C.webkit_web_view_go_back(w.native.wv)
1026	return nil
1027}
1028
1029func (w *WebView) GoForward() error {
1030	if w == nil || w.native == nil || w.native.wv == nil {
1031		return ErrNotImplemented
1032	}
1033	C.webkit_web_view_go_forward(w.native.wv)
1034	return nil
1035}
1036
1037// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
1038func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
1039
1040func (w *WebView) dispatchScriptMessage(payload string) {
1041	if w != nil && w.msgHandler != nil {
1042		w.msgHandler(payload)
1043	}
1044}
1045
1046// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
1047func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
1048
1049func (w *WebView) dispatchTitleChanged(title string) {
1050	if w != nil && w.titleHandler != nil {
1051		w.titleHandler(title)
1052	}
1053}
1054
1055// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
1056func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
1057
1058func (w *WebView) dispatchURIChanged(uri string) {
1059	if w != nil && w.uriHandler != nil {
1060		w.uriHandler(uri)
1061	}
1062}
1063
1064// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
1065func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
1066
1067func (w *WebView) dispatchZoomChanged(level float64) {
1068	if w != nil && w.zoomHandler != nil {
1069		w.zoomHandler(level)
1070	}
1071}
1072
1073// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
1074func (w *WebView) enableUserContentManager() {
1075	if w == nil || w.native == nil || w.native.ucm == nil {
1076		return
1077	}
1078	// Register handler "dumber"
1079	cname := C.CString("dumber")
1080	ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
1081	if ok == 0 {
1082		log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
1083	}
1084	C.free(unsafe.Pointer(cname))
1085
1086	// Inject color-scheme preference script at document-start to inform sites of system theme
1087	preferDark := C.gtk_prefers_dark() != 0
1088	if preferDark {
1089		log.Printf("[theme] GTK prefers: dark")
1090	} else {
1091		log.Printf("[theme] GTK prefers: light")
1092	}
1093	var schemeJS string
1094	if preferDark {
1095		schemeJS = "(() => { try { const d=true; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='dark light'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:dark;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
1096	} else {
1097		schemeJS = "(() => { try { const d=false; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='light dark'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:light;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
1098	}
1099	cScheme := C.CString(schemeJS)
1100	defer C.free(unsafe.Pointer(cScheme))
1101	schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1102	if schemeScript != nil {
1103		C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
1104		C.webkit_user_script_unref(schemeScript)
1105	}
1106
1107	// Add user script at document-start (omnibox/find reusable component)
1108	src := C.CString(getOmniboxScript())
1109	defer C.free(unsafe.Pointer(src))
1110	script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1111	if script != nil {
1112		C.webkit_user_content_manager_add_script(w.native.ucm, script)
1113		C.webkit_user_script_unref(script)
1114		log.Printf("[webkit] UCM omnibox script injected at document-start")
1115	}
1116
1117	// Add user script at document-start (toast notification system)
1118	toastSrc := C.CString(getToastScript())
1119	defer C.free(unsafe.Pointer(toastSrc))
1120	toastScript := C.webkit_user_script_new((*C.gchar)(toastSrc), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1121	if toastScript != nil {
1122		C.webkit_user_content_manager_add_script(w.native.ucm, toastScript)
1123		C.webkit_user_script_unref(toastScript)
1124		log.Printf("[webkit] UCM toast script injected at document-start")
1125	}
1126
1127	// Inject Wails runtime fetch interceptor for homepage bridging
1128	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(_){} })();`
1129	cWails := C.CString(wailsBridge)
1130	defer C.free(unsafe.Pointer(cWails))
1131	wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
1132	if wailsScript != nil {
1133		C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
1134		C.webkit_user_script_unref(wailsScript)
1135	}
1136
1137	// No JS fallback bridge — native UCM is active
1138}
1139
1140// startPeriodicGC starts a ticker to trigger JavaScript garbage collection periodically
1141func (w *WebView) startPeriodicGC(interval time.Duration) {
1142	if w == nil || w.gcTicker != nil {
1143		return // Already running or invalid
1144	}
1145
1146	w.gcTicker = time.NewTicker(interval)
1147
1148	go func() {
1149		defer func() {
1150			if w.gcTicker != nil {
1151				w.gcTicker.Stop()
1152				w.gcTicker = nil
1153			}
1154		}()
1155
1156		for {
1157			select {
1158			case <-w.gcTicker.C:
1159				w.triggerGC()
1160			case <-w.gcDone:
1161				return
1162			}
1163		}
1164	}()
1165}
1166
1167// stopPeriodicGC stops the periodic garbage collection
1168func (w *WebView) stopPeriodicGC() {
1169	if w == nil {
1170		return
1171	}
1172
1173	if w.gcDone != nil {
1174		close(w.gcDone)
1175		w.gcDone = nil
1176	}
1177
1178	if w.gcTicker != nil {
1179		w.gcTicker.Stop()
1180		w.gcTicker = nil
1181	}
1182}
1183
1184// triggerGC manually triggers JavaScript garbage collection
1185func (w *WebView) triggerGC() {
1186	if w == nil || w.memStats == nil {
1187		return
1188	}
1189
1190	// Get default WebContext and trigger GC
1191	ctx := C.webkit_web_context_get_default()
1192	if ctx != nil {
1193		C.maybe_collect_js(ctx)
1194		w.memStats.lastGCTime = time.Now()
1195
1196		if w.config.Memory.EnableMemoryMonitoring {
1197			log.Printf("[webkit] JavaScript GC triggered (page loads: %d)", w.memStats.pageLoadCount)
1198		}
1199	}
1200}
1201
1202// GetMemoryStats returns current memory statistics for this WebView
1203func (w *WebView) GetMemoryStats() map[string]interface{} {
1204	if w == nil || w.memStats == nil {
1205		return nil
1206	}
1207
1208	return map[string]interface{}{
1209		"page_load_count":              w.memStats.pageLoadCount,
1210		"last_gc_time":                 w.memStats.lastGCTime,
1211		"has_memory_pressure_settings": w.memStats.memoryPressureSettings != nil,
1212	}
1213}
1214
1215// TriggerMemoryCleanup manually triggers garbage collection and memory cleanup
1216func (w *WebView) TriggerMemoryCleanup() {
1217	if w == nil {
1218		return
1219	}
1220
1221	w.triggerGC()
1222
1223	if w.config.Memory.EnableMemoryMonitoring {
1224		log.Printf("[webkit] Manual memory cleanup triggered")
1225	}
1226}
1227
1228//export goHandleTLSError
1229func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
1230	uri := C.GoString(failingURI)
1231	hostname := C.GoString(host)
1232	certificateInfo := C.GoString(certInfo)
1233
1234	log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
1235
1236	// Find the WebView that triggered this error
1237	// For now, we'll get the most recently created view
1238	// In a more sophisticated implementation, we'd pass the view ID through user_data
1239	var webView *WebView
1240	regMu.RLock()
1241	for _, v := range viewByID {
1242		if v != nil {
1243			webView = v
1244			break
1245		}
1246	}
1247	regMu.RUnlock()
1248
1249	if webView == nil {
1250		log.Printf("[tls] No WebView found to handle TLS error")
1251		return C.FALSE // Don't proceed
1252	}
1253
1254	// Check if we've already allowed this host
1255	if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
1256		log.Printf("[tls] Certificate exception already granted for %s", hostname)
1257		return C.TRUE // Proceed with the load
1258	}
1259
1260	// Show warning dialog and get user decision
1261	if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
1262		webView.tlsExceptions[hostname] = true
1263		log.Printf("[tls] User accepted certificate exception for %s", hostname)
1264		return C.TRUE // Proceed with the load
1265	}
1266
1267	log.Printf("[tls] User rejected certificate for %s", hostname)
1268	return C.FALSE // Don't proceed
1269}
1270
1271// showTLSWarningDialog displays a warning dialog for TLS certificate errors
1272func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
1273	if w == nil || w.native == nil || w.native.win == nil {
1274		log.Printf("[tls] Cannot show dialog: WebView or window is nil")
1275		return false
1276	}
1277
1278	log.Printf("[tls] Showing certificate error dialog for %s", hostname)
1279
1280	// Format the error message with certificate information
1281	errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
1282
1283	// Convert strings to C strings
1284	cHostname := C.CString(hostname)
1285	defer C.free(unsafe.Pointer(cHostname))
1286
1287	cErrorMsg := C.CString(errorMsg)
1288	defer C.free(unsafe.Pointer(cErrorMsg))
1289
1290	// Show the dialog and get user response
1291	result := C.show_tls_warning_dialog_sync(
1292		(*C.GtkWindow)(unsafe.Pointer(w.native.win)),
1293		cHostname,
1294		cErrorMsg,
1295	)
1296
1297	userAccepted := result != 0
1298	log.Printf("[tls] User %s certificate for %s",
1299		map[bool]string{true: "accepted", false: "rejected"}[userAccepted],
1300		hostname)
1301
1302	return userAccepted
1303}
1304
1305// formatTLSErrorMessage creates a detailed error message based on the error flags
1306func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
1307	msg := "Website: " + hostname + "\n\n"
1308
1309	// Add certificate information
1310	if certificateInfo != "" {
1311		msg += "Certificate Details:\n" + certificateInfo + "\n"
1312	}
1313
1314	msg += "Security Issues:\n"
1315	// Decode GTlsCertificateFlags (common values)
1316	if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
1317		msg += "• Certificate authority is not trusted\n"
1318	}
1319	if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
1320		msg += "• Certificate does not match the website identity\n"
1321	}
1322	if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
1323		msg += "• Certificate is not yet valid\n"
1324	}
1325	if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
1326		msg += "• Certificate has expired\n"
1327	}
1328	if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
1329		msg += "• Certificate has been revoked\n"
1330	}
1331	if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
1332		msg += "• Certificate uses weak cryptography\n"
1333	}
1334
1335	msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
1336
1337	return msg
1338}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close