v0.27.2 5960dd8

context.go RawHistoryFolder

1package webkit
2
3import (
4	"context"
5	"fmt"
6	"path/filepath"
7	"sync"
8
9	"github.com/bnema/dumber/internal/application/port"
10	"github.com/bnema/dumber/internal/logging"
11	"github.com/bnema/puregotk-webkit/webkit"
12	"github.com/rs/zerolog"
13)
14
15// FaviconDatabase is a type alias for webkit.FaviconDatabase.
16// Re-exported for use by UI layer without direct puregotk-webkit import.
17type FaviconDatabase = webkit.FaviconDatabase
18
19// WebKitContext manages the shared WebContext and persistent NetworkSession.
20// IMPORTANT: This MUST be initialized before creating any WebViews.
21type WebKitContext struct {
22	webContext     *webkit.WebContext
23	networkSession *webkit.NetworkSession
24	faviconDB      *webkit.FaviconDatabase
25
26	dataDir  string
27	cacheDir string
28
29	logger          zerolog.Logger
30	mu              sync.RWMutex
31	initialized     bool
32	downloadHandler *DownloadHandler
33}
34
35// NewWebKitContext creates and initializes a WebKitContext with a persistent NetworkSession.
36// The dataDir and cacheDir are used for cookie storage, cache, and other persistent data.
37// This MUST be called before creating any WebViews to ensure they use persistent storage.
38func NewWebKitContext(ctx context.Context, dataDir, cacheDir string) (*WebKitContext, error) {
39	return NewWebKitContextWithOptions(ctx, port.WebKitContextOptions{
40		DataDir:      dataDir,
41		CacheDir:     cacheDir,
42		CookiePolicy: port.WebKitCookiePolicyNoThirdParty,
43		ITPEnabled:   true,
44	})
45}
46
47// NewWebKitContextWithOptions creates and initializes a WebKitContext with the given options.
48// This allows configuring memory pressure settings for web and network processes.
49func NewWebKitContextWithOptions(ctx context.Context, opts port.WebKitContextOptions) (*WebKitContext, error) {
50	log := logging.FromContext(ctx).With().Str("component", "webkit-context").Logger()
51
52	if opts.DataDir == "" {
53		return nil, fmt.Errorf("data directory cannot be empty")
54	}
55	if opts.CacheDir == "" {
56		return nil, fmt.Errorf("cache directory cannot be empty")
57	}
58
59	wkCtx := &WebKitContext{
60		dataDir:  opts.DataDir,
61		cacheDir: opts.CacheDir,
62		logger:   log,
63	}
64
65	// Apply network process memory pressure settings BEFORE creating session
66	// Per WebKitGTK docs: must be called before creating any NetworkSession
67	if opts.IsNetworkProcessMemoryConfigured() {
68		applier := NewMemoryPressureApplier()
69		if err := applier.ApplyNetworkProcessSettings(ctx, opts.NetworkProcessMemory); err != nil {
70			log.Warn().Err(err).Msg("failed to apply network process memory pressure settings")
71		}
72	}
73
74	// Create persistent network session FIRST
75	// Per WebKitGTK 6.0 docs: "The first WebKitNetworkSession created becomes the default"
76	if err := wkCtx.initNetworkSession(opts); err != nil {
77		return nil, fmt.Errorf("failed to init network session: %w", err)
78	}
79
80	// Create WebContext - use custom constructor if memory pressure settings are configured
81	if opts.IsWebProcessMemoryConfigured() {
82		memSettings := buildMemoryPressureSettings(opts.WebProcessMemory)
83		if memSettings != nil {
84			wkCtx.webContext = webkit.NewWebContextWithMemoryPressureSettings(memSettings)
85			if wkCtx.webContext != nil {
86				log.Info().
87					Int("limit_mb", opts.WebProcessMemory.MemoryLimitMB).
88					Float64("poll_sec", opts.WebProcessMemory.PollIntervalSec).
89					Float64("conservative", opts.WebProcessMemory.ConservativeThreshold).
90					Float64("strict", opts.WebProcessMemory.StrictThreshold).
91					Msg("created WebContext with memory pressure settings")
92			}
93		}
94	}
95
96	// Fall back to default WebContext if custom creation failed or wasn't needed
97	if wkCtx.webContext == nil {
98		wkCtx.webContext = webkit.WebContextGetDefault()
99	}
100	if wkCtx.webContext == nil {
101		return nil, fmt.Errorf("failed to create or get WebContext")
102	}
103
104	// Set cache model for browser-style caching
105	wkCtx.webContext.SetCacheModel(webkit.CacheModelWebBrowserValue)
106
107	wkCtx.initialized = true
108	log.Info().
109		Str("data_dir", opts.DataDir).
110		Str("cache_dir", opts.CacheDir).
111		Msg("webkit context initialized")
112
113	return wkCtx, nil
114}
115
116// initNetworkSession creates and configures the persistent network session.
117func (c *WebKitContext) initNetworkSession(opts port.WebKitContextOptions) error {
118	// Create persistent network session
119	session := webkit.NewNetworkSession(&c.dataDir, &c.cacheDir)
120	if session == nil {
121		return fmt.Errorf("failed to create network session")
122	}
123
124	// Verify session is not ephemeral
125	if session.IsEphemeral() {
126		return fmt.Errorf("network session is ephemeral despite providing data directories")
127	}
128
129	// Verify WebsiteDataManager is not ephemeral
130	dataManager := session.GetWebsiteDataManager()
131	if dataManager == nil {
132		return fmt.Errorf("failed to get website data manager")
133	}
134	if dataManager.IsEphemeral() {
135		return fmt.Errorf("website data manager is ephemeral")
136	}
137
138	// Debug: verify the directories WebKit is actually using
139	actualDataDir := dataManager.GetBaseDataDirectory()
140	actualCacheDir := dataManager.GetBaseCacheDirectory()
141	c.logger.Info().
142		Str("expected_data_dir", c.dataDir).
143		Str("actual_data_dir", actualDataDir).
144		Str("expected_cache_dir", c.cacheDir).
145		Str("actual_cache_dir", actualCacheDir).
146		Msg("webkit directories configured")
147
148	// Enable favicon collection and get database reference
149	dataManager.SetFaviconsEnabled(true)
150	c.faviconDB = dataManager.GetFaviconDatabase()
151	if c.faviconDB != nil {
152		c.logger.Debug().Msg("favicon database enabled")
153	}
154
155	// Configure cookie storage
156	cookieManager := session.GetCookieManager()
157	if cookieManager == nil {
158		return fmt.Errorf("failed to get cookie manager")
159	}
160
161	cookiePath := filepath.Join(c.dataDir, "cookies.db")
162	cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqliteValue)
163	cookiePolicy, cookiePolicyLabel := mapCookiePolicy(opts.CookiePolicy)
164	cookieManager.SetAcceptPolicy(cookiePolicy)
165	session.SetItpEnabled(opts.ITPEnabled)
166
167	c.logger.Info().
168		Str("cookie_path", cookiePath).
169		Str("cookie_policy", cookiePolicyLabel).
170		Bool("itp_enabled", opts.ITPEnabled).
171		Msg("cookie storage configured")
172
173	// Enable persistent credential storage
174	session.SetPersistentCredentialStorageEnabled(true)
175
176	// Configure TLS errors to emit signals (enables load-failed-with-tls-errors)
177	session.SetTlsErrorsPolicy(webkit.TlsErrorsPolicyFailValue)
178
179	// Verify default session
180	defaultSession := webkit.NetworkSessionGetDefault()
181	if defaultSession == nil || defaultSession.IsEphemeral() {
182		c.logger.Warn().Msg("default session may not be persistent - created session might not be used by default")
183	}
184
185	c.networkSession = session
186	c.logger.Debug().Msg("network session initialized as persistent")
187
188	return nil
189}
190
191func mapCookiePolicy(policy port.WebKitCookiePolicy) (webkit.CookieAcceptPolicy, string) {
192	switch policy {
193	case port.WebKitCookiePolicyAlways:
194		return webkit.CookiePolicyAcceptAlwaysValue, string(port.WebKitCookiePolicyAlways)
195	case port.WebKitCookiePolicyNever:
196		return webkit.CookiePolicyAcceptNeverValue, string(port.WebKitCookiePolicyNever)
197	case port.WebKitCookiePolicyNoThirdParty, "":
198		return webkit.CookiePolicyAcceptNoThirdPartyValue, string(port.WebKitCookiePolicyNoThirdParty)
199	default:
200		return webkit.CookiePolicyAcceptNoThirdPartyValue, string(port.WebKitCookiePolicyNoThirdParty)
201	}
202}
203
204// Context returns the shared WebContext.
205func (c *WebKitContext) Context() *webkit.WebContext {
206	c.mu.RLock()
207	defer c.mu.RUnlock()
208	return c.webContext
209}
210
211// NetworkSession returns the persistent NetworkSession.
212func (c *WebKitContext) NetworkSession() *webkit.NetworkSession {
213	c.mu.RLock()
214	defer c.mu.RUnlock()
215	return c.networkSession
216}
217
218// FaviconDatabase returns the favicon database for persistent favicon storage.
219func (c *WebKitContext) FaviconDatabase() *webkit.FaviconDatabase {
220	c.mu.RLock()
221	defer c.mu.RUnlock()
222	return c.faviconDB
223}
224
225// DataDir returns the data directory path.
226func (c *WebKitContext) DataDir() string {
227	return c.dataDir
228}
229
230// CacheDir returns the cache directory path.
231func (c *WebKitContext) CacheDir() string {
232	return c.cacheDir
233}
234
235// IsInitialized returns true if the context has been successfully initialized.
236func (c *WebKitContext) IsInitialized() bool {
237	c.mu.RLock()
238	defer c.mu.RUnlock()
239	return c.initialized
240}
241
242// PrefetchDNS prefetches DNS for the given hostname to speed up future requests.
243func (c *WebKitContext) PrefetchDNS(hostname string) {
244	if hostname == "" || c.networkSession == nil {
245		return
246	}
247	c.networkSession.PrefetchDns(hostname)
248}
249
250// Close performs cleanup. Currently a no-op as WebKit handles cleanup internally.
251func (c *WebKitContext) Close() error {
252	c.mu.Lock()
253	defer c.mu.Unlock()
254
255	c.initialized = false
256	c.logger.Debug().Msg("webkit context closed")
257	return nil
258}
259
260// SetDownloadHandler configures download handling for this context.
261// The handler will be notified of all download events from the NetworkSession.
262func (c *WebKitContext) SetDownloadHandler(ctx context.Context, handler *DownloadHandler) {
263	c.mu.Lock()
264	c.downloadHandler = handler
265	session := c.networkSession
266	c.mu.Unlock()
267
268	if session == nil || handler == nil {
269		return
270	}
271
272	// Connect download-started signal on NetworkSession.
273	downloadStartedCb := func(_ webkit.NetworkSession, downloadPtr uintptr) {
274		download := webkit.DownloadNewFromInternalPtr(downloadPtr)
275		if download != nil {
276			handler.HandleDownload(ctx, download)
277		}
278	}
279	session.ConnectDownloadStarted(&downloadStartedCb)
280
281	c.logger.Debug().Msg("download handler connected to network session")
282}

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