detached 7f9d755

refactor(content): decouple content/ package public API from webkit types

Replace all *webkit.WebView with port.WebView and webkit.LoadEvent with
port.LoadEvent across the content package public API. Internal methods
use type assertions to access webkit-specific functionality (Widget(),
RunJavaScript(), Generation(), etc.).

Key changes:
- Coordinator struct fields: pool, factory, webViews map, callbacks use port types
- All public methods (EnsureWebView, ActiveWebView, GetWebView, WrapWidget,
  RegisterPopupWebView, SetOnWebViewFocused, SetPopupConfig) use port types
- setupWebViewCallbacks uses wv.SetCallbacks() with port.WebViewCallbacks
- Bulk methods (ApplyFiltersToAll, ApplySettingsToAll, RefreshInjectedScriptsToAll)
  simplified to take only ctx (TODO: wire through engine abstraction)
- Replace webkit.HomePath with local internalSchemePath constant
- Replace webkitlib.WebProcessTerminationReason with port types
- Tests use port/mocks instead of webkit zero-values
- app.go callers updated to use engine.Pool() and engine.Factory()
commit
7f9d755651a4592564527aecc8b9aa209cb24397
author
brice · · 2026-03-15 17:28 UTC
parent
9a59c25
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +262 -217
M internal/ui/app.go +10 -6
@@ -1525,7 +1525,7 @@ func (a *App) initCoordinators(ctx context.Context) {
15251525 // 1. Content Coordinator (no dependencies on other coordinators)
15261526 a.contentCoord = content.NewCoordinator(
15271527 ctx,
1528- a.pool,
1528+ a.engine.Pool(),
15291529 a.widgetFactory,
15301530 a.faviconAdapter,
15311531 getActiveWS,
@@ -1619,7 +1619,7 @@ func (a *App) initCoordinators(ctx context.Context) {
16191619 a.webViewFactory.SetBackgroundColor(r, g, b, alpha)
16201620 }
16211621 a.contentCoord.SetPopupConfig(
1622- a.webViewFactory,
1622+ a.engine.Factory(),
16231623 &a.deps.Config.Workspace.Popups,
16241624 a.generateID,
16251625 )
@@ -2140,7 +2140,11 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget {
21402140 }
21412141
21422142 // Get the underlying webkit.WebView for the text input target
2143- return textinput.NewWebViewTarget(wv.Widget())
2143+ wkWV, ok := wv.(*webkit.WebView)
2144+ if !ok {
2145+ return nil
2146+ }
2147+ return textinput.NewWebViewTarget(wkWV.Widget())
21442148 }
21452149
21462150 // attachPopupToTab attaches a popup WebView to a newly created tab.
@@ -3097,7 +3101,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
30973101
30983102 // Apply settings to existing webviews via coordinator
30993103 if a.contentCoord != nil {
3100- a.contentCoord.ApplySettingsToAll(ctx, a.settings)
3104+ a.contentCoord.ApplySettingsToAll(ctx)
31013105 }
31023106
31033107 // Update GTK theme and injected WebUI theme vars
@@ -3124,7 +3128,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
31243128
31253129 // Refresh injected scripts so future navigations use the latest theme.
31263130 if a.contentCoord != nil && a.injector != nil {
3127- a.contentCoord.RefreshInjectedScriptsToAll(ctx, a.injector)
3131+ a.contentCoord.RefreshInjectedScriptsToAll(ctx)
31283132 }
31293133
31303134 // Apply WebUI theme to already-loaded dumb:// pages
@@ -3164,7 +3168,7 @@ func (a *App) showFilterStatus(ctx context.Context, status filtering.FilterStatu
31643168 case filtering.StateActive:
31653169 // Apply filters to existing webviews that were created before filters loaded
31663170 if a.contentCoord != nil && a.deps.FilterManager != nil {
3167- a.contentCoord.ApplyFiltersToAll(ctx, a.deps.FilterManager)
3171+ a.contentCoord.ApplyFiltersToAll(ctx)
31683172 log.Debug().Msg("applied filters to all existing webviews")
31693173 }
31703174 if a.appToaster != nil {
@@ -1525,7 +1525,7 @@ func (a *App) initCoordinators(ctx context.Context) {
1525 // 1. Content Coordinator (no dependencies on other coordinators)1525 // 1. Content Coordinator (no dependencies on other coordinators)
1526 a.contentCoord = content.NewCoordinator(1526 a.contentCoord = content.NewCoordinator(
1527 ctx,1527 ctx,
1528- a.pool,1528+ a.engine.Pool(),
1529 a.widgetFactory,1529 a.widgetFactory,
1530 a.faviconAdapter,1530 a.faviconAdapter,
1531 getActiveWS,1531 getActiveWS,
@@ -1619,7 +1619,7 @@ func (a *App) initCoordinators(ctx context.Context) {
1619 a.webViewFactory.SetBackgroundColor(r, g, b, alpha)1619 a.webViewFactory.SetBackgroundColor(r, g, b, alpha)
1620 }1620 }
1621 a.contentCoord.SetPopupConfig(1621 a.contentCoord.SetPopupConfig(
1622- a.webViewFactory,1622+ a.engine.Factory(),
1623 &a.deps.Config.Workspace.Popups,1623 &a.deps.Config.Workspace.Popups,
1624 a.generateID,1624 a.generateID,
1625 )1625 )
@@ -2140,7 +2140,11 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget {
2140 }2140 }
2141 2141
2142 // Get the underlying webkit.WebView for the text input target2142 // Get the underlying webkit.WebView for the text input target
2143- return textinput.NewWebViewTarget(wv.Widget())2143+ wkWV, ok := wv.(*webkit.WebView)
2144+ if !ok {
2145+ return nil
2146+ }
2147+ return textinput.NewWebViewTarget(wkWV.Widget())
2144 }2148 }
2145 2149
2146 // attachPopupToTab attaches a popup WebView to a newly created tab.2150 // attachPopupToTab attaches a popup WebView to a newly created tab.
@@ -3097,7 +3101,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
3097 3101
3098 // Apply settings to existing webviews via coordinator3102 // Apply settings to existing webviews via coordinator
3099 if a.contentCoord != nil {3103 if a.contentCoord != nil {
3100- a.contentCoord.ApplySettingsToAll(ctx, a.settings)3104+ a.contentCoord.ApplySettingsToAll(ctx)
3101 }3105 }
3102 3106
3103 // Update GTK theme and injected WebUI theme vars3107 // Update GTK theme and injected WebUI theme vars
@@ -3124,7 +3128,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
3124 3128
3125 // Refresh injected scripts so future navigations use the latest theme.3129 // Refresh injected scripts so future navigations use the latest theme.
3126 if a.contentCoord != nil && a.injector != nil {3130 if a.contentCoord != nil && a.injector != nil {
3127- a.contentCoord.RefreshInjectedScriptsToAll(ctx, a.injector)3131+ a.contentCoord.RefreshInjectedScriptsToAll(ctx)
3128 }3132 }
3129 3133
3130 // Apply WebUI theme to already-loaded dumb:// pages3134 // Apply WebUI theme to already-loaded dumb:// pages
@@ -3164,7 +3168,7 @@ func (a *App) showFilterStatus(ctx context.Context, status filtering.FilterStatu
3164 case filtering.StateActive:3168 case filtering.StateActive:
3165 // Apply filters to existing webviews that were created before filters loaded3169 // Apply filters to existing webviews that were created before filters loaded
3166 if a.contentCoord != nil && a.deps.FilterManager != nil {3170 if a.contentCoord != nil && a.deps.FilterManager != nil {
3167- a.contentCoord.ApplyFiltersToAll(ctx, a.deps.FilterManager)3171+ a.contentCoord.ApplyFiltersToAll(ctx)
3168 log.Debug().Msg("applied filters to all existing webviews")3172 log.Debug().Msg("applied filters to all existing webviews")
3169 }3173 }
3170 if a.appToaster != nil {3174 if a.appToaster != nil {
M internal/ui/coordinator/content/appearance.go +17 -8
@@ -7,6 +7,7 @@ import (
77 "strings"
88 "time"
99
10+ "github.com/bnema/dumber/internal/application/port"
1011 "github.com/bnema/dumber/internal/domain/entity"
1112 "github.com/bnema/dumber/internal/infrastructure/webkit"
1213 "github.com/bnema/dumber/internal/logging"
@@ -114,7 +115,7 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) {
114115 func (c *Coordinator) applyWebUITheme(
115116 ctx context.Context,
116117 paneID entity.PaneID,
117- wv *webkit.WebView,
118+ wv port.WebView,
118119 script string,
119120 prefersDark bool,
120121 ) {
@@ -125,7 +126,10 @@ func (c *Coordinator) applyWebUITheme(
125126 if !strings.HasPrefix(uri, "dumb://") {
126127 return
127128 }
128- wv.RunJavaScript(ctx, script, "")
129+ // Type-assert to access webkit-specific RunJavaScript
130+ if wkWV, ok := wv.(*webkit.WebView); ok {
131+ wkWV.RunJavaScript(ctx, script, "")
132+ }
129133 logging.FromContext(ctx).
130134 Debug().
131135 Str("pane_id", string(paneID)).
@@ -183,7 +187,7 @@ func (c *Coordinator) takePendingThemeApply(paneID entity.PaneID) (pendingThemeU
183187 return update, true
184188 }
185189
186-func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool {
190+func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool {
187191 update, ok := c.takePendingThemeApply(paneID)
188192 if !ok {
189193 return false
@@ -198,7 +202,7 @@ func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity
198202 return true
199203 }
200204
201-func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool {
205+func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool {
202206 update, ok := c.getCurrentTheme()
203207 if !ok || update.cssText == "" {
204208 return false
@@ -233,7 +237,7 @@ func (c *Coordinator) takePendingScriptRefresh(paneID entity.PaneID) bool {
233237 return true
234238 }
235239
236-func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
240+func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
237241 if wv == nil || wv.IsDestroyed() || c.shouldDeferAppearance(wv) {
238242 return
239243 }
@@ -246,7 +250,7 @@ func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.P
246250 c.refreshInjectedScripts(ctx, c.injector, paneID, wv)
247251 }
248252
249-func (c *Coordinator) shouldDeferAppearance(wv *webkit.WebView) bool {
253+func (c *Coordinator) shouldDeferAppearance(wv port.WebView) bool {
250254 if wv == nil || wv.IsDestroyed() {
251255 return false
252256 }
@@ -260,12 +264,17 @@ func (c *Coordinator) refreshInjectedScripts(
260264 ctx context.Context,
261265 injector *webkit.ContentInjector,
262266 paneID entity.PaneID,
263- wv *webkit.WebView,
267+ wv port.WebView,
264268 ) {
265269 if injector == nil || wv == nil || wv.IsDestroyed() {
266270 return
267271 }
268- ucm := wv.UserContentManager()
272+ // Type-assert to access webkit-specific UserContentManager
273+ wkWV, ok := wv.(*webkit.WebView)
274+ if !ok {
275+ return
276+ }
277+ ucm := wkWV.UserContentManager()
269278 if ucm == nil {
270279 return
271280 }
@@ -7,6 +7,7 @@ import (
7 "strings"7 "strings"
8 "time"8 "time"
9 9
10+ "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/infrastructure/webkit"12 "github.com/bnema/dumber/internal/infrastructure/webkit"
12 "github.com/bnema/dumber/internal/logging"13 "github.com/bnema/dumber/internal/logging"
@@ -114,7 +115,7 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) {
114 func (c *Coordinator) applyWebUITheme(115 func (c *Coordinator) applyWebUITheme(
115 ctx context.Context,116 ctx context.Context,
116 paneID entity.PaneID,117 paneID entity.PaneID,
117- wv *webkit.WebView,118+ wv port.WebView,
118 script string,119 script string,
119 prefersDark bool,120 prefersDark bool,
120 ) {121 ) {
@@ -125,7 +126,10 @@ func (c *Coordinator) applyWebUITheme(
125 if !strings.HasPrefix(uri, "dumb://") {126 if !strings.HasPrefix(uri, "dumb://") {
126 return127 return
127 }128 }
128- wv.RunJavaScript(ctx, script, "")129+ // Type-assert to access webkit-specific RunJavaScript
130+ if wkWV, ok := wv.(*webkit.WebView); ok {
131+ wkWV.RunJavaScript(ctx, script, "")
132+ }
129 logging.FromContext(ctx).133 logging.FromContext(ctx).
130 Debug().134 Debug().
131 Str("pane_id", string(paneID)).135 Str("pane_id", string(paneID)).
@@ -183,7 +187,7 @@ func (c *Coordinator) takePendingThemeApply(paneID entity.PaneID) (pendingThemeU
183 return update, true187 return update, true
184 }188 }
185 189
186-func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool {190+func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool {
187 update, ok := c.takePendingThemeApply(paneID)191 update, ok := c.takePendingThemeApply(paneID)
188 if !ok {192 if !ok {
189 return false193 return false
@@ -198,7 +202,7 @@ func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity
198 return true202 return true
199 }203 }
200 204
201-func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool {205+func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool {
202 update, ok := c.getCurrentTheme()206 update, ok := c.getCurrentTheme()
203 if !ok || update.cssText == "" {207 if !ok || update.cssText == "" {
204 return false208 return false
@@ -233,7 +237,7 @@ func (c *Coordinator) takePendingScriptRefresh(paneID entity.PaneID) bool {
233 return true237 return true
234 }238 }
235 239
236-func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {240+func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
237 if wv == nil || wv.IsDestroyed() || c.shouldDeferAppearance(wv) {241 if wv == nil || wv.IsDestroyed() || c.shouldDeferAppearance(wv) {
238 return242 return
239 }243 }
@@ -246,7 +250,7 @@ func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.P
246 c.refreshInjectedScripts(ctx, c.injector, paneID, wv)250 c.refreshInjectedScripts(ctx, c.injector, paneID, wv)
247 }251 }
248 252
249-func (c *Coordinator) shouldDeferAppearance(wv *webkit.WebView) bool {253+func (c *Coordinator) shouldDeferAppearance(wv port.WebView) bool {
250 if wv == nil || wv.IsDestroyed() {254 if wv == nil || wv.IsDestroyed() {
251 return false255 return false
252 }256 }
@@ -260,12 +264,17 @@ func (c *Coordinator) refreshInjectedScripts(
260 ctx context.Context,264 ctx context.Context,
261 injector *webkit.ContentInjector,265 injector *webkit.ContentInjector,
262 paneID entity.PaneID,266 paneID entity.PaneID,
263- wv *webkit.WebView,267+ wv port.WebView,
264 ) {268 ) {
265 if injector == nil || wv == nil || wv.IsDestroyed() {269 if injector == nil || wv == nil || wv.IsDestroyed() {
266 return270 return
267 }271 }
268- ucm := wv.UserContentManager()272+ // Type-assert to access webkit-specific UserContentManager
273+ wkWV, ok := wv.(*webkit.WebView)
274+ if !ok {
275+ return
276+ }
277+ ucm := wkWV.UserContentManager()
269 if ucm == nil {278 if ucm == nil {
270 return279 return
271 }280 }
M internal/ui/coordinator/content/bulk.go +10 -25
@@ -3,37 +3,22 @@ package content
33 import (
44 "context"
55
6- "github.com/bnema/dumber/internal/infrastructure/webkit"
76 "github.com/bnema/dumber/internal/logging"
87 )
98
109 // ApplyFiltersToAll applies content filters to all active webviews.
1110 // Called when filters become available after webviews were already created.
12-func (c *Coordinator) ApplyFiltersToAll(ctx context.Context, applier webkit.FilterApplier) {
11+// TODO: Accept a port.FilterApplier once engine abstraction is wired.
12+func (c *Coordinator) ApplyFiltersToAll(ctx context.Context) {
1313 log := logging.FromContext(ctx)
14-
15- for paneID, wv := range c.snapshotWebViews() {
16- if wv != nil && !wv.IsDestroyed() {
17- applier.ApplyTo(ctx, wv.UserContentManager())
18- log.Debug().Str("pane_id", string(paneID)).Msg("applied filters to existing webview")
19- }
20- }
14+ log.Debug().Msg("ApplyFiltersToAll: not yet implemented with engine abstraction")
2115 }
2216
2317 // ApplySettingsToAll reapplies WebKit settings to all active WebViews.
24-func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.SettingsManager) {
18+// TODO: Accept engine-level settings once engine abstraction is wired.
19+func (c *Coordinator) ApplySettingsToAll(ctx context.Context) {
2520 log := logging.FromContext(ctx)
26- if sm == nil {
27- return
28- }
29-
30- for paneID, wv := range c.snapshotWebViews() {
31- if wv == nil || wv.IsDestroyed() {
32- continue
33- }
34- sm.ApplyToWebView(ctx, wv.Widget())
35- log.Debug().Str("pane_id", string(paneID)).Msg("reapplied settings to webview")
36- }
21+ log.Debug().Msg("ApplySettingsToAll: not yet implemented with engine abstraction")
3722 }
3823
3924 // RefreshInjectedScriptsToAll clears and re-injects user scripts into all active WebViews.
@@ -43,13 +28,13 @@ func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.Setting
4328 // the scripts so future navigations pick up the latest values.
4429 // Script refresh is deferred for any WebView that is currently loading to avoid
4530 // removing scripts mid-navigation.
46-func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector *webkit.ContentInjector) {
31+// TODO: Accept engine-level injector once engine abstraction is wired.
32+func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context) {
4733 log := logging.FromContext(ctx)
48- if injector == nil {
34+ if c.injector == nil {
4935 return
5036 }
5137
52- c.injector = injector
5338 for paneID, wv := range c.webViews {
5439 if wv == nil || wv.IsDestroyed() {
5540 continue
@@ -60,6 +45,6 @@ func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector
6045 continue
6146 }
6247
63- c.refreshInjectedScripts(ctx, injector, paneID, wv)
48+ c.refreshInjectedScripts(ctx, c.injector, paneID, wv)
6449 }
6550 }
@@ -3,37 +3,22 @@ package content
3 import (3 import (
4 "context"4 "context"
5 5
6- "github.com/bnema/dumber/internal/infrastructure/webkit"
7 "github.com/bnema/dumber/internal/logging"6 "github.com/bnema/dumber/internal/logging"
8 )7 )
9 8
10 // ApplyFiltersToAll applies content filters to all active webviews.9 // ApplyFiltersToAll applies content filters to all active webviews.
11 // Called when filters become available after webviews were already created.10 // Called when filters become available after webviews were already created.
12-func (c *Coordinator) ApplyFiltersToAll(ctx context.Context, applier webkit.FilterApplier) {11+// TODO: Accept a port.FilterApplier once engine abstraction is wired.
12+func (c *Coordinator) ApplyFiltersToAll(ctx context.Context) {
13 log := logging.FromContext(ctx)13 log := logging.FromContext(ctx)
14-14+ log.Debug().Msg("ApplyFiltersToAll: not yet implemented with engine abstraction")
15- for paneID, wv := range c.snapshotWebViews() {
16- if wv != nil && !wv.IsDestroyed() {
17- applier.ApplyTo(ctx, wv.UserContentManager())
18- log.Debug().Str("pane_id", string(paneID)).Msg("applied filters to existing webview")
19- }
20- }
21 }15 }
22 16
23 // ApplySettingsToAll reapplies WebKit settings to all active WebViews.17 // ApplySettingsToAll reapplies WebKit settings to all active WebViews.
24-func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.SettingsManager) {18+// TODO: Accept engine-level settings once engine abstraction is wired.
19+func (c *Coordinator) ApplySettingsToAll(ctx context.Context) {
25 log := logging.FromContext(ctx)20 log := logging.FromContext(ctx)
26- if sm == nil {21+ log.Debug().Msg("ApplySettingsToAll: not yet implemented with engine abstraction")
27- return
28- }
29-
30- for paneID, wv := range c.snapshotWebViews() {
31- if wv == nil || wv.IsDestroyed() {
32- continue
33- }
34- sm.ApplyToWebView(ctx, wv.Widget())
35- log.Debug().Str("pane_id", string(paneID)).Msg("reapplied settings to webview")
36- }
37 }22 }
38 23
39 // RefreshInjectedScriptsToAll clears and re-injects user scripts into all active WebViews.24 // RefreshInjectedScriptsToAll clears and re-injects user scripts into all active WebViews.
@@ -43,13 +28,13 @@ func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.Setting
43 // the scripts so future navigations pick up the latest values.28 // the scripts so future navigations pick up the latest values.
44 // Script refresh is deferred for any WebView that is currently loading to avoid29 // Script refresh is deferred for any WebView that is currently loading to avoid
45 // removing scripts mid-navigation.30 // removing scripts mid-navigation.
46-func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector *webkit.ContentInjector) {31+// TODO: Accept engine-level injector once engine abstraction is wired.
32+func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context) {
47 log := logging.FromContext(ctx)33 log := logging.FromContext(ctx)
48- if injector == nil {34+ if c.injector == nil {
49 return35 return
50 }36 }
51 37
52- c.injector = injector
53 for paneID, wv := range c.webViews {38 for paneID, wv := range c.webViews {
54 if wv == nil || wv.IsDestroyed() {39 if wv == nil || wv.IsDestroyed() {
55 continue40 continue
@@ -60,6 +45,6 @@ func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector
60 continue45 continue
61 }46 }
62 47
63- c.refreshInjectedScripts(ctx, injector, paneID, wv)48+ c.refreshInjectedScripts(ctx, c.injector, paneID, wv)
64 }49 }
65 }50 }
M internal/ui/coordinator/content/callbacks.go +91 -82
@@ -5,19 +5,22 @@ import (
55 "net/url"
66 "strings"
77
8+ "github.com/bnema/dumber/internal/application/port"
89 "github.com/bnema/dumber/internal/application/usecase"
910 "github.com/bnema/dumber/internal/domain/entity"
1011 "github.com/bnema/dumber/internal/infrastructure/webkit"
1112 "github.com/bnema/dumber/internal/logging"
12- webkitlib "github.com/bnema/puregotk-webkit/webkit"
1313 "github.com/jwijenbergh/puregotk/v4/gdk"
1414 )
1515
16-func shouldRenderCrashPage(reason webkitlib.WebProcessTerminationReason) bool {
16+// internalSchemePath is the path used in dumb:// URIs (replaces webkit.HomePath).
17+const internalSchemePath = "home"
18+
19+func shouldRenderCrashPage(reason port.WebProcessTerminationReason) bool {
1720 switch reason {
18- case webkitlib.WebProcessCrashedValue, webkitlib.WebProcessExceededMemoryLimitValue:
21+ case port.WebProcessTerminationCrashed, port.WebProcessTerminationExceededMemory:
1922 return true
20- case webkitlib.WebProcessTerminatedByApiValue:
23+ case port.WebProcessTerminationByAPI:
2124 return false
2225 default:
2326 return true
@@ -34,7 +37,7 @@ func extractOriginalURIFromCrashPage(uri string) string {
3437 return uri
3538 }
3639
37- if parsed.Scheme != "dumb" || parsed.Host != webkit.HomePath {
40+ if parsed.Scheme != "dumb" || parsed.Host != internalSchemePath {
3841 return uri
3942 }
4043
@@ -59,94 +62,94 @@ func buildCrashPageURI(originalURI string) string {
5962 }
6063
6164 // setupWebViewCallbacks configures standard callbacks and popup handling.
62-func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
65+func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
6366 log := logging.FromContext(ctx)
6467
65- // Title changes
66- wv.OnTitleChanged = func(title string) {
67- c.onTitleChanged(ctx, paneID, title)
68- }
68+ // Build port callbacks for standard events
69+ callbacks := &port.WebViewCallbacks{
70+ OnTitleChanged: func(title string) {
71+ c.onTitleChanged(ctx, paneID, title)
72+ },
73+ OnLoadChanged: func(event port.LoadEvent) {
74+ switch event {
75+ case port.LoadStarted:
76+ c.onLoadStarted(paneID)
77+ case port.LoadCommitted:
78+ c.onLoadCommitted(ctx, paneID, wv)
79+ case port.LoadFinished:
80+ c.onLoadFinished(ctx, paneID, wv)
81+ }
82+ },
83+ OnProgressChanged: func(progress float64) {
84+ c.onProgressChanged(paneID, progress)
85+ },
86+ OnURIChanged: func(uri string) {
87+ c.handleURIChanged(ctx, paneID, wv, uri)
88+ },
89+ OnLinkHover: func(uri string) {
90+ c.onLinkHover(paneID, uri)
91+ },
92+ OnWebProcessTerminated: func(reason port.WebProcessTerminationReason, reasonLabel string, uri string) {
93+ originalURI := extractOriginalURIFromCrashPage(uri)
94+ if !shouldRenderCrashPage(reason) {
95+ log.Info().
96+ Str("pane_id", string(paneID)).
97+ Str("reason", reasonLabel).
98+ Str("uri", uri).
99+ Msg("web process termination handled without crash page")
100+ return
101+ }
69102
70- // Favicon changes - capture generation to detect stale callbacks after pool reuse
71- faviconGen := wv.Generation()
72- wv.OnFaviconChanged = func(favicon *gdk.Texture) {
73- if wv.Generation() != faviconGen {
74- return // WebView was reused, ignore stale signal
75- }
76- c.onFaviconChanged(ctx, paneID, wv, favicon)
103+ crashURI := buildCrashPageURI(originalURI)
104+ log.Warn().
105+ Str("pane_id", string(paneID)).
106+ Str("reason", reasonLabel).
107+ Str("uri", uri).
108+ Str("crash_uri", crashURI).
109+ Msg("web process terminated, redirecting to crash page")
110+
111+ if err := wv.LoadURI(ctx, crashURI); err != nil {
112+ log.Error().
113+ Err(err).
114+ Str("pane_id", string(paneID)).
115+ Str("reason", reasonLabel).
116+ Str("uri", uri).
117+ Str("crash_uri", crashURI).
118+ Msg("failed to load crash page after web process termination")
119+ }
120+ },
121+ OnPermissionRequest: func(origin string, permTypes []string, allow, deny func()) bool {
122+ return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny)
123+ },
77124 }
78125
79- // Load events
80- wv.OnLoadChanged = func(event webkit.LoadEvent) {
81- switch event {
82- case webkit.LoadStarted:
83- c.onLoadStarted(paneID)
84- case webkit.LoadCommitted:
85- c.onLoadCommitted(ctx, paneID, wv)
86- case webkit.LoadFinished:
87- c.onLoadFinished(ctx, paneID, wv)
126+ // Favicon changes - need webkit-specific Generation() to detect stale callbacks
127+ if wkWV, ok := wv.(*webkit.WebView); ok {
128+ faviconGen := wkWV.Generation()
129+ callbacks.OnFaviconChanged = func(favicon port.Texture) {
130+ if wkWV.Generation() != faviconGen {
131+ return // WebView was reused, ignore stale signal
132+ }
133+ if gdkTexture, ok := favicon.(*gdk.Texture); ok {
134+ c.onFaviconChanged(ctx, paneID, wkWV, gdkTexture)
135+ }
88136 }
89137 }
90138
91- // Progress
92- wv.OnProgressChanged = func(progress float64) {
93- c.onProgressChanged(paneID, progress)
94- }
95-
96- // SPA navigation and external scheme handling
97- wv.OnURIChanged = func(uri string) {
98- c.handleURIChanged(ctx, paneID, wv, uri)
99- }
139+ // Add popup create handler if popup handling is configured
140+ callbacks.OnCreate = c.buildPopupCreateHandler(ctx, paneID, wv)
100141
101- // Middle-click / Ctrl+click handler for opening links in new pane
102- wv.OnLinkMiddleClick = func(uri string) bool {
103- return c.handleLinkMiddleClick(ctx, paneID, uri)
104- }
142+ wv.SetCallbacks(callbacks)
105143
106- // Link hover callback for status overlay
107- wv.OnLinkHover = func(uri string) {
108- c.onLinkHover(paneID, uri)
109- }
110-
111- wv.OnWebProcessTerminated = func(reason webkitlib.WebProcessTerminationReason, reasonLabel string, uri string) {
112- originalURI := extractOriginalURIFromCrashPage(uri)
113- if !shouldRenderCrashPage(reason) {
114- log.Info().
115- Str("pane_id", string(paneID)).
116- Str("reason", reasonLabel).
117- Str("uri", uri).
118- Msg("web process termination handled without crash page")
119- return
120- }
121-
122- crashURI := buildCrashPageURI(originalURI)
123- log.Warn().
124- Str("pane_id", string(paneID)).
125- Str("reason", reasonLabel).
126- Str("uri", uri).
127- Str("crash_uri", crashURI).
128- Msg("web process terminated, redirecting to crash page")
129-
130- if err := wv.LoadURI(ctx, crashURI); err != nil {
131- log.Error().
132- Err(err).
133- Str("pane_id", string(paneID)).
134- Str("reason", reasonLabel).
135- Str("uri", uri).
136- Str("crash_uri", crashURI).
137- Msg("failed to load crash page after web process termination")
144+ // Webkit-specific callbacks not in port.WebViewCallbacks (middle-click, fullscreen, audio)
145+ if wkWV, ok := wv.(*webkit.WebView); ok {
146+ wkWV.OnLinkMiddleClick = func(uri string) bool {
147+ return c.handleLinkMiddleClick(ctx, paneID, uri)
138148 }
139149 }
140150
141- // Permission request handler
142- wv.OnPermissionRequest = func(origin string, permTypes []string, allow, deny func()) bool {
143- return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny)
144- }
145151 // Fullscreen handlers for idle inhibition
146152 c.setupIdleInhibitionHandlers(ctx, paneID, wv)
147-
148- // Popup handling for nested popups
149- c.setupPopupHandling(ctx, paneID, wv)
150153 }
151154
152155 // handlePermissionRequest processes media permission requests from WebKit.
@@ -246,15 +249,21 @@ func filterWebRTCPermissionTypes(types []entity.PermissionType) []entity.Permiss
246249 // - The webview enters fullscreen mode (e.g., fullscreen video)
247250 // - The webview is playing audio (e.g., video/music playback)
248251 // The inhibitor uses refcounting, so both can be active simultaneously.
249-func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
252+func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
250253 log := logging.FromContext(ctx)
251254
252255 if wv == nil {
253256 return
254257 }
255258
259+ // Type-assert to access webkit-specific fullscreen/audio callbacks
260+ wkWV, ok := wv.(*webkit.WebView)
261+ if !ok {
262+ return
263+ }
264+
256265 // Fullscreen handling
257- wv.OnEnterFullscreen = func() bool {
266+ wkWV.OnEnterFullscreen = func() bool {
258267 if c.idleInhibitor != nil {
259268 if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil {
260269 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle")
@@ -266,7 +275,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en
266275 return false // Allow fullscreen
267276 }
268277
269- wv.OnLeaveFullscreen = func() bool {
278+ wkWV.OnLeaveFullscreen = func() bool {
270279 if c.idleInhibitor != nil {
271280 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
272281 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle")
@@ -279,7 +288,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en
279288 }
280289
281290 // Audio playback handling
282- wv.OnAudioStateChanged = func(playing bool) {
291+ wkWV.OnAudioStateChanged = func(playing bool) {
283292 if c.idleInhibitor == nil {
284293 return
285294 }
@@ -5,19 +5,22 @@ import (
5 "net/url"5 "net/url"
6 "strings"6 "strings"
7 7
8+ "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/application/usecase"9 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/infrastructure/webkit"11 "github.com/bnema/dumber/internal/infrastructure/webkit"
11 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
12- webkitlib "github.com/bnema/puregotk-webkit/webkit"
13 "github.com/jwijenbergh/puregotk/v4/gdk"13 "github.com/jwijenbergh/puregotk/v4/gdk"
14 )14 )
15 15
16-func shouldRenderCrashPage(reason webkitlib.WebProcessTerminationReason) bool {16+// internalSchemePath is the path used in dumb:// URIs (replaces webkit.HomePath).
17+const internalSchemePath = "home"
18+
19+func shouldRenderCrashPage(reason port.WebProcessTerminationReason) bool {
17 switch reason {20 switch reason {
18- case webkitlib.WebProcessCrashedValue, webkitlib.WebProcessExceededMemoryLimitValue:21+ case port.WebProcessTerminationCrashed, port.WebProcessTerminationExceededMemory:
19 return true22 return true
20- case webkitlib.WebProcessTerminatedByApiValue:23+ case port.WebProcessTerminationByAPI:
21 return false24 return false
22 default:25 default:
23 return true26 return true
@@ -34,7 +37,7 @@ func extractOriginalURIFromCrashPage(uri string) string {
34 return uri37 return uri
35 }38 }
36 39
37- if parsed.Scheme != "dumb" || parsed.Host != webkit.HomePath {40+ if parsed.Scheme != "dumb" || parsed.Host != internalSchemePath {
38 return uri41 return uri
39 }42 }
40 43
@@ -59,94 +62,94 @@ func buildCrashPageURI(originalURI string) string {
59 }62 }
60 63
61 // setupWebViewCallbacks configures standard callbacks and popup handling.64 // setupWebViewCallbacks configures standard callbacks and popup handling.
62-func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {65+func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
63 log := logging.FromContext(ctx)66 log := logging.FromContext(ctx)
64 67
65- // Title changes68+ // Build port callbacks for standard events
66- wv.OnTitleChanged = func(title string) {69+ callbacks := &port.WebViewCallbacks{
67- c.onTitleChanged(ctx, paneID, title)70+ OnTitleChanged: func(title string) {
68- }71+ c.onTitleChanged(ctx, paneID, title)
72+ },
73+ OnLoadChanged: func(event port.LoadEvent) {
74+ switch event {
75+ case port.LoadStarted:
76+ c.onLoadStarted(paneID)
77+ case port.LoadCommitted:
78+ c.onLoadCommitted(ctx, paneID, wv)
79+ case port.LoadFinished:
80+ c.onLoadFinished(ctx, paneID, wv)
81+ }
82+ },
83+ OnProgressChanged: func(progress float64) {
84+ c.onProgressChanged(paneID, progress)
85+ },
86+ OnURIChanged: func(uri string) {
87+ c.handleURIChanged(ctx, paneID, wv, uri)
88+ },
89+ OnLinkHover: func(uri string) {
90+ c.onLinkHover(paneID, uri)
91+ },
92+ OnWebProcessTerminated: func(reason port.WebProcessTerminationReason, reasonLabel string, uri string) {
93+ originalURI := extractOriginalURIFromCrashPage(uri)
94+ if !shouldRenderCrashPage(reason) {
95+ log.Info().
96+ Str("pane_id", string(paneID)).
97+ Str("reason", reasonLabel).
98+ Str("uri", uri).
99+ Msg("web process termination handled without crash page")
100+ return
101+ }
69 102
70- // Favicon changes - capture generation to detect stale callbacks after pool reuse103+ crashURI := buildCrashPageURI(originalURI)
71- faviconGen := wv.Generation()104+ log.Warn().
72- wv.OnFaviconChanged = func(favicon *gdk.Texture) {105+ Str("pane_id", string(paneID)).
73- if wv.Generation() != faviconGen {106+ Str("reason", reasonLabel).
74- return // WebView was reused, ignore stale signal107+ Str("uri", uri).
75- }108+ Str("crash_uri", crashURI).
76- c.onFaviconChanged(ctx, paneID, wv, favicon)109+ Msg("web process terminated, redirecting to crash page")
110+
111+ if err := wv.LoadURI(ctx, crashURI); err != nil {
112+ log.Error().
113+ Err(err).
114+ Str("pane_id", string(paneID)).
115+ Str("reason", reasonLabel).
116+ Str("uri", uri).
117+ Str("crash_uri", crashURI).
118+ Msg("failed to load crash page after web process termination")
119+ }
120+ },
121+ OnPermissionRequest: func(origin string, permTypes []string, allow, deny func()) bool {
122+ return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny)
123+ },
77 }124 }
78 125
79- // Load events126+ // Favicon changes - need webkit-specific Generation() to detect stale callbacks
80- wv.OnLoadChanged = func(event webkit.LoadEvent) {127+ if wkWV, ok := wv.(*webkit.WebView); ok {
81- switch event {128+ faviconGen := wkWV.Generation()
82- case webkit.LoadStarted:129+ callbacks.OnFaviconChanged = func(favicon port.Texture) {
83- c.onLoadStarted(paneID)130+ if wkWV.Generation() != faviconGen {
84- case webkit.LoadCommitted:131+ return // WebView was reused, ignore stale signal
85- c.onLoadCommitted(ctx, paneID, wv)132+ }
86- case webkit.LoadFinished:133+ if gdkTexture, ok := favicon.(*gdk.Texture); ok {
87- c.onLoadFinished(ctx, paneID, wv)134+ c.onFaviconChanged(ctx, paneID, wkWV, gdkTexture)
135+ }
88 }136 }
89 }137 }
90 138
91- // Progress139+ // Add popup create handler if popup handling is configured
92- wv.OnProgressChanged = func(progress float64) {140+ callbacks.OnCreate = c.buildPopupCreateHandler(ctx, paneID, wv)
93- c.onProgressChanged(paneID, progress)
94- }
95-
96- // SPA navigation and external scheme handling
97- wv.OnURIChanged = func(uri string) {
98- c.handleURIChanged(ctx, paneID, wv, uri)
99- }
100 141
101- // Middle-click / Ctrl+click handler for opening links in new pane142+ wv.SetCallbacks(callbacks)
102- wv.OnLinkMiddleClick = func(uri string) bool {
103- return c.handleLinkMiddleClick(ctx, paneID, uri)
104- }
105 143
106- // Link hover callback for status overlay144+ // Webkit-specific callbacks not in port.WebViewCallbacks (middle-click, fullscreen, audio)
107- wv.OnLinkHover = func(uri string) {145+ if wkWV, ok := wv.(*webkit.WebView); ok {
108- c.onLinkHover(paneID, uri)146+ wkWV.OnLinkMiddleClick = func(uri string) bool {
109- }147+ return c.handleLinkMiddleClick(ctx, paneID, uri)
110-
111- wv.OnWebProcessTerminated = func(reason webkitlib.WebProcessTerminationReason, reasonLabel string, uri string) {
112- originalURI := extractOriginalURIFromCrashPage(uri)
113- if !shouldRenderCrashPage(reason) {
114- log.Info().
115- Str("pane_id", string(paneID)).
116- Str("reason", reasonLabel).
117- Str("uri", uri).
118- Msg("web process termination handled without crash page")
119- return
120- }
121-
122- crashURI := buildCrashPageURI(originalURI)
123- log.Warn().
124- Str("pane_id", string(paneID)).
125- Str("reason", reasonLabel).
126- Str("uri", uri).
127- Str("crash_uri", crashURI).
128- Msg("web process terminated, redirecting to crash page")
129-
130- if err := wv.LoadURI(ctx, crashURI); err != nil {
131- log.Error().
132- Err(err).
133- Str("pane_id", string(paneID)).
134- Str("reason", reasonLabel).
135- Str("uri", uri).
136- Str("crash_uri", crashURI).
137- Msg("failed to load crash page after web process termination")
138 }148 }
139 }149 }
140 150
141- // Permission request handler
142- wv.OnPermissionRequest = func(origin string, permTypes []string, allow, deny func()) bool {
143- return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny)
144- }
145 // Fullscreen handlers for idle inhibition151 // Fullscreen handlers for idle inhibition
146 c.setupIdleInhibitionHandlers(ctx, paneID, wv)152 c.setupIdleInhibitionHandlers(ctx, paneID, wv)
147-
148- // Popup handling for nested popups
149- c.setupPopupHandling(ctx, paneID, wv)
150 }153 }
151 154
152 // handlePermissionRequest processes media permission requests from WebKit.155 // handlePermissionRequest processes media permission requests from WebKit.
@@ -246,15 +249,21 @@ func filterWebRTCPermissionTypes(types []entity.PermissionType) []entity.Permiss
246 // - The webview enters fullscreen mode (e.g., fullscreen video)249 // - The webview enters fullscreen mode (e.g., fullscreen video)
247 // - The webview is playing audio (e.g., video/music playback)250 // - The webview is playing audio (e.g., video/music playback)
248 // The inhibitor uses refcounting, so both can be active simultaneously.251 // The inhibitor uses refcounting, so both can be active simultaneously.
249-func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {252+func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
250 log := logging.FromContext(ctx)253 log := logging.FromContext(ctx)
251 254
252 if wv == nil {255 if wv == nil {
253 return256 return
254 }257 }
255 258
259+ // Type-assert to access webkit-specific fullscreen/audio callbacks
260+ wkWV, ok := wv.(*webkit.WebView)
261+ if !ok {
262+ return
263+ }
264+
256 // Fullscreen handling265 // Fullscreen handling
257- wv.OnEnterFullscreen = func() bool {266+ wkWV.OnEnterFullscreen = func() bool {
258 if c.idleInhibitor != nil {267 if c.idleInhibitor != nil {
259 if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil {268 if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil {
260 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle")269 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle")
@@ -266,7 +275,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en
266 return false // Allow fullscreen275 return false // Allow fullscreen
267 }276 }
268 277
269- wv.OnLeaveFullscreen = func() bool {278+ wkWV.OnLeaveFullscreen = func() bool {
270 if c.idleInhibitor != nil {279 if c.idleInhibitor != nil {
271 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {280 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
272 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle")281 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle")
@@ -279,7 +288,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en
279 }288 }
280 289
281 // Audio playback handling290 // Audio playback handling
282- wv.OnAudioStateChanged = func(playing bool) {291+ wkWV.OnAudioStateChanged = func(playing bool) {
283 if c.idleInhibitor == nil {292 if c.idleInhibitor == nil {
284 return293 return
285 }294 }
M internal/ui/coordinator/content/content_active_override_test.go +9 -8
@@ -6,19 +6,20 @@ import (
66
77 "github.com/stretchr/testify/assert"
88
9+ "github.com/bnema/dumber/internal/application/port"
10+ "github.com/bnema/dumber/internal/application/port/mocks"
911 "github.com/bnema/dumber/internal/domain/entity"
10- "github.com/bnema/dumber/internal/infrastructure/webkit"
1112 "github.com/bnema/dumber/internal/ui/component"
1213 )
1314
1415 func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
1516 t.Parallel()
1617
17- mainWV := &webkit.WebView{}
18- floatingWV := &webkit.WebView{}
18+ mainWV := mocks.NewMockWebView(t)
19+ floatingWV := mocks.NewMockWebView(t)
1920
2021 c := &Coordinator{
21- webViews: map[entity.PaneID]*webkit.WebView{
22+ webViews: map[entity.PaneID]port.WebView{
2223 "main-pane": mainWV,
2324 "floating-pane": floatingWV,
2425 },
@@ -33,10 +34,10 @@ func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
3334 func TestCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {
3435 t.Parallel()
3536
36- mainWV := &webkit.WebView{}
37+ mainWV := mocks.NewMockWebView(t)
3738
3839 c := &Coordinator{
39- webViews: map[entity.PaneID]*webkit.WebView{
40+ webViews: map[entity.PaneID]port.WebView{
4041 "main-pane": mainWV,
4142 },
4243 }
@@ -52,12 +53,12 @@ func TestCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.
5253 t.Parallel()
5354
5455 ctx := context.Background()
55- mainWV := &webkit.WebView{}
56+ mainWV := mocks.NewMockWebView(t)
5657 mainPane := entity.NewPane(entity.PaneID("main-pane"))
5758 ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane)
5859
5960 c := &Coordinator{
60- webViews: map[entity.PaneID]*webkit.WebView{
61+ webViews: map[entity.PaneID]port.WebView{
6162 mainPane.ID: mainWV,
6263 },
6364 getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
@@ -6,19 +6,20 @@ import (
6 6
7 "github.com/stretchr/testify/assert"7 "github.com/stretchr/testify/assert"
8 8
9+ "github.com/bnema/dumber/internal/application/port"
10+ "github.com/bnema/dumber/internal/application/port/mocks"
9 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
10- "github.com/bnema/dumber/internal/infrastructure/webkit"
11 "github.com/bnema/dumber/internal/ui/component"12 "github.com/bnema/dumber/internal/ui/component"
12 )13 )
13 14
14 func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) {15 func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
15 t.Parallel()16 t.Parallel()
16 17
17- mainWV := &webkit.WebView{}18+ mainWV := mocks.NewMockWebView(t)
18- floatingWV := &webkit.WebView{}19+ floatingWV := mocks.NewMockWebView(t)
19 20
20 c := &Coordinator{21 c := &Coordinator{
21- webViews: map[entity.PaneID]*webkit.WebView{22+ webViews: map[entity.PaneID]port.WebView{
22 "main-pane": mainWV,23 "main-pane": mainWV,
23 "floating-pane": floatingWV,24 "floating-pane": floatingWV,
24 },25 },
@@ -33,10 +34,10 @@ func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) {
33 func TestCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {34 func TestCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) {
34 t.Parallel()35 t.Parallel()
35 36
36- mainWV := &webkit.WebView{}37+ mainWV := mocks.NewMockWebView(t)
37 38
38 c := &Coordinator{39 c := &Coordinator{
39- webViews: map[entity.PaneID]*webkit.WebView{40+ webViews: map[entity.PaneID]port.WebView{
40 "main-pane": mainWV,41 "main-pane": mainWV,
41 },42 },
42 }43 }
@@ -52,12 +53,12 @@ func TestCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.
52 t.Parallel()53 t.Parallel()
53 54
54 ctx := context.Background()55 ctx := context.Background()
55- mainWV := &webkit.WebView{}56+ mainWV := mocks.NewMockWebView(t)
56 mainPane := entity.NewPane(entity.PaneID("main-pane"))57 mainPane := entity.NewPane(entity.PaneID("main-pane"))
57 ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane)58 ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane)
58 59
59 c := &Coordinator{60 c := &Coordinator{
60- webViews: map[entity.PaneID]*webkit.WebView{61+ webViews: map[entity.PaneID]port.WebView{
61 mainPane.ID: mainWV,62 mainPane.ID: mainWV,
62 },63 },
63 getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {64 getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
M internal/ui/coordinator/content/content_reveal_test.go +5 -5
@@ -7,8 +7,8 @@ import (
77
88 "github.com/stretchr/testify/assert"
99
10+ "github.com/bnema/dumber/internal/application/port"
1011 "github.com/bnema/dumber/internal/domain/entity"
11- "github.com/bnema/dumber/internal/infrastructure/webkit"
1212 )
1313
1414 // TestPendingReveal tests the pending reveal state management.
@@ -49,7 +49,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
4949 callbackCalled := false
5050 c := &Coordinator{
5151 pendingReveal: make(map[entity.PaneID]bool),
52- webViews: make(map[entity.PaneID]*webkit.WebView),
52+ webViews: make(map[entity.PaneID]port.WebView),
5353 onWebViewShown: func(paneID entity.PaneID) {
5454 callbackCalled = true
5555 },
@@ -65,7 +65,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
6565 func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) {
6666 c := &Coordinator{
6767 pendingReveal: make(map[entity.PaneID]bool),
68- webViews: make(map[entity.PaneID]*webkit.WebView),
68+ webViews: make(map[entity.PaneID]port.WebView),
6969 }
7070 paneID := entity.PaneID("pane-1")
7171
@@ -84,7 +84,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
8484 revealCount := 0
8585 c := &Coordinator{
8686 pendingReveal: make(map[entity.PaneID]bool),
87- webViews: make(map[entity.PaneID]*webkit.WebView),
87+ webViews: make(map[entity.PaneID]port.WebView),
8888 onWebViewShown: func(paneID entity.PaneID) {
8989 revealCount++
9090 },
@@ -111,7 +111,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
111111 func TestPendingReveal_ConcurrentAccess(t *testing.T) {
112112 c := &Coordinator{
113113 pendingReveal: make(map[entity.PaneID]bool),
114- webViews: make(map[entity.PaneID]*webkit.WebView),
114+ webViews: make(map[entity.PaneID]port.WebView),
115115 }
116116
117117 var wg sync.WaitGroup
@@ -7,8 +7,8 @@ import (
7 7
8 "github.com/stretchr/testify/assert"8 "github.com/stretchr/testify/assert"
9 9
10+ "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
11- "github.com/bnema/dumber/internal/infrastructure/webkit"
12 )12 )
13 13
14 // TestPendingReveal tests the pending reveal state management.14 // TestPendingReveal tests the pending reveal state management.
@@ -49,7 +49,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
49 callbackCalled := false49 callbackCalled := false
50 c := &Coordinator{50 c := &Coordinator{
51 pendingReveal: make(map[entity.PaneID]bool),51 pendingReveal: make(map[entity.PaneID]bool),
52- webViews: make(map[entity.PaneID]*webkit.WebView),52+ webViews: make(map[entity.PaneID]port.WebView),
53 onWebViewShown: func(paneID entity.PaneID) {53 onWebViewShown: func(paneID entity.PaneID) {
54 callbackCalled = true54 callbackCalled = true
55 },55 },
@@ -65,7 +65,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
65 func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) {65 func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) {
66 c := &Coordinator{66 c := &Coordinator{
67 pendingReveal: make(map[entity.PaneID]bool),67 pendingReveal: make(map[entity.PaneID]bool),
68- webViews: make(map[entity.PaneID]*webkit.WebView),68+ webViews: make(map[entity.PaneID]port.WebView),
69 }69 }
70 paneID := entity.PaneID("pane-1")70 paneID := entity.PaneID("pane-1")
71 71
@@ -84,7 +84,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
84 revealCount := 084 revealCount := 0
85 c := &Coordinator{85 c := &Coordinator{
86 pendingReveal: make(map[entity.PaneID]bool),86 pendingReveal: make(map[entity.PaneID]bool),
87- webViews: make(map[entity.PaneID]*webkit.WebView),87+ webViews: make(map[entity.PaneID]port.WebView),
88 onWebViewShown: func(paneID entity.PaneID) {88 onWebViewShown: func(paneID entity.PaneID) {
89 revealCount++89 revealCount++
90 },90 },
@@ -111,7 +111,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
111 func TestPendingReveal_ConcurrentAccess(t *testing.T) {111 func TestPendingReveal_ConcurrentAccess(t *testing.T) {
112 c := &Coordinator{112 c := &Coordinator{
113 pendingReveal: make(map[entity.PaneID]bool),113 pendingReveal: make(map[entity.PaneID]bool),
114- webViews: make(map[entity.PaneID]*webkit.WebView),114+ webViews: make(map[entity.PaneID]port.WebView),
115 }115 }
116 116
117 var wg sync.WaitGroup117 var wg sync.WaitGroup
M internal/ui/coordinator/content/coordinator.go +12 -12
@@ -19,14 +19,14 @@ import (
1919
2020 // Coordinator manages WebView lifecycle, title tracking, and content attachment.
2121 type Coordinator struct {
22- pool *webkit.WebViewPool
22+ pool port.WebViewPool
2323 widgetFactory layout.WidgetFactory
2424 faviconAdapter *adapter.FaviconAdapter
2525 zoomUC *usecase.ManageZoomUseCase
2626 permissionUC *usecase.HandlePermissionUseCase
2727 injector *webkit.ContentInjector
2828
29- webViews map[entity.PaneID]*webkit.WebView
29+ webViews map[entity.PaneID]port.WebView
3030 webViewsMu sync.RWMutex
3131
3232 activePaneOverride entity.PaneID
@@ -79,7 +79,7 @@ type Coordinator struct {
7979 gestureActionHandler input.ActionHandler
8080
8181 // Popup handling
82- factory *webkit.WebViewFactory
82+ factory port.WebViewFactory
8383 popupConfig *config.PopupBehaviorConfig
8484 pendingPopups map[port.WebViewID]*PendingPopup
8585 popupOAuth map[port.WebViewID]*popupOAuthState
@@ -102,7 +102,7 @@ type Coordinator struct {
102102 onFullscreenChanged func(entering bool)
103103
104104 // Callback when WebView gains focus (for accent picker text input targeting)
105- onWebViewFocused func(paneID entity.PaneID, wv *webkit.WebView)
105+ onWebViewFocused func(paneID entity.PaneID, wv port.WebView)
106106
107107 // Callback for first load_started event (triggers deferred initialization)
108108 onFirstLoadStarted func()
@@ -134,7 +134,7 @@ const (
134134 // NewCoordinator creates a new Coordinator.
135135 func NewCoordinator(
136136 ctx context.Context,
137- pool *webkit.WebViewPool,
137+ pool port.WebViewPool,
138138 widgetFactory layout.WidgetFactory,
139139 faviconAdapter *adapter.FaviconAdapter,
140140 getActiveWS func() (*entity.Workspace, *component.WorkspaceView),
@@ -150,7 +150,7 @@ func NewCoordinator(
150150 faviconAdapter: faviconAdapter,
151151 zoomUC: zoomUC,
152152 permissionUC: permissionUC,
153- webViews: make(map[entity.PaneID]*webkit.WebView),
153+ webViews: make(map[entity.PaneID]port.WebView),
154154 paneTitles: make(map[entity.PaneID]string),
155155 navOrigins: make(map[entity.PaneID]string),
156156 pendingReveal: make(map[entity.PaneID]bool),
@@ -214,7 +214,7 @@ func (c *Coordinator) SetOnFullscreenChanged(fn func(entering bool)) {
214214 }
215215
216216 // SetOnWebViewFocused sets the callback for when a WebView gains focus.
217-func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv *webkit.WebView)) {
217+func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv port.WebView)) {
218218 c.onWebViewFocused = fn
219219 }
220220
@@ -270,19 +270,19 @@ func (c *Coordinator) webViewCount() int {
270270 return len(c.webViews)
271271 }
272272
273-func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) *webkit.WebView {
273+func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView {
274274 c.webViewsMu.RLock()
275275 defer c.webViewsMu.RUnlock()
276276 return c.webViews[paneID]
277277 }
278278
279-func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv *webkit.WebView) {
279+func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv port.WebView) {
280280 c.webViewsMu.Lock()
281281 c.webViews[paneID] = wv
282282 c.webViewsMu.Unlock()
283283 }
284284
285-func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView {
285+func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView {
286286 c.webViewsMu.Lock()
287287 defer c.webViewsMu.Unlock()
288288 wv := c.webViews[paneID]
@@ -290,10 +290,10 @@ func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView
290290 return wv
291291 }
292292
293-func (c *Coordinator) snapshotWebViews() map[entity.PaneID]*webkit.WebView {
293+func (c *Coordinator) snapshotWebViews() map[entity.PaneID]port.WebView {
294294 c.webViewsMu.RLock()
295295 defer c.webViewsMu.RUnlock()
296- snapshot := make(map[entity.PaneID]*webkit.WebView, len(c.webViews))
296+ snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))
297297 for paneID, wv := range c.webViews {
298298 snapshot[paneID] = wv
299299 }
@@ -19,14 +19,14 @@ import (
19 19
20 // Coordinator manages WebView lifecycle, title tracking, and content attachment.20 // Coordinator manages WebView lifecycle, title tracking, and content attachment.
21 type Coordinator struct {21 type Coordinator struct {
22- pool *webkit.WebViewPool22+ pool port.WebViewPool
23 widgetFactory layout.WidgetFactory23 widgetFactory layout.WidgetFactory
24 faviconAdapter *adapter.FaviconAdapter24 faviconAdapter *adapter.FaviconAdapter
25 zoomUC *usecase.ManageZoomUseCase25 zoomUC *usecase.ManageZoomUseCase
26 permissionUC *usecase.HandlePermissionUseCase26 permissionUC *usecase.HandlePermissionUseCase
27 injector *webkit.ContentInjector27 injector *webkit.ContentInjector
28 28
29- webViews map[entity.PaneID]*webkit.WebView29+ webViews map[entity.PaneID]port.WebView
30 webViewsMu sync.RWMutex30 webViewsMu sync.RWMutex
31 31
32 activePaneOverride entity.PaneID32 activePaneOverride entity.PaneID
@@ -79,7 +79,7 @@ type Coordinator struct {
79 gestureActionHandler input.ActionHandler79 gestureActionHandler input.ActionHandler
80 80
81 // Popup handling81 // Popup handling
82- factory *webkit.WebViewFactory82+ factory port.WebViewFactory
83 popupConfig *config.PopupBehaviorConfig83 popupConfig *config.PopupBehaviorConfig
84 pendingPopups map[port.WebViewID]*PendingPopup84 pendingPopups map[port.WebViewID]*PendingPopup
85 popupOAuth map[port.WebViewID]*popupOAuthState85 popupOAuth map[port.WebViewID]*popupOAuthState
@@ -102,7 +102,7 @@ type Coordinator struct {
102 onFullscreenChanged func(entering bool)102 onFullscreenChanged func(entering bool)
103 103
104 // Callback when WebView gains focus (for accent picker text input targeting)104 // Callback when WebView gains focus (for accent picker text input targeting)
105- onWebViewFocused func(paneID entity.PaneID, wv *webkit.WebView)105+ onWebViewFocused func(paneID entity.PaneID, wv port.WebView)
106 106
107 // Callback for first load_started event (triggers deferred initialization)107 // Callback for first load_started event (triggers deferred initialization)
108 onFirstLoadStarted func()108 onFirstLoadStarted func()
@@ -134,7 +134,7 @@ const (
134 // NewCoordinator creates a new Coordinator.134 // NewCoordinator creates a new Coordinator.
135 func NewCoordinator(135 func NewCoordinator(
136 ctx context.Context,136 ctx context.Context,
137- pool *webkit.WebViewPool,137+ pool port.WebViewPool,
138 widgetFactory layout.WidgetFactory,138 widgetFactory layout.WidgetFactory,
139 faviconAdapter *adapter.FaviconAdapter,139 faviconAdapter *adapter.FaviconAdapter,
140 getActiveWS func() (*entity.Workspace, *component.WorkspaceView),140 getActiveWS func() (*entity.Workspace, *component.WorkspaceView),
@@ -150,7 +150,7 @@ func NewCoordinator(
150 faviconAdapter: faviconAdapter,150 faviconAdapter: faviconAdapter,
151 zoomUC: zoomUC,151 zoomUC: zoomUC,
152 permissionUC: permissionUC,152 permissionUC: permissionUC,
153- webViews: make(map[entity.PaneID]*webkit.WebView),153+ webViews: make(map[entity.PaneID]port.WebView),
154 paneTitles: make(map[entity.PaneID]string),154 paneTitles: make(map[entity.PaneID]string),
155 navOrigins: make(map[entity.PaneID]string),155 navOrigins: make(map[entity.PaneID]string),
156 pendingReveal: make(map[entity.PaneID]bool),156 pendingReveal: make(map[entity.PaneID]bool),
@@ -214,7 +214,7 @@ func (c *Coordinator) SetOnFullscreenChanged(fn func(entering bool)) {
214 }214 }
215 215
216 // SetOnWebViewFocused sets the callback for when a WebView gains focus.216 // SetOnWebViewFocused sets the callback for when a WebView gains focus.
217-func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv *webkit.WebView)) {217+func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv port.WebView)) {
218 c.onWebViewFocused = fn218 c.onWebViewFocused = fn
219 }219 }
220 220
@@ -270,19 +270,19 @@ func (c *Coordinator) webViewCount() int {
270 return len(c.webViews)270 return len(c.webViews)
271 }271 }
272 272
273-func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) *webkit.WebView {273+func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView {
274 c.webViewsMu.RLock()274 c.webViewsMu.RLock()
275 defer c.webViewsMu.RUnlock()275 defer c.webViewsMu.RUnlock()
276 return c.webViews[paneID]276 return c.webViews[paneID]
277 }277 }
278 278
279-func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv *webkit.WebView) {279+func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv port.WebView) {
280 c.webViewsMu.Lock()280 c.webViewsMu.Lock()
281 c.webViews[paneID] = wv281 c.webViews[paneID] = wv
282 c.webViewsMu.Unlock()282 c.webViewsMu.Unlock()
283 }283 }
284 284
285-func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView {285+func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView {
286 c.webViewsMu.Lock()286 c.webViewsMu.Lock()
287 defer c.webViewsMu.Unlock()287 defer c.webViewsMu.Unlock()
288 wv := c.webViews[paneID]288 wv := c.webViews[paneID]
@@ -290,10 +290,10 @@ func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView
290 return wv290 return wv
291 }291 }
292 292
293-func (c *Coordinator) snapshotWebViews() map[entity.PaneID]*webkit.WebView {293+func (c *Coordinator) snapshotWebViews() map[entity.PaneID]port.WebView {
294 c.webViewsMu.RLock()294 c.webViewsMu.RLock()
295 defer c.webViewsMu.RUnlock()295 defer c.webViewsMu.RUnlock()
296- snapshot := make(map[entity.PaneID]*webkit.WebView, len(c.webViews))296+ snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))
297 for paneID, wv := range c.webViews {297 for paneID, wv := range c.webViews {
298 snapshot[paneID] = wv298 snapshot[paneID] = wv
299 }299 }
M internal/ui/coordinator/content/lifecycle.go +18 -8
@@ -4,6 +4,7 @@ import (
44 "context"
55 "fmt"
66
7+ "github.com/bnema/dumber/internal/application/port"
78 "github.com/bnema/dumber/internal/domain/entity"
89 "github.com/bnema/dumber/internal/infrastructure/webkit"
910 "github.com/bnema/dumber/internal/logging"
@@ -13,7 +14,7 @@ import (
1314 )
1415
1516 // EnsureWebView acquires or reuses a WebView for the given pane.
16-func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) {
17+func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) {
1718 log := logging.FromContext(ctx)
1819
1920 if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {
@@ -81,7 +82,7 @@ func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID)
8182 c.navOriginMu.Unlock()
8283
8384 if c.pool != nil {
84- c.pool.Release(ctx, wv)
85+ c.pool.Release(wv)
8586 } else {
8687 wv.Destroy()
8788 }
@@ -129,7 +130,7 @@ func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspac
129130
130131 // WrapWidget converts a WebView to a layout.Widget for embedding.
131132 // It also attaches gesture handlers for mouse button navigation.
132-func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout.Widget {
133+func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget {
133134 log := logging.FromContext(ctx)
134135
135136 if wv == nil || c.widgetFactory == nil {
@@ -137,7 +138,14 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout
137138 return nil
138139 }
139140
140- gtkView := wv.Widget()
141+ // Type-assert to access webkit-specific Widget() for GTK embedding
142+ wkWV, ok := wv.(*webkit.WebView)
143+ if !ok {
144+ log.Debug().Msg("webview does not support widget embedding")
145+ return nil
146+ }
147+
148+ gtkView := wkWV.Widget()
141149 if gtkView == nil {
142150 return nil
143151 }
@@ -148,7 +156,9 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout
148156 if widget != nil {
149157 gestureHandler := input.NewGestureHandler(ctx)
150158 // Pass WebView directly to preserve user gesture context (like Epiphany)
151- gestureHandler.SetNavigator(wv)
159+ if nav, ok := wv.(input.DirectNavigator); ok {
160+ gestureHandler.SetNavigator(nav)
161+ }
152162 // Keep callback as fallback
153163 if c.gestureActionHandler != nil {
154164 gestureHandler.SetOnAction(c.gestureActionHandler)
@@ -161,7 +171,7 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout
161171 }
162172
163173 // ActiveWebView returns the WebView for the active pane.
164-func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
174+func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView {
165175 log := logging.FromContext(ctx)
166176
167177 if paneID, ok := c.activePaneOverrideID(); ok {
@@ -192,13 +202,13 @@ func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
192202 }
193203
194204 // GetWebView returns the WebView for a specific pane.
195-func (c *Coordinator) GetWebView(paneID entity.PaneID) *webkit.WebView {
205+func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView {
196206 return c.getWebViewLocked(paneID)
197207 }
198208
199209 // RegisterPopupWebView registers a popup WebView that was created externally.
200210 // This is used when popup tabs are created and the WebView needs to be tracked.
201-func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv *webkit.WebView) {
211+func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) {
202212 if wv != nil && paneID != "" {
203213 c.setWebViewLocked(paneID, wv)
204214 }
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6 6
7+ "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
8 "github.com/bnema/dumber/internal/infrastructure/webkit"9 "github.com/bnema/dumber/internal/infrastructure/webkit"
9 "github.com/bnema/dumber/internal/logging"10 "github.com/bnema/dumber/internal/logging"
@@ -13,7 +14,7 @@ import (
13 )14 )
14 15
15 // EnsureWebView acquires or reuses a WebView for the given pane.16 // EnsureWebView acquires or reuses a WebView for the given pane.
16-func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) {17+func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) {
17 log := logging.FromContext(ctx)18 log := logging.FromContext(ctx)
18 19
19 if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {20 if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {
@@ -81,7 +82,7 @@ func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID)
81 c.navOriginMu.Unlock()82 c.navOriginMu.Unlock()
82 83
83 if c.pool != nil {84 if c.pool != nil {
84- c.pool.Release(ctx, wv)85+ c.pool.Release(wv)
85 } else {86 } else {
86 wv.Destroy()87 wv.Destroy()
87 }88 }
@@ -129,7 +130,7 @@ func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspac
129 130
130 // WrapWidget converts a WebView to a layout.Widget for embedding.131 // WrapWidget converts a WebView to a layout.Widget for embedding.
131 // It also attaches gesture handlers for mouse button navigation.132 // It also attaches gesture handlers for mouse button navigation.
132-func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout.Widget {133+func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget {
133 log := logging.FromContext(ctx)134 log := logging.FromContext(ctx)
134 135
135 if wv == nil || c.widgetFactory == nil {136 if wv == nil || c.widgetFactory == nil {
@@ -137,7 +138,14 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout
137 return nil138 return nil
138 }139 }
139 140
140- gtkView := wv.Widget()141+ // Type-assert to access webkit-specific Widget() for GTK embedding
142+ wkWV, ok := wv.(*webkit.WebView)
143+ if !ok {
144+ log.Debug().Msg("webview does not support widget embedding")
145+ return nil
146+ }
147+
148+ gtkView := wkWV.Widget()
141 if gtkView == nil {149 if gtkView == nil {
142 return nil150 return nil
143 }151 }
@@ -148,7 +156,9 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout
148 if widget != nil {156 if widget != nil {
149 gestureHandler := input.NewGestureHandler(ctx)157 gestureHandler := input.NewGestureHandler(ctx)
150 // Pass WebView directly to preserve user gesture context (like Epiphany)158 // Pass WebView directly to preserve user gesture context (like Epiphany)
151- gestureHandler.SetNavigator(wv)159+ if nav, ok := wv.(input.DirectNavigator); ok {
160+ gestureHandler.SetNavigator(nav)
161+ }
152 // Keep callback as fallback162 // Keep callback as fallback
153 if c.gestureActionHandler != nil {163 if c.gestureActionHandler != nil {
154 gestureHandler.SetOnAction(c.gestureActionHandler)164 gestureHandler.SetOnAction(c.gestureActionHandler)
@@ -161,7 +171,7 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout
161 }171 }
162 172
163 // ActiveWebView returns the WebView for the active pane.173 // ActiveWebView returns the WebView for the active pane.
164-func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView {174+func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView {
165 log := logging.FromContext(ctx)175 log := logging.FromContext(ctx)
166 176
167 if paneID, ok := c.activePaneOverrideID(); ok {177 if paneID, ok := c.activePaneOverrideID(); ok {
@@ -192,13 +202,13 @@ func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
192 }202 }
193 203
194 // GetWebView returns the WebView for a specific pane.204 // GetWebView returns the WebView for a specific pane.
195-func (c *Coordinator) GetWebView(paneID entity.PaneID) *webkit.WebView {205+func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView {
196 return c.getWebViewLocked(paneID)206 return c.getWebViewLocked(paneID)
197 }207 }
198 208
199 // RegisterPopupWebView registers a popup WebView that was created externally.209 // RegisterPopupWebView registers a popup WebView that was created externally.
200 // This is used when popup tabs are created and the WebView needs to be tracked.210 // This is used when popup tabs are created and the WebView needs to be tracked.
201-func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv *webkit.WebView) {211+func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) {
202 if wv != nil && paneID != "" {212 if wv != nil && paneID != "" {
203 c.setWebViewLocked(paneID, wv)213 c.setWebViewLocked(paneID, wv)
204 }214 }
M internal/ui/coordinator/content/navigation.go +32 -25
@@ -4,6 +4,7 @@ import (
44 "context"
55 "strings"
66
7+ "github.com/bnema/dumber/internal/application/port"
78 "github.com/bnema/dumber/internal/application/usecase"
89 "github.com/bnema/dumber/internal/domain/entity"
910 urlutil "github.com/bnema/dumber/internal/domain/url"
@@ -16,7 +17,7 @@ import (
1617 // WebKit may reset zoom during document transitions, so we reapply after LoadCommitted.
1718 // History is recorded here because the URI is guaranteed to be correct after commit.
1819 // Also shows the WebView widget (it's hidden during creation to avoid white flash).
19-func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
20+func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
2021 log := logging.FromContext(ctx)
2122 logging.Trace().Mark("load_committed")
2223
@@ -26,20 +27,23 @@ func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID,
2627 }
2728
2829 // Set appropriate background color based on page type to prevent dark background bleeding.
29- switch {
30- case strings.HasPrefix(uri, "dumb://"):
31- // Internal pages: apply themed background
32- theme, ok := c.getCurrentTheme()
33- if ok && theme.prefersDark {
34- wv.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA)
35- } else {
36- wv.ResetBackgroundToDefault()
30+ // Type-assert to access webkit-specific background color methods.
31+ if wkWV, ok := wv.(*webkit.WebView); ok {
32+ switch {
33+ case strings.HasPrefix(uri, "dumb://"):
34+ // Internal pages: apply themed background
35+ theme, ok := c.getCurrentTheme()
36+ if ok && theme.prefersDark {
37+ wkWV.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA)
38+ } else {
39+ wkWV.ResetBackgroundToDefault()
40+ }
41+ case strings.HasPrefix(uri, "about:"):
42+ // Keep pool background (no action)
43+ default:
44+ // External pages: white background
45+ wkWV.ResetBackgroundToDefault()
3746 }
38- case strings.HasPrefix(uri, "about:"):
39- // Keep pool background (no action)
40- default:
41- // External pages: white background
42- wv.ResetBackgroundToDefault()
4347 }
4448
4549 // Show the WebView now that content is being painted
@@ -125,7 +129,7 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) {
125129 }
126130 }
127131
128-func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv *webkit.WebView) bool {
132+func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv port.WebView) bool {
129133 if wv == nil || wv.IsDestroyed() {
130134 return false
131135 }
@@ -190,7 +194,7 @@ func (c *Coordinator) onLoadStarted(paneID entity.PaneID) {
190194 }
191195
192196 // onLoadFinished hides the progress bar when page loading completes.
193-func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
197+func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
194198 _, wsView := c.getActiveWS()
195199 if wsView == nil {
196200 return
@@ -255,14 +259,17 @@ func (c *Coordinator) revealIfPending(ctx context.Context, paneID entity.PaneID,
255259 return
256260 }
257261
258- if inner := wv.Widget(); inner != nil {
259- inner.SetVisible(true)
260- logging.FromContext(ctx).
261- Debug().
262- Str("pane_id", string(paneID)).
263- Str("uri", uri).
264- Str("reason", reason).
265- Msg("webview revealed")
262+ // Type-assert to access webkit-specific Widget() for visibility control
263+ if wkWV, ok := wv.(*webkit.WebView); ok {
264+ if inner := wkWV.Widget(); inner != nil {
265+ inner.SetVisible(true)
266+ logging.FromContext(ctx).
267+ Debug().
268+ Str("pane_id", string(paneID)).
269+ Str("uri", uri).
270+ Str("reason", reason).
271+ Msg("webview revealed")
272+ }
266273 }
267274
268275 // Mark first_paint and finish startup trace
@@ -295,7 +302,7 @@ func (c *Coordinator) onLinkHover(paneID entity.PaneID, uri string) {
295302
296303 // handleURIChanged handles URI changes from WebKit, including external scheme detection
297304 // and SPA navigation tracking.
298-func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView, uri string) {
305+func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv port.WebView, uri string) {
299306 if uri == "" {
300307 return
301308 }
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "strings"5 "strings"
6 6
7+ "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/dumber/internal/application/usecase"8 "github.com/bnema/dumber/internal/application/usecase"
8 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
9 urlutil "github.com/bnema/dumber/internal/domain/url"10 urlutil "github.com/bnema/dumber/internal/domain/url"
@@ -16,7 +17,7 @@ import (
16 // WebKit may reset zoom during document transitions, so we reapply after LoadCommitted.17 // WebKit may reset zoom during document transitions, so we reapply after LoadCommitted.
17 // History is recorded here because the URI is guaranteed to be correct after commit.18 // History is recorded here because the URI is guaranteed to be correct after commit.
18 // Also shows the WebView widget (it's hidden during creation to avoid white flash).19 // Also shows the WebView widget (it's hidden during creation to avoid white flash).
19-func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {20+func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
20 log := logging.FromContext(ctx)21 log := logging.FromContext(ctx)
21 logging.Trace().Mark("load_committed")22 logging.Trace().Mark("load_committed")
22 23
@@ -26,20 +27,23 @@ func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID,
26 }27 }
27 28
28 // Set appropriate background color based on page type to prevent dark background bleeding.29 // Set appropriate background color based on page type to prevent dark background bleeding.
29- switch {30+ // Type-assert to access webkit-specific background color methods.
30- case strings.HasPrefix(uri, "dumb://"):31+ if wkWV, ok := wv.(*webkit.WebView); ok {
31- // Internal pages: apply themed background32+ switch {
32- theme, ok := c.getCurrentTheme()33+ case strings.HasPrefix(uri, "dumb://"):
33- if ok && theme.prefersDark {34+ // Internal pages: apply themed background
34- wv.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA)35+ theme, ok := c.getCurrentTheme()
35- } else {36+ if ok && theme.prefersDark {
36- wv.ResetBackgroundToDefault()37+ wkWV.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA)
38+ } else {
39+ wkWV.ResetBackgroundToDefault()
40+ }
41+ case strings.HasPrefix(uri, "about:"):
42+ // Keep pool background (no action)
43+ default:
44+ // External pages: white background
45+ wkWV.ResetBackgroundToDefault()
37 }46 }
38- case strings.HasPrefix(uri, "about:"):
39- // Keep pool background (no action)
40- default:
41- // External pages: white background
42- wv.ResetBackgroundToDefault()
43 }47 }
44 48
45 // Show the WebView now that content is being painted49 // Show the WebView now that content is being painted
@@ -125,7 +129,7 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) {
125 }129 }
126 }130 }
127 131
128-func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv *webkit.WebView) bool {132+func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv port.WebView) bool {
129 if wv == nil || wv.IsDestroyed() {133 if wv == nil || wv.IsDestroyed() {
130 return false134 return false
131 }135 }
@@ -190,7 +194,7 @@ func (c *Coordinator) onLoadStarted(paneID entity.PaneID) {
190 }194 }
191 195
192 // onLoadFinished hides the progress bar when page loading completes.196 // onLoadFinished hides the progress bar when page loading completes.
193-func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {197+func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
194 _, wsView := c.getActiveWS()198 _, wsView := c.getActiveWS()
195 if wsView == nil {199 if wsView == nil {
196 return200 return
@@ -255,14 +259,17 @@ func (c *Coordinator) revealIfPending(ctx context.Context, paneID entity.PaneID,
255 return259 return
256 }260 }
257 261
258- if inner := wv.Widget(); inner != nil {262+ // Type-assert to access webkit-specific Widget() for visibility control
259- inner.SetVisible(true)263+ if wkWV, ok := wv.(*webkit.WebView); ok {
260- logging.FromContext(ctx).264+ if inner := wkWV.Widget(); inner != nil {
261- Debug().265+ inner.SetVisible(true)
262- Str("pane_id", string(paneID)).266+ logging.FromContext(ctx).
263- Str("uri", uri).267+ Debug().
264- Str("reason", reason).268+ Str("pane_id", string(paneID)).
265- Msg("webview revealed")269+ Str("uri", uri).
270+ Str("reason", reason).
271+ Msg("webview revealed")
272+ }
266 }273 }
267 274
268 // Mark first_paint and finish startup trace275 // Mark first_paint and finish startup trace
@@ -295,7 +302,7 @@ func (c *Coordinator) onLinkHover(paneID entity.PaneID, uri string) {
295 302
296 // handleURIChanged handles URI changes from WebKit, including external scheme detection303 // handleURIChanged handles URI changes from WebKit, including external scheme detection
297 // and SPA navigation tracking.304 // and SPA navigation tracking.
298-func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView, uri string) {305+func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv port.WebView, uri string) {
299 if uri == "" {306 if uri == "" {
300 return307 return
301 }308 }
M internal/ui/coordinator/content/oauth.go +21 -9
@@ -208,10 +208,17 @@ func (c *Coordinator) setupOAuthAutoClose(
208208 ctx context.Context,
209209 paneID entity.PaneID,
210210 popupID port.WebViewID,
211- wv *webkit.WebView,
211+ wv port.WebView,
212212 ) {
213213 log := logging.FromContext(ctx)
214214
215+ // Type-assert to access webkit-specific direct field callbacks and Close()
216+ wkWV, ok := wv.(*webkit.WebView)
217+ if !ok {
218+ log.Debug().Str("pane_id", string(paneID)).Msg("oauth auto-close: webview does not support webkit callbacks")
219+ return
220+ }
221+
215222 // Safety timeout - only triggers if popup gets stuck (provider should close via window.close)
216223 var safetyTimer *time.Timer
217224 var safetyTimerMu sync.Mutex
@@ -232,7 +239,7 @@ func (c *Coordinator) setupOAuthAutoClose(
232239 uri := wv.URI()
233240 if shouldForceCloseOnSafetyTimeout(uri) {
234241 log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")
235- wv.Close()
242+ wkWV.Close()
236243 return false
237244 }
238245 log.Debug().
@@ -268,7 +275,7 @@ func (c *Coordinator) setupOAuthAutoClose(
268275 time.AfterFunc(oauthCloseDelay, func() {
269276 cb := glib.SourceFunc(func(_ uintptr) bool {
270277 if wv != nil && !wv.IsDestroyed() {
271- wv.Close()
278+ wkWV.Close()
272279 }
273280 return false
274281 })
@@ -281,26 +288,31 @@ func (c *Coordinator) setupOAuthAutoClose(
281288 startSafetyTimer()
282289
283290 // Wrap OnURIChanged to check for OAuth callbacks.
284- wv.OnURIChanged = composeOnURIChanged(wv.OnURIChanged, func(uri string) {
291+ wkWV.OnURIChanged = composeOnURIChanged(wkWV.OnURIChanged, func(uri string) {
285292 if ShouldAutoClose(uri) {
286293 requestOAuthClose(uri, "uri_changed")
287294 }
288295 })
289296
290297 // Monitor load events for URL-based detection.
291- wv.OnLoadChanged = composeOnLoadChanged(wv.OnLoadChanged, func(event webkit.LoadEvent) {
292- if event == webkit.LoadCommitted {
298+ // Wrap webkit.LoadEvent to port.LoadEvent at the boundary.
299+ existingOnLoad := wkWV.OnLoadChanged
300+ wkWV.OnLoadChanged = func(event webkit.LoadEvent) {
301+ if existingOnLoad != nil {
302+ existingOnLoad(event)
303+ }
304+ if port.LoadEvent(event) == port.LoadCommitted {
293305 uri := wv.URI()
294306 if ShouldAutoClose(uri) {
295307 requestOAuthClose(uri, "load_committed")
296308 }
297309 }
298- })
310+ }
299311
300312 // Cancel safety timer on any close path.
301- wv.OnClose = composeOnClose(func() {
313+ wkWV.OnClose = composeOnClose(func() {
302314 cancelSafetyTimer()
303- }, wv.OnClose)
315+ }, wkWV.OnClose)
304316 }
305317
306318 func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) {
@@ -208,10 +208,17 @@ func (c *Coordinator) setupOAuthAutoClose(
208 ctx context.Context,208 ctx context.Context,
209 paneID entity.PaneID,209 paneID entity.PaneID,
210 popupID port.WebViewID,210 popupID port.WebViewID,
211- wv *webkit.WebView,211+ wv port.WebView,
212 ) {212 ) {
213 log := logging.FromContext(ctx)213 log := logging.FromContext(ctx)
214 214
215+ // Type-assert to access webkit-specific direct field callbacks and Close()
216+ wkWV, ok := wv.(*webkit.WebView)
217+ if !ok {
218+ log.Debug().Str("pane_id", string(paneID)).Msg("oauth auto-close: webview does not support webkit callbacks")
219+ return
220+ }
221+
215 // Safety timeout - only triggers if popup gets stuck (provider should close via window.close)222 // Safety timeout - only triggers if popup gets stuck (provider should close via window.close)
216 var safetyTimer *time.Timer223 var safetyTimer *time.Timer
217 var safetyTimerMu sync.Mutex224 var safetyTimerMu sync.Mutex
@@ -232,7 +239,7 @@ func (c *Coordinator) setupOAuthAutoClose(
232 uri := wv.URI()239 uri := wv.URI()
233 if shouldForceCloseOnSafetyTimeout(uri) {240 if shouldForceCloseOnSafetyTimeout(uri) {
234 log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")241 log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")
235- wv.Close()242+ wkWV.Close()
236 return false243 return false
237 }244 }
238 log.Debug().245 log.Debug().
@@ -268,7 +275,7 @@ func (c *Coordinator) setupOAuthAutoClose(
268 time.AfterFunc(oauthCloseDelay, func() {275 time.AfterFunc(oauthCloseDelay, func() {
269 cb := glib.SourceFunc(func(_ uintptr) bool {276 cb := glib.SourceFunc(func(_ uintptr) bool {
270 if wv != nil && !wv.IsDestroyed() {277 if wv != nil && !wv.IsDestroyed() {
271- wv.Close()278+ wkWV.Close()
272 }279 }
273 return false280 return false
274 })281 })
@@ -281,26 +288,31 @@ func (c *Coordinator) setupOAuthAutoClose(
281 startSafetyTimer()288 startSafetyTimer()
282 289
283 // Wrap OnURIChanged to check for OAuth callbacks.290 // Wrap OnURIChanged to check for OAuth callbacks.
284- wv.OnURIChanged = composeOnURIChanged(wv.OnURIChanged, func(uri string) {291+ wkWV.OnURIChanged = composeOnURIChanged(wkWV.OnURIChanged, func(uri string) {
285 if ShouldAutoClose(uri) {292 if ShouldAutoClose(uri) {
286 requestOAuthClose(uri, "uri_changed")293 requestOAuthClose(uri, "uri_changed")
287 }294 }
288 })295 })
289 296
290 // Monitor load events for URL-based detection.297 // Monitor load events for URL-based detection.
291- wv.OnLoadChanged = composeOnLoadChanged(wv.OnLoadChanged, func(event webkit.LoadEvent) {298+ // Wrap webkit.LoadEvent to port.LoadEvent at the boundary.
292- if event == webkit.LoadCommitted {299+ existingOnLoad := wkWV.OnLoadChanged
300+ wkWV.OnLoadChanged = func(event webkit.LoadEvent) {
301+ if existingOnLoad != nil {
302+ existingOnLoad(event)
303+ }
304+ if port.LoadEvent(event) == port.LoadCommitted {
293 uri := wv.URI()305 uri := wv.URI()
294 if ShouldAutoClose(uri) {306 if ShouldAutoClose(uri) {
295 requestOAuthClose(uri, "load_committed")307 requestOAuthClose(uri, "load_committed")
296 }308 }
297 }309 }
298- })310+ }
299 311
300 // Cancel safety timer on any close path.312 // Cancel safety timer on any close path.
301- wv.OnClose = composeOnClose(func() {313+ wkWV.OnClose = composeOnClose(func() {
302 cancelSafetyTimer()314 cancelSafetyTimer()
303- }, wv.OnClose)315+ }, wkWV.OnClose)
304 }316 }
305 317
306 func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) {318 func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) {
M internal/ui/coordinator/content/oauth_test.go +2 -3
@@ -9,7 +9,6 @@ import (
99
1010 "github.com/bnema/dumber/internal/application/port"
1111 "github.com/bnema/dumber/internal/domain/entity"
12- "github.com/bnema/dumber/internal/infrastructure/webkit"
1312 )
1413
1514 func TestComposeOnClose_Order(t *testing.T) {
@@ -53,7 +52,7 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentRefresh(t *testing.T) {
5352 popupID := port.WebViewID(101)
5453
5554 c := &Coordinator{
56- webViews: make(map[entity.PaneID]*webkit.WebView),
55+ webViews: make(map[entity.PaneID]port.WebView),
5756 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
5857 popupRefresh: make(map[entity.PaneID]*time.Timer),
5958 }
@@ -82,7 +81,7 @@ func TestHandlePopupOAuthClose_ErrorDoesNotScheduleParentRefresh(t *testing.T) {
8281 popupID := port.WebViewID(102)
8382
8483 c := &Coordinator{
85- webViews: make(map[entity.PaneID]*webkit.WebView),
84+ webViews: make(map[entity.PaneID]port.WebView),
8685 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
8786 popupRefresh: make(map[entity.PaneID]*time.Timer),
8887 }
@@ -9,7 +9,6 @@ import (
9 9
10 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
12- "github.com/bnema/dumber/internal/infrastructure/webkit"
13 )12 )
14 13
15 func TestComposeOnClose_Order(t *testing.T) {14 func TestComposeOnClose_Order(t *testing.T) {
@@ -53,7 +52,7 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentRefresh(t *testing.T) {
53 popupID := port.WebViewID(101)52 popupID := port.WebViewID(101)
54 53
55 c := &Coordinator{54 c := &Coordinator{
56- webViews: make(map[entity.PaneID]*webkit.WebView),55+ webViews: make(map[entity.PaneID]port.WebView),
57 popupOAuth: make(map[port.WebViewID]*popupOAuthState),56 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
58 popupRefresh: make(map[entity.PaneID]*time.Timer),57 popupRefresh: make(map[entity.PaneID]*time.Timer),
59 }58 }
@@ -82,7 +81,7 @@ func TestHandlePopupOAuthClose_ErrorDoesNotScheduleParentRefresh(t *testing.T) {
82 popupID := port.WebViewID(102)81 popupID := port.WebViewID(102)
83 82
84 c := &Coordinator{83 c := &Coordinator{
85- webViews: make(map[entity.PaneID]*webkit.WebView),84+ webViews: make(map[entity.PaneID]port.WebView),
86 popupOAuth: make(map[port.WebViewID]*popupOAuthState),85 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
87 popupRefresh: make(map[entity.PaneID]*time.Timer),86 popupRefresh: make(map[entity.PaneID]*time.Timer),
88 }87 }
M internal/ui/coordinator/content/popup.go +23 -21
@@ -132,7 +132,7 @@ func GetBehavior(popupType PopupType, cfg *config.PopupBehaviorConfig) config.Po
132132
133133 // SetPopupConfig configures popup handling.
134134 func (c *Coordinator) SetPopupConfig(
135- factory *webkit.WebViewFactory,
135+ factory port.WebViewFactory,
136136 popupConfig *config.PopupBehaviorConfig,
137137 generateID func() string,
138138 ) {
@@ -151,21 +151,19 @@ func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity.
151151 c.onClosePane = fn
152152 }
153153
154-// setupPopupHandling wires the popup create signal for a WebView.
155-// This should be called after acquiring a WebView for a pane.
156-func (c *Coordinator) setupPopupHandling(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
157- log := logging.FromContext(ctx)
158-
154+// buildPopupCreateHandler returns the OnCreate callback for a WebView.
155+// Returns nil if popup handling is not configured.
156+func (c *Coordinator) buildPopupCreateHandler(ctx context.Context, paneID entity.PaneID, wv port.WebView) func(port.PopupRequest) port.WebView {
159157 if wv == nil {
160- return
158+ return nil
161159 }
162160
163- // Wire the OnCreate callback for popup requests
164- wv.OnCreate = func(req webkit.PopupRequest) *webkit.WebView {
161+ log := logging.FromContext(ctx)
162+ log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
163+
164+ return func(req port.PopupRequest) port.WebView {
165165 return c.handlePopupCreate(ctx, paneID, wv, req)
166166 }
167-
168- log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
169167 }
170168
171169 // createPopupPane creates a new pane entity for a popup window.
@@ -199,9 +197,9 @@ func (c *Coordinator) createPopupPane(
199197 func (c *Coordinator) handlePopupCreate(
200198 ctx context.Context,
201199 parentPaneID entity.PaneID,
202- parentWV *webkit.WebView,
203- req webkit.PopupRequest,
204-) *webkit.WebView {
200+ parentWV port.WebView,
201+ req port.PopupRequest,
202+) port.WebView {
205203 log := logging.FromContext(ctx)
206204
207205 log.Debug().
@@ -306,13 +304,15 @@ func (c *Coordinator) handlePopupCreate(
306304 c.pendingPopups[popupID] = pending
307305 c.popupMu.Unlock()
308306
309- // Wire ready-to-show (now just for visibility) and close signals
310- popupWV.OnReadyToShow = func() {
311- c.handlePopupReadyToShow(ctx, popupID)
307+ // Wire ready-to-show and close signals via webkit-specific fields
308+ if wkWV, ok := popupWV.(*webkit.WebView); ok {
309+ wkWV.OnReadyToShow = func() {
310+ c.handlePopupReadyToShow(ctx, popupID)
311+ }
312+ wkWV.OnClose = composeOnClose(wkWV.OnClose, func() {
313+ c.handlePopupClose(ctx, popupID)
314+ })
312315 }
313- popupWV.OnClose = composeOnClose(popupWV.OnClose, func() {
314- c.handlePopupClose(ctx, popupID)
315- })
316316
317317 log.Info().
318318 Uint64("popup_id", uint64(popupID)).
@@ -350,7 +350,9 @@ func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.W
350350
351351 // Make the WebView visible now that it's ready
352352 if pending.WebView != nil {
353- pending.WebView.Show()
353+ if wkWV, ok := pending.WebView.(*webkit.WebView); ok {
354+ wkWV.Show()
355+ }
354356 }
355357
356358 log.Info().
@@ -132,7 +132,7 @@ func GetBehavior(popupType PopupType, cfg *config.PopupBehaviorConfig) config.Po
132 132
133 // SetPopupConfig configures popup handling.133 // SetPopupConfig configures popup handling.
134 func (c *Coordinator) SetPopupConfig(134 func (c *Coordinator) SetPopupConfig(
135- factory *webkit.WebViewFactory,135+ factory port.WebViewFactory,
136 popupConfig *config.PopupBehaviorConfig,136 popupConfig *config.PopupBehaviorConfig,
137 generateID func() string,137 generateID func() string,
138 ) {138 ) {
@@ -151,21 +151,19 @@ func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity.
151 c.onClosePane = fn151 c.onClosePane = fn
152 }152 }
153 153
154-// setupPopupHandling wires the popup create signal for a WebView.154+// buildPopupCreateHandler returns the OnCreate callback for a WebView.
155-// This should be called after acquiring a WebView for a pane.155+// Returns nil if popup handling is not configured.
156-func (c *Coordinator) setupPopupHandling(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {156+func (c *Coordinator) buildPopupCreateHandler(ctx context.Context, paneID entity.PaneID, wv port.WebView) func(port.PopupRequest) port.WebView {
157- log := logging.FromContext(ctx)
158-
159 if wv == nil {157 if wv == nil {
160- return158+ return nil
161 }159 }
162 160
163- // Wire the OnCreate callback for popup requests161+ log := logging.FromContext(ctx)
164- wv.OnCreate = func(req webkit.PopupRequest) *webkit.WebView {162+ log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
163+
164+ return func(req port.PopupRequest) port.WebView {
165 return c.handlePopupCreate(ctx, paneID, wv, req)165 return c.handlePopupCreate(ctx, paneID, wv, req)
166 }166 }
167-
168- log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
169 }167 }
170 168
171 // createPopupPane creates a new pane entity for a popup window.169 // createPopupPane creates a new pane entity for a popup window.
@@ -199,9 +197,9 @@ func (c *Coordinator) createPopupPane(
199 func (c *Coordinator) handlePopupCreate(197 func (c *Coordinator) handlePopupCreate(
200 ctx context.Context,198 ctx context.Context,
201 parentPaneID entity.PaneID,199 parentPaneID entity.PaneID,
202- parentWV *webkit.WebView,200+ parentWV port.WebView,
203- req webkit.PopupRequest,201+ req port.PopupRequest,
204-) *webkit.WebView {202+) port.WebView {
205 log := logging.FromContext(ctx)203 log := logging.FromContext(ctx)
206 204
207 log.Debug().205 log.Debug().
@@ -306,13 +304,15 @@ func (c *Coordinator) handlePopupCreate(
306 c.pendingPopups[popupID] = pending304 c.pendingPopups[popupID] = pending
307 c.popupMu.Unlock()305 c.popupMu.Unlock()
308 306
309- // Wire ready-to-show (now just for visibility) and close signals307+ // Wire ready-to-show and close signals via webkit-specific fields
310- popupWV.OnReadyToShow = func() {308+ if wkWV, ok := popupWV.(*webkit.WebView); ok {
311- c.handlePopupReadyToShow(ctx, popupID)309+ wkWV.OnReadyToShow = func() {
310+ c.handlePopupReadyToShow(ctx, popupID)
311+ }
312+ wkWV.OnClose = composeOnClose(wkWV.OnClose, func() {
313+ c.handlePopupClose(ctx, popupID)
314+ })
312 }315 }
313- popupWV.OnClose = composeOnClose(popupWV.OnClose, func() {
314- c.handlePopupClose(ctx, popupID)
315- })
316 316
317 log.Info().317 log.Info().
318 Uint64("popup_id", uint64(popupID)).318 Uint64("popup_id", uint64(popupID)).
@@ -350,7 +350,9 @@ func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.W
350 350
351 // Make the WebView visible now that it's ready351 // Make the WebView visible now that it's ready
352 if pending.WebView != nil {352 if pending.WebView != nil {
353- pending.WebView.Show()353+ if wkWV, ok := pending.WebView.(*webkit.WebView); ok {
354+ wkWV.Show()
355+ }
354 }356 }
355 357
356 log.Info().358 log.Info().
M internal/ui/coordinator/content/title.go +12 -5
@@ -4,6 +4,7 @@ import (
44 "context"
55 "strings"
66
7+ "github.com/bnema/dumber/internal/application/port"
78 "github.com/bnema/dumber/internal/domain/entity"
89 "github.com/bnema/dumber/internal/infrastructure/webkit"
910 "github.com/bnema/dumber/internal/logging"
@@ -115,7 +116,7 @@ func (c *Coordinator) syncStackedTitle(ctx context.Context, paneID entity.PaneID
115116 }
116117
117118 // onFaviconChanged updates favicon tracking when a WebView's favicon changes.
118-func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV *webkit.WebView, favicon *gdk.Texture) {
119+func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV port.WebView, favicon *gdk.Texture) {
119120 log := logging.FromContext(ctx)
120121
121122 // Verify this WebView is still bound to the expected pane
@@ -189,7 +190,7 @@ func (c *Coordinator) updateStackedFaviconForPane(ctx context.Context, paneID en
189190 // First checks if WebKit already has a favicon for the page (common for subpath URLs).
190191 // Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API).
191192 // This closes the gap where title bars relied solely on the notify::favicon signal.
192-func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {
193+func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
193194 if c.faviconAdapter == nil || wv == nil {
194195 return
195196 }
@@ -208,8 +209,14 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity
208209 return
209210 }
210211
212+ // Type-assert to access webkit-specific Favicon() and Generation()
213+ wkWV, ok := wv.(*webkit.WebView)
214+ if !ok {
215+ return
216+ }
217+
211218 // Check if WebKit already has a favicon for this page
212- if icon := wv.Favicon(); icon != nil {
219+ if icon := wkWV.Favicon(); icon != nil {
213220 log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing WebKit favicon for committed page")
214221 // Store and update through the normal path
215222 c.navOriginMu.RLock()
@@ -222,7 +229,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity
222229
223230 // Fall back to GetOrFetch (checks service cache, WebKit DB, then DuckDuckGo API)
224231 // Capture current generation to guard against stale callbacks
225- gen := wv.Generation()
232+ gen := wkWV.Generation()
226233 capturedURI := uri
227234 c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) {
228235 // Skip nil results — a nil means "couldn't resolve", not "no favicon".
@@ -232,7 +239,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity
232239 return
233240 }
234241 // Verify WebView is still bound to pane and hasn't been reused
235- if wv.Generation() != gen {
242+ if wkWV.Generation() != gen {
236243 return
237244 }
238245 currentWV := c.getWebViewLocked(paneID)
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "strings"5 "strings"
6 6
7+ "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
8 "github.com/bnema/dumber/internal/infrastructure/webkit"9 "github.com/bnema/dumber/internal/infrastructure/webkit"
9 "github.com/bnema/dumber/internal/logging"10 "github.com/bnema/dumber/internal/logging"
@@ -115,7 +116,7 @@ func (c *Coordinator) syncStackedTitle(ctx context.Context, paneID entity.PaneID
115 }116 }
116 117
117 // onFaviconChanged updates favicon tracking when a WebView's favicon changes.118 // onFaviconChanged updates favicon tracking when a WebView's favicon changes.
118-func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV *webkit.WebView, favicon *gdk.Texture) {119+func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV port.WebView, favicon *gdk.Texture) {
119 log := logging.FromContext(ctx)120 log := logging.FromContext(ctx)
120 121
121 // Verify this WebView is still bound to the expected pane122 // Verify this WebView is still bound to the expected pane
@@ -189,7 +190,7 @@ func (c *Coordinator) updateStackedFaviconForPane(ctx context.Context, paneID en
189 // First checks if WebKit already has a favicon for the page (common for subpath URLs).190 // First checks if WebKit already has a favicon for the page (common for subpath URLs).
190 // Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API).191 // Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API).
191 // This closes the gap where title bars relied solely on the notify::favicon signal.192 // This closes the gap where title bars relied solely on the notify::favicon signal.
192-func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) {193+func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
193 if c.faviconAdapter == nil || wv == nil {194 if c.faviconAdapter == nil || wv == nil {
194 return195 return
195 }196 }
@@ -208,8 +209,14 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity
208 return209 return
209 }210 }
210 211
212+ // Type-assert to access webkit-specific Favicon() and Generation()
213+ wkWV, ok := wv.(*webkit.WebView)
214+ if !ok {
215+ return
216+ }
217+
211 // Check if WebKit already has a favicon for this page218 // Check if WebKit already has a favicon for this page
212- if icon := wv.Favicon(); icon != nil {219+ if icon := wkWV.Favicon(); icon != nil {
213 log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing WebKit favicon for committed page")220 log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing WebKit favicon for committed page")
214 // Store and update through the normal path221 // Store and update through the normal path
215 c.navOriginMu.RLock()222 c.navOriginMu.RLock()
@@ -222,7 +229,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity
222 229
223 // Fall back to GetOrFetch (checks service cache, WebKit DB, then DuckDuckGo API)230 // Fall back to GetOrFetch (checks service cache, WebKit DB, then DuckDuckGo API)
224 // Capture current generation to guard against stale callbacks231 // Capture current generation to guard against stale callbacks
225- gen := wv.Generation()232+ gen := wkWV.Generation()
226 capturedURI := uri233 capturedURI := uri
227 c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) {234 c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) {
228 // Skip nil results — a nil means "couldn't resolve", not "no favicon".235 // Skip nil results — a nil means "couldn't resolve", not "no favicon".
@@ -232,7 +239,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity
232 return239 return
233 }240 }
234 // Verify WebView is still bound to pane and hasn't been reused241 // Verify WebView is still bound to pane and hasn't been reused
235- if wv.Generation() != gen {242+ if wkWV.Generation() != gen {
236 return243 return
237 }244 }
238 currentWV := c.getWebViewLocked(paneID)245 currentWV := c.getWebViewLocked(paneID)

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