v0.12.0 d035193

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

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