v0.12.1 1453d21

webview.go RawHistoryFolder

1package webkit
2
3import (
4	"encoding/json"
5	"fmt"
6	"log"
7	"strconv"
8	"sync"
9	"sync/atomic"
10
11	webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
12	"github.com/diamondburned/gotk4/pkg/gdk/v4"
13	"github.com/diamondburned/gotk4/pkg/gtk/v4"
14)
15
16var (
17	viewIDCounter uint64
18	viewRegistry  = make(map[uint64]*WebView)
19	viewMu        sync.RWMutex
20)
21
22// WebView wraps a WebKitGTK WebView
23type WebView struct {
24	view      *webkit.WebView
25	container *gtk.Box // Container that holds the WebView
26	window    *Window  // Optional window (only if CreateWindow=true)
27	id        uint64
28
29	// State
30	config    *Config
31	destroyed bool
32	mu        sync.RWMutex
33
34	// Event handlers
35	onScriptMessage       func(string)
36	onTitleChanged        func(string)
37	onURIChanged          func(string)
38	onFaviconChanged      func([]byte)
39	onFaviconURIChanged   func(pageURI, faviconURI string)
40	onZoomChanged         func(float64)
41	onPopupCreate         func(*webkit.NavigationAction) *WebView // New WebKit create signal handler
42	onReadyToShow         func()                                  // WebKit ready-to-show signal handler
43	onClose               func()
44	onNavigationPolicy    func(url string, isUserGesture bool) bool
45	onWindowTypeDetected  func(WindowType, *WindowFeatures)
46	onWorkspaceNavigation func(direction string) bool // Workspace pane navigation
47	onPaneModeShortcut    func(action string) bool    // Pane mode shortcuts (enter, actions, exit)
48	isPaneModeActive      func() bool                 // Check if pane mode is active
49}
50
51// NewWebView creates a new WebView with the given configuration
52func NewWebView(cfg *Config) (*WebView, error) {
53	if cfg == nil {
54		cfg = GetDefaultConfig()
55	}
56
57	// Generate unique ID immediately
58	id := atomic.AddUint64(&viewIDCounter, 1)
59	log.Printf("[webkit] Generated WebView ID: %d (CreateWindow=%v)", id, cfg.CreateWindow)
60
61	InitMainThread()
62
63	// Initialize persistent session - REQUIRED, no ephemeral fallback
64	// This must be done BEFORE creating the WebView
65	if cfg.DataDir == "" || cfg.CacheDir == "" {
66		return nil, fmt.Errorf("data and cache directories are required for persistent storage")
67	}
68
69	if err := InitPersistentSession(cfg.DataDir, cfg.CacheDir); err != nil {
70		return nil, fmt.Errorf("failed to initialize persistent storage: %w", err)
71	}
72
73	// Create WebView - it will automatically use the persistent session
74	// Per WebKitGTK 6.0: newly created session becomes the default for all WebViews
75	wkView := webkit.NewWebView()
76	if wkView == nil {
77		return nil, ErrWebViewNotInitialized
78	}
79
80	// Set background color based on user's theme preference to prevent white/black flash
81	// This must be done before any content loads
82	var bg gdk.RGBA
83	if PrefersDarkTheme() {
84		bg = gdk.NewRGBA(0.11, 0.11, 0.11, 1.0) // #1c1c1c - dark background
85	} else {
86		bg = gdk.NewRGBA(0.96, 0.96, 0.96, 1.0) // #f5f5f5 - light background
87	}
88	wkView.SetBackgroundColor(&bg)
89
90	// Verify the WebView is using the persistent session
91	viewSession := wkView.NetworkSession()
92	if viewSession == nil {
93		return nil, fmt.Errorf("WebView has no network session")
94	}
95	if viewSession.IsEphemeral() {
96		return nil, fmt.Errorf("WebView is using ephemeral session despite persistent session initialization")
97	}
98
99	// CRITICAL: Configure CookieManager on the WebView's actual session
100	// This ensures cookies are persisted regardless of whether the WebView
101	// uses our global session or creates its own
102	cookieManager := viewSession.CookieManager()
103	if cookieManager == nil {
104		return nil, fmt.Errorf("failed to get cookie manager from WebView's network session")
105	}
106	cookiePath := cfg.DataDir + "/cookies.db"
107	cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
108	cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
109
110	// Setup UserContentManager and inject GUI scripts (including webview ID)
111	// This must be done BEFORE any pages are loaded
112	if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON, id); err != nil {
113		return nil, fmt.Errorf("failed to setup user content manager: %w", err)
114	}
115
116	// Create container (GtkBox) to hold the WebView
117	// This allows the WebView to be reparented into workspace panes
118	container := gtk.NewBox(gtk.OrientationVertical, 0)
119	container.SetHExpand(true)
120	container.SetVExpand(true)
121
122	// Configure WebView widget for expansion
123	wkView.SetHExpand(true)
124	wkView.SetVExpand(true)
125
126	// Add WebView to container
127	container.Append(wkView)
128
129	wv := &WebView{
130		view:      wkView,
131		container: container,
132		window:    nil, // Will be set below if CreateWindow is true
133		id:        id,
134		config:    cfg,
135	}
136
137	// Apply configuration
138	if err := wv.applyConfig(); err != nil {
139		return nil, err
140	}
141
142	// Setup event handlers
143	wv.setupEventHandlers()
144
145	// Attach keyboard bridge to forward keyboard events to JavaScript
146	wv.AttachKeyboardBridge()
147
148	// Attach mouse gesture controls for hardware navigation buttons
149	wv.AttachMouseGestures()
150
151	// Attach touchpad gesture controls for swipe navigation
152	wv.AttachTouchpadGestures()
153
154	// Only create window if requested (standalone WebViews)
155	// Workspace panes will set CreateWindow=false and manage the container themselves
156	if cfg.CreateWindow {
157		window, err := NewWindow("Dumber Browser")
158		if err != nil {
159			return nil, err
160		}
161		wv.window = window
162		// Add container as child of window
163		window.SetChild(container)
164		log.Printf("[webkit] Created standalone window for WebView ID %d", id)
165	} else {
166		log.Printf("[webkit] Created WebView ID %d without window (for workspace embedding)", id)
167	}
168
169	// Register in global registry
170	viewMu.Lock()
171	viewRegistry[id] = wv
172	viewMu.Unlock()
173
174	return wv, nil
175}
176
177// applyConfig applies the configuration to the WebView settings
178func (w *WebView) applyConfig() error {
179	settings := w.view.Settings()
180	if settings == nil {
181		return fmt.Errorf("webkit: failed to get settings")
182	}
183
184	// Apply settings from config
185	settings.SetEnableJavascript(w.config.EnableJavaScript)
186	settings.SetEnableWebgl(w.config.EnableWebGL)
187	settings.SetDefaultFontSize(uint32(w.config.DefaultFontSize))
188	settings.SetMinimumFontSize(uint32(w.config.MinimumFontSize))
189
190	if w.config.UserAgent != "" {
191		settings.SetUserAgent(w.config.UserAgent)
192	}
193
194	// Enable developer tools (F12, inspector)
195	settings.SetEnableDeveloperExtras(true)
196
197	// Enable hardware acceleration if configured
198	settings.SetHardwareAccelerationPolicy(webkit.HardwareAccelerationPolicyAlways)
199
200	return nil
201}
202
203// setupEventHandlers connects GTK signals to internal handlers
204func (w *WebView) setupEventHandlers() {
205	// Title changed - connect to notify::title signal
206	w.view.Connect("notify::title", func() {
207		if w.onTitleChanged != nil {
208			title := w.view.Title()
209			w.onTitleChanged(title)
210		}
211	})
212
213	// URI changed - connect to notify::uri signal
214	w.view.Connect("notify::uri", func() {
215		if w.onURIChanged != nil {
216			uri := w.view.URI()
217			w.onURIChanged(uri)
218		}
219	})
220
221	// Zoom level changed - connect to notify::zoom-level signal
222	w.view.Connect("notify::zoom-level", func() {
223		if w.onZoomChanged != nil {
224			zoomLevel := w.view.ZoomLevel()
225			w.onZoomChanged(zoomLevel)
226		}
227	})
228
229	// Favicon changed - connect to FaviconDatabase
230	w.setupFaviconHandlers()
231
232	// Load changed
233	w.view.ConnectLoadChanged(func(event webkit.LoadEvent) {
234		// Handle load events if needed
235	})
236
237	// TLS error handling - connect to load-failed-with-tls-errors signal
238	w.setupTLSErrorHandler()
239
240	// Close
241	w.view.ConnectClose(func() {
242		if w.onClose != nil {
243			w.onClose()
244		}
245	})
246
247	// Create signal - for popup lifecycle management
248	w.view.ConnectCreate(func(navigationAction *webkit.NavigationAction) gtk.Widgetter {
249		log.Printf("[webkit] ConnectCreate callback fired for parent WebView ID=%d", w.id)
250		if w.onPopupCreate != nil {
251			log.Printf("[webkit] Calling onPopupCreate handler")
252			newWebView := w.onPopupCreate(navigationAction)
253			if newWebView != nil {
254				log.Printf("[webkit] onPopupCreate returned WebView wrapper ID=%d", newWebView.id)
255				log.Printf("[webkit] Extracting underlying gotk4 WebView from wrapper")
256				underlyingView := newWebView.view
257				log.Printf("[webkit] Extracted gotk4 WebView=%p, about to return to WebKit", underlyingView)
258				// Return the underlying WebView widget to WebKit
259				return underlyingView
260			}
261			log.Printf("[webkit] onPopupCreate returned nil, blocking popup")
262		}
263		// Return nil to cancel popup creation
264		log.Printf("[webkit] No onPopupCreate handler, blocking popup")
265		return nil
266	})
267
268	// Ready-to-show signal - emitted when popup is ready to be displayed
269	w.view.ConnectReadyToShow(func() {
270		if w.onReadyToShow != nil {
271			w.onReadyToShow()
272		}
273	})
274
275	// Script message received - connect to UserContentManager's script-message-received signal
276	// This receives messages from JavaScript via webkit.messageHandlers.dumber.postMessage()
277	// The signal name includes the handler name as a detail: "script-message-received::dumber"
278	ucm := w.view.UserContentManager()
279	if ucm != nil {
280		// GTK signals pass the sender as the first parameter, the actual signal data as subsequent parameters
281		ucm.Connect("script-message-received::dumber", func(sender interface{}, jscValue interface{}) {
282			if w.onScriptMessage != nil && jscValue != nil {
283				// Convert JSCValue to string using gotk4 javascriptcore bindings
284				valueStr := JSCValueToString(jscValue)
285				if valueStr != "" {
286					w.onScriptMessage(valueStr)
287				}
288			}
289		})
290	}
291}
292
293// LoadURL loads the given URL in the WebView
294func (w *WebView) LoadURL(url string) error {
295	w.mu.RLock()
296	defer w.mu.RUnlock()
297
298	if w.destroyed {
299		return ErrWebViewDestroyed
300	}
301
302	if url == "" {
303		return ErrInvalidURL
304	}
305
306	w.view.LoadURI(url)
307	return nil
308}
309
310// GetCurrentURL returns the current URL
311func (w *WebView) GetCurrentURL() string {
312	w.mu.RLock()
313	defer w.mu.RUnlock()
314
315	if w.destroyed {
316		return ""
317	}
318
319	return w.view.URI()
320}
321
322// GetTitle returns the current page title
323func (w *WebView) GetTitle() string {
324	w.mu.RLock()
325	defer w.mu.RUnlock()
326
327	if w.destroyed {
328		return ""
329	}
330
331	return w.view.Title()
332}
333
334// GoBack navigates back in history
335func (w *WebView) GoBack() error {
336	w.mu.RLock()
337	defer w.mu.RUnlock()
338
339	if w.destroyed {
340		return ErrWebViewDestroyed
341	}
342
343	w.view.GoBack()
344	return nil
345}
346
347// GoForward navigates forward in history
348func (w *WebView) GoForward() error {
349	w.mu.RLock()
350	defer w.mu.RUnlock()
351
352	if w.destroyed {
353		return ErrWebViewDestroyed
354	}
355
356	w.view.GoForward()
357	return nil
358}
359
360// Reload reloads the current page
361func (w *WebView) Reload() error {
362	w.mu.RLock()
363	defer w.mu.RUnlock()
364
365	if w.destroyed {
366		return ErrWebViewDestroyed
367	}
368
369	w.view.Reload()
370	return nil
371}
372
373// ReloadBypassCache reloads the current page, bypassing cache
374func (w *WebView) ReloadBypassCache() error {
375	w.mu.RLock()
376	defer w.mu.RUnlock()
377
378	if w.destroyed {
379		return ErrWebViewDestroyed
380	}
381
382	w.view.ReloadBypassCache()
383	return nil
384}
385
386// Show makes the WebView visible
387func (w *WebView) Show() error {
388	w.mu.RLock()
389	defer w.mu.RUnlock()
390
391	if w.destroyed {
392		return ErrWebViewDestroyed
393	}
394
395	// Make the WebView widget visible
396	w.view.SetVisible(true)
397
398	// Show the window containing the WebView
399	if w.window != nil {
400		w.window.Show()
401	}
402
403	return nil
404}
405
406// Hide makes the WebView invisible
407func (w *WebView) Hide() error {
408	w.mu.RLock()
409	defer w.mu.RUnlock()
410
411	if w.destroyed {
412		return ErrWebViewDestroyed
413	}
414
415	w.view.SetVisible(false)
416	return nil
417}
418
419// Destroy destroys the WebView and releases resources
420func (w *WebView) Destroy() error {
421	w.mu.Lock()
422	defer w.mu.Unlock()
423
424	if w.destroyed {
425		return nil
426	}
427
428	w.destroyed = true
429
430	// Unregister from global registry
431	viewMu.Lock()
432	delete(viewRegistry, w.id)
433	viewMu.Unlock()
434
435	// The GTK widget will be cleaned up by Go GC
436	return nil
437}
438
439// IsDestroyed returns true if the WebView has been destroyed
440func (w *WebView) IsDestroyed() bool {
441	w.mu.RLock()
442	defer w.mu.RUnlock()
443	return w.destroyed
444}
445
446// ID returns the unique identifier for this WebView
447func (w *WebView) ID() uint64 {
448	return w.id
449}
450
451// IDString returns the WebView ID as a string
452// This is a convenience method for compatibility with code expecting string IDs
453func (w *WebView) IDString() string {
454	return fmt.Sprintf("%d", w.id)
455}
456
457// ParseWebViewID converts a string WebView ID to uint64
458// Returns 0 if the string cannot be parsed
459func ParseWebViewID(idStr string) uint64 {
460	id, err := strconv.ParseUint(idStr, 10, 64)
461	if err != nil {
462		return 0
463	}
464	return id
465}
466
467// AsWidget returns the WebView as a gtk.Widgetter
468func (w *WebView) AsWidget() gtk.Widgetter {
469	if w == nil || w.view == nil {
470		return nil
471	}
472	return w.view
473}
474
475// Event handler registration methods
476
477// RegisterScriptMessageHandler registers a handler for script messages
478func (w *WebView) RegisterScriptMessageHandler(handler func(string)) {
479	w.mu.Lock()
480	defer w.mu.Unlock()
481	w.onScriptMessage = handler
482}
483
484// RegisterTitleChangedHandler registers a handler for title changes
485func (w *WebView) RegisterTitleChangedHandler(handler func(string)) {
486	w.mu.Lock()
487	defer w.mu.Unlock()
488	w.onTitleChanged = handler
489}
490
491// RegisterURIChangedHandler registers a handler for URI changes
492func (w *WebView) RegisterURIChangedHandler(handler func(string)) {
493	w.mu.Lock()
494	defer w.mu.Unlock()
495	w.onURIChanged = handler
496}
497
498// RegisterFaviconChangedHandler registers a handler for favicon changes
499func (w *WebView) RegisterFaviconChangedHandler(handler func([]byte)) {
500	w.mu.Lock()
501	defer w.mu.Unlock()
502	w.onFaviconChanged = handler
503}
504
505// RegisterFaviconURIChangedHandler registers a handler for favicon URI changes
506func (w *WebView) RegisterFaviconURIChangedHandler(handler func(pageURI, faviconURI string)) {
507	w.mu.Lock()
508	defer w.mu.Unlock()
509	w.onFaviconURIChanged = handler
510}
511
512// RegisterZoomChangedHandler registers a handler for zoom changes
513func (w *WebView) RegisterZoomChangedHandler(handler func(float64)) {
514	w.mu.Lock()
515	defer w.mu.Unlock()
516	w.onZoomChanged = handler
517}
518
519// RegisterCloseHandler registers a handler for close requests
520func (w *WebView) RegisterCloseHandler(handler func()) {
521	w.mu.Lock()
522	defer w.mu.Unlock()
523	w.onClose = handler
524}
525
526// RegisterPopupCreateHandler registers a handler for WebKit's create signal
527// This is called when JavaScript calls window.open() or a link with target="_blank" is clicked
528// The handler should create and return a new WebView for the popup, or return nil to block it
529// The returned WebView should NOT be shown yet - WebKit will emit ready-to-show when it's ready
530func (w *WebView) RegisterPopupCreateHandler(handler func(*webkit.NavigationAction) *WebView) {
531	w.mu.Lock()
532	defer w.mu.Unlock()
533	w.onPopupCreate = handler
534}
535
536// RegisterReadyToShowHandler registers a handler for WebKit's ready-to-show signal
537// This is called when a popup WebView is ready to be displayed
538// At this point it's safe to insert the popup into the UI (workspace, window, etc.)
539func (w *WebView) RegisterReadyToShowHandler(handler func()) {
540	w.mu.Lock()
541	defer w.mu.Unlock()
542	w.onReadyToShow = handler
543}
544
545// RegisterNavigationPolicyHandler registers a handler for navigation policy decisions
546func (w *WebView) RegisterNavigationPolicyHandler(handler func(url string, isUserGesture bool) bool) {
547	w.mu.Lock()
548	defer w.mu.Unlock()
549	w.onNavigationPolicy = handler
550}
551
552// OnWindowTypeDetected registers a handler for window type detection
553func (w *WebView) OnWindowTypeDetected(handler func(WindowType, *WindowFeatures)) {
554	w.mu.Lock()
555	defer w.mu.Unlock()
556	w.onWindowTypeDetected = handler
557}
558
559// RunOnMainThread executes a function on the GTK main thread
560func (w *WebView) RunOnMainThread(fn func()) {
561	RunOnMainThread(fn)
562}
563
564// GetWebView returns the underlying webkit.WebView for advanced operations
565func (w *WebView) GetWebView() *webkit.WebView {
566	return w.view
567}
568
569// Widget returns the WebView widget (for compatibility with old code expecting uintptr)
570// Deprecated: Use AsWidget() instead
571func (w *WebView) Widget() gtk.Widgetter {
572	return w.AsWidget()
573}
574
575// RootWidget returns the root container widget for this WebView
576// This is the GtkBox container that holds the WebView and can be reparented
577func (w *WebView) RootWidget() gtk.Widgetter {
578	if w.container != nil {
579		return w.container
580	}
581	// Fallback to WebView if container is not available (shouldn't happen)
582	return w.AsWidget()
583}
584
585// SetZoom sets the zoom level of the WebView
586func (w *WebView) SetZoom(zoom float64) error {
587	w.mu.RLock()
588	defer w.mu.RUnlock()
589
590	if w.destroyed {
591		return ErrWebViewDestroyed
592	}
593
594	w.view.SetZoomLevel(zoom)
595	return nil
596}
597
598// GetZoom returns the current zoom level
599func (w *WebView) GetZoom() float64 {
600	w.mu.RLock()
601	defer w.mu.RUnlock()
602
603	if w.destroyed {
604		return 1.0
605	}
606
607	return w.view.ZoomLevel()
608}
609
610// UsesDomZoom indicates if this WebView uses DOM-based zoom
611// In gotk4/WebKitGTK, zoom is always viewport-based
612func (w *WebView) UsesDomZoom() bool {
613	return false
614}
615
616// SeedDomZoom is a no-op in gotk4 as we use viewport zoom
617func (w *WebView) SeedDomZoom(zoom float64) error {
618	// Not needed in gotk4 - zoom is handled differently
619	return nil
620}
621
622// InjectScript executes JavaScript in the WebView
623func (w *WebView) InjectScript(script string) error {
624	w.mu.RLock()
625	defer w.mu.RUnlock()
626
627	if w.destroyed {
628		return ErrWebViewDestroyed
629	}
630
631	// Execute JavaScript using our CGO wrapper
632	EvaluateJavascript(w.view, script)
633	return nil
634}
635
636// DispatchCustomEvent dispatches a custom event via JavaScript
637func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {
638	// Serialize data to JSON
639	jsonData, err := json.Marshal(data)
640	if err != nil {
641		return fmt.Errorf("failed to marshal event data: %w", err)
642	}
643
644	log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData))
645
646	// Use document.dispatchEvent (not window) so events cross JavaScript world boundaries
647	// The GUI scripts run in an isolated world but share the same Document object
648	script := fmt.Sprintf(`
649		(function() {
650			try {
651				var detail = %s;
652				console.log('[dumber] Dispatching event: %s', detail);
653				document.dispatchEvent(new CustomEvent('%s', { detail: detail }));
654			} catch (e) {
655				console.error('[dumber] Failed to dispatch %s', e);
656			}
657		})();
658	`, string(jsonData), eventName, eventName, eventName)
659	return w.InjectScript(script)
660}
661
662// ShowDevTools opens the WebKit inspector/developer tools
663func (w *WebView) ShowDevTools() error {
664	w.mu.RLock()
665	defer w.mu.RUnlock()
666
667	if w.destroyed {
668		return ErrWebViewDestroyed
669	}
670
671	inspector := w.view.Inspector()
672	if inspector != nil {
673		inspector.Show()
674	}
675	return nil
676}
677
678// ShowPrintDialog shows the print dialog for the current page
679func (w *WebView) ShowPrintDialog() error {
680	w.mu.RLock()
681	defer w.mu.RUnlock()
682
683	if w.destroyed {
684		return ErrWebViewDestroyed
685	}
686
687	// Create and run print operation with GTK print dialog
688	printOp := webkit.NewPrintOperation(w.view)
689	printOp.RunDialog(nil) // nil = no parent window, uses active window
690	return nil
691}
692
693// RegisterKeyboardShortcut registers a keyboard shortcut handler
694// This is a compatibility method - actual shortcut handling is done at the window level
695func (w *WebView) RegisterKeyboardShortcut(key string, modifiers uint, handler func()) error {
696	// TODO: Implement keyboard shortcut registration if needed
697	// For now, shortcuts are handled at the window/application level
698	return nil
699}
700
701// SetWindowFeatures sets window features for popup windows
702func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
703	// This is typically used for popup windows
704	// The features would be applied when creating the window
705}
706
707// IsActive returns whether this WebView is currently active/focused
708func (w *WebView) IsActive() bool {
709	w.mu.RLock()
710	defer w.mu.RUnlock()
711
712	if w.destroyed {
713		return false
714	}
715
716	widget := w.view
717	if widget != nil {
718		return widget.IsFocus()
719	}
720	return false
721}
722
723// Window returns the parent Window of this WebView
724func (w *WebView) Window() *Window {
725	w.mu.RLock()
726	defer w.mu.RUnlock()
727
728	if w.destroyed {
729		return nil
730	}
731
732	return w.window
733}
734
735// GetFaviconDatabase returns the WebKit FaviconDatabase for this WebView
736// Returns nil if the favicon database is not available
737func (w *WebView) GetFaviconDatabase() *webkit.FaviconDatabase {
738	w.mu.RLock()
739	defer w.mu.RUnlock()
740
741	if w.destroyed || w.view == nil {
742		return nil
743	}
744
745	session := w.view.NetworkSession()
746	if session == nil {
747		return nil
748	}
749
750	dataManager := session.WebsiteDataManager()
751	if dataManager == nil {
752		return nil
753	}
754
755	return dataManager.FaviconDatabase()
756}
757
758// UpdateContentFilters updates the content filtering rules
759func (w *WebView) UpdateContentFilters(rules string) error {
760	w.mu.RLock()
761	defer w.mu.RUnlock()
762
763	if w.destroyed {
764		return ErrWebViewDestroyed
765	}
766
767	// Note: Content filtering is handled via InitializeContentBlocking()
768	// This method is kept for backward compatibility but is deprecated
769	log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
770	return nil
771}
772
773// InitializeContentBlocking initializes content blocking with filter lists
774func (w *WebView) InitializeContentBlocking(filterJSON []byte) error {
775	w.mu.RLock()
776	defer w.mu.RUnlock()
777
778	if w.destroyed {
779		return ErrWebViewDestroyed
780	}
781
782	if len(filterJSON) == 0 {
783		return fmt.Errorf("no filter rules provided")
784	}
785
786	// Apply filters using the content blocking API
787	return ApplyFiltersToWebView(w, filterJSON)
788}
789
790// OnNavigate registers a navigation handler
791func (w *WebView) OnNavigate(handler func(url string)) {
792	w.mu.Lock()
793	defer w.mu.Unlock()
794
795	// This wraps the URI changed handler
796	w.onURIChanged = handler
797}
798
799// RegisterWorkspaceNavigationHandler registers a handler for workspace pane navigation
800// The handler receives the direction ("up", "down", "left", "right") and returns true if handled
801func (w *WebView) RegisterWorkspaceNavigationHandler(handler func(direction string) bool) {
802	w.mu.Lock()
803	defer w.mu.Unlock()
804	w.onWorkspaceNavigation = handler
805}
806
807// RegisterPaneModeHandler registers a handler for pane mode shortcuts
808// The handler receives the action ("enter", "close", "split-right", etc.) and returns true if handled
809func (w *WebView) RegisterPaneModeHandler(handler func(action string) bool, isActiveChecker func() bool) {
810	w.mu.Lock()
811	defer w.mu.Unlock()
812	w.onPaneModeShortcut = handler
813	w.isPaneModeActive = isActiveChecker
814}
815
816// setupFaviconHandlers enables favicons for this WebView
817// Note: The actual favicon-changed handler is registered ONCE at the FaviconService level
818// to avoid duplicate handlers when multiple WebViews exist
819func (w *WebView) setupFaviconHandlers() {
820	// Get the NetworkSession from the WebView
821	session := w.view.NetworkSession()
822	if session == nil {
823		log.Printf("[webkit] Warning: No NetworkSession available for favicon handling")
824		return
825	}
826
827	// Get the WebsiteDataManager from the NetworkSession
828	dataManager := session.WebsiteDataManager()
829	if dataManager == nil {
830		log.Printf("[webkit] Warning: No WebsiteDataManager available for favicon handling")
831		return
832	}
833
834	// Enable favicons if not already enabled
835	if !dataManager.FaviconsEnabled() {
836		dataManager.SetFaviconsEnabled(true)
837		log.Printf("[webkit] Enabled favicons for WebView ID %d", w.id)
838	}
839
840}
841
842// GtkWebView returns the underlying gotk4 WebView for advanced operations
843// This is used when creating related views (popups) that need to share the same session
844func (w *WebView) GtkWebView() *webkit.WebView {
845	w.mu.RLock()
846	defer w.mu.RUnlock()
847	return w.view
848}
849
850// WrapBareWebView creates a minimal WebView wrapper around a bare gotk4 WebView
851// This is used during popup creation to return a WebView to WebKit before initialization
852// Full initialization (container, scripts, event handlers) should be done later
853func WrapBareWebView(bareView *webkit.WebView) *WebView {
854	if bareView == nil {
855		return nil
856	}
857
858	// Generate unique ID
859	id := atomic.AddUint64(&viewIDCounter, 1)
860	log.Printf("[webkit] Created minimal wrapper for bare WebView (ID: %d)", id)
861
862	// Create minimal wrapper - NO initialization yet
863	wv := &WebView{
864		view:      bareView,
865		container: nil, // Will be created during full initialization
866		window:    nil,
867		id:        id,
868		config:    nil, // Will be set during full initialization
869	}
870
871	// Register in global registry
872	viewMu.Lock()
873	viewRegistry[id] = wv
874	viewMu.Unlock()
875
876	return wv
877}
878
879// InitializeFromBare completes initialization of a bare WebView wrapper
880// This should be called in ready-to-show after WebKit has configured the view
881func (w *WebView) InitializeFromBare(cfg *Config) error {
882	w.mu.Lock()
883	defer w.mu.Unlock()
884
885	if w.config != nil {
886		return fmt.Errorf("WebView already initialized")
887	}
888
889	if cfg == nil {
890		cfg = GetDefaultConfig()
891	}
892
893	w.config = cfg
894
895	// Verify persistent session
896	viewSession := w.view.NetworkSession()
897	if viewSession == nil {
898		return fmt.Errorf("WebView has no network session")
899	}
900
901	// Setup UserContentManager and inject GUI scripts
902	if err := SetupUserContentManager(w.view, cfg.AppearanceConfigJSON, w.id); err != nil {
903		return fmt.Errorf("failed to setup user content manager: %w", err)
904	}
905
906	// Create container (GtkBox) to hold the WebView
907	container := gtk.NewBox(gtk.OrientationVertical, 0)
908	container.SetHExpand(true)
909	container.SetVExpand(true)
910
911	// Configure WebView widget for expansion
912	w.view.SetHExpand(true)
913	w.view.SetVExpand(true)
914
915	// Add WebView to container
916	container.Append(w.view)
917
918	w.container = container
919
920	// Apply configuration
921	if err := w.applyConfig(); err != nil {
922		return err
923	}
924
925	// Setup event handlers
926	w.setupEventHandlers()
927
928	// Attach keyboard bridge
929	w.AttachKeyboardBridge()
930
931	// Attach mouse gesture controls
932	w.AttachMouseGestures()
933
934	// Attach touchpad gesture controls
935	w.AttachTouchpadGestures()
936
937	log.Printf("[webkit] Completed initialization for WebView ID %d", w.id)
938
939	return nil
940}

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