v0.20.0 c741a39

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: 2,
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	closed atomic.Bool
53	wg     sync.WaitGroup
54}
55
56// NewWebViewPool creates a new WebView pool.
57func NewWebViewPool(
58	ctx context.Context,
59	wkCtx *WebKitContext,
60	settings *SettingsManager,
61	cfg PoolConfig,
62	injector *ContentInjector,
63	router *MessageRouter,
64) *WebViewPool {
65	if cfg.MaxSize <= 0 {
66		cfg.MaxSize = 8
67	}
68	if cfg.MinSize <= 0 {
69		cfg.MinSize = 2
70	}
71	if cfg.MinSize > cfg.MaxSize {
72		cfg.MinSize = cfg.MaxSize
73	}
74	if ctx == nil {
75		ctx = context.Background()
76	}
77
78	p := &WebViewPool{
79		wkCtx:    wkCtx,
80		settings: settings,
81		config:   cfg,
82		pool:     make(chan *WebView, cfg.MaxSize),
83		injector: injector,
84		router:   router,
85	}
86
87	log := logging.FromContext(ctx)
88	log.Debug().Msg("creating webview pool")
89
90	return p
91}
92
93// SetFilterApplier sets the content filter applier.
94// Filters will be applied to all newly created WebViews.
95func (p *WebViewPool) SetFilterApplier(applier FilterApplier) {
96	p.filterApplier = applier
97}
98
99// Acquire gets a WebView from the pool or creates a new one.
100func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) {
101	log := logging.FromContext(ctx)
102
103	if p.closed.Load() {
104		return nil, context.Canceled
105	}
106
107	// Try to get from pool first (non-blocking)
108	select {
109	case wv := <-p.pool:
110		if wv != nil && !wv.IsDestroyed() {
111			// Ensure frontend is attached even if this WebView was pooled before injection was configured.
112			_ = wv.AttachFrontend(ctx, p.injector, p.router)
113			// Apply filters if available (may not have been applied during prewarm)
114			if p.filterApplier != nil {
115				p.filterApplier.ApplyTo(ctx, wv.ucm)
116			}
117			log.Debug().Uint64("id", uint64(wv.ID())).Msg("acquired webview from pool")
118			return wv, nil
119		}
120		// Fall through to create new if the pooled one was destroyed
121	default:
122		// Pool empty
123	}
124
125	// Pool empty or pooled view was destroyed, create new
126	log.Debug().Msg("pool empty, creating new webview")
127	return p.createWebView(ctx)
128}
129
130func (p *WebViewPool) createWebView(ctx context.Context) (*WebView, error) {
131	log := logging.FromContext(ctx)
132
133	wv, err := NewWebView(ctx, p.wkCtx, p.settings)
134	if err != nil {
135		return nil, err
136	}
137
138	// Ensure WebView is visible when used
139	wv.inner.SetVisible(true)
140
141	if err := wv.AttachFrontend(ctx, p.injector, p.router); err != nil {
142		log.Warn().Err(err).Msg("failed to attach frontend to new webview")
143	}
144
145	// Apply content filters if configured
146	if p.filterApplier != nil {
147		p.filterApplier.ApplyTo(ctx, wv.ucm)
148	}
149
150	return wv, nil
151}
152
153// Release returns a WebView to the pool.
154// For safety, we always destroy and never pool WebViews that have been used.
155// This prevents crashes from GLib signals firing after the widget tree is modified.
156// The pool is only used for prewarmed WebViews that haven't been attached to UI yet.
157func (p *WebViewPool) Release(ctx context.Context, wv *WebView) {
158	log := logging.FromContext(ctx)
159
160	if wv == nil || wv.IsDestroyed() || p.closed.Load() {
161		if wv != nil && !wv.IsDestroyed() {
162			wv.Destroy()
163		}
164		return
165	}
166
167	// CRITICAL: Always destroy used WebViews instead of pooling them.
168	// GLib signals are connected with closures that can fire asynchronously
169	// even after we clear Go callbacks. Loading about:blank triggers signals
170	// that can crash if the widget tree has been modified.
171	//
172	// The pool is designed for prewarmed WebViews only, not for recycling
173	// WebViews that have been attached to the UI.
174	log.Debug().Uint64("id", uint64(wv.ID())).Msg("destroying webview (not pooling used views)")
175	wv.Destroy()
176}
177
178// Prewarm creates WebViews synchronously to populate the pool.
179// Must be called from the GTK main thread (after GTK application is initialized).
180func (p *WebViewPool) Prewarm(ctx context.Context, count int) {
181	if count <= 0 {
182		count = p.config.PrewarmCount
183	}
184	if count <= 0 {
185		return
186	}
187	p.prewarmSync(ctx, count)
188}
189
190// prewarmSync creates WebViews synchronously.
191func (p *WebViewPool) prewarmSync(ctx context.Context, count int) {
192	log := logging.FromContext(ctx)
193
194	for i := 0; i < count && !p.closed.Load(); i++ {
195		// Check if pool is already at capacity
196		if len(p.pool) >= p.config.MaxSize {
197			break
198		}
199
200		wv, err := p.createWebView(ctx)
201		if err != nil {
202			log.Warn().Err(err).Msg("failed to prewarm webview")
203			continue
204		}
205
206		// Try to add to pool
207		select {
208		case p.pool <- wv:
209			log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool")
210		default:
211			// Pool full, destroy
212			wv.Destroy()
213		}
214
215		// Small delay between creations to avoid overwhelming the system
216		time.Sleep(50 * time.Millisecond)
217	}
218}
219
220// Size returns the current number of WebViews in the pool.
221func (p *WebViewPool) Size() int {
222	return len(p.pool)
223}
224
225// Close shuts down the pool and destroys all pooled WebViews.
226func (p *WebViewPool) Close(ctx context.Context) {
227	if p.closed.Swap(true) {
228		return // Already closed
229	}
230
231	log := logging.FromContext(ctx)
232
233	// Wait for any background operations
234	p.wg.Wait()
235
236	// Drain and destroy all pooled views
237	close(p.pool)
238	for wv := range p.pool {
239		if wv != nil && !wv.IsDestroyed() {
240			wv.Destroy()
241		}
242	}
243
244	log.Debug().Msg("webview pool closed")
245}

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