v0.21.0 73bf644

pool.go RawHistoryFolder

1package webkit
2
3import (
4	"context"
5	"sync"
6	"sync/atomic"
7	"time"
8
9	"github.com/bnema/dumber/internal/logging"
10	"github.com/bnema/puregotk-webkit/webkit"
11)
12
13// FilterApplier applies content filters to a UserContentManager.
14// This interface decouples the pool from the filtering package.
15type FilterApplier interface {
16	ApplyTo(ctx context.Context, ucm *webkit.UserContentManager)
17}
18
19// PoolConfig configures the WebView pool behavior.
20type PoolConfig struct {
21	// MinSize is the minimum number of warm WebViews to maintain.
22	MinSize int
23	// MaxSize is the maximum number of WebViews to keep in the pool.
24	MaxSize int
25	// PrewarmCount is the number of WebViews to pre-create on startup.
26	PrewarmCount int
27	// IdleTimeout is how long idle views stay in the pool before being destroyed.
28	IdleTimeout time.Duration
29}
30
31// DefaultPoolConfig returns sensible defaults for the pool.
32func DefaultPoolConfig() PoolConfig {
33	return PoolConfig{
34		MinSize:      2,
35		MaxSize:      8,
36		PrewarmCount: 4, // Pre-create 4 WebViews for faster initial tab creation
37		IdleTimeout:  5 * time.Minute,
38	}
39}
40
41// WebViewPool manages a pool of pre-created WebViews for fast tab creation.
42type WebViewPool struct {
43	wkCtx    *WebKitContext
44	settings *SettingsManager
45	config   PoolConfig
46
47	pool          chan *WebView
48	injector      *ContentInjector
49	router        *MessageRouter
50	filterApplier FilterApplier // Optional content filter applier
51
52	// Background color for WebViews (eliminates white flash)
53	bgR, bgG, bgB, bgA float32
54	bgMu               sync.RWMutex
55
56	closed atomic.Bool
57	wg     sync.WaitGroup
58}
59
60// NewWebViewPool creates a new WebView pool.
61func NewWebViewPool(
62	ctx context.Context,
63	wkCtx *WebKitContext,
64	settings *SettingsManager,
65	cfg PoolConfig,
66	injector *ContentInjector,
67	router *MessageRouter,
68) *WebViewPool {
69	if cfg.MaxSize <= 0 {
70		cfg.MaxSize = 8
71	}
72	if cfg.MinSize <= 0 {
73		cfg.MinSize = 2
74	}
75	if cfg.MinSize > cfg.MaxSize {
76		cfg.MinSize = cfg.MaxSize
77	}
78	if ctx == nil {
79		ctx = context.Background()
80	}
81
82	p := &WebViewPool{
83		wkCtx:    wkCtx,
84		settings: settings,
85		config:   cfg,
86		pool:     make(chan *WebView, cfg.MaxSize),
87		injector: injector,
88		router:   router,
89	}
90
91	log := logging.FromContext(ctx)
92	log.Debug().Msg("creating webview pool")
93
94	return p
95}
96
97// SetFilterApplier sets the content filter applier.
98// Filters will be applied to all newly created WebViews.
99func (p *WebViewPool) SetFilterApplier(applier FilterApplier) {
100	p.filterApplier = applier
101}
102
103// SetBackgroundColor sets the background color for newly created WebViews.
104// This color is shown before content is painted, eliminating white flash.
105func (p *WebViewPool) SetBackgroundColor(r, g, b, a float32) {
106	p.bgMu.Lock()
107	p.bgR, p.bgG, p.bgB, p.bgA = r, g, b, a
108	p.bgMu.Unlock()
109}
110
111// Acquire gets a WebView from the pool or creates a new one.
112func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) {
113	log := logging.FromContext(ctx)
114
115	if p.closed.Load() {
116		return nil, context.Canceled
117	}
118
119	// Try to get from pool first (non-blocking)
120	select {
121	case wv := <-p.pool:
122		if wv != nil && !wv.IsDestroyed() {
123			// Ensure frontend is attached even if this WebView was pooled before injection was configured.
124			_ = wv.AttachFrontend(ctx, p.injector, p.router)
125			// Apply filters if available (may not have been applied during prewarm)
126			if p.filterApplier != nil {
127				p.filterApplier.ApplyTo(ctx, wv.ucm)
128			}
129			log.Debug().Uint64("id", uint64(wv.ID())).Msg("acquired webview from pool")
130			return wv, nil
131		}
132		// Fall through to create new if the pooled one was destroyed
133	default:
134		// Pool empty
135	}
136
137	// Pool empty or pooled view was destroyed, create new
138	log.Debug().Msg("pool empty, creating new webview")
139	return p.createWebView(ctx)
140}
141
142func (p *WebViewPool) createWebView(ctx context.Context) (*WebView, error) {
143	log := logging.FromContext(ctx)
144
145	wv, err := NewWebView(ctx, p.wkCtx, p.settings)
146	if err != nil {
147		return nil, err
148	}
149
150	// Set background color to match theme (eliminates white flash)
151	p.bgMu.RLock()
152	r, g, b, a := p.bgR, p.bgG, p.bgB, p.bgA
153	p.bgMu.RUnlock()
154	if a > 0 { // Only set if a valid color was configured
155		wv.SetBackgroundColor(r, g, b, a)
156	}
157
158	// Keep WebView hidden until content is painted (see content.go:onLoadCommitted)
159	wv.inner.SetVisible(false)
160
161	if err := wv.AttachFrontend(ctx, p.injector, p.router); err != nil {
162		log.Warn().Err(err).Msg("failed to attach frontend to new webview")
163	}
164
165	// Apply content filters if configured
166	if p.filterApplier != nil {
167		p.filterApplier.ApplyTo(ctx, wv.ucm)
168	}
169
170	return wv, nil
171}
172
173// Release returns a WebView to the pool.
174// For safety, we always destroy and never pool WebViews that have been used.
175// This prevents crashes from GLib signals firing after the widget tree is modified.
176// The pool is only used for prewarmed WebViews that haven't been attached to UI yet.
177func (p *WebViewPool) Release(ctx context.Context, wv *WebView) {
178	log := logging.FromContext(ctx)
179
180	if wv == nil || wv.IsDestroyed() || p.closed.Load() {
181		if wv != nil && !wv.IsDestroyed() {
182			wv.Destroy()
183		}
184		return
185	}
186
187	// CRITICAL: Always destroy used WebViews instead of pooling them.
188	// GLib signals are connected with closures that can fire asynchronously
189	// even after we clear Go callbacks. Loading about:blank triggers signals
190	// that can crash if the widget tree has been modified.
191	//
192	// The pool is designed for prewarmed WebViews only, not for recycling
193	// WebViews that have been attached to the UI.
194	log.Debug().Uint64("id", uint64(wv.ID())).Msg("destroying webview (not pooling used views)")
195	wv.Destroy()
196}
197
198// Prewarm creates WebViews synchronously to populate the pool.
199// Must be called from the GTK main thread (after GTK application is initialized).
200func (p *WebViewPool) Prewarm(ctx context.Context, count int) {
201	if count <= 0 {
202		count = p.config.PrewarmCount
203	}
204	if count <= 0 {
205		return
206	}
207	p.prewarmSync(ctx, count)
208}
209
210// prewarmSync creates WebViews synchronously.
211func (p *WebViewPool) prewarmSync(ctx context.Context, count int) {
212	log := logging.FromContext(ctx)
213
214	for i := 0; i < count && !p.closed.Load(); i++ {
215		// Check if pool is already at capacity
216		if len(p.pool) >= p.config.MaxSize {
217			break
218		}
219
220		wv, err := p.createWebView(ctx)
221		if err != nil {
222			log.Warn().Err(err).Msg("failed to prewarm webview")
223			continue
224		}
225
226		// Try to add to pool
227		select {
228		case p.pool <- wv:
229			log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool")
230		default:
231			// Pool full, destroy
232			wv.Destroy()
233		}
234
235		// Small delay between creations to avoid overwhelming the system
236		time.Sleep(50 * time.Millisecond)
237	}
238}
239
240// Size returns the current number of WebViews in the pool.
241func (p *WebViewPool) Size() int {
242	return len(p.pool)
243}
244
245// Close shuts down the pool and destroys all pooled WebViews.
246func (p *WebViewPool) Close(ctx context.Context) {
247	if p.closed.Swap(true) {
248		return // Already closed
249	}
250
251	log := logging.FromContext(ctx)
252
253	// Wait for any background operations
254	p.wg.Wait()
255
256	// Drain and destroy all pooled views
257	close(p.pool)
258	for wv := range p.pool {
259		if wv != nil && !wv.IsDestroyed() {
260			wv.Destroy()
261		}
262	}
263
264	log.Debug().Msg("webview pool closed")
265}

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