v0.32.0 0617278

context.go RawHistoryFolder

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

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