v0.8.0 0fa6785

webview.go RawHistoryFolder

1package browser
2
3import (
4	"context"
5	"log"
6	"os"
7	"path/filepath"
8	"strings"
9	"time"
10
11	"github.com/bnema/dumber/internal/app/constants"
12	"github.com/bnema/dumber/internal/app/control"
13	"github.com/bnema/dumber/internal/app/environment"
14	"github.com/bnema/dumber/internal/config"
15	"github.com/bnema/dumber/internal/filtering"
16	"github.com/bnema/dumber/pkg/webkit"
17)
18
19// buildMemoryConfig converts config.WebkitMemoryConfig to webkit.MemoryConfig
20func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig {
21	mc := webkit.MemoryConfig{
22		MemoryLimitMB:           cfg.MemoryLimitMB,
23		ConservativeThreshold:   cfg.ConservativeThreshold,
24		StrictThreshold:         cfg.StrictThreshold,
25		KillThreshold:           cfg.KillThreshold,
26		PollIntervalSeconds:     cfg.PollIntervalSeconds,
27		EnableGCInterval:        cfg.EnableGCInterval,
28		ProcessRecycleThreshold: cfg.ProcessRecycleThreshold,
29		EnablePageCache:         cfg.EnablePageCache,
30		EnableMemoryMonitoring:  cfg.EnableMemoryMonitoring,
31	}
32
33	// Convert string cache model to webkit constant
34	switch strings.ToLower(cfg.CacheModel) {
35	case "document_viewer", "documentviewer", "doc":
36		mc.CacheModel = webkit.CacheModelDocumentViewer
37	case "primary_web_browser", "primary", "primarywebbrowser":
38		mc.CacheModel = webkit.CacheModelPrimaryWebBrowser
39	default:
40		mc.CacheModel = webkit.CacheModelWebBrowser
41	}
42
43	return mc
44}
45
46// createWebView creates and configures the WebView
47func (app *BrowserApp) createWebView() error {
48	log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable())
49	dataDir, _ := config.GetDataDir()
50	stateDir, _ := config.GetStateDir()
51	webkitData := filepath.Join(dataDir, "webkit")
52	webkitCache := filepath.Join(stateDir, "webkit-cache")
53	_ = os.MkdirAll(webkitData, constants.DirPerm)
54	_ = os.MkdirAll(webkitCache, constants.DirPerm)
55
56	view, err := webkit.NewWebView(&webkit.Config{
57		Assets:                app.assets,
58		InitialURL:            "dumb://homepage",
59		ZoomDefault:           1.0,
60		EnableDeveloperExtras: true,
61		DataDir:               webkitData,
62		CacheDir:              webkitCache,
63		APIToken:              app.config.APISecurity.Token,
64		DefaultSansFont:       app.config.Appearance.SansFont,
65		DefaultSerifFont:      app.config.Appearance.SerifFont,
66		DefaultMonospaceFont:  app.config.Appearance.MonospaceFont,
67		DefaultFontSize:       app.config.Appearance.DefaultFontSize,
68		Rendering:             webkit.RenderingConfig{Mode: string(app.config.RenderingMode)},
69		VideoAcceleration: webkit.VideoAccelerationConfig{
70			EnableVAAPI:      app.config.VideoAcceleration.EnableVAAPI,
71			AutoDetectGPU:    app.config.VideoAcceleration.AutoDetectGPU,
72			VAAPIDriverName:  app.config.VideoAcceleration.VAAPIDriverName,
73			EnableAllDrivers: app.config.VideoAcceleration.EnableAllDrivers,
74			LegacyVAAPI:      app.config.VideoAcceleration.LegacyVAAPI,
75		},
76		Memory: buildMemoryConfig(app.config.WebkitMemory),
77		CodecPreferences: webkit.CodecPreferencesConfig{
78			PreferredCodecs:           strings.Split(app.config.CodecPreferences.PreferredCodecs, ","),
79			BlockedCodecs:             environment.BuildBlockedCodecsList(app.config.CodecPreferences),
80			ForceAV1:                  app.config.CodecPreferences.ForceAV1,
81			CustomUserAgent:           app.config.CodecPreferences.CustomUserAgent,
82			DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl,
83		},
84	})
85
86	if err != nil {
87		return err
88	}
89
90	app.webView = view
91	app.setupWebViewIntegration()
92	app.setupWebViewHandlers()
93	app.setupControllers()
94	app.setupKeyboardShortcuts()
95
96	// Apply initial zoom and show window
97	app.zoomController.ApplyInitialZoom()
98
99	log.Printf("Showing WebView window…")
100	if err := view.Show(); err != nil {
101		log.Printf("Warning: failed to show WebView: %v", err)
102	} else if !webkit.IsNativeAvailable() {
103		log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.")
104	}
105
106	return nil
107}
108
109// setupWebViewIntegration connects the WebView to browser services
110func (app *BrowserApp) setupWebViewIntegration() {
111	app.browserService.AttachWebView(app.webView)
112
113	// GUI bundle is loaded via WebKit user scripts in enableUserContentManager
114	// No need to load separately in browser service
115
116	// Use native window as title updater
117	if win := app.webView.Window(); win != nil {
118		app.browserService.SetWindowTitleUpdater(win)
119	}
120}
121
122// setupWebViewHandlers configures WebView event handlers
123func (app *BrowserApp) setupWebViewHandlers() {
124	// Persist page titles to DB when they change
125	app.webView.RegisterTitleChangedHandler(func(title string) {
126		ctx := context.Background()
127		url := app.webView.GetCurrentURL()
128		if url != "" && title != "" {
129			if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil {
130				log.Printf("Warning: failed to update page title: %v", err)
131			}
132		}
133	})
134
135	// Set WebView reference for message handler and register script messages
136	app.messageHandler.SetWebView(app.webView)
137	app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle)
138
139}
140
141// setupControllers initializes and configures controller objects
142func (app *BrowserApp) setupControllers() {
143	// Initialize controllers
144	app.zoomController = control.NewZoomController(app.browserService, app.webView)
145	app.navigationController = control.NewNavigationController(
146		app.parserService,
147		app.browserService,
148		app.webView,
149		app.zoomController,
150	)
151	app.clipboardController = control.NewClipboardController(app.webView)
152
153	// Register controller handlers
154	app.zoomController.RegisterHandlers()
155}
156
157// setupKeyboardShortcuts registers keyboard shortcuts
158func (app *BrowserApp) setupKeyboardShortcuts() {
159	app.shortcutHandler = NewShortcutHandler(app.webView, app.clipboardController)
160	app.shortcutHandler.RegisterShortcuts()
161}
162
163// setupContentBlocking initializes the content blocking system with proper timing
164func (app *BrowserApp) setupContentBlocking() error {
165	log.Printf("Initializing content blocking system...")
166
167	// Enable WebKit debug logging if requested
168	webkit.SetupWebKitDebugLogging(app.config)
169
170	defer func() {
171		if r := recover(); r != nil {
172			log.Printf("Content blocking setup panic recovered: %v", r)
173		}
174	}()
175
176	// Setup filter system
177	filterManager, err := filtering.SetupFilterSystem()
178	if err != nil {
179		log.Printf("Warning: Failed to setup filter system: %v", err)
180		return nil // Don't fail browser startup, continue without filters
181	}
182
183	// Store reference to filter manager
184	app.filterManager = filterManager
185
186	// Set up callback to re-apply network filters when they become ready
187	// This callback will be called from the async filter loading process
188	filterManager.SetFiltersReadyCallback(func() {
189		// Add small delay to avoid race conditions with WebView initialization
190		go func() {
191			time.Sleep(300 * time.Millisecond)
192			if app.webView != nil {
193				if err := app.webView.UpdateContentFilters(filterManager); err != nil {
194					log.Printf("Failed to apply filters after loading: %v", err)
195				} else {
196					log.Printf("Successfully applied filters after async loading")
197				}
198			}
199		}()
200	})
201
202	// Start async filter loading early (before WebView content blocking initialization)
203	// This allows filters to be ready when the WebView needs them
204	go func() {
205		if err := filtering.InitializeFiltersAsync(filterManager); err != nil {
206			log.Printf("Warning: failed to initialize filters asynchronously: %v", err)
207		}
208	}()
209
210	// Initialize content blocking in WebView with delay
211	// Wait for WebView to be fully loaded before setting up content blocking
212	go func() {
213		// Wait extra time for the first navigation to avoid preconnect interference
214		log.Printf("Waiting for WebView to complete initial load before enabling content blocking...")
215		time.Sleep(3000 * time.Millisecond) // 3 seconds for first load stability
216
217		if err := app.webView.InitializeContentBlocking(filterManager); err != nil {
218			log.Printf("Warning: Failed to initialize content blocking: %v", err)
219			// Continue without content blocking rather than failing
220		}
221
222		// Register navigation handler for domain-specific filtering and GUI injection
223		// Only register after content blocking is initialized
224		app.webView.RegisterURIChangedHandler(func(uri string) {
225			// Add small delay to avoid conflicts with page load
226			go func() {
227				time.Sleep(200 * time.Millisecond) // Slightly longer delay for stability
228
229				// Apply content filtering if available
230				if app.filterManager != nil {
231					app.webView.OnNavigate(uri, app.filterManager)
232				}
233
234				// Note: GUI bundle (controls and toast) is now injected as User Script
235				// in WebKit's enableUserContentManager, so it persists across all navigations
236			}()
237		})
238	}()
239
240	log.Printf("Content blocking system initialization started")
241	return nil
242}

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