v0.30.0 eb3068f

factory.go RawHistoryFolder

1package webkit
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/dumber/internal/application/port"
8	"github.com/bnema/dumber/internal/logging"
9)
10
11// WebViewFactory creates WebView instances for the application.
12// It supports creating regular WebViews (optionally via pool) and
13// related WebViews that share session/cookies with a parent (for popups).
14type WebViewFactory struct {
15	wkCtx         *WebKitContext
16	settings      *SettingsManager
17	pool          *WebViewPool
18	injector      *ContentInjector
19	router        *MessageRouter
20	filterApplier FilterApplier        // Optional content filter applier
21	ctxMenu       *contextMenuPipeline // Optional context menu pipeline
22
23	// Background color for WebViews (eliminates white flash)
24	bg bgColor
25}
26
27// NewWebViewFactory creates a new WebViewFactory.
28// The pool parameter is optional; if nil, WebViews are created directly.
29func NewWebViewFactory(
30	wkCtx *WebKitContext,
31	settings *SettingsManager,
32	pool *WebViewPool,
33	injector *ContentInjector,
34	router *MessageRouter,
35) *WebViewFactory {
36	return &WebViewFactory{
37		wkCtx:    wkCtx,
38		settings: settings,
39		pool:     pool,
40		injector: injector,
41		router:   router,
42	}
43}
44
45// SetFilterApplier sets the content filter applier.
46// Filters will be applied to all newly created WebViews.
47func (f *WebViewFactory) SetFilterApplier(applier FilterApplier) {
48	f.filterApplier = applier
49	// Also propagate to the pool if present
50	if f.pool != nil {
51		f.pool.SetFilterApplier(applier)
52	}
53}
54
55// SetBackgroundColor sets the background color for newly created WebViews.
56// This color is shown before content is painted, eliminating white flash.
57func (f *WebViewFactory) SetBackgroundColor(r, g, b, a float32) {
58	f.bg.set(r, g, b, a)
59	// Also propagate to the pool if present
60	if f.pool != nil {
61		f.pool.SetBackgroundColor(r, g, b, a)
62	}
63}
64
65// SetContextMenuPipeline sets the context menu pipeline for newly created WebViews.
66// Propagates to the pool if present.
67func (f *WebViewFactory) SetContextMenuPipeline(pipeline *contextMenuPipeline) {
68	f.ctxMenu = pipeline
69	if f.pool != nil {
70		f.pool.SetContextMenuPipeline(pipeline)
71	}
72}
73
74// Create creates a new WebView instance.
75// If a pool is configured, it will try to acquire from the pool first.
76func (f *WebViewFactory) Create(ctx context.Context) (*WebView, error) {
77	log := logging.FromContext(ctx)
78
79	// Try pool first if available
80	if f.pool != nil {
81		wv, err := f.pool.Acquire(ctx)
82		if err == nil && wv != nil {
83			log.Debug().Uint64("id", uint64(wv.ID())).Msg("acquired webview from pool")
84			return wv, nil
85		}
86		log.Debug().Err(err).Msg("pool acquire failed, creating directly")
87	}
88
89	// Create directly
90	return f.createDirect(ctx)
91}
92
93// CreateRelated creates a WebView that shares session/cookies with the parent.
94// This is required for popup windows to maintain authentication state (OAuth).
95// Related WebViews bypass the pool since they must be linked to a specific parent.
96func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebViewID) (*WebView, error) {
97	log := logging.FromContext(ctx)
98
99	// Look up parent WebView
100	parent := LookupWebView(parentID)
101	if parent == nil {
102		return nil, fmt.Errorf("parent webview %d not found", parentID)
103	}
104	if parent.IsDestroyed() {
105		return nil, fmt.Errorf("parent webview %d is destroyed", parentID)
106	}
107
108	// Create related WebView
109	wv, err := NewWebViewWithRelated(ctx, parent, f.settings)
110	if err != nil {
111		return nil, fmt.Errorf("create related webview: %w", err)
112	}
113
114	// Set background color to match theme (eliminates white flash)
115	if r, g, b, a := f.bg.get(); a > 0 {
116		wv.SetBackgroundColor(float64(r), float64(g), float64(b), float64(a))
117	}
118
119	// Keep hidden until content is painted
120	wv.inner.SetVisible(false)
121
122	// Attach frontend (scripts, message handler)
123	if err := wv.AttachFrontend(ctx, f.injector, f.router); err != nil {
124		log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to related webview")
125	}
126
127	// Apply content filters if configured
128	if f.filterApplier != nil {
129		f.filterApplier.ApplyTo(ctx, wv.ucm)
130	}
131
132	// Wire context menu if configured
133	if f.ctxMenu != nil {
134		wv.connectContextMenuSignal(f.ctxMenu)
135	}
136
137	log.Debug().
138		Uint64("id", uint64(wv.ID())).
139		Uint64("parent_id", uint64(parentID)).
140		Msg("created related webview for popup")
141
142	return wv, nil
143}
144
145// createDirect creates a WebView without using the pool.
146func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) {
147	log := logging.FromContext(ctx)
148
149	wv, err := NewWebView(ctx, f.wkCtx, f.settings, f.bg.toGdkRGBA())
150	if err != nil {
151		return nil, err
152	}
153
154	// Add CSS class for theme background styling (prevents white flash)
155	wv.inner.AddCssClass("webview-themed")
156
157	// Keep hidden until content is painted
158	wv.inner.SetVisible(false)
159
160	// Attach frontend
161	if err := wv.AttachFrontend(ctx, f.injector, f.router); err != nil {
162		log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to webview")
163	}
164
165	// Apply content filters if configured
166	if f.filterApplier != nil {
167		f.filterApplier.ApplyTo(ctx, wv.ucm)
168	}
169
170	// Wire context menu if configured
171	if f.ctxMenu != nil {
172		wv.connectContextMenuSignal(f.ctxMenu)
173	}
174
175	log.Debug().Uint64("id", uint64(wv.ID())).Msg("created webview directly")
176	return wv, nil
177}
178
179// Release returns a WebView to the pool if available, otherwise destroys it.
180// Related WebViews (popups) should be destroyed directly, not released to pool.
181func (f *WebViewFactory) Release(ctx context.Context, wv *WebView) {
182	if wv == nil || wv.IsDestroyed() {
183		return
184	}
185
186	if f.pool != nil {
187		f.pool.Release(ctx, wv)
188	} else {
189		wv.Destroy()
190	}
191}

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