v0.2.0 759cc92

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 <gtk/gtk.h>
9#include <webkit/webkit.h>
10#include <glib-object.h>
11#include <gdk/gdk.h>
12#include <jsc/jsc.h>
13#include <glib.h>
14#include <gio/gio.h>
15
16static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
17// GTK4: gtk_main_quit removed; leave as no-op for now (Quit handled in Go)
18static void connect_destroy_quit(GtkWidget* w) { (void)w; }
19static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
20// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
21
22// Forward declare helpers used below
23static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
24
25// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
26static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm) {
27    WebKitUserContentManager* u = webkit_user_content_manager_new();
28    if (!u) return NULL;
29    // WebKitGTK 6: create NetworkSession with data/cache directories
30    WebKitNetworkSession* sess = webkit_network_session_new(
31        data_dir ? data_dir : "",
32        cache_dir ? cache_dir : "");
33    if (!sess) { g_object_unref(u); return NULL; }
34    // Configure cookie persistence and policy (no third-party by default)
35    WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
36    if (cm) {
37        if (cookie_path && cookie_path[0]) {
38            webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
39        }
40        // 2 => no-third-party (default), 0 => always, 1 => never
41        maybe_set_cookie_policy(cm, 2);
42    }
43    // Persist credentials (HTTP auth, etc.)
44    webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
45    GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
46        "user-content-manager", u,
47        "network-session", sess,
48        NULL));
49    // WebView holds refs to provided objects; drop our temporary refs
50    g_object_unref(sess);
51    if (out_ucm) { *out_ucm = u; }
52    return w;
53}
54
55static gboolean gtk_prefers_dark() {
56    GtkSettings* settings = gtk_settings_get_default();
57    if (!settings) return FALSE;
58    gboolean prefer = FALSE;
59    g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
60    return prefer;
61}
62
63// Note: preferred color scheme is handled via a user script injection to support
64// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
65
66extern void goOnUcmMessage(unsigned long id, const char* json);
67extern void goOnTitleChanged(unsigned long id, const char* title);
68extern void goOnURIChanged(unsigned long id, const char* uri);
69extern void goOnThemeChanged(unsigned long id, int prefer_dark);
70extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
71
72void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
73    (void)pspec;
74    WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
75    const gchar* title = webkit_web_view_get_title(view);
76    GtkWindow* win = GTK_WINDOW(user_data);
77    if (title && win) {
78        gtk_window_set_title(win, title);
79    }
80}
81
82void on_title_notify_id(GObject* obj, GParamSpec* pspec, gpointer user_data) {
83    (void)pspec; (void)obj;
84    const gchar* title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(obj));
85    if (title) { goOnTitleChanged((unsigned long)user_data, title); }
86}
87
88void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
89    (void)pspec; (void)obj;
90    const gchar* uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(obj));
91    if (uri) { goOnURIChanged((unsigned long)user_data, uri); }
92}
93
94// React to GTK theme preference changes at runtime
95void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
96    (void)pspec;
97    GtkSettings* settings = GTK_SETTINGS(obj);
98    if (!settings) return;
99    gboolean prefer = FALSE;
100    g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
101    goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
102}
103
104// NOTE: UCM message callback signature changed in WebKitGTK 6; will be reimplemented later.
105
106void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
107    (void)user_data;
108    const gchar* uri = webkit_uri_scheme_request_get_uri(request);
109    size_t n = 0;
110    char* mime = NULL;
111    void* buf = goResolveURIScheme((char*)uri, &n, &mime);
112    if (buf && n > 0 && mime) {
113        GInputStream* stream = g_memory_input_stream_new_from_data(buf, (gssize)n, g_free);
114        webkit_uri_scheme_request_finish(request, stream, (gint64)n, mime);
115        g_object_unref(stream);
116        g_free(mime);
117    } else {
118        GError* err = g_error_new_literal(g_quark_from_string("dumber"), 404, "Not found");
119        webkit_uri_scheme_request_finish_error(request, err);
120        g_error_free(err);
121    }
122}
123
124// Script message handler wiring for WebKitGTK 6
125// Forward declaration for callback used in g_signal_connect_data
126// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
127static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
128// Uses WebKitScriptMessage and script-message-received::NAME signal.
129static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
130    (void)ucm;
131    if (!val) return;
132    gchar* s = jsc_value_to_string(val);
133    if (!s) return;
134    goOnUcmMessage((unsigned long)user_data, s);
135    g_free(s);
136}
137
138static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
139    if (!ucm || !name) return FALSE;
140    // WebKitGTK 6 signature: (ucm, name, world_name)
141    webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
142    gchar* signal = g_strdup_printf("script-message-received::%s", name);
143    if (!signal) return FALSE;
144    g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
145    g_free(signal);
146    return TRUE;
147}
148
149// ----- Conditional helpers for settings across WebKit versions -----
150#ifndef WEBKIT_CHECK_VERSION
151#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
152#endif
153
154static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
155#if WEBKIT_CHECK_VERSION(2,40,0)
156    // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
157    if (!settings) return;
158    if (mode == 1) {
159        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
160    } else if (mode == 2) {
161        webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
162    } else {
163        // Leave default policy (auto) without forcing a value to avoid referencing
164        // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
165    }
166#else
167    (void)settings; (void)mode;
168#endif
169}
170
171static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
172#if WEBKIT_CHECK_VERSION(2,36,0)
173    if (!settings) return;
174    webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
175#else
176    (void)settings; (void)enable;
177#endif
178}
179
180static void maybe_set_webgl(WebKitSettings* settings, int enable) {
181#if WEBKIT_CHECK_VERSION(2,20,0)
182    if (!settings) return;
183    webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
184#else
185    (void)settings; (void)enable;
186#endif
187}
188
189static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
190#if WEBKIT_CHECK_VERSION(2,20,0)
191    if (!settings) return;
192    webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
193#else
194    (void)settings; (void)enable;
195#endif
196}
197
198static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
199#if WEBKIT_CHECK_VERSION(2,20,0)
200    if (!settings) return;
201    webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
202#else
203    (void)settings; (void)require;
204#endif
205}
206
207// Set cookie accept policy with compile-time compatibility across WebKit versions
208static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
209    if (!cm) return;
210#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
211    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
212    if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
213    else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
214    webkit_cookie_manager_set_accept_policy(cm, p);
215#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
216    WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
217    if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
218    else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
219    webkit_cookie_manager_set_accept_policy(cm, p);
220#else
221    (void)policy; // Not supported in this header version
222#endif
223}
224*/
225import "C"
226
227import (
228    "errors"
229    "log"
230    "os"
231    "path/filepath"
232    "runtime"
233    "unsafe"
234)
235
236const omniboxDefaultScript = `(() => {
237  try {
238    if (window.__dumber_omnibox_loaded) return; window.__dumber_omnibox_loaded = true;
239    const H = { el:null,input:null,list:null,visible:false,suggestions:[],debounceTimer:0,
240      post(m){ try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(m)); }catch(_){} },
241      render(){ if(!this.el) this.mount(); this.el.style.display=this.visible?'block':'none'; if(this.visible) this.input.focus(); },
242      mount(){ const r=document.createElement('div'); r.style.cssText='position:fixed;inset:0;z-index:2147483647;display:none;';
243        const b=document.createElement('div'); b.style.cssText='max-width:720px;margin:8vh auto;padding:10px 12px;background:#16181a;color:#e6e6e6;border:1px solid #2a2e33;border-radius:10px;box-shadow:0 12px 36px rgba(0,0,0,.55);backdrop-filter:saturate(120%) blur(2px);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,\"Helvetica Neue\",Arial,sans-serif;';
244        const i=document.createElement('input'); i.type='text'; i.placeholder='Type URL or search…'; i.style.cssText='display:block;width:100%;box-sizing:border-box;padding:12px 14px;border-radius:8px;border:1px solid #3a3f45;background:#0f1113;color:#e6e6e6;font-size:15px;outline:none;';
245        const l=document.createElement('div'); l.style.cssText='margin-top:10px;max-height:52vh;overflow:auto;border-top:1px solid #2a2e33;';
246        b.appendChild(i); b.appendChild(l); r.appendChild(b); document.documentElement.appendChild(r);
247        i.addEventListener('keydown', (e)=>{ if(e.key==='Escape'){H.toggle(false);} else if(e.key==='Enter'){ const pick=H.suggestions[H.selectedIndex|0]; const v=(pick&&pick.url)||i.value||''; if(v) H.post({type:'navigate', url:v}); H.toggle(false);} else if(e.key==='ArrowDown' || e.key==='ArrowUp'){ e.preventDefault(); const n=H.suggestions.length; if(n){ H.selectedIndex=(H.selectedIndex||0)+(e.key==='ArrowDown'?1:-1); if(H.selectedIndex<0)H.selectedIndex=n-1; if(H.selectedIndex>=n)H.selectedIndex=0; H.paintList(); } } });
248        i.addEventListener('input', ()=>{ clearTimeout(H.debounceTimer); const q=i.value; H.debounceTimer=setTimeout(()=>H.post({type:'query', q, limit:10}), 120); });
249        this.el=r; this.input=i; this.list=l; this.selectedIndex=-1; this.paintList(); },
250      paintList(){ const l=this.list; if(!l) return; l.textContent=''; this.suggestions.forEach((s,i)=>{ const it=document.createElement('div'); it.style.cssText='padding:10px 12px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #252a2f;'+(i===this.selectedIndex?'background:#0c0f12;':'');
251          const icon=document.createElement('img'); icon.src=s.favicon||''; icon.width=18; icon.height=18; icon.loading='lazy'; icon.style.cssText='flex:0 0 18px;width:18px;height:18px;border-radius:4px;opacity:.95;'; icon.onerror=()=>{ icon.style.display='none'; };
252          const url=document.createElement('div'); url.textContent=s.url||''; url.style.cssText='flex:1;min-width:0;color:#cfe6ff;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
253          it.appendChild(icon); it.appendChild(url);
254          it.addEventListener('mouseenter',()=>{ H.selectedIndex=i; H.paintList(); });
255          it.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
256          l.appendChild(it);
257        }); },
258      toggle(v){ this.visible=(typeof v==='boolean')?v:!this.visible; this.render(); }, setSuggestions(a){ this.suggestions=Array.isArray(a)?a:[]; this.selectedIndex=-1; this.paintList(); }
259    };
260    window.addEventListener('keydown', (e)=>{ if((e.ctrlKey||e.metaKey) && (e.key==='l'||e.key==='L')){ e.preventDefault(); H.toggle(true); } }, true);
261    window.__dumber_setSuggestions = (a)=> H.setSuggestions(a);
262    window.__dumber_toggle = ()=> H.toggle();
263  } catch(_){}
264})();`
265
266type nativeView struct {
267    win  *C.GtkWidget
268    view *C.GtkWidget
269    wv   *C.WebKitWebView
270    ucm  *C.WebKitUserContentManager
271}
272
273// WebView represents a browser view powered by WebKit2GTK.
274type WebView struct {
275    config    *Config
276    zoom      float64
277    url       string
278    destroyed bool
279    native    *nativeView
280    window    *Window
281    id        uintptr
282    msgHandler func(payload string)
283    titleHandler func(title string)
284    uriHandler   func(uri string)
285}
286
287// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
288func NewWebView(cfg *Config) (*WebView, error) {
289    log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
290    if C.gtk_init_check() == 0 {
291        return nil, errors.New("failed to initialize GTK")
292    }
293
294    if cfg == nil {
295        cfg = &Config{}
296    }
297
298    // Prepare persistent website data/caches
299    dataDir := cfg.DataDir
300    cacheDir := cfg.CacheDir
301    if dataDir == "" { dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data") }
302    if cacheDir == "" { cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache") }
303    _ = os.MkdirAll(dataDir, 0o755)
304    _ = os.MkdirAll(cacheDir, 0o755)
305
306    cData := C.CString(dataDir)
307    cCache := C.CString(cacheDir)
308    cookieFile := filepath.Join(dataDir, "cookies.db")
309    cCookie := C.CString(cookieFile)
310    defer C.free(unsafe.Pointer(cData))
311    defer C.free(unsafe.Pointer(cCache))
312    defer C.free(unsafe.Pointer(cCookie))
313    // Use default WebContext to register the custom URI scheme handler; the
314    // WebView itself is created with a WebKitNetworkSession for persistence.
315    ctx := C.webkit_web_context_get_default()
316
317    // Register custom URI scheme handler for "dumb://"
318    {
319        sch := C.CString("dumb")
320        C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)
321        C.free(unsafe.Pointer(sch))
322    }
323    // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
324
325    // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
326    var createdUcm *C.WebKitUserContentManager
327    viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm)
328    if viewWidget == nil {
329        return nil, errors.New("failed to create WebKitWebView")
330    }
331
332    // Create a top-level window to host the view
333    win := C.new_window()
334    if win == nil {
335        return nil, errors.New("failed to create GtkWindow")
336    }
337
338    // Pack view widget into the window
339    // GTK4: containers removed; use gtk_window_set_child
340    C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
341    C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
342    C.connect_destroy_quit(win)
343
344    v := &WebView{
345        config: cfg,
346        zoom:   1.0,
347        native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
348        window: &Window{win: win},
349    }
350    // Assign an ID for accelerator dispatch
351    v.id = nextViewID()
352    registerView(v.id, v)
353    // Attach GTK4 input controllers (keyboard, mouse)
354    AttachKeyboardControllers(v)
355
356    // Watch for GTK theme changes and propagate to page at runtime
357    if settings := C.gtk_settings_get_default(); settings != nil {
358        cprop := C.CString("notify::gtk-application-prefer-dark-theme")
359        C.g_signal_connect_data(C.gpointer(unsafe.Pointer(settings)), cprop, C.GCallback(C.on_theme_changed), C.gpointer(v.id), nil, 0)
360        C.free(unsafe.Pointer(cprop))
361    }
362
363    // Native zoom shortcuts (independent of app services)
364    // Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
365    _ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
366        nz := v.zoom
367        if nz <= 0 { nz = 1.0 }
368        nz *= 1.1
369        if nz < 0.25 { nz = 0.25 }
370        if nz > 5.0 { nz = 5.0 }
371        _ = v.SetZoom(nz)
372    })
373    _ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
374        nz := v.zoom
375        if nz <= 0 { nz = 1.0 }
376        nz *= 1.1
377        if nz < 0.25 { nz = 0.25 }
378        if nz > 5.0 { nz = 5.0 }
379        _ = v.SetZoom(nz)
380    })
381    _ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
382        nz := v.zoom
383        if nz <= 0 { nz = 1.0 }
384        nz /= 1.1
385        if nz < 0.25 { nz = 0.25 }
386        if nz > 5.0 { nz = 5.0 }
387        _ = v.SetZoom(nz)
388    })
389    _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
390    // Update window title when page title changes
391    cNotifyTitle1 := C.CString("notify::title")
392    C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
393    C.free(unsafe.Pointer(cNotifyTitle1))
394    // Also dispatch title change to Go with view id
395    cNotifyTitle2 := C.CString("notify::title")
396    C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle2, C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
397    C.free(unsafe.Pointer(cNotifyTitle2))
398    // Notify URI changes to Go to keep current URL in sync
399    cNotifyURI := C.CString("notify::uri")
400    C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyURI, C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
401    C.free(unsafe.Pointer(cNotifyURI))
402    // Apply hardware acceleration and related settings based on cfg.Rendering
403    if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
404        // Hardware acceleration policy (guarded by version)
405        switch cfg.Rendering.Mode {
406        case "gpu":
407            C.maybe_set_hw_policy(settings, 1)
408            C.maybe_set_webgl(settings, 1)
409            C.maybe_set_canvas_accel(settings, 1)
410        case "cpu":
411            C.maybe_set_hw_policy(settings, 2)
412            C.maybe_set_webgl(settings, 0)
413            C.maybe_set_canvas_accel(settings, 0)
414        default: // auto
415            C.maybe_set_hw_policy(settings, 0)
416            C.maybe_set_webgl(settings, 1)
417            C.maybe_set_canvas_accel(settings, 1)
418        }
419        // Optional compositing indicators for debugging (guarded by version)
420        if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) }
421        // Reduce media pipeline churn by requiring a user gesture for playback
422        C.maybe_set_media_user_gesture(settings, 1)
423    }
424
425    if cfg.ZoomDefault > 0 {
426        v.zoom = cfg.ZoomDefault
427        C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
428    }
429    if cfg.EnableDeveloperExtras {
430        settings := C.webkit_web_view_get_settings(v.native.wv)
431        if settings != nil {
432            C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
433        }
434    }
435    // Apply default fonts if provided in cfg
436    if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
437        if cfg.DefaultSansFont != "" {
438            csans := C.CString(cfg.DefaultSansFont)
439            C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
440            C.free(unsafe.Pointer(csans))
441            // Also set default font family to sans
442            csans2 := C.CString(cfg.DefaultSansFont)
443            C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
444            C.free(unsafe.Pointer(csans2))
445        }
446        if cfg.DefaultSerifFont != "" {
447            cserif := C.CString(cfg.DefaultSerifFont)
448            C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
449            C.free(unsafe.Pointer(cserif))
450        }
451        if cfg.DefaultMonospaceFont != "" {
452            cmono := C.CString(cfg.DefaultMonospaceFont)
453            C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
454            C.free(unsafe.Pointer(cmono))
455        }
456        if cfg.DefaultFontSize > 0 {
457            C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
458        }
459    }
460    if cfg.InitialURL != "" {
461        _ = v.LoadURL(cfg.InitialURL)
462    }
463    // Ensure native resources are released if user forgets to call Destroy
464    runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
465    log.Printf("[webkit] WebView created (CGO)")
466
467    // Initialize UCM scripts and handlers
468    if v.native != nil && v.native.ucm != nil {
469        v.enableUserContentManager()
470    }
471    return v, nil
472}
473
474func (w *WebView) LoadURL(rawURL string) error {
475    if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
476        return ErrNotImplemented
477    }
478    w.url = rawURL
479    curl := C.CString(rawURL)
480    defer C.free(unsafe.Pointer(curl))
481    C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
482    log.Printf("[webkit] LoadURL: %s", rawURL)
483    return nil
484}
485
486func (w *WebView) Show() error {
487    if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
488        return ErrNotImplemented
489    }
490    C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
491    C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
492    C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
493    log.Printf("[webkit] Show window")
494    return nil
495}
496
497func (w *WebView) Hide() error {
498    if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
499        return ErrNotImplemented
500    }
501    C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
502    C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
503    return nil
504}
505
506func (w *WebView) Destroy() error {
507    if w == nil || w.native == nil || w.native.win == nil {
508        return ErrNotImplemented
509    }
510    // GTK4: destroy windows via gtk_window_destroy
511    C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
512    w.destroyed = true
513    unregisterView(w.id)
514    log.Printf("[webkit] Destroy window")
515    return nil
516}
517
518// Window returns the associated native window.
519func (w *WebView) Window() *Window { return w.window }
520
521// GetCurrentURL returns the current URI from WebKit.
522func (w *WebView) GetCurrentURL() string {
523    if w == nil || w.native == nil || w.native.wv == nil { return "" }
524    uri := C.webkit_web_view_get_uri(w.native.wv)
525    if uri == nil { return "" }
526    return C.GoString((*C.char)(unsafe.Pointer(uri)))
527}
528
529func (w *WebView) GoBack() error {
530    if w == nil || w.native == nil || w.native.wv == nil { return ErrNotImplemented }
531    C.webkit_web_view_go_back(w.native.wv)
532    return nil
533}
534
535func (w *WebView) GoForward() error {
536    if w == nil || w.native == nil || w.native.wv == nil { return ErrNotImplemented }
537    C.webkit_web_view_go_forward(w.native.wv)
538    return nil
539}
540
541// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
542func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
543
544func (w *WebView) dispatchScriptMessage(payload string) {
545    if w != nil && w.msgHandler != nil { w.msgHandler(payload) }
546}
547
548// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
549func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
550
551func (w *WebView) dispatchTitleChanged(title string) {
552    if w != nil && w.titleHandler != nil { w.titleHandler(title) }
553}
554
555// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
556func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
557
558func (w *WebView) dispatchURIChanged(uri string) {
559    if w != nil && w.uriHandler != nil { w.uriHandler(uri) }
560}
561
562// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
563func (w *WebView) enableUserContentManager() {
564    if w == nil || w.native == nil || w.native.ucm == nil { return }
565    // Register handler "dumber"
566    cname := C.CString("dumber")
567    ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
568    if ok == 0 {
569        log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
570    }
571    C.free(unsafe.Pointer(cname))
572
573    // Inject color-scheme preference script at document-start to inform sites of system theme
574    preferDark := C.gtk_prefers_dark() != 0
575    if preferDark {
576        log.Printf("[theme] GTK prefers: dark")
577    } else {
578        log.Printf("[theme] GTK prefers: light")
579    }
580    var schemeJS string
581    if preferDark {
582        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) } })();"
583    } else {
584        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) } })();"
585    }
586    cScheme := C.CString(schemeJS)
587    defer C.free(unsafe.Pointer(cScheme))
588    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)
589    if schemeScript != nil {
590        C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
591        C.webkit_user_script_unref(schemeScript)
592    }
593
594    // Add user script at document-start
595    src := C.CString(omniboxDefaultScript)
596    defer C.free(unsafe.Pointer(src))
597    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)
598    if script != nil {
599        C.webkit_user_content_manager_add_script(w.native.ucm, script)
600        C.webkit_user_script_unref(script)
601        log.Printf("[webkit] UCM omnibox script injected at document-start")
602    }
603
604    // Inject Wails runtime fetch interceptor for homepage bridging
605    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(_){} })();`
606    cWails := C.CString(wailsBridge)
607    defer C.free(unsafe.Pointer(cWails))
608    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)
609    if wailsScript != nil {
610        C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
611        C.webkit_user_script_unref(wailsScript)
612    }
613
614    // No JS fallback bridge — native UCM is active
615}

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