v0.13.0 976176a

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	// Performance optimizations for faster page transitions
201	// Enable page cache for instant back/forward navigation (bfcache)
202	settings.SetEnablePageCache(w.config.EnablePageCache)
203
204	// Enable smooth scrolling for better UX
205	settings.SetEnableSmoothScrolling(w.config.EnableSmoothScrolling)
206
207	return nil
208}
209
210// setupEventHandlers connects GTK signals to internal handlers
211func (w *WebView) setupEventHandlers() {
212	// Title changed - connect to notify::title signal
213	w.view.Connect("notify::title", func() {
214		if w.onTitleChanged != nil {
215			title := w.view.Title()
216			w.onTitleChanged(title)
217		}
218	})
219
220	// URI changed - connect to notify::uri signal
221	w.view.Connect("notify::uri", func() {
222		if w.onURIChanged != nil {
223			uri := w.view.URI()
224			w.onURIChanged(uri)
225		}
226	})
227
228	// Zoom level changed - connect to notify::zoom-level signal
229	w.view.Connect("notify::zoom-level", func() {
230		if w.onZoomChanged != nil {
231			zoomLevel := w.view.ZoomLevel()
232			w.onZoomChanged(zoomLevel)
233		}
234	})
235
236	// Favicon changed - connect to FaviconDatabase
237	w.setupFaviconHandlers()
238
239	// TLS error handling - connect to load-failed-with-tls-errors signal
240	w.setupTLSErrorHandler()
241
242	// Close
243	w.view.ConnectClose(func() {
244		if w.onClose != nil {
245			w.onClose()
246		}
247	})
248
249	// Create signal - for popup lifecycle management
250	w.view.ConnectCreate(func(navigationAction *webkit.NavigationAction) gtk.Widgetter {
251		log.Printf("[webkit] ConnectCreate callback fired for parent WebView ID=%d", w.id)
252		if w.onPopupCreate != nil {
253			log.Printf("[webkit] Calling onPopupCreate handler")
254			newWebView := w.onPopupCreate(navigationAction)
255			if newWebView != nil {
256				log.Printf("[webkit] onPopupCreate returned WebView wrapper ID=%d", newWebView.id)
257				log.Printf("[webkit] Extracting underlying gotk4 WebView from wrapper")
258				underlyingView := newWebView.view
259				log.Printf("[webkit] Extracted gotk4 WebView=%p, about to return to WebKit", underlyingView)
260				// Return the underlying WebView widget to WebKit
261				return underlyingView
262			}
263			log.Printf("[webkit] onPopupCreate returned nil, blocking popup")
264		}
265		// Return nil to cancel popup creation
266		log.Printf("[webkit] No onPopupCreate handler, blocking popup")
267		return nil
268	})
269
270	// Ready-to-show signal - emitted when popup is ready to be displayed
271	w.view.ConnectReadyToShow(func() {
272		if w.onReadyToShow != nil {
273			w.onReadyToShow()
274		}
275	})
276
277	// Script message received - connect to UserContentManager's script-message-received signal
278	// This receives messages from JavaScript via webkit.messageHandlers.dumber.postMessage()
279	// The signal name includes the handler name as a detail: "script-message-received::dumber"
280	ucm := w.view.UserContentManager()
281	if ucm != nil {
282		// GTK signals pass the sender as the first parameter, the actual signal data as subsequent parameters
283		ucm.Connect("script-message-received::dumber", func(sender interface{}, jscValue interface{}) {
284			if w.onScriptMessage != nil && jscValue != nil {
285				// Convert JSCValue to string using gotk4 javascriptcore bindings
286				valueStr := JSCValueToString(jscValue)
287				if valueStr != "" {
288					w.onScriptMessage(valueStr)
289				}
290			}
291		})
292	}
293}
294
295// LoadURL loads the given URL in the WebView
296func (w *WebView) LoadURL(url string) error {
297	w.mu.RLock()
298	defer w.mu.RUnlock()
299
300	if w.destroyed {
301		return ErrWebViewDestroyed
302	}
303
304	if url == "" {
305		return ErrInvalidURL
306	}
307
308	w.view.LoadURI(url)
309	return nil
310}
311
312// GetCurrentURL returns the current URL
313func (w *WebView) GetCurrentURL() string {
314	w.mu.RLock()
315	defer w.mu.RUnlock()
316
317	if w.destroyed {
318		return ""
319	}
320
321	return w.view.URI()
322}
323
324// GetTitle returns the current page title
325func (w *WebView) GetTitle() string {
326	w.mu.RLock()
327	defer w.mu.RUnlock()
328
329	if w.destroyed {
330		return ""
331	}
332
333	return w.view.Title()
334}
335
336// GoBack navigates back in history
337func (w *WebView) GoBack() error {
338	w.mu.RLock()
339	defer w.mu.RUnlock()
340
341	if w.destroyed {
342		return ErrWebViewDestroyed
343	}
344
345	w.view.GoBack()
346	return nil
347}
348
349// GoForward navigates forward in history
350func (w *WebView) GoForward() error {
351	w.mu.RLock()
352	defer w.mu.RUnlock()
353
354	if w.destroyed {
355		return ErrWebViewDestroyed
356	}
357
358	w.view.GoForward()
359	return nil
360}
361
362// Reload reloads the current page
363func (w *WebView) Reload() error {
364	w.mu.RLock()
365	defer w.mu.RUnlock()
366
367	if w.destroyed {
368		return ErrWebViewDestroyed
369	}
370
371	w.view.Reload()
372	return nil
373}
374
375// ReloadBypassCache reloads the current page, bypassing cache
376func (w *WebView) ReloadBypassCache() error {
377	w.mu.RLock()
378	defer w.mu.RUnlock()
379
380	if w.destroyed {
381		return ErrWebViewDestroyed
382	}
383
384	w.view.ReloadBypassCache()
385	return nil
386}
387
388// Show makes the WebView visible
389func (w *WebView) Show() error {
390	w.mu.RLock()
391	defer w.mu.RUnlock()
392
393	if w.destroyed {
394		return ErrWebViewDestroyed
395	}
396
397	// Make the WebView widget visible
398	w.view.SetVisible(true)
399
400	// Show the window containing the WebView
401	if w.window != nil {
402		w.window.Show()
403	}
404
405	return nil
406}
407
408// Hide makes the WebView invisible
409func (w *WebView) Hide() error {
410	w.mu.RLock()
411	defer w.mu.RUnlock()
412
413	if w.destroyed {
414		return ErrWebViewDestroyed
415	}
416
417	w.view.SetVisible(false)
418	return nil
419}
420
421// Destroy destroys the WebView and releases resources
422func (w *WebView) Destroy() error {
423	// DO NOT acquire w.mu lock here to avoid deadlock!
424	// After GTK main loop exits, any goroutine holding the lock and calling GTK methods
425	// will block forever waiting for the main loop. This causes Destroy() to deadlock
426	// trying to acquire the same lock.
427	//
428	// During shutdown, thread safety is not critical since we're tearing everything down.
429
430	if w.destroyed {
431		return nil
432	}
433
434	w.destroyed = true
435
436	// Unregister from global registry
437	viewMu.Lock()
438	delete(viewRegistry, w.id)
439	viewMu.Unlock()
440
441	// DO NOT call StopLoading() or any other GTK methods here!
442	// After the main loop exits, GTK calls will block forever.
443	// The GTK widget will be cleaned up by Go GC.
444
445	return nil
446}
447
448// IsDestroyed returns true if the WebView has been destroyed
449func (w *WebView) IsDestroyed() bool {
450	// DO NOT acquire lock - read of bool is atomic on all platforms Go supports
451	// This prevents deadlock during shutdown when GTK signal handlers check IsDestroyed
452	return w.destroyed
453}
454
455// ID returns the unique identifier for this WebView
456func (w *WebView) ID() uint64 {
457	return w.id
458}
459
460// IDString returns the WebView ID as a string
461// This is a convenience method for compatibility with code expecting string IDs
462func (w *WebView) IDString() string {
463	return fmt.Sprintf("%d", w.id)
464}
465
466// ParseWebViewID converts a string WebView ID to uint64
467// Returns 0 if the string cannot be parsed
468func ParseWebViewID(idStr string) uint64 {
469	id, err := strconv.ParseUint(idStr, 10, 64)
470	if err != nil {
471		return 0
472	}
473	return id
474}
475
476// AsWidget returns the WebView as a gtk.Widgetter
477func (w *WebView) AsWidget() gtk.Widgetter {
478	if w == nil || w.view == nil {
479		return nil
480	}
481	return w.view
482}
483
484// Event handler registration methods
485
486// RegisterScriptMessageHandler registers a handler for script messages
487func (w *WebView) RegisterScriptMessageHandler(handler func(string)) {
488	w.mu.Lock()
489	defer w.mu.Unlock()
490	w.onScriptMessage = handler
491}
492
493// RegisterTitleChangedHandler registers a handler for title changes
494func (w *WebView) RegisterTitleChangedHandler(handler func(string)) {
495	w.mu.Lock()
496	defer w.mu.Unlock()
497	w.onTitleChanged = handler
498}
499
500// RegisterURIChangedHandler registers a handler for URI changes
501func (w *WebView) RegisterURIChangedHandler(handler func(string)) {
502	w.mu.Lock()
503	defer w.mu.Unlock()
504	w.onURIChanged = handler
505}
506
507// RegisterFaviconChangedHandler registers a handler for favicon changes
508func (w *WebView) RegisterFaviconChangedHandler(handler func([]byte)) {
509	w.mu.Lock()
510	defer w.mu.Unlock()
511	w.onFaviconChanged = handler
512}
513
514// RegisterFaviconURIChangedHandler registers a handler for favicon URI changes
515func (w *WebView) RegisterFaviconURIChangedHandler(handler func(pageURI, faviconURI string)) {
516	w.mu.Lock()
517	defer w.mu.Unlock()
518	w.onFaviconURIChanged = handler
519}
520
521// RegisterZoomChangedHandler registers a handler for zoom changes
522func (w *WebView) RegisterZoomChangedHandler(handler func(float64)) {
523	w.mu.Lock()
524	defer w.mu.Unlock()
525	w.onZoomChanged = handler
526}
527
528// RegisterCloseHandler registers a handler for close requests
529func (w *WebView) RegisterCloseHandler(handler func()) {
530	w.mu.Lock()
531	defer w.mu.Unlock()
532	w.onClose = handler
533}
534
535// RegisterPopupCreateHandler registers a handler for WebKit's create signal
536// This is called when JavaScript calls window.open() or a link with target="_blank" is clicked
537// The handler should create and return a new WebView for the popup, or return nil to block it
538// The returned WebView should NOT be shown yet - WebKit will emit ready-to-show when it's ready
539func (w *WebView) RegisterPopupCreateHandler(handler func(*webkit.NavigationAction) *WebView) {
540	w.mu.Lock()
541	defer w.mu.Unlock()
542	w.onPopupCreate = handler
543}
544
545// RegisterReadyToShowHandler registers a handler for WebKit's ready-to-show signal
546// This is called when a popup WebView is ready to be displayed
547// At this point it's safe to insert the popup into the UI (workspace, window, etc.)
548func (w *WebView) RegisterReadyToShowHandler(handler func()) {
549	w.mu.Lock()
550	defer w.mu.Unlock()
551	w.onReadyToShow = handler
552}
553
554// RegisterNavigationPolicyHandler registers a handler for navigation policy decisions
555func (w *WebView) RegisterNavigationPolicyHandler(handler func(url string, isUserGesture bool) bool) {
556	w.mu.Lock()
557	defer w.mu.Unlock()
558	w.onNavigationPolicy = handler
559}
560
561// OnWindowTypeDetected registers a handler for window type detection
562func (w *WebView) OnWindowTypeDetected(handler func(WindowType, *WindowFeatures)) {
563	w.mu.Lock()
564	defer w.mu.Unlock()
565	w.onWindowTypeDetected = handler
566}
567
568// RunOnMainThread executes a function on the GTK main thread
569func (w *WebView) RunOnMainThread(fn func()) {
570	RunOnMainThread(fn)
571}
572
573// GetWebView returns the underlying webkit.WebView for advanced operations
574func (w *WebView) GetWebView() *webkit.WebView {
575	return w.view
576}
577
578// Widget returns the WebView widget (for compatibility with old code expecting uintptr)
579// Deprecated: Use AsWidget() instead
580func (w *WebView) Widget() gtk.Widgetter {
581	return w.AsWidget()
582}
583
584// RootWidget returns the root container widget for this WebView
585// This is the GtkBox container that holds the WebView and can be reparented
586func (w *WebView) RootWidget() gtk.Widgetter {
587	if w.container != nil {
588		return w.container
589	}
590	// Fallback to WebView if container is not available (shouldn't happen)
591	return w.AsWidget()
592}
593
594// SetZoom sets the zoom level of the WebView
595func (w *WebView) SetZoom(zoom float64) error {
596	w.mu.RLock()
597	defer w.mu.RUnlock()
598
599	if w.destroyed {
600		return ErrWebViewDestroyed
601	}
602
603	w.view.SetZoomLevel(zoom)
604	return nil
605}
606
607// GetZoom returns the current zoom level
608func (w *WebView) GetZoom() float64 {
609	w.mu.RLock()
610	defer w.mu.RUnlock()
611
612	if w.destroyed {
613		return 1.0
614	}
615
616	return w.view.ZoomLevel()
617}
618
619// UsesDomZoom indicates if this WebView uses DOM-based zoom
620// In gotk4/WebKitGTK, zoom is always viewport-based
621func (w *WebView) UsesDomZoom() bool {
622	return false
623}
624
625// SeedDomZoom is a no-op in gotk4 as we use viewport zoom
626func (w *WebView) SeedDomZoom(zoom float64) error {
627	// Not needed in gotk4 - zoom is handled differently
628	return nil
629}
630
631// InjectScript executes JavaScript in the WebView
632func (w *WebView) InjectScript(script string) error {
633	w.mu.RLock()
634	defer w.mu.RUnlock()
635
636	if w.destroyed {
637		return ErrWebViewDestroyed
638	}
639
640	// Execute JavaScript using our CGO wrapper
641	EvaluateJavascript(w.view, script)
642	return nil
643}
644
645// DispatchCustomEvent dispatches a custom event via JavaScript
646func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {
647	// Serialize data to JSON
648	jsonData, err := json.Marshal(data)
649	if err != nil {
650		return fmt.Errorf("failed to marshal event data: %w", err)
651	}
652
653	log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData))
654
655	// Use document.dispatchEvent (not window) so events cross JavaScript world boundaries
656	// The GUI scripts run in an isolated world but share the same Document object
657	script := fmt.Sprintf(`
658		(function() {
659			try {
660				var detail = %s;
661				console.log('[dumber] Dispatching event: %s', detail);
662				document.dispatchEvent(new CustomEvent('%s', { detail: detail }));
663			} catch (e) {
664				console.error('[dumber] Failed to dispatch %s', e);
665			}
666		})();
667	`, string(jsonData), eventName, eventName, eventName)
668	return w.InjectScript(script)
669}
670
671// ShowDevTools opens the WebKit inspector/developer tools
672func (w *WebView) ShowDevTools() error {
673	w.mu.RLock()
674	defer w.mu.RUnlock()
675
676	if w.destroyed {
677		return ErrWebViewDestroyed
678	}
679
680	inspector := w.view.Inspector()
681	if inspector != nil {
682		inspector.Show()
683	}
684	return nil
685}
686
687// ShowPrintDialog shows the print dialog for the current page
688func (w *WebView) ShowPrintDialog() error {
689	w.mu.RLock()
690	defer w.mu.RUnlock()
691
692	if w.destroyed {
693		return ErrWebViewDestroyed
694	}
695
696	// Create and run print operation with GTK print dialog
697	printOp := webkit.NewPrintOperation(w.view)
698	printOp.RunDialog(nil) // nil = no parent window, uses active window
699	return nil
700}
701
702// RegisterKeyboardShortcut registers a keyboard shortcut handler
703// This is a compatibility method - actual shortcut handling is done at the window level
704func (w *WebView) RegisterKeyboardShortcut(key string, modifiers uint, handler func()) error {
705	// TODO: Implement keyboard shortcut registration if needed
706	// For now, shortcuts are handled at the window/application level
707	return nil
708}
709
710// SetWindowFeatures sets window features for popup windows
711func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
712	// This is typically used for popup windows
713	// The features would be applied when creating the window
714}
715
716// IsActive returns whether this WebView is currently active/focused
717func (w *WebView) IsActive() bool {
718	w.mu.RLock()
719	defer w.mu.RUnlock()
720
721	if w.destroyed {
722		return false
723	}
724
725	widget := w.view
726	if widget != nil {
727		return widget.IsFocus()
728	}
729	return false
730}
731
732// Window returns the parent Window of this WebView
733func (w *WebView) Window() *Window {
734	w.mu.RLock()
735	defer w.mu.RUnlock()
736
737	if w.destroyed {
738		return nil
739	}
740
741	return w.window
742}
743
744// GetFaviconDatabase returns the WebKit FaviconDatabase for this WebView
745// Returns nil if the favicon database is not available
746func (w *WebView) GetFaviconDatabase() *webkit.FaviconDatabase {
747	w.mu.RLock()
748	defer w.mu.RUnlock()
749
750	if w.destroyed || w.view == nil {
751		return nil
752	}
753
754	session := w.view.NetworkSession()
755	if session == nil {
756		return nil
757	}
758
759	dataManager := session.WebsiteDataManager()
760	if dataManager == nil {
761		return nil
762	}
763
764	return dataManager.FaviconDatabase()
765}
766
767// UpdateContentFilters updates the content filtering rules
768func (w *WebView) UpdateContentFilters(rules string) error {
769	w.mu.RLock()
770	defer w.mu.RUnlock()
771
772	if w.destroyed {
773		return ErrWebViewDestroyed
774	}
775
776	// Note: Content filtering is handled via InitializeContentBlocking()
777	// This method is kept for backward compatibility but is deprecated
778	log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
779	return nil
780}
781
782// InitializeContentBlocking initializes content blocking with filter lists
783func (w *WebView) InitializeContentBlocking(filterJSON []byte) error {
784	w.mu.RLock()
785	defer w.mu.RUnlock()
786
787	if w.destroyed {
788		return ErrWebViewDestroyed
789	}
790
791	if len(filterJSON) == 0 {
792		return fmt.Errorf("no filter rules provided")
793	}
794
795	// Apply filters using the content blocking API
796	return ApplyFiltersToWebView(w, filterJSON)
797}
798
799// OnNavigate registers a navigation handler
800func (w *WebView) OnNavigate(handler func(url string)) {
801	w.mu.Lock()
802	defer w.mu.Unlock()
803
804	// This wraps the URI changed handler
805	w.onURIChanged = handler
806}
807
808// RegisterWorkspaceNavigationHandler registers a handler for workspace pane navigation
809// The handler receives the direction ("up", "down", "left", "right") and returns true if handled
810func (w *WebView) RegisterWorkspaceNavigationHandler(handler func(direction string) bool) {
811	w.mu.Lock()
812	defer w.mu.Unlock()
813	w.onWorkspaceNavigation = handler
814}
815
816// RegisterPaneModeHandler registers a handler for pane mode shortcuts
817// The handler receives the action ("enter", "close", "split-right", etc.) and returns true if handled
818func (w *WebView) RegisterPaneModeHandler(handler func(action string) bool, isActiveChecker func() bool) {
819	w.mu.Lock()
820	defer w.mu.Unlock()
821	w.onPaneModeShortcut = handler
822	w.isPaneModeActive = isActiveChecker
823}
824
825// setupFaviconHandlers enables favicons for this WebView
826// Note: The actual favicon-changed handler is registered ONCE at the FaviconService level
827// to avoid duplicate handlers when multiple WebViews exist
828func (w *WebView) setupFaviconHandlers() {
829	// Get the NetworkSession from the WebView
830	session := w.view.NetworkSession()
831	if session == nil {
832		log.Printf("[webkit] Warning: No NetworkSession available for favicon handling")
833		return
834	}
835
836	// Get the WebsiteDataManager from the NetworkSession
837	dataManager := session.WebsiteDataManager()
838	if dataManager == nil {
839		log.Printf("[webkit] Warning: No WebsiteDataManager available for favicon handling")
840		return
841	}
842
843	// Enable favicons if not already enabled
844	if !dataManager.FaviconsEnabled() {
845		dataManager.SetFaviconsEnabled(true)
846		log.Printf("[webkit] Enabled favicons for WebView ID %d", w.id)
847	}
848
849}
850
851// GtkWebView returns the underlying gotk4 WebView for advanced operations
852// This is used when creating related views (popups) that need to share the same session
853func (w *WebView) GtkWebView() *webkit.WebView {
854	w.mu.RLock()
855	defer w.mu.RUnlock()
856	return w.view
857}
858
859// WrapBareWebView creates a minimal WebView wrapper around a bare gotk4 WebView
860// This is used during popup creation to return a WebView to WebKit before initialization
861// Full initialization (container, scripts, event handlers) should be done later
862func WrapBareWebView(bareView *webkit.WebView) *WebView {
863	if bareView == nil {
864		return nil
865	}
866
867	// Generate unique ID
868	id := atomic.AddUint64(&viewIDCounter, 1)
869	log.Printf("[webkit] Created minimal wrapper for bare WebView (ID: %d)", id)
870
871	// Create minimal wrapper - NO initialization yet
872	wv := &WebView{
873		view:      bareView,
874		container: nil, // Will be created during full initialization
875		window:    nil,
876		id:        id,
877		config:    nil, // Will be set during full initialization
878	}
879
880	// Register in global registry
881	viewMu.Lock()
882	viewRegistry[id] = wv
883	viewMu.Unlock()
884
885	return wv
886}
887
888// InitializeFromBare completes initialization of a bare WebView wrapper
889// This should be called in ready-to-show after WebKit has configured the view
890func (w *WebView) InitializeFromBare(cfg *Config) error {
891	w.mu.Lock()
892	defer w.mu.Unlock()
893
894	if w.config != nil {
895		return fmt.Errorf("WebView already initialized")
896	}
897
898	if cfg == nil {
899		cfg = GetDefaultConfig()
900	}
901
902	w.config = cfg
903
904	// Verify persistent session
905	viewSession := w.view.NetworkSession()
906	if viewSession == nil {
907		return fmt.Errorf("WebView has no network session")
908	}
909
910	// Setup UserContentManager and inject GUI scripts
911	if err := SetupUserContentManager(w.view, cfg.AppearanceConfigJSON, w.id); err != nil {
912		return fmt.Errorf("failed to setup user content manager: %w", err)
913	}
914
915	// Create container (GtkBox) to hold the WebView
916	container := gtk.NewBox(gtk.OrientationVertical, 0)
917	container.SetHExpand(true)
918	container.SetVExpand(true)
919
920	// Configure WebView widget for expansion
921	w.view.SetHExpand(true)
922	w.view.SetVExpand(true)
923
924	// Add WebView to container
925	container.Append(w.view)
926
927	w.container = container
928
929	// Apply configuration
930	if err := w.applyConfig(); err != nil {
931		return err
932	}
933
934	// Setup event handlers
935	w.setupEventHandlers()
936
937	// Attach keyboard bridge
938	w.AttachKeyboardBridge()
939
940	// Attach mouse gesture controls
941	w.AttachMouseGestures()
942
943	// Attach touchpad gesture controls
944	w.AttachTouchpadGestures()
945
946	log.Printf("[webkit] Completed initialization for WebView ID %d", w.id)
947
948	return nil
949}

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