v0.6.0 5c50dd3

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

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