detached 10992b6

feat(cef): add Engine implementing port.Engine with adapters

Introduce Engine struct that implements port.Engine for the CEF backend.
NewEngine initializes CEF, starts the message pump via glib.IdleAdd, and
creates the GL loader, factory, and pool. Adapter types bridge concrete
CEF types to port interfaces, with no-op stubs for Phase 1 subsystems
(ContentInjector, SettingsApplier, FaviconDatabase).
commit
10992b61b815caa224e9d069a31a3ea9022e5eb0
author
brice · · 2026-03-19 15:44 UTC
committer
brice · · 2026-03-28 04:35 UTC
parent
440f59b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +267 -0
A internal/infrastructure/cef/engine.go +100 -0
@@ -0,0 +1,100 @@
1+package cef
2+
3+import (
4+ "context"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ "github.com/rs/zerolog"
8+
9+ "github.com/bnema/dumber/internal/application/port"
10+)
11+
12+// Compile-time interface check.
13+var _ port.Engine = (*Engine)(nil)
14+
15+// Engine implements port.Engine for the CEF browser backend.
16+// It manages the CEF lifecycle and provides access to all engine subsystems.
17+type Engine struct {
18+ ctx context.Context
19+ gl *glLoader
20+ factory *WebViewFactory
21+ pool *WebViewPool
22+ logger zerolog.Logger
23+}
24+
25+// Factory returns the WebViewFactory for creating new WebView instances.
26+func (e *Engine) Factory() port.WebViewFactory {
27+ return &webViewFactoryAdapter{factory: e.factory}
28+}
29+
30+// Pool returns the WebViewPool for acquiring pre-warmed WebView instances.
31+func (e *Engine) Pool() port.WebViewPool {
32+ return &webViewPoolAdapter{pool: e.pool, ctx: e.ctx}
33+}
34+
35+// ContentInjector returns a no-op injector (Phase 1 stub).
36+func (e *Engine) ContentInjector() port.ContentInjector {
37+ return &noopContentInjector{}
38+}
39+
40+// SettingsApplier returns a no-op applier (Phase 1 stub).
41+func (e *Engine) SettingsApplier() port.SettingsApplier {
42+ return &noopSettingsApplier{}
43+}
44+
45+// FilterApplier returns nil (content filtering not yet supported for CEF).
46+func (e *Engine) FilterApplier() port.FilterApplier {
47+ return nil
48+}
49+
50+// FaviconDatabase returns a no-op database (Phase 1 stub).
51+func (e *Engine) FaviconDatabase() port.FaviconDatabase {
52+ return &noopFaviconDatabase{}
53+}
54+
55+// InternalSchemePath returns the URI scheme used for internal app resources.
56+func (e *Engine) InternalSchemePath() string {
57+ return "cef://dumber/"
58+}
59+
60+// Close releases all resources held by the engine.
61+func (e *Engine) Close() error {
62+ e.pool.Close()
63+ purecef.Shutdown()
64+ return nil
65+}
66+
67+// RegisterHandlers registers all WebUI message bridge handlers (Phase 1 stub).
68+func (e *Engine) RegisterHandlers(_ context.Context, _ port.HandlerDependencies) error {
69+ return nil
70+}
71+
72+// RegisterAccentHandlers registers accent/dead-key input handlers (Phase 1 stub).
73+func (e *Engine) RegisterAccentHandlers(_ context.Context, _ port.AccentKeyHandler) error {
74+ return nil
75+}
76+
77+// ConfigureDownloads sets up download handling (Phase 1 stub).
78+func (e *Engine) ConfigureDownloads(_ context.Context, _ string, _ port.DownloadEventHandler, _ port.DownloadPreparer) error {
79+ return nil
80+}
81+
82+// OnToolkitReady is called after the UI toolkit has initialized (Phase 1 stub).
83+func (e *Engine) OnToolkitReady(_ context.Context) error {
84+ return nil
85+}
86+
87+// UpdateAppearance updates default background color for new WebViews (Phase 1 stub).
88+func (e *Engine) UpdateAppearance(_ context.Context, _, _, _, _ float64) error {
89+ return nil
90+}
91+
92+// UpdateSettings applies runtime config changes to engine internals (Phase 1 stub).
93+func (e *Engine) UpdateSettings(_ context.Context, _ port.EngineSettingsUpdate) error {
94+ return nil
95+}
96+
97+// SetHandlerContext sets the base context for message handler dispatch.
98+func (e *Engine) SetHandlerContext(ctx context.Context) {
99+ e.ctx = ctx
100+}
@@ -0,0 +1,100 @@
1+package cef
2+
3+import (
4+ "context"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ "github.com/rs/zerolog"
8+
9+ "github.com/bnema/dumber/internal/application/port"
10+)
11+
12+// Compile-time interface check.
13+var _ port.Engine = (*Engine)(nil)
14+
15+// Engine implements port.Engine for the CEF browser backend.
16+// It manages the CEF lifecycle and provides access to all engine subsystems.
17+type Engine struct {
18+ ctx context.Context
19+ gl *glLoader
20+ factory *WebViewFactory
21+ pool *WebViewPool
22+ logger zerolog.Logger
23+}
24+
25+// Factory returns the WebViewFactory for creating new WebView instances.
26+func (e *Engine) Factory() port.WebViewFactory {
27+ return &webViewFactoryAdapter{factory: e.factory}
28+}
29+
30+// Pool returns the WebViewPool for acquiring pre-warmed WebView instances.
31+func (e *Engine) Pool() port.WebViewPool {
32+ return &webViewPoolAdapter{pool: e.pool, ctx: e.ctx}
33+}
34+
35+// ContentInjector returns a no-op injector (Phase 1 stub).
36+func (e *Engine) ContentInjector() port.ContentInjector {
37+ return &noopContentInjector{}
38+}
39+
40+// SettingsApplier returns a no-op applier (Phase 1 stub).
41+func (e *Engine) SettingsApplier() port.SettingsApplier {
42+ return &noopSettingsApplier{}
43+}
44+
45+// FilterApplier returns nil (content filtering not yet supported for CEF).
46+func (e *Engine) FilterApplier() port.FilterApplier {
47+ return nil
48+}
49+
50+// FaviconDatabase returns a no-op database (Phase 1 stub).
51+func (e *Engine) FaviconDatabase() port.FaviconDatabase {
52+ return &noopFaviconDatabase{}
53+}
54+
55+// InternalSchemePath returns the URI scheme used for internal app resources.
56+func (e *Engine) InternalSchemePath() string {
57+ return "cef://dumber/"
58+}
59+
60+// Close releases all resources held by the engine.
61+func (e *Engine) Close() error {
62+ e.pool.Close()
63+ purecef.Shutdown()
64+ return nil
65+}
66+
67+// RegisterHandlers registers all WebUI message bridge handlers (Phase 1 stub).
68+func (e *Engine) RegisterHandlers(_ context.Context, _ port.HandlerDependencies) error {
69+ return nil
70+}
71+
72+// RegisterAccentHandlers registers accent/dead-key input handlers (Phase 1 stub).
73+func (e *Engine) RegisterAccentHandlers(_ context.Context, _ port.AccentKeyHandler) error {
74+ return nil
75+}
76+
77+// ConfigureDownloads sets up download handling (Phase 1 stub).
78+func (e *Engine) ConfigureDownloads(_ context.Context, _ string, _ port.DownloadEventHandler, _ port.DownloadPreparer) error {
79+ return nil
80+}
81+
82+// OnToolkitReady is called after the UI toolkit has initialized (Phase 1 stub).
83+func (e *Engine) OnToolkitReady(_ context.Context) error {
84+ return nil
85+}
86+
87+// UpdateAppearance updates default background color for new WebViews (Phase 1 stub).
88+func (e *Engine) UpdateAppearance(_ context.Context, _, _, _, _ float64) error {
89+ return nil
90+}
91+
92+// UpdateSettings applies runtime config changes to engine internals (Phase 1 stub).
93+func (e *Engine) UpdateSettings(_ context.Context, _ port.EngineSettingsUpdate) error {
94+ return nil
95+}
96+
97+// SetHandlerContext sets the base context for message handler dispatch.
98+func (e *Engine) SetHandlerContext(ctx context.Context) {
99+ e.ctx = ctx
100+}
A internal/infrastructure/cef/engine_adapters.go +109 -0
@@ -0,0 +1,109 @@
1+package cef
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+ "github.com/bnema/dumber/internal/logging"
9+)
10+
11+// --- WebViewFactory adapter ---
12+
13+// webViewFactoryAdapter bridges *WebViewFactory to port.WebViewFactory.
14+// WebViewFactory.Create and CreateRelated return port.WebView directly,
15+// so this adapter is thin but ensures the concrete type stays private.
16+type webViewFactoryAdapter struct {
17+ factory *WebViewFactory
18+}
19+
20+func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) {
21+ wv, err := a.factory.Create(ctx)
22+ if err != nil {
23+ return nil, err
24+ }
25+ return wv, nil
26+}
27+
28+func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) {
29+ wv, err := a.factory.CreateRelated(ctx, parentID)
30+ if err != nil {
31+ return nil, err
32+ }
33+ return wv, nil
34+}
35+
36+// --- WebViewPool adapter ---
37+
38+// webViewPoolAdapter bridges *WebViewPool to port.WebViewPool.
39+type webViewPoolAdapter struct {
40+ pool *WebViewPool
41+ ctx context.Context
42+}
43+
44+func (a *webViewPoolAdapter) Acquire(ctx context.Context) (port.WebView, error) {
45+ wv, err := a.pool.Acquire(ctx)
46+ if err != nil {
47+ return nil, err
48+ }
49+ return wv, nil
50+}
51+
52+func (a *webViewPoolAdapter) Release(wv port.WebView) {
53+ if wv == nil {
54+ return
55+ }
56+ cwv, ok := wv.(*WebView)
57+ if !ok {
58+ logging.FromContext(a.ctx).Warn().
59+ Str("concrete_type", fmt.Sprintf("%T", wv)).
60+ Msg("webViewPoolAdapter.Release: unexpected type, cannot release to pool")
61+ return
62+ }
63+ a.pool.Release(cwv)
64+}
65+
66+func (a *webViewPoolAdapter) Prewarm(count int) {
67+ a.pool.Prewarm(count)
68+}
69+
70+func (a *webViewPoolAdapter) PrewarmAsync(ctx context.Context, count int) {
71+ a.pool.PrewarmAsync(ctx, count)
72+}
73+
74+func (a *webViewPoolAdapter) Size() int {
75+ return a.pool.Size()
76+}
77+
78+func (a *webViewPoolAdapter) Close() {
79+ a.pool.Close()
80+}
81+
82+// --- No-op stubs for Phase 1 ---
83+
84+// noopContentInjector implements port.ContentInjector as a no-op.
85+type noopContentInjector struct{}
86+
87+func (n *noopContentInjector) InjectThemeCSS(_ context.Context, _ string) error {
88+ return nil
89+}
90+
91+func (n *noopContentInjector) InjectFindHighlightCSS(_ context.Context, _ string) error {
92+ return nil
93+}
94+
95+func (n *noopContentInjector) RefreshScripts(_ context.Context, _ port.WebView) error {
96+ return nil
97+}
98+
99+// noopSettingsApplier implements port.SettingsApplier as a no-op.
100+type noopSettingsApplier struct{}
101+
102+func (n *noopSettingsApplier) ApplyToAll(_ context.Context, _ []port.WebView) {}
103+
104+// noopFaviconDatabase implements port.FaviconDatabase as a no-op.
105+type noopFaviconDatabase struct{}
106+
107+func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) {
108+ callback(nil)
109+}
@@ -0,0 +1,109 @@
1+package cef
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+ "github.com/bnema/dumber/internal/logging"
9+)
10+
11+// --- WebViewFactory adapter ---
12+
13+// webViewFactoryAdapter bridges *WebViewFactory to port.WebViewFactory.
14+// WebViewFactory.Create and CreateRelated return port.WebView directly,
15+// so this adapter is thin but ensures the concrete type stays private.
16+type webViewFactoryAdapter struct {
17+ factory *WebViewFactory
18+}
19+
20+func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) {
21+ wv, err := a.factory.Create(ctx)
22+ if err != nil {
23+ return nil, err
24+ }
25+ return wv, nil
26+}
27+
28+func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) {
29+ wv, err := a.factory.CreateRelated(ctx, parentID)
30+ if err != nil {
31+ return nil, err
32+ }
33+ return wv, nil
34+}
35+
36+// --- WebViewPool adapter ---
37+
38+// webViewPoolAdapter bridges *WebViewPool to port.WebViewPool.
39+type webViewPoolAdapter struct {
40+ pool *WebViewPool
41+ ctx context.Context
42+}
43+
44+func (a *webViewPoolAdapter) Acquire(ctx context.Context) (port.WebView, error) {
45+ wv, err := a.pool.Acquire(ctx)
46+ if err != nil {
47+ return nil, err
48+ }
49+ return wv, nil
50+}
51+
52+func (a *webViewPoolAdapter) Release(wv port.WebView) {
53+ if wv == nil {
54+ return
55+ }
56+ cwv, ok := wv.(*WebView)
57+ if !ok {
58+ logging.FromContext(a.ctx).Warn().
59+ Str("concrete_type", fmt.Sprintf("%T", wv)).
60+ Msg("webViewPoolAdapter.Release: unexpected type, cannot release to pool")
61+ return
62+ }
63+ a.pool.Release(cwv)
64+}
65+
66+func (a *webViewPoolAdapter) Prewarm(count int) {
67+ a.pool.Prewarm(count)
68+}
69+
70+func (a *webViewPoolAdapter) PrewarmAsync(ctx context.Context, count int) {
71+ a.pool.PrewarmAsync(ctx, count)
72+}
73+
74+func (a *webViewPoolAdapter) Size() int {
75+ return a.pool.Size()
76+}
77+
78+func (a *webViewPoolAdapter) Close() {
79+ a.pool.Close()
80+}
81+
82+// --- No-op stubs for Phase 1 ---
83+
84+// noopContentInjector implements port.ContentInjector as a no-op.
85+type noopContentInjector struct{}
86+
87+func (n *noopContentInjector) InjectThemeCSS(_ context.Context, _ string) error {
88+ return nil
89+}
90+
91+func (n *noopContentInjector) InjectFindHighlightCSS(_ context.Context, _ string) error {
92+ return nil
93+}
94+
95+func (n *noopContentInjector) RefreshScripts(_ context.Context, _ port.WebView) error {
96+ return nil
97+}
98+
99+// noopSettingsApplier implements port.SettingsApplier as a no-op.
100+type noopSettingsApplier struct{}
101+
102+func (n *noopSettingsApplier) ApplyToAll(_ context.Context, _ []port.WebView) {}
103+
104+// noopFaviconDatabase implements port.FaviconDatabase as a no-op.
105+type noopFaviconDatabase struct{}
106+
107+func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) {
108+ callback(nil)
109+}
A internal/infrastructure/cef/engine_init.go +58 -0
@@ -0,0 +1,58 @@
1+package cef
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ purecef "github.com/bnema/purego-cef/cef"
8+ "github.com/jwijenbergh/puregotk/v4/glib"
9+ "github.com/rs/zerolog"
10+
11+ "github.com/bnema/dumber/internal/infrastructure/config"
12+)
13+
14+// NewEngine initializes the CEF runtime and returns a ready-to-use Engine.
15+func NewEngine(ctx context.Context, cfg config.CEFEngineConfig, logger zerolog.Logger) (*Engine, error) {
16+ // 1. Initialize CEF.
17+ settings := purecef.DefaultSettings()
18+ if cfg.CEFDir != "" {
19+ settings.CEFDir = cfg.CEFDir
20+ }
21+ if cfg.LogSeverity != 0 {
22+ settings.LogSeverity = cfg.LogSeverity
23+ }
24+
25+ purecef.MaybeExitSubprocess()
26+
27+ if err := purecef.Init(settings); err != nil {
28+ return nil, fmt.Errorf("cef.Init: %w", err)
29+ }
30+
31+ // 2. Pump CEF messages via the glib idle loop so CEF work executes on the
32+ // GTK main thread alongside normal UI processing.
33+ cb := glib.SourceFunc(func(_ uintptr) bool {
34+ purecef.DoMessageLoopWork()
35+ return true // keep firing
36+ })
37+ glib.IdleAdd(&cb, 0)
38+
39+ // 3. Load GL.
40+ gl, err := newGLLoader()
41+ if err != nil {
42+ purecef.Shutdown()
43+ return nil, fmt.Errorf("GL loader: %w", err)
44+ }
45+
46+ // 4. Create factory + pool.
47+ scale := int32(1) // TODO: detect from GDK
48+ factory := newWebViewFactory(gl, scale)
49+ pool := newWebViewPool(factory)
50+
51+ return &Engine{
52+ ctx: ctx,
53+ gl: gl,
54+ factory: factory,
55+ pool: pool,
56+ logger: logger,
57+ }, nil
58+}
@@ -0,0 +1,58 @@
1+package cef
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ purecef "github.com/bnema/purego-cef/cef"
8+ "github.com/jwijenbergh/puregotk/v4/glib"
9+ "github.com/rs/zerolog"
10+
11+ "github.com/bnema/dumber/internal/infrastructure/config"
12+)
13+
14+// NewEngine initializes the CEF runtime and returns a ready-to-use Engine.
15+func NewEngine(ctx context.Context, cfg config.CEFEngineConfig, logger zerolog.Logger) (*Engine, error) {
16+ // 1. Initialize CEF.
17+ settings := purecef.DefaultSettings()
18+ if cfg.CEFDir != "" {
19+ settings.CEFDir = cfg.CEFDir
20+ }
21+ if cfg.LogSeverity != 0 {
22+ settings.LogSeverity = cfg.LogSeverity
23+ }
24+
25+ purecef.MaybeExitSubprocess()
26+
27+ if err := purecef.Init(settings); err != nil {
28+ return nil, fmt.Errorf("cef.Init: %w", err)
29+ }
30+
31+ // 2. Pump CEF messages via the glib idle loop so CEF work executes on the
32+ // GTK main thread alongside normal UI processing.
33+ cb := glib.SourceFunc(func(_ uintptr) bool {
34+ purecef.DoMessageLoopWork()
35+ return true // keep firing
36+ })
37+ glib.IdleAdd(&cb, 0)
38+
39+ // 3. Load GL.
40+ gl, err := newGLLoader()
41+ if err != nil {
42+ purecef.Shutdown()
43+ return nil, fmt.Errorf("GL loader: %w", err)
44+ }
45+
46+ // 4. Create factory + pool.
47+ scale := int32(1) // TODO: detect from GDK
48+ factory := newWebViewFactory(gl, scale)
49+ pool := newWebViewPool(factory)
50+
51+ return &Engine{
52+ ctx: ctx,
53+ gl: gl,
54+ factory: factory,
55+ pool: pool,
56+ logger: logger,
57+ }, nil
58+}

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