detached f7e5518

feat(webkit): add Engine struct with port.Engine implementation and adapters

Introduces Engine struct wrapping all webkit infrastructure types and
implementing port.Engine. Adds engine_adapters.go with adapter types
bridging internal webkit types to port interfaces.
commit
f7e55189ecd5d6c2fb63983c484e4194a93650c6
author
brice · · 2026-03-15 16:20 UTC
parent
f6521de
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +244 -0
A internal/infrastructure/webkit/engine.go +85 -0
@@ -0,0 +1,85 @@
1+package webkit
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/infrastructure/filtering"
8+)
9+
10+// Engine implements port.Engine for the WebKit browser engine.
11+type Engine struct {
12+ wkCtx *WebKitContext
13+ settings *SettingsManager
14+ injector *ContentInjector
15+ messageRouter *MessageRouter
16+ pool *WebViewPool
17+ factory *WebViewFactory
18+ filterManager *filtering.Manager
19+ schemeHandler *DumbSchemeHandler
20+ schemePath string
21+}
22+
23+// Compile-time check that Engine implements port.Engine.
24+var _ port.Engine = (*Engine)(nil)
25+
26+// Init is a no-op for WebKit — initialization happens in NewEngine.
27+func (e *Engine) Init(_ context.Context, _ port.EngineOptions) error {
28+ return nil
29+}
30+
31+// Factory returns the WebViewFactory wrapped as a port.WebViewFactory.
32+func (e *Engine) Factory() port.WebViewFactory {
33+ return &webViewFactoryAdapter{factory: e.factory}
34+}
35+
36+// Pool returns the WebViewPool wrapped as a port.WebViewPool.
37+func (e *Engine) Pool() port.WebViewPool {
38+ return &webViewPoolAdapter{pool: e.pool}
39+}
40+
41+// ContentInjector returns the ContentInjector implementing port.ContentInjector.
42+func (e *Engine) ContentInjector() port.ContentInjector {
43+ return e.injector
44+}
45+
46+// InternalSchemePath returns the URI scheme used for internal app resources.
47+func (e *Engine) InternalSchemePath() string {
48+ return e.schemePath
49+}
50+
51+// Close releases all resources held by the engine.
52+func (e *Engine) Close() error {
53+ if e.pool != nil {
54+ e.pool.Close(context.Background())
55+ }
56+ return nil
57+}
58+
59+// SchemeHandler returns a port.SchemeHandler adapter for the DumbSchemeHandler.
60+func (e *Engine) SchemeHandler() port.SchemeHandler {
61+ return &schemeHandlerAdapter{handler: e.schemeHandler}
62+}
63+
64+// MessageRouter returns a port.MessageRouter adapter for the internal MessageRouter.
65+func (e *Engine) MessageRouter() port.MessageRouter {
66+ return &messageRouterAdapter{router: e.messageRouter}
67+}
68+
69+// SettingsApplier returns a port.SettingsApplier adapter for the SettingsManager.
70+func (e *Engine) SettingsApplier() port.SettingsApplier {
71+ return &settingsApplierAdapter{settings: e.settings}
72+}
73+
74+// FilterApplier returns a port.FilterApplier adapter, or nil if filtering is disabled.
75+func (e *Engine) FilterApplier() port.FilterApplier {
76+ if e.filterManager == nil {
77+ return nil
78+ }
79+ return &filterApplierAdapter{manager: e.filterManager}
80+}
81+
82+// FaviconDatabase returns a port.FaviconDatabase adapter for async favicon lookups.
83+func (e *Engine) FaviconDatabase() port.FaviconDatabase {
84+ return &faviconDatabaseAdapter{wkCtx: e.wkCtx}
85+}
@@ -0,0 +1,85 @@
1+package webkit
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/infrastructure/filtering"
8+)
9+
10+// Engine implements port.Engine for the WebKit browser engine.
11+type Engine struct {
12+ wkCtx *WebKitContext
13+ settings *SettingsManager
14+ injector *ContentInjector
15+ messageRouter *MessageRouter
16+ pool *WebViewPool
17+ factory *WebViewFactory
18+ filterManager *filtering.Manager
19+ schemeHandler *DumbSchemeHandler
20+ schemePath string
21+}
22+
23+// Compile-time check that Engine implements port.Engine.
24+var _ port.Engine = (*Engine)(nil)
25+
26+// Init is a no-op for WebKit — initialization happens in NewEngine.
27+func (e *Engine) Init(_ context.Context, _ port.EngineOptions) error {
28+ return nil
29+}
30+
31+// Factory returns the WebViewFactory wrapped as a port.WebViewFactory.
32+func (e *Engine) Factory() port.WebViewFactory {
33+ return &webViewFactoryAdapter{factory: e.factory}
34+}
35+
36+// Pool returns the WebViewPool wrapped as a port.WebViewPool.
37+func (e *Engine) Pool() port.WebViewPool {
38+ return &webViewPoolAdapter{pool: e.pool}
39+}
40+
41+// ContentInjector returns the ContentInjector implementing port.ContentInjector.
42+func (e *Engine) ContentInjector() port.ContentInjector {
43+ return e.injector
44+}
45+
46+// InternalSchemePath returns the URI scheme used for internal app resources.
47+func (e *Engine) InternalSchemePath() string {
48+ return e.schemePath
49+}
50+
51+// Close releases all resources held by the engine.
52+func (e *Engine) Close() error {
53+ if e.pool != nil {
54+ e.pool.Close(context.Background())
55+ }
56+ return nil
57+}
58+
59+// SchemeHandler returns a port.SchemeHandler adapter for the DumbSchemeHandler.
60+func (e *Engine) SchemeHandler() port.SchemeHandler {
61+ return &schemeHandlerAdapter{handler: e.schemeHandler}
62+}
63+
64+// MessageRouter returns a port.MessageRouter adapter for the internal MessageRouter.
65+func (e *Engine) MessageRouter() port.MessageRouter {
66+ return &messageRouterAdapter{router: e.messageRouter}
67+}
68+
69+// SettingsApplier returns a port.SettingsApplier adapter for the SettingsManager.
70+func (e *Engine) SettingsApplier() port.SettingsApplier {
71+ return &settingsApplierAdapter{settings: e.settings}
72+}
73+
74+// FilterApplier returns a port.FilterApplier adapter, or nil if filtering is disabled.
75+func (e *Engine) FilterApplier() port.FilterApplier {
76+ if e.filterManager == nil {
77+ return nil
78+ }
79+ return &filterApplierAdapter{manager: e.filterManager}
80+}
81+
82+// FaviconDatabase returns a port.FaviconDatabase adapter for async favicon lookups.
83+func (e *Engine) FaviconDatabase() port.FaviconDatabase {
84+ return &faviconDatabaseAdapter{wkCtx: e.wkCtx}
85+}
A internal/infrastructure/webkit/engine_adapters.go +159 -0
@@ -0,0 +1,159 @@
1+package webkit
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+ "github.com/bnema/dumber/internal/infrastructure/filtering"
9+)
10+
11+// --- WebViewFactory adapter ---
12+
13+// webViewFactoryAdapter bridges *WebViewFactory to port.WebViewFactory.
14+// WebViewFactory.Create and CreateRelated return *WebView, not port.WebView,
15+// so this adapter wraps the return values.
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+// WebViewPool methods use *WebView and require a context; this adapter adapts
40+// the signatures to match the port interface.
41+type webViewPoolAdapter struct {
42+ pool *WebViewPool
43+}
44+
45+func (a *webViewPoolAdapter) Acquire(ctx context.Context) (port.WebView, error) {
46+ wv, err := a.pool.Acquire(ctx)
47+ if err != nil {
48+ return nil, err
49+ }
50+ return wv, nil
51+}
52+
53+func (a *webViewPoolAdapter) Release(wv port.WebView) {
54+ if wv == nil {
55+ return
56+ }
57+ if wwv, ok := wv.(*WebView); ok {
58+ a.pool.Release(context.Background(), wwv)
59+ }
60+}
61+
62+func (a *webViewPoolAdapter) Prewarm(count int) {
63+ a.pool.Prewarm(context.Background(), count)
64+}
65+
66+func (a *webViewPoolAdapter) Size() int {
67+ return a.pool.Size()
68+}
69+
70+func (a *webViewPoolAdapter) Close() {
71+ a.pool.Close(context.Background())
72+}
73+
74+// --- SchemeHandler adapter ---
75+
76+// schemeHandlerAdapter bridges *DumbSchemeHandler to port.SchemeHandler.
77+// DumbSchemeHandler uses a page-based registration pattern (RegisterPage) rather
78+// than a generic scheme+handler registration; this adapter is a stub until
79+// consumers are migrated.
80+type schemeHandlerAdapter struct {
81+ handler *DumbSchemeHandler
82+}
83+
84+func (a *schemeHandlerAdapter) RegisterScheme(_ string, _ func(uri string) ([]byte, string, error)) {
85+ // DumbSchemeHandler handles the "dumb" scheme exclusively via RegisterPage/RegisterWithContext.
86+ // Generic scheme registration is not yet wired through this adapter.
87+ panic("not implemented — DumbSchemeHandler uses RegisterPage/RegisterWithContext pattern")
88+}
89+
90+// --- MessageRouter adapter ---
91+
92+// messageRouterAdapter bridges *MessageRouter to port.MessageRouter.
93+// The internal MessageRouter uses a typed MessageHandler interface rather than
94+// the simple func(string)(string,error) signature in the port interface.
95+// PostMessage is also not directly available on the internal type.
96+type messageRouterAdapter struct {
97+ router *MessageRouter
98+}
99+
100+func (a *messageRouterAdapter) RegisterHandler(_ string, _ func(message string) (string, error)) {
101+ // Internal MessageRouter.RegisterHandler takes a MessageHandler interface, not a plain func.
102+ // Wire up via RegisterHandler(msgType, MessageHandlerFunc{...}) when consumers are migrated.
103+ panic("not implemented — use MessageRouter.RegisterHandler(msgType, MessageHandler) directly")
104+}
105+
106+func (a *messageRouterAdapter) PostMessage(webviewID port.WebViewID, message string) error {
107+ wv := LookupWebView(webviewID)
108+ if wv == nil {
109+ return fmt.Errorf("webview %d not found", webviewID)
110+ }
111+ wv.RunJavaScript(context.Background(), message, "")
112+ return nil
113+}
114+
115+// --- SettingsApplier adapter ---
116+
117+// settingsApplierAdapter bridges *SettingsManager to port.SettingsApplier.
118+type settingsApplierAdapter struct {
119+ settings *SettingsManager
120+}
121+
122+func (a *settingsApplierAdapter) ApplyToAll(ctx context.Context, webviews []port.WebView) {
123+ for _, wv := range webviews {
124+ if wwv, ok := wv.(*WebView); ok && !wwv.IsDestroyed() {
125+ a.settings.ApplyToWebView(ctx, wwv.Widget())
126+ }
127+ }
128+}
129+
130+// --- FilterApplier adapter ---
131+
132+// filterApplierAdapter bridges *filtering.Manager to port.FilterApplier.
133+type filterApplierAdapter struct {
134+ manager *filtering.Manager
135+}
136+
137+func (a *filterApplierAdapter) ApplyToAll(ctx context.Context, webviews []port.WebView) {
138+ for _, wv := range webviews {
139+ if wwv, ok := wv.(*WebView); ok && !wwv.IsDestroyed() {
140+ a.manager.ApplyTo(ctx, wwv.UserContentManager())
141+ }
142+ }
143+}
144+
145+// --- FaviconDatabase adapter ---
146+
147+// faviconDatabaseAdapter bridges *WebKitContext to port.FaviconDatabase.
148+// The WebKitContext exposes the favicon database; async lookup is a stub until
149+// the coordinator layer is migrated to use this port.
150+type faviconDatabaseAdapter struct {
151+ wkCtx *WebKitContext
152+}
153+
154+func (a *faviconDatabaseAdapter) GetFaviconAsync(_ string, callback func(port.Texture)) {
155+ // FaviconDatabase async lookup is not yet wired through this adapter.
156+ // The underlying API is: wkCtx.FaviconDatabase().GetFavicon(uri, callback).
157+ // This stub satisfies the interface contract; callers will receive nil.
158+ callback(nil)
159+}
@@ -0,0 +1,159 @@
1+package webkit
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+ "github.com/bnema/dumber/internal/infrastructure/filtering"
9+)
10+
11+// --- WebViewFactory adapter ---
12+
13+// webViewFactoryAdapter bridges *WebViewFactory to port.WebViewFactory.
14+// WebViewFactory.Create and CreateRelated return *WebView, not port.WebView,
15+// so this adapter wraps the return values.
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+// WebViewPool methods use *WebView and require a context; this adapter adapts
40+// the signatures to match the port interface.
41+type webViewPoolAdapter struct {
42+ pool *WebViewPool
43+}
44+
45+func (a *webViewPoolAdapter) Acquire(ctx context.Context) (port.WebView, error) {
46+ wv, err := a.pool.Acquire(ctx)
47+ if err != nil {
48+ return nil, err
49+ }
50+ return wv, nil
51+}
52+
53+func (a *webViewPoolAdapter) Release(wv port.WebView) {
54+ if wv == nil {
55+ return
56+ }
57+ if wwv, ok := wv.(*WebView); ok {
58+ a.pool.Release(context.Background(), wwv)
59+ }
60+}
61+
62+func (a *webViewPoolAdapter) Prewarm(count int) {
63+ a.pool.Prewarm(context.Background(), count)
64+}
65+
66+func (a *webViewPoolAdapter) Size() int {
67+ return a.pool.Size()
68+}
69+
70+func (a *webViewPoolAdapter) Close() {
71+ a.pool.Close(context.Background())
72+}
73+
74+// --- SchemeHandler adapter ---
75+
76+// schemeHandlerAdapter bridges *DumbSchemeHandler to port.SchemeHandler.
77+// DumbSchemeHandler uses a page-based registration pattern (RegisterPage) rather
78+// than a generic scheme+handler registration; this adapter is a stub until
79+// consumers are migrated.
80+type schemeHandlerAdapter struct {
81+ handler *DumbSchemeHandler
82+}
83+
84+func (a *schemeHandlerAdapter) RegisterScheme(_ string, _ func(uri string) ([]byte, string, error)) {
85+ // DumbSchemeHandler handles the "dumb" scheme exclusively via RegisterPage/RegisterWithContext.
86+ // Generic scheme registration is not yet wired through this adapter.
87+ panic("not implemented — DumbSchemeHandler uses RegisterPage/RegisterWithContext pattern")
88+}
89+
90+// --- MessageRouter adapter ---
91+
92+// messageRouterAdapter bridges *MessageRouter to port.MessageRouter.
93+// The internal MessageRouter uses a typed MessageHandler interface rather than
94+// the simple func(string)(string,error) signature in the port interface.
95+// PostMessage is also not directly available on the internal type.
96+type messageRouterAdapter struct {
97+ router *MessageRouter
98+}
99+
100+func (a *messageRouterAdapter) RegisterHandler(_ string, _ func(message string) (string, error)) {
101+ // Internal MessageRouter.RegisterHandler takes a MessageHandler interface, not a plain func.
102+ // Wire up via RegisterHandler(msgType, MessageHandlerFunc{...}) when consumers are migrated.
103+ panic("not implemented — use MessageRouter.RegisterHandler(msgType, MessageHandler) directly")
104+}
105+
106+func (a *messageRouterAdapter) PostMessage(webviewID port.WebViewID, message string) error {
107+ wv := LookupWebView(webviewID)
108+ if wv == nil {
109+ return fmt.Errorf("webview %d not found", webviewID)
110+ }
111+ wv.RunJavaScript(context.Background(), message, "")
112+ return nil
113+}
114+
115+// --- SettingsApplier adapter ---
116+
117+// settingsApplierAdapter bridges *SettingsManager to port.SettingsApplier.
118+type settingsApplierAdapter struct {
119+ settings *SettingsManager
120+}
121+
122+func (a *settingsApplierAdapter) ApplyToAll(ctx context.Context, webviews []port.WebView) {
123+ for _, wv := range webviews {
124+ if wwv, ok := wv.(*WebView); ok && !wwv.IsDestroyed() {
125+ a.settings.ApplyToWebView(ctx, wwv.Widget())
126+ }
127+ }
128+}
129+
130+// --- FilterApplier adapter ---
131+
132+// filterApplierAdapter bridges *filtering.Manager to port.FilterApplier.
133+type filterApplierAdapter struct {
134+ manager *filtering.Manager
135+}
136+
137+func (a *filterApplierAdapter) ApplyToAll(ctx context.Context, webviews []port.WebView) {
138+ for _, wv := range webviews {
139+ if wwv, ok := wv.(*WebView); ok && !wwv.IsDestroyed() {
140+ a.manager.ApplyTo(ctx, wwv.UserContentManager())
141+ }
142+ }
143+}
144+
145+// --- FaviconDatabase adapter ---
146+
147+// faviconDatabaseAdapter bridges *WebKitContext to port.FaviconDatabase.
148+// The WebKitContext exposes the favicon database; async lookup is a stub until
149+// the coordinator layer is migrated to use this port.
150+type faviconDatabaseAdapter struct {
151+ wkCtx *WebKitContext
152+}
153+
154+func (a *faviconDatabaseAdapter) GetFaviconAsync(_ string, callback func(port.Texture)) {
155+ // FaviconDatabase async lookup is not yet wired through this adapter.
156+ // The underlying API is: wkCtx.FaviconDatabase().GetFavicon(uri, callback).
157+ // This stub satisfies the interface contract; callers will receive nil.
158+ callback(nil)
159+}

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