refactor(M6): decouple ui/app.go from infrastructure/webkit
- Add runtime methods to port.Engine: RegisterHandlers, RegisterAccentHandlers, ConfigureDownloads, OnToolkitReady, UpdateAppearance, UpdateSettings - Add HandlerDependencies struct to port for handler registration - Add TextInputTargetProvider optional capability to port.WebView - Implement all new methods on webkit.Engine with function injection to avoid import cycle (webkit -> handlers -> webkit) - Wire handler registrar closures through bootstrap/engine.go - Rewrite app.go to use port methods exclusively, removing all wkEngine type assertions and Internal*() escape hatches - Remove webkit and webkit/handlers imports from ui/app.go M6 invariants verified: - No infrastructure/webkit imports in internal/ui/ - Bootstrap only imports webkit in engine.go (dispatch) - Port layer has no infrastructure imports - port/webkit_context.go already deleted
- commit
- cfe4a9c0b53ca2e32e1b1379a6ace06776e5bc4a
- author
- brice · · 2026-03-17 08:56 UTC
- parent
- 5967a65
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +219 -111
M internal/application/port/content_injector.go +3 -0
| @@ -9,6 +9,9 @@ type ContentInjector interface { | |||
| 9 | 9 | // The css parameter should contain CSS custom property declarations. | |
| 10 | 10 | InjectThemeCSS(ctx context.Context, css string) error | |
| 11 | 11 | ||
| 12 | + | // InjectFindHighlightCSS injects CSS used to style in-page find highlights. | |
| 13 | + | InjectFindHighlightCSS(ctx context.Context, css string) error | |
| 14 | + | ||
| 12 | 15 | // RefreshScripts clears and re-injects user scripts for a single WebView. | |
| 13 | 16 | // Called when appearance settings change so future navigations pick up latest values. | |
| 14 | 17 | // Returns an error if the refresh could not be performed. | |
| @@ -9,6 +9,9 @@ type ContentInjector interface { | |||||
| 9 | // The css parameter should contain CSS custom property declarations. | 9 | // The css parameter should contain CSS custom property declarations. | ||
| 10 | InjectThemeCSS(ctx context.Context, css string) error | 10 | InjectThemeCSS(ctx context.Context, css string) error | ||
| 11 | 11 | ||||
| 12 | + | // InjectFindHighlightCSS injects CSS used to style in-page find highlights. | |||
| 13 | + | InjectFindHighlightCSS(ctx context.Context, css string) error | |||
| 14 | + | ||||
| 12 | // RefreshScripts clears and re-injects user scripts for a single WebView. | 15 | // RefreshScripts clears and re-injects user scripts for a single WebView. | ||
| 13 | // Called when appearance settings change so future navigations pick up latest values. | 16 | // Called when appearance settings change so future navigations pick up latest values. | ||
| 14 | // Returns an error if the refresh could not be performed. | 17 | // Returns an error if the refresh could not be performed. | ||
M internal/application/port/engine.go +27 -0
| @@ -122,6 +122,33 @@ type Engine interface { | |||
| 122 | 122 | ||
| 123 | 123 | // Close releases all resources held by the engine. | |
| 124 | 124 | Close() error | |
| 125 | + | ||
| 126 | + | // RegisterHandlers registers all WebUI message bridge handlers. | |
| 127 | + | RegisterHandlers(ctx context.Context, deps HandlerDependencies) error | |
| 128 | + | ||
| 129 | + | // RegisterAccentHandlers registers accent/dead-key input handlers. | |
| 130 | + | RegisterAccentHandlers(ctx context.Context, handler any) error | |
| 131 | + | ||
| 132 | + | // ConfigureDownloads sets up download handling for this engine. | |
| 133 | + | ConfigureDownloads(ctx context.Context, downloadPath string, eventHandler DownloadEventHandler, prepareUC any) error | |
| 134 | + | ||
| 135 | + | // OnToolkitReady is called after the UI toolkit has initialized. | |
| 136 | + | OnToolkitReady(ctx context.Context) error | |
| 137 | + | ||
| 138 | + | // UpdateAppearance updates default background color for new WebViews. | |
| 139 | + | UpdateAppearance(ctx context.Context, r, g, b, alpha float64) error | |
| 140 | + | ||
| 141 | + | // UpdateSettings applies runtime config changes to engine internals. | |
| 142 | + | UpdateSettings(ctx context.Context, cfg any) error | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // HandlerDependencies holds use cases needed by WebUI message handlers. | |
| 146 | + | type HandlerDependencies struct { | |
| 147 | + | HistoryUC any | |
| 148 | + | FavoritesUC any | |
| 149 | + | Clipboard Clipboard | |
| 150 | + | ConfigGetter func() any | |
| 151 | + | OnClipboardCopied func(textLen int) | |
| 125 | 152 | } | |
| 126 | 153 | ||
| 127 | 154 | // SchemeHandler defines the port interface for registering custom URI schemes. | |
| @@ -122,6 +122,33 @@ type Engine interface { | |||||
| 122 | 122 | ||||
| 123 | // Close releases all resources held by the engine. | 123 | // Close releases all resources held by the engine. | ||
| 124 | Close() error | 124 | Close() error | ||
| 125 | + | ||||
| 126 | + | // RegisterHandlers registers all WebUI message bridge handlers. | |||
| 127 | + | RegisterHandlers(ctx context.Context, deps HandlerDependencies) error | |||
| 128 | + | ||||
| 129 | + | // RegisterAccentHandlers registers accent/dead-key input handlers. | |||
| 130 | + | RegisterAccentHandlers(ctx context.Context, handler any) error | |||
| 131 | + | ||||
| 132 | + | // ConfigureDownloads sets up download handling for this engine. | |||
| 133 | + | ConfigureDownloads(ctx context.Context, downloadPath string, eventHandler DownloadEventHandler, prepareUC any) error | |||
| 134 | + | ||||
| 135 | + | // OnToolkitReady is called after the UI toolkit has initialized. | |||
| 136 | + | OnToolkitReady(ctx context.Context) error | |||
| 137 | + | ||||
| 138 | + | // UpdateAppearance updates default background color for new WebViews. | |||
| 139 | + | UpdateAppearance(ctx context.Context, r, g, b, alpha float64) error | |||
| 140 | + | ||||
| 141 | + | // UpdateSettings applies runtime config changes to engine internals. | |||
| 142 | + | UpdateSettings(ctx context.Context, cfg any) error | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // HandlerDependencies holds use cases needed by WebUI message handlers. | |||
| 146 | + | type HandlerDependencies struct { | |||
| 147 | + | HistoryUC any | |||
| 148 | + | FavoritesUC any | |||
| 149 | + | Clipboard Clipboard | |||
| 150 | + | ConfigGetter func() any | |||
| 151 | + | OnClipboardCopied func(textLen int) | |||
| 125 | } | 152 | } | ||
| 126 | 153 | ||||
| 127 | // SchemeHandler defines the port interface for registering custom URI schemes. | 154 | // SchemeHandler defines the port interface for registering custom URI schemes. | ||
M internal/application/port/webview.go +6 -0
| @@ -332,3 +332,9 @@ type OAuthCallbackCapable interface { | |||
| 332 | 332 | // Close triggers a programmatic close of the WebView (equivalent to window.close()). | |
| 333 | 333 | Close() | |
| 334 | 334 | } | |
| 335 | + | ||
| 336 | + | // TextInputTargetProvider is an optional capability for WebViews that support | |
| 337 | + | // text input method integration. | |
| 338 | + | type TextInputTargetProvider interface { | |
| 339 | + | TextInputTarget() TextInputTarget | |
| 340 | + | } | |
| @@ -332,3 +332,9 @@ type OAuthCallbackCapable interface { | |||||
| 332 | // Close triggers a programmatic close of the WebView (equivalent to window.close()). | 332 | // Close triggers a programmatic close of the WebView (equivalent to window.close()). | ||
| 333 | Close() | 333 | Close() | ||
| 334 | } | 334 | } | ||
| 335 | + | ||||
| 336 | + | // TextInputTargetProvider is an optional capability for WebViews that support | |||
| 337 | + | // text input method integration. | |||
| 338 | + | type TextInputTargetProvider interface { | |||
| 339 | + | TextInputTarget() TextInputTarget | |||
| 340 | + | } | |||
M internal/bootstrap/engine.go +37 -0
| @@ -5,8 +5,10 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 8 | 9 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 9 | 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | |
| 10 | 12 | "github.com/bnema/dumber/internal/ui/theme" | |
| 11 | 13 | "github.com/rs/zerolog" | |
| 12 | 14 | ) | |
| @@ -41,6 +43,41 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||
| 41 | 43 | return webkit.NewEngine( | |
| 42 | 44 | input.Ctx, cfg, opts, wkCfg, | |
| 43 | 45 | input.ThemeManager, input.ColorResolver, input.Logger, | |
| 46 | + | func(ctx context.Context, router *webkit.MessageRouter, deps port.HandlerDependencies) error { | |
| 47 | + | var historyUC *usecase.SearchHistoryUseCase | |
| 48 | + | if deps.HistoryUC != nil { | |
| 49 | + | historyUC, _ = deps.HistoryUC.(*usecase.SearchHistoryUseCase) | |
| 50 | + | } | |
| 51 | + | var favoritesUC *usecase.ManageFavoritesUseCase | |
| 52 | + | if deps.FavoritesUC != nil { | |
| 53 | + | favoritesUC, _ = deps.FavoritesUC.(*usecase.ManageFavoritesUseCase) | |
| 54 | + | } | |
| 55 | + | var configGetter func() *config.Config | |
| 56 | + | if deps.ConfigGetter != nil { | |
| 57 | + | configGetter = func() *config.Config { | |
| 58 | + | raw := deps.ConfigGetter() | |
| 59 | + | if raw == nil { | |
| 60 | + | return nil | |
| 61 | + | } | |
| 62 | + | cfg, _ := raw.(*config.Config) | |
| 63 | + | return cfg | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | return handlers.RegisterAll(ctx, router, handlers.Config{ | |
| 67 | + | HistoryUC: historyUC, | |
| 68 | + | FavoritesUC: favoritesUC, | |
| 69 | + | Clipboard: deps.Clipboard, | |
| 70 | + | ConfigGetter: configGetter, | |
| 71 | + | OnClipboardCopied: deps.OnClipboardCopied, | |
| 72 | + | }) | |
| 73 | + | }, | |
| 74 | + | func(ctx context.Context, router *webkit.MessageRouter, handler any) error { | |
| 75 | + | accentHandler, ok := handler.(handlers.AccentKeyHandler) | |
| 76 | + | if !ok { | |
| 77 | + | return fmt.Errorf("handler does not implement AccentKeyHandler") | |
| 78 | + | } | |
| 79 | + | return handlers.RegisterAccentHandlers(ctx, router, accentHandler) | |
| 80 | + | }, | |
| 44 | 81 | ) | |
| 45 | 82 | case "cef": | |
| 46 | 83 | return nil, fmt.Errorf("CEF engine not yet implemented") | |
| @@ -5,8 +5,10 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | 6 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 8 | "github.com/bnema/dumber/internal/infrastructure/config" | 9 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | |||
| 10 | "github.com/bnema/dumber/internal/ui/theme" | 12 | "github.com/bnema/dumber/internal/ui/theme" | ||
| 11 | "github.com/rs/zerolog" | 13 | "github.com/rs/zerolog" | ||
| 12 | ) | 14 | ) | ||
| @@ -41,6 +43,41 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||||
| 41 | return webkit.NewEngine( | 43 | return webkit.NewEngine( | ||
| 42 | input.Ctx, cfg, opts, wkCfg, | 44 | input.Ctx, cfg, opts, wkCfg, | ||
| 43 | input.ThemeManager, input.ColorResolver, input.Logger, | 45 | input.ThemeManager, input.ColorResolver, input.Logger, | ||
| 46 | + | func(ctx context.Context, router *webkit.MessageRouter, deps port.HandlerDependencies) error { | |||
| 47 | + | var historyUC *usecase.SearchHistoryUseCase | |||
| 48 | + | if deps.HistoryUC != nil { | |||
| 49 | + | historyUC, _ = deps.HistoryUC.(*usecase.SearchHistoryUseCase) | |||
| 50 | + | } | |||
| 51 | + | var favoritesUC *usecase.ManageFavoritesUseCase | |||
| 52 | + | if deps.FavoritesUC != nil { | |||
| 53 | + | favoritesUC, _ = deps.FavoritesUC.(*usecase.ManageFavoritesUseCase) | |||
| 54 | + | } | |||
| 55 | + | var configGetter func() *config.Config | |||
| 56 | + | if deps.ConfigGetter != nil { | |||
| 57 | + | configGetter = func() *config.Config { | |||
| 58 | + | raw := deps.ConfigGetter() | |||
| 59 | + | if raw == nil { | |||
| 60 | + | return nil | |||
| 61 | + | } | |||
| 62 | + | cfg, _ := raw.(*config.Config) | |||
| 63 | + | return cfg | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | return handlers.RegisterAll(ctx, router, handlers.Config{ | |||
| 67 | + | HistoryUC: historyUC, | |||
| 68 | + | FavoritesUC: favoritesUC, | |||
| 69 | + | Clipboard: deps.Clipboard, | |||
| 70 | + | ConfigGetter: configGetter, | |||
| 71 | + | OnClipboardCopied: deps.OnClipboardCopied, | |||
| 72 | + | }) | |||
| 73 | + | }, | |||
| 74 | + | func(ctx context.Context, router *webkit.MessageRouter, handler any) error { | |||
| 75 | + | accentHandler, ok := handler.(handlers.AccentKeyHandler) | |||
| 76 | + | if !ok { | |||
| 77 | + | return fmt.Errorf("handler does not implement AccentKeyHandler") | |||
| 78 | + | } | |||
| 79 | + | return handlers.RegisterAccentHandlers(ctx, router, accentHandler) | |||
| 80 | + | }, | |||
| 44 | ) | 81 | ) | ||
| 45 | case "cef": | 82 | case "cef": | ||
| 46 | return nil, fmt.Errorf("CEF engine not yet implemented") | 83 | return nil, fmt.Errorf("CEF engine not yet implemented") | ||
M internal/infrastructure/webkit/engine.go +74 -0
| @@ -2,8 +2,11 @@ package webkit | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "fmt" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 7 | 10 | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 8 | 11 | "github.com/rs/zerolog" | |
| 9 | 12 | ) | |
| @@ -21,6 +24,10 @@ type Engine struct { | |||
| 21 | 24 | schemeHandler *DumbSchemeHandler | |
| 22 | 25 | schemePath string | |
| 23 | 26 | logger zerolog.Logger | |
| 27 | + | ||
| 28 | + | // Handler registrars - injected at construction to avoid import cycles. | |
| 29 | + | handlerRegistrar func(ctx context.Context, router *MessageRouter, deps port.HandlerDependencies) error | |
| 30 | + | accentHandlerRegistrar func(ctx context.Context, router *MessageRouter, handler any) error | |
| 24 | 31 | } | |
| 25 | 32 | ||
| 26 | 33 | // Compile-time check that Engine implements port.Engine. | |
| @@ -92,3 +99,70 @@ func (e *Engine) InternalMessageRouter() *MessageRouter { return e.messageRo | |||
| 92 | 99 | func (e *Engine) InternalPool() *WebViewPool { return e.pool } | |
| 93 | 100 | func (e *Engine) InternalFactory() *WebViewFactory { return e.factory } | |
| 94 | 101 | func (e *Engine) InternalFilterManager() *filtering.Manager { return e.filterManager } | |
| 102 | + | ||
| 103 | + | // RegisterHandlers registers all WebUI message bridge handlers. | |
| 104 | + | func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependencies) error { | |
| 105 | + | if e.messageRouter == nil { | |
| 106 | + | return fmt.Errorf("message router not initialized") | |
| 107 | + | } | |
| 108 | + | if e.handlerRegistrar == nil { | |
| 109 | + | return fmt.Errorf("handler registrar not configured") | |
| 110 | + | } | |
| 111 | + | return e.handlerRegistrar(ctx, e.messageRouter, deps) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // RegisterAccentHandlers registers accent/dead-key input handlers. | |
| 115 | + | func (e *Engine) RegisterAccentHandlers(ctx context.Context, handler any) error { | |
| 116 | + | if e.messageRouter == nil { | |
| 117 | + | return fmt.Errorf("message router not initialized") | |
| 118 | + | } | |
| 119 | + | if e.accentHandlerRegistrar == nil { | |
| 120 | + | return fmt.Errorf("accent handler registrar not configured") | |
| 121 | + | } | |
| 122 | + | return e.accentHandlerRegistrar(ctx, e.messageRouter, handler) | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // ConfigureDownloads sets up download handling. | |
| 126 | + | func (e *Engine) ConfigureDownloads(ctx context.Context, downloadPath string, eventHandler port.DownloadEventHandler, prepareUC any) error { | |
| 127 | + | if e.wkCtx == nil { | |
| 128 | + | return fmt.Errorf("webkit context not initialized") | |
| 129 | + | } | |
| 130 | + | uc, ok := prepareUC.(*usecase.PrepareDownloadUseCase) | |
| 131 | + | if !ok { | |
| 132 | + | return fmt.Errorf("prepareUC does not implement PrepareDownloadUseCase") | |
| 133 | + | } | |
| 134 | + | handler := NewDownloadHandler(downloadPath, eventHandler, uc) | |
| 135 | + | e.wkCtx.SetDownloadHandler(ctx, handler) | |
| 136 | + | return nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // OnToolkitReady refreshes pooled WebViews after toolkit init. | |
| 140 | + | func (e *Engine) OnToolkitReady(ctx context.Context) error { | |
| 141 | + | if e.pool != nil { | |
| 142 | + | e.pool.RefreshScripts(ctx) | |
| 143 | + | } | |
| 144 | + | return nil | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // UpdateAppearance updates default background color for pool and factory. | |
| 148 | + | func (e *Engine) UpdateAppearance(_ context.Context, r, g, b, alpha float64) error { | |
| 149 | + | if e.pool != nil { | |
| 150 | + | e.pool.SetBackgroundColor(float32(r), float32(g), float32(b), float32(alpha)) | |
| 151 | + | } | |
| 152 | + | if e.factory != nil { | |
| 153 | + | e.factory.SetBackgroundColor(float32(r), float32(g), float32(b), float32(alpha)) | |
| 154 | + | } | |
| 155 | + | return nil | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | // UpdateSettings applies runtime config changes to engine settings. | |
| 159 | + | func (e *Engine) UpdateSettings(ctx context.Context, cfg any) error { | |
| 160 | + | configCfg, ok := cfg.(*config.Config) | |
| 161 | + | if !ok { | |
| 162 | + | return fmt.Errorf("expected *config.Config, got %T", cfg) | |
| 163 | + | } | |
| 164 | + | if e.settings != nil { | |
| 165 | + | e.settings.UpdateFromConfig(ctx, configCfg) | |
| 166 | + | } | |
| 167 | + | return nil | |
| 168 | + | } | |
| @@ -2,8 +2,11 @@ package webkit | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "fmt" | |||
| 5 | 6 | ||||
| 6 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 7 | "github.com/bnema/dumber/internal/infrastructure/filtering" | 10 | "github.com/bnema/dumber/internal/infrastructure/filtering" | ||
| 8 | "github.com/rs/zerolog" | 11 | "github.com/rs/zerolog" | ||
| 9 | ) | 12 | ) | ||
| @@ -21,6 +24,10 @@ type Engine struct { | |||||
| 21 | schemeHandler *DumbSchemeHandler | 24 | schemeHandler *DumbSchemeHandler | ||
| 22 | schemePath string | 25 | schemePath string | ||
| 23 | logger zerolog.Logger | 26 | logger zerolog.Logger | ||
| 27 | + | ||||
| 28 | + | // Handler registrars - injected at construction to avoid import cycles. | |||
| 29 | + | handlerRegistrar func(ctx context.Context, router *MessageRouter, deps port.HandlerDependencies) error | |||
| 30 | + | accentHandlerRegistrar func(ctx context.Context, router *MessageRouter, handler any) error | |||
| 24 | } | 31 | } | ||
| 25 | 32 | ||||
| 26 | // Compile-time check that Engine implements port.Engine. | 33 | // Compile-time check that Engine implements port.Engine. | ||
| @@ -92,3 +99,70 @@ func (e *Engine) InternalMessageRouter() *MessageRouter { return e.messageRo | |||||
| 92 | func (e *Engine) InternalPool() *WebViewPool { return e.pool } | 99 | func (e *Engine) InternalPool() *WebViewPool { return e.pool } | ||
| 93 | func (e *Engine) InternalFactory() *WebViewFactory { return e.factory } | 100 | func (e *Engine) InternalFactory() *WebViewFactory { return e.factory } | ||
| 94 | func (e *Engine) InternalFilterManager() *filtering.Manager { return e.filterManager } | 101 | func (e *Engine) InternalFilterManager() *filtering.Manager { return e.filterManager } | ||
| 102 | + | ||||
| 103 | + | // RegisterHandlers registers all WebUI message bridge handlers. | |||
| 104 | + | func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependencies) error { | |||
| 105 | + | if e.messageRouter == nil { | |||
| 106 | + | return fmt.Errorf("message router not initialized") | |||
| 107 | + | } | |||
| 108 | + | if e.handlerRegistrar == nil { | |||
| 109 | + | return fmt.Errorf("handler registrar not configured") | |||
| 110 | + | } | |||
| 111 | + | return e.handlerRegistrar(ctx, e.messageRouter, deps) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // RegisterAccentHandlers registers accent/dead-key input handlers. | |||
| 115 | + | func (e *Engine) RegisterAccentHandlers(ctx context.Context, handler any) error { | |||
| 116 | + | if e.messageRouter == nil { | |||
| 117 | + | return fmt.Errorf("message router not initialized") | |||
| 118 | + | } | |||
| 119 | + | if e.accentHandlerRegistrar == nil { | |||
| 120 | + | return fmt.Errorf("accent handler registrar not configured") | |||
| 121 | + | } | |||
| 122 | + | return e.accentHandlerRegistrar(ctx, e.messageRouter, handler) | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // ConfigureDownloads sets up download handling. | |||
| 126 | + | func (e *Engine) ConfigureDownloads(ctx context.Context, downloadPath string, eventHandler port.DownloadEventHandler, prepareUC any) error { | |||
| 127 | + | if e.wkCtx == nil { | |||
| 128 | + | return fmt.Errorf("webkit context not initialized") | |||
| 129 | + | } | |||
| 130 | + | uc, ok := prepareUC.(*usecase.PrepareDownloadUseCase) | |||
| 131 | + | if !ok { | |||
| 132 | + | return fmt.Errorf("prepareUC does not implement PrepareDownloadUseCase") | |||
| 133 | + | } | |||
| 134 | + | handler := NewDownloadHandler(downloadPath, eventHandler, uc) | |||
| 135 | + | e.wkCtx.SetDownloadHandler(ctx, handler) | |||
| 136 | + | return nil | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | // OnToolkitReady refreshes pooled WebViews after toolkit init. | |||
| 140 | + | func (e *Engine) OnToolkitReady(ctx context.Context) error { | |||
| 141 | + | if e.pool != nil { | |||
| 142 | + | e.pool.RefreshScripts(ctx) | |||
| 143 | + | } | |||
| 144 | + | return nil | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // UpdateAppearance updates default background color for pool and factory. | |||
| 148 | + | func (e *Engine) UpdateAppearance(_ context.Context, r, g, b, alpha float64) error { | |||
| 149 | + | if e.pool != nil { | |||
| 150 | + | e.pool.SetBackgroundColor(float32(r), float32(g), float32(b), float32(alpha)) | |||
| 151 | + | } | |||
| 152 | + | if e.factory != nil { | |||
| 153 | + | e.factory.SetBackgroundColor(float32(r), float32(g), float32(b), float32(alpha)) | |||
| 154 | + | } | |||
| 155 | + | return nil | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // UpdateSettings applies runtime config changes to engine settings. | |||
| 159 | + | func (e *Engine) UpdateSettings(ctx context.Context, cfg any) error { | |||
| 160 | + | configCfg, ok := cfg.(*config.Config) | |||
| 161 | + | if !ok { | |||
| 162 | + | return fmt.Errorf("expected *config.Config, got %T", cfg) | |||
| 163 | + | } | |||
| 164 | + | if e.settings != nil { | |||
| 165 | + | e.settings.UpdateFromConfig(ctx, configCfg) | |||
| 166 | + | } | |||
| 167 | + | return nil | |||
| 168 | + | } | |||
M internal/infrastructure/webkit/engine_init.go +15 -11
| @@ -26,6 +26,8 @@ func NewEngine( | |||
| 26 | 26 | themeManager *theme.Manager, | |
| 27 | 27 | colorResolver port.ColorSchemeResolver, | |
| 28 | 28 | logger zerolog.Logger, | |
| 29 | + | handlerRegistrar func(ctx context.Context, router *MessageRouter, deps port.HandlerDependencies) error, | |
| 30 | + | accentHandlerRegistrar func(ctx context.Context, router *MessageRouter, handler any) error, | |
| 29 | 31 | ) (*Engine, error) { | |
| 30 | 32 | // --- Hardware survey and performance profile resolution --- | |
| 31 | 33 | perfSettings := engineSurveyHardwareAndResolveProfile(ctx, cfg, logger) | |
| @@ -104,17 +106,19 @@ func NewEngine( | |||
| 104 | 106 | ||
| 105 | 107 | // --- Assemble Engine --- | |
| 106 | 108 | engine := &Engine{ | |
| 107 | - | ctx: ctx, | |
| 108 | - | wkCtx: wkCtx, | |
| 109 | - | settings: settings, | |
| 110 | - | injector: injector, | |
| 111 | - | messageRouter: messageRouter, | |
| 112 | - | pool: pool, | |
| 113 | - | factory: factory, | |
| 114 | - | filterManager: filterManager, | |
| 115 | - | schemeHandler: schemeHandler, | |
| 116 | - | schemePath: "dumb://", | |
| 117 | - | logger: logger, | |
| 109 | + | ctx: ctx, | |
| 110 | + | wkCtx: wkCtx, | |
| 111 | + | settings: settings, | |
| 112 | + | injector: injector, | |
| 113 | + | messageRouter: messageRouter, | |
| 114 | + | pool: pool, | |
| 115 | + | factory: factory, | |
| 116 | + | filterManager: filterManager, | |
| 117 | + | schemeHandler: schemeHandler, | |
| 118 | + | schemePath: "dumb://", | |
| 119 | + | logger: logger, | |
| 120 | + | handlerRegistrar: handlerRegistrar, | |
| 121 | + | accentHandlerRegistrar: accentHandlerRegistrar, | |
| 118 | 122 | } | |
| 119 | 123 | ||
| 120 | 124 | return engine, nil | |
| @@ -26,6 +26,8 @@ func NewEngine( | |||||
| 26 | themeManager *theme.Manager, | 26 | themeManager *theme.Manager, | ||
| 27 | colorResolver port.ColorSchemeResolver, | 27 | colorResolver port.ColorSchemeResolver, | ||
| 28 | logger zerolog.Logger, | 28 | logger zerolog.Logger, | ||
| 29 | + | handlerRegistrar func(ctx context.Context, router *MessageRouter, deps port.HandlerDependencies) error, | |||
| 30 | + | accentHandlerRegistrar func(ctx context.Context, router *MessageRouter, handler any) error, | |||
| 29 | ) (*Engine, error) { | 31 | ) (*Engine, error) { | ||
| 30 | // --- Hardware survey and performance profile resolution --- | 32 | // --- Hardware survey and performance profile resolution --- | ||
| 31 | perfSettings := engineSurveyHardwareAndResolveProfile(ctx, cfg, logger) | 33 | perfSettings := engineSurveyHardwareAndResolveProfile(ctx, cfg, logger) | ||
| @@ -104,17 +106,19 @@ func NewEngine( | |||||
| 104 | 106 | ||||
| 105 | // --- Assemble Engine --- | 107 | // --- Assemble Engine --- | ||
| 106 | engine := &Engine{ | 108 | engine := &Engine{ | ||
| 107 | - | ctx: ctx, | 109 | + | ctx: ctx, |
| 108 | - | wkCtx: wkCtx, | 110 | + | wkCtx: wkCtx, |
| 109 | - | settings: settings, | 111 | + | settings: settings, |
| 110 | - | injector: injector, | 112 | + | injector: injector, |
| 111 | - | messageRouter: messageRouter, | 113 | + | messageRouter: messageRouter, |
| 112 | - | pool: pool, | 114 | + | pool: pool, |
| 113 | - | factory: factory, | 115 | + | factory: factory, |
| 114 | - | filterManager: filterManager, | 116 | + | filterManager: filterManager, |
| 115 | - | schemeHandler: schemeHandler, | 117 | + | schemeHandler: schemeHandler, |
| 116 | - | schemePath: "dumb://", | 118 | + | schemePath: "dumb://", |
| 117 | - | logger: logger, | 119 | + | logger: logger, |
| 120 | + | handlerRegistrar: handlerRegistrar, | |||
| 121 | + | accentHandlerRegistrar: accentHandlerRegistrar, | |||
| 118 | } | 122 | } | ||
| 119 | 123 | ||||
| 120 | return engine, nil | 124 | return engine, nil | ||
M internal/ui/app.go +57 -100
| @@ -20,9 +20,7 @@ import ( | |||
| 20 | 20 | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 21 | 21 | "github.com/bnema/dumber/internal/infrastructure/snapshot" | |
| 22 | 22 | "github.com/bnema/dumber/internal/infrastructure/textinput" | |
| 23 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 24 | 23 | ||
| 25 | - | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | |
| 26 | 24 | "github.com/bnema/dumber/internal/logging" | |
| 27 | 25 | "github.com/bnema/dumber/internal/ui/adapter" | |
| 28 | 26 | "github.com/bnema/dumber/internal/ui/component" | |
| @@ -100,9 +98,8 @@ type App struct { | |||
| 100 | 98 | // Floating pane sessions keyed by tab and profile session ID. | |
| 101 | 99 | floatingSessions map[floatingSessionKey]*floatingWorkspaceSession | |
| 102 | 100 | ||
| 103 | - | // Engine (port-level and concrete for webkit-specific operations) | |
| 104 | - | engine port.Engine | |
| 105 | - | wkEngine *webkit.Engine // single webkit escape hatch; all webkit-specific ops go through this | |
| 101 | + | // Engine | |
| 102 | + | engine port.Engine | |
| 106 | 103 | ||
| 107 | 104 | // Web content (managed by content.Coordinator) | |
| 108 | 105 | faviconAdapter *adapter.FaviconAdapter | |
| @@ -183,45 +180,28 @@ func New(deps *Dependencies) (*App, error) { | |||
| 183 | 180 | configManager: config.GetManager(), | |
| 184 | 181 | cancel: cancel, | |
| 185 | 182 | } | |
| 186 | - | // Type-assert engine to concrete *webkit.Engine for webkit-specific operations. | |
| 187 | - | if wke, ok := deps.Engine.(*webkit.Engine); ok { | |
| 188 | - | app.wkEngine = wke | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | // Resolve the message router via wkEngine, with a standalone fallback. | |
| 192 | - | var router *webkit.MessageRouter | |
| 193 | - | if app.wkEngine != nil { | |
| 194 | - | router = app.wkEngine.InternalMessageRouter() | |
| 195 | - | } | |
| 196 | - | if router == nil { | |
| 197 | - | router = webkit.NewMessageRouter(ctx) | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | // Register message handlers | |
| 201 | - | if router != nil { | |
| 202 | - | if err := handlers.RegisterAll(ctx, router, handlers.Config{ | |
| 203 | - | HistoryUC: deps.HistoryUC, | |
| 204 | - | FavoritesUC: deps.FavoritesUC, | |
| 205 | - | Clipboard: deps.Clipboard, | |
| 206 | - | ConfigGetter: config.Get, | |
| 207 | - | // AccentHandler is registered after initAccentPicker in onActivate | |
| 208 | - | OnClipboardCopied: func(textLen int) { | |
| 209 | - | // Show brief toast on auto-copy (similar to zellij footer notification) | |
| 210 | - | // Must schedule on GTK main thread since this is called from WebKit handler | |
| 211 | - | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 212 | - | if app.appToaster != nil { | |
| 213 | - | app.appToaster.Show(ctx, "Copied to clipboard", component.ToastInfo, | |
| 214 | - | component.WithDuration(component.ToastBriefDurationMs), | |
| 215 | - | component.WithPosition(component.ToastPositionBottomRight), | |
| 216 | - | ) | |
| 217 | - | } | |
| 218 | - | return false | |
| 219 | - | }) | |
| 220 | - | glib.IdleAdd(&cb, 0) | |
| 221 | - | }, | |
| 222 | - | }); err != nil { | |
| 223 | - | return nil, err | |
| 224 | - | } | |
| 183 | + | // Register message handlers through the engine. | |
| 184 | + | if err := deps.Engine.RegisterHandlers(ctx, port.HandlerDependencies{ | |
| 185 | + | HistoryUC: deps.HistoryUC, | |
| 186 | + | FavoritesUC: deps.FavoritesUC, | |
| 187 | + | Clipboard: deps.Clipboard, | |
| 188 | + | ConfigGetter: func() any { | |
| 189 | + | return config.Get() | |
| 190 | + | }, | |
| 191 | + | OnClipboardCopied: func(textLen int) { | |
| 192 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 193 | + | if app.appToaster != nil { | |
| 194 | + | app.appToaster.Show(ctx, "Copied to clipboard", component.ToastInfo, | |
| 195 | + | component.WithDuration(component.ToastBriefDurationMs), | |
| 196 | + | component.WithPosition(component.ToastPositionBottomRight), | |
| 197 | + | ) | |
| 198 | + | } | |
| 199 | + | return false | |
| 200 | + | }) | |
| 201 | + | glib.IdleAdd(&cb, 0) | |
| 202 | + | }, | |
| 203 | + | }); err != nil { | |
| 204 | + | return nil, err | |
| 225 | 205 | } | |
| 226 | 206 | ||
| 227 | 207 | return app, nil | |
| @@ -246,10 +226,8 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||
| 246 | 226 | ||
| 247 | 227 | // Refresh scripts in pooled WebViews that were prewarmed before adw.Init(). | |
| 248 | 228 | // They have the wrong dark mode preference injected; re-inject with correct value. | |
| 249 | - | if a.wkEngine != nil { | |
| 250 | - | if pool := a.wkEngine.InternalPool(); pool != nil { | |
| 251 | - | pool.RefreshScripts(ctx) | |
| 252 | - | } | |
| 229 | + | if a.engine != nil { | |
| 230 | + | _ = a.engine.OnToolkitReady(ctx) | |
| 253 | 231 | } | |
| 254 | 232 | } | |
| 255 | 233 | ||
| @@ -371,38 +349,30 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) { | |||
| 371 | 349 | ||
| 372 | 350 | func (a *App) setupPoolBackgroundColor(ctx context.Context) { | |
| 373 | 351 | log := logging.FromContext(ctx) | |
| 374 | - | ||
| 375 | - | // Set theme background color on pool to eliminate white flash. | |
| 376 | - | // Must be done before any WebView creation so WebViews get the correct color. | |
| 377 | - | if a.wkEngine == nil { | |
| 378 | - | log.Debug().Msg("webview pool not available; skipping background color setup") | |
| 379 | - | return | |
| 380 | - | } | |
| 381 | - | pool := a.wkEngine.InternalPool() | |
| 382 | - | if pool == nil { | |
| 383 | - | log.Debug().Msg("webview pool not available; skipping background color setup") | |
| 352 | + | if a.engine == nil { | |
| 353 | + | log.Debug().Msg("engine not available; skipping background color setup") | |
| 384 | 354 | return | |
| 385 | 355 | } | |
| 386 | 356 | if a.deps == nil || a.deps.Theme == nil { | |
| 387 | 357 | log.Debug().Msg("theme not available; skipping webview pool background color setup") | |
| 388 | 358 | return | |
| 389 | 359 | } | |
| 390 | - | ||
| 391 | 360 | r, g, b, alpha := a.deps.Theme.GetBackgroundRGBA() | |
| 392 | - | pool.SetBackgroundColor(r, g, b, alpha) | |
| 361 | + | if err := a.engine.UpdateAppearance(ctx, float64(r), float64(g), float64(b), float64(alpha)); err != nil { | |
| 362 | + | log.Warn().Err(err).Msg("failed to update engine appearance") | |
| 363 | + | } | |
| 393 | 364 | log.Debug().Msg("configured webview pool background color") | |
| 394 | 365 | } | |
| 395 | 366 | ||
| 396 | - | func (a *App) prewarmWebViewPoolAsync(ctx context.Context) { | |
| 397 | - | // Prewarm WebView pool after startup so cold-start navigation is not blocked. | |
| 398 | - | if a.wkEngine == nil { | |
| 367 | + | func (a *App) prewarmWebViewPoolAsync(_ context.Context) { | |
| 368 | + | if a.engine == nil { | |
| 399 | 369 | return | |
| 400 | 370 | } | |
| 401 | - | pool := a.wkEngine.InternalPool() | |
| 371 | + | pool := a.engine.Pool() | |
| 402 | 372 | if pool == nil { | |
| 403 | 373 | return | |
| 404 | 374 | } | |
| 405 | - | pool.PrewarmAsync(ctx, 0) | |
| 375 | + | pool.Prewarm(0) | |
| 406 | 376 | } | |
| 407 | 377 | ||
| 408 | 378 | func (a *App) createMainWindow(ctx context.Context) error { | |
| @@ -570,16 +540,12 @@ func (a *App) registerAccentHandlers(ctx context.Context) { | |||
| 570 | 540 | log.Debug().Msg("insertAccentUC not available, skipping accent handler registration") | |
| 571 | 541 | return | |
| 572 | 542 | } | |
| 573 | - | var router *webkit.MessageRouter | |
| 574 | - | if a.wkEngine != nil { | |
| 575 | - | router = a.wkEngine.InternalMessageRouter() | |
| 576 | - | } | |
| 577 | - | if router == nil { | |
| 578 | - | log.Debug().Msg("router not available, skipping accent handler registration") | |
| 543 | + | if a.engine == nil { | |
| 544 | + | log.Debug().Msg("engine not available, skipping accent handler registration") | |
| 579 | 545 | return | |
| 580 | 546 | } | |
| 581 | 547 | ||
| 582 | - | if err := handlers.RegisterAccentHandlers(ctx, router, a.insertAccentUC); err != nil { | |
| 548 | + | if err := a.engine.RegisterAccentHandlers(ctx, a.insertAccentUC); err != nil { | |
| 583 | 549 | log.Error().Err(err).Msg("failed to register accent handlers") | |
| 584 | 550 | } else { | |
| 585 | 551 | log.Info().Msg("registered accent key handlers") | |
| @@ -589,7 +555,7 @@ func (a *App) registerAccentHandlers(ctx context.Context) { | |||
| 589 | 555 | func (a *App) initDownloadHandler(ctx context.Context) { | |
| 590 | 556 | log := logging.FromContext(ctx) | |
| 591 | 557 | ||
| 592 | - | if a.deps == nil || a.wkEngine == nil { | |
| 558 | + | if a.deps == nil || a.engine == nil { | |
| 593 | 559 | log.Debug().Msg("WebContext not available, skipping download handler") | |
| 594 | 560 | return | |
| 595 | 561 | } | |
| @@ -623,9 +589,10 @@ func (a *App) initDownloadHandler(ctx context.Context) { | |||
| 623 | 589 | // Create use case for preparing download destinations with file deduplication. | |
| 624 | 590 | prepareDownloadUC := usecase.NewPrepareDownloadUseCase(filesystem.New()) | |
| 625 | 591 | ||
| 626 | - | // Create and wire the download handler. | |
| 627 | - | handler := webkit.NewDownloadHandler(downloadPath, eventAdapter, prepareDownloadUC) | |
| 628 | - | a.wkEngine.InternalContext().SetDownloadHandler(ctx, handler) | |
| 592 | + | if err := a.engine.ConfigureDownloads(ctx, downloadPath, eventAdapter, prepareDownloadUC); err != nil { | |
| 593 | + | log.Error().Err(err).Msg("failed to configure downloads") | |
| 594 | + | return | |
| 595 | + | } | |
| 629 | 596 | ||
| 630 | 597 | log.Info().Str("path", downloadPath).Msg("download handler initialized") | |
| 631 | 598 | } | |
| @@ -1618,11 +1585,9 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1618 | 1585 | ||
| 1619 | 1586 | // Wire popup handling | |
| 1620 | 1587 | // Set theme background color on the engine's popup factory to eliminate white flash. | |
| 1621 | - | if a.wkEngine != nil && a.deps.Theme != nil { | |
| 1588 | + | if a.engine != nil && a.deps.Theme != nil { | |
| 1622 | 1589 | r, g, b, alpha := a.deps.Theme.GetBackgroundRGBA() | |
| 1623 | - | if f := a.wkEngine.InternalFactory(); f != nil { | |
| 1624 | - | f.SetBackgroundColor(r, g, b, alpha) | |
| 1625 | - | } | |
| 1590 | + | _ = a.engine.UpdateAppearance(context.Background(), float64(r), float64(g), float64(b), float64(alpha)) | |
| 1626 | 1591 | } | |
| 1627 | 1592 | a.contentCoord.SetPopupConfig( | |
| 1628 | 1593 | a.engine.Factory(), | |
| @@ -2145,12 +2110,10 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget { | |||
| 2145 | 2110 | return nil | |
| 2146 | 2111 | } | |
| 2147 | 2112 | ||
| 2148 | - | // Get the underlying webkit.WebView for the text input target | |
| 2149 | - | wkWV, ok := wv.(*webkit.WebView) | |
| 2150 | - | if !ok { | |
| 2151 | - | return nil | |
| 2113 | + | if provider, ok := wv.(port.TextInputTargetProvider); ok { | |
| 2114 | + | return provider.TextInputTarget() | |
| 2152 | 2115 | } | |
| 2153 | - | return textinput.NewWebViewTarget(wkWV.Widget()) | |
| 2116 | + | return nil | |
| 2154 | 2117 | } | |
| 2155 | 2118 | ||
| 2156 | 2119 | // attachPopupToTab attaches a popup WebView to a newly created tab. | |
| @@ -3100,11 +3063,8 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||
| 3100 | 3063 | *a.deps.Config = *cfg | |
| 3101 | 3064 | } | |
| 3102 | 3065 | ||
| 3103 | - | // Update WebKit settings manager | |
| 3104 | - | if a.wkEngine != nil { | |
| 3105 | - | if settings := a.wkEngine.InternalSettings(); settings != nil { | |
| 3106 | - | settings.UpdateFromConfig(ctx, cfg) | |
| 3107 | - | } | |
| 3066 | + | if a.engine != nil { | |
| 3067 | + | _ = a.engine.UpdateSettings(ctx, cfg) | |
| 3108 | 3068 | } | |
| 3109 | 3069 | ||
| 3110 | 3070 | // Apply settings to existing webviews via coordinator | |
| @@ -3120,28 +3080,25 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||
| 3120 | 3080 | } | |
| 3121 | 3081 | a.deps.Theme.UpdateFromConfig(ctx, cfg, display) | |
| 3122 | 3082 | ||
| 3123 | - | // Resolve injector once for all webkit-specific theme updates below. | |
| 3124 | - | var injector *webkit.ContentInjector | |
| 3125 | - | if a.wkEngine != nil { | |
| 3126 | - | injector = a.wkEngine.InternalInjector() | |
| 3083 | + | var inj port.ContentInjector | |
| 3084 | + | if a.engine != nil { | |
| 3085 | + | inj = a.engine.ContentInjector() | |
| 3127 | 3086 | } | |
| 3128 | 3087 | ||
| 3129 | - | // Update find highlight CSS for future navigations | |
| 3130 | - | if injector != nil { | |
| 3088 | + | if inj != nil { | |
| 3131 | 3089 | findCSS := theme.GenerateFindHighlightCSS(a.deps.Theme.GetCurrentPalette()) | |
| 3132 | - | if err := injector.InjectFindHighlightCSS(ctx, findCSS); err != nil { | |
| 3090 | + | if err := inj.InjectFindHighlightCSS(ctx, findCSS); err != nil { | |
| 3133 | 3091 | log.Warn().Err(err).Msg("failed to update find highlight CSS") | |
| 3134 | 3092 | } | |
| 3135 | 3093 | } | |
| 3136 | 3094 | ||
| 3137 | - | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | |
| 3095 | + | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(inj) | |
| 3138 | 3096 | cssText := a.deps.Theme.GetWebUIThemeCSS() | |
| 3139 | 3097 | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: cssText}); err != nil { | |
| 3140 | 3098 | log.Warn().Err(err).Msg("failed to update WebUI theme CSS") | |
| 3141 | 3099 | } | |
| 3142 | 3100 | ||
| 3143 | - | // Refresh injected scripts so future navigations use the latest theme. | |
| 3144 | - | if a.contentCoord != nil && injector != nil { | |
| 3101 | + | if a.contentCoord != nil && inj != nil { | |
| 3145 | 3102 | a.contentCoord.RefreshInjectedScriptsToAll(ctx) | |
| 3146 | 3103 | } | |
| 3147 | 3104 | ||
| @@ -20,9 +20,7 @@ import ( | |||||
| 20 | "github.com/bnema/dumber/internal/infrastructure/filtering" | 20 | "github.com/bnema/dumber/internal/infrastructure/filtering" | ||
| 21 | "github.com/bnema/dumber/internal/infrastructure/snapshot" | 21 | "github.com/bnema/dumber/internal/infrastructure/snapshot" | ||
| 22 | "github.com/bnema/dumber/internal/infrastructure/textinput" | 22 | "github.com/bnema/dumber/internal/infrastructure/textinput" | ||
| 23 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 24 | 23 | ||||
| 25 | - | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | |||
| 26 | "github.com/bnema/dumber/internal/logging" | 24 | "github.com/bnema/dumber/internal/logging" | ||
| 27 | "github.com/bnema/dumber/internal/ui/adapter" | 25 | "github.com/bnema/dumber/internal/ui/adapter" | ||
| 28 | "github.com/bnema/dumber/internal/ui/component" | 26 | "github.com/bnema/dumber/internal/ui/component" | ||
| @@ -100,9 +98,8 @@ type App struct { | |||||
| 100 | // Floating pane sessions keyed by tab and profile session ID. | 98 | // Floating pane sessions keyed by tab and profile session ID. | ||
| 101 | floatingSessions map[floatingSessionKey]*floatingWorkspaceSession | 99 | floatingSessions map[floatingSessionKey]*floatingWorkspaceSession | ||
| 102 | 100 | ||||
| 103 | - | // Engine (port-level and concrete for webkit-specific operations) | 101 | + | // Engine |
| 104 | - | engine port.Engine | 102 | + | engine port.Engine |
| 105 | - | wkEngine *webkit.Engine // single webkit escape hatch; all webkit-specific ops go through this | |||
| 106 | 103 | ||||
| 107 | // Web content (managed by content.Coordinator) | 104 | // Web content (managed by content.Coordinator) | ||
| 108 | faviconAdapter *adapter.FaviconAdapter | 105 | faviconAdapter *adapter.FaviconAdapter | ||
| @@ -183,45 +180,28 @@ func New(deps *Dependencies) (*App, error) { | |||||
| 183 | configManager: config.GetManager(), | 180 | configManager: config.GetManager(), | ||
| 184 | cancel: cancel, | 181 | cancel: cancel, | ||
| 185 | } | 182 | } | ||
| 186 | - | // Type-assert engine to concrete *webkit.Engine for webkit-specific operations. | 183 | + | // Register message handlers through the engine. |
| 187 | - | if wke, ok := deps.Engine.(*webkit.Engine); ok { | 184 | + | if err := deps.Engine.RegisterHandlers(ctx, port.HandlerDependencies{ |
| 188 | - | app.wkEngine = wke | 185 | + | HistoryUC: deps.HistoryUC, |
| 189 | - | } | 186 | + | FavoritesUC: deps.FavoritesUC, |
| 190 | - | 187 | + | Clipboard: deps.Clipboard, | |
| 191 | - | // Resolve the message router via wkEngine, with a standalone fallback. | 188 | + | ConfigGetter: func() any { |
| 192 | - | var router *webkit.MessageRouter | 189 | + | return config.Get() |
| 193 | - | if app.wkEngine != nil { | 190 | + | }, |
| 194 | - | router = app.wkEngine.InternalMessageRouter() | 191 | + | OnClipboardCopied: func(textLen int) { |
| 195 | - | } | 192 | + | cb := glib.SourceFunc(func(_ uintptr) bool { |
| 196 | - | if router == nil { | 193 | + | if app.appToaster != nil { |
| 197 | - | router = webkit.NewMessageRouter(ctx) | 194 | + | app.appToaster.Show(ctx, "Copied to clipboard", component.ToastInfo, |
| 198 | - | } | 195 | + | component.WithDuration(component.ToastBriefDurationMs), |
| 199 | - | 196 | + | component.WithPosition(component.ToastPositionBottomRight), | |
| 200 | - | // Register message handlers | 197 | + | ) |
| 201 | - | if router != nil { | 198 | + | } |
| 202 | - | if err := handlers.RegisterAll(ctx, router, handlers.Config{ | 199 | + | return false |
| 203 | - | HistoryUC: deps.HistoryUC, | 200 | + | }) |
| 204 | - | FavoritesUC: deps.FavoritesUC, | 201 | + | glib.IdleAdd(&cb, 0) |
| 205 | - | Clipboard: deps.Clipboard, | 202 | + | }, |
| 206 | - | ConfigGetter: config.Get, | 203 | + | }); err != nil { |
| 207 | - | // AccentHandler is registered after initAccentPicker in onActivate | 204 | + | return nil, err |
| 208 | - | OnClipboardCopied: func(textLen int) { | |||
| 209 | - | // Show brief toast on auto-copy (similar to zellij footer notification) | |||
| 210 | - | // Must schedule on GTK main thread since this is called from WebKit handler | |||
| 211 | - | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 212 | - | if app.appToaster != nil { | |||
| 213 | - | app.appToaster.Show(ctx, "Copied to clipboard", component.ToastInfo, | |||
| 214 | - | component.WithDuration(component.ToastBriefDurationMs), | |||
| 215 | - | component.WithPosition(component.ToastPositionBottomRight), | |||
| 216 | - | ) | |||
| 217 | - | } | |||
| 218 | - | return false | |||
| 219 | - | }) | |||
| 220 | - | glib.IdleAdd(&cb, 0) | |||
| 221 | - | }, | |||
| 222 | - | }); err != nil { | |||
| 223 | - | return nil, err | |||
| 224 | - | } | |||
| 225 | } | 205 | } | ||
| 226 | 206 | ||||
| 227 | return app, nil | 207 | return app, nil | ||
| @@ -246,10 +226,8 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||||
| 246 | 226 | ||||
| 247 | // Refresh scripts in pooled WebViews that were prewarmed before adw.Init(). | 227 | // Refresh scripts in pooled WebViews that were prewarmed before adw.Init(). | ||
| 248 | // They have the wrong dark mode preference injected; re-inject with correct value. | 228 | // They have the wrong dark mode preference injected; re-inject with correct value. | ||
| 249 | - | if a.wkEngine != nil { | 229 | + | if a.engine != nil { |
| 250 | - | if pool := a.wkEngine.InternalPool(); pool != nil { | 230 | + | _ = a.engine.OnToolkitReady(ctx) |
| 251 | - | pool.RefreshScripts(ctx) | |||
| 252 | - | } | |||
| 253 | } | 231 | } | ||
| 254 | } | 232 | } | ||
| 255 | 233 | ||||
| @@ -371,38 +349,30 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) { | |||||
| 371 | 349 | ||||
| 372 | func (a *App) setupPoolBackgroundColor(ctx context.Context) { | 350 | func (a *App) setupPoolBackgroundColor(ctx context.Context) { | ||
| 373 | log := logging.FromContext(ctx) | 351 | log := logging.FromContext(ctx) | ||
| 374 | - | 352 | + | if a.engine == nil { | |
| 375 | - | // Set theme background color on pool to eliminate white flash. | 353 | + | log.Debug().Msg("engine not available; skipping background color setup") |
| 376 | - | // Must be done before any WebView creation so WebViews get the correct color. | |||
| 377 | - | if a.wkEngine == nil { | |||
| 378 | - | log.Debug().Msg("webview pool not available; skipping background color setup") | |||
| 379 | - | return | |||
| 380 | - | } | |||
| 381 | - | pool := a.wkEngine.InternalPool() | |||
| 382 | - | if pool == nil { | |||
| 383 | - | log.Debug().Msg("webview pool not available; skipping background color setup") | |||
| 384 | return | 354 | return | ||
| 385 | } | 355 | } | ||
| 386 | if a.deps == nil || a.deps.Theme == nil { | 356 | if a.deps == nil || a.deps.Theme == nil { | ||
| 387 | log.Debug().Msg("theme not available; skipping webview pool background color setup") | 357 | log.Debug().Msg("theme not available; skipping webview pool background color setup") | ||
| 388 | return | 358 | return | ||
| 389 | } | 359 | } | ||
| 390 | - | ||||
| 391 | r, g, b, alpha := a.deps.Theme.GetBackgroundRGBA() | 360 | r, g, b, alpha := a.deps.Theme.GetBackgroundRGBA() | ||
| 392 | - | pool.SetBackgroundColor(r, g, b, alpha) | 361 | + | if err := a.engine.UpdateAppearance(ctx, float64(r), float64(g), float64(b), float64(alpha)); err != nil { |
| 362 | + | log.Warn().Err(err).Msg("failed to update engine appearance") | |||
| 363 | + | } | |||
| 393 | log.Debug().Msg("configured webview pool background color") | 364 | log.Debug().Msg("configured webview pool background color") | ||
| 394 | } | 365 | } | ||
| 395 | 366 | ||||
| 396 | - | func (a *App) prewarmWebViewPoolAsync(ctx context.Context) { | 367 | + | func (a *App) prewarmWebViewPoolAsync(_ context.Context) { |
| 397 | - | // Prewarm WebView pool after startup so cold-start navigation is not blocked. | 368 | + | if a.engine == nil { |
| 398 | - | if a.wkEngine == nil { | |||
| 399 | return | 369 | return | ||
| 400 | } | 370 | } | ||
| 401 | - | pool := a.wkEngine.InternalPool() | 371 | + | pool := a.engine.Pool() |
| 402 | if pool == nil { | 372 | if pool == nil { | ||
| 403 | return | 373 | return | ||
| 404 | } | 374 | } | ||
| 405 | - | pool.PrewarmAsync(ctx, 0) | 375 | + | pool.Prewarm(0) |
| 406 | } | 376 | } | ||
| 407 | 377 | ||||
| 408 | func (a *App) createMainWindow(ctx context.Context) error { | 378 | func (a *App) createMainWindow(ctx context.Context) error { | ||
| @@ -570,16 +540,12 @@ func (a *App) registerAccentHandlers(ctx context.Context) { | |||||
| 570 | log.Debug().Msg("insertAccentUC not available, skipping accent handler registration") | 540 | log.Debug().Msg("insertAccentUC not available, skipping accent handler registration") | ||
| 571 | return | 541 | return | ||
| 572 | } | 542 | } | ||
| 573 | - | var router *webkit.MessageRouter | 543 | + | if a.engine == nil { |
| 574 | - | if a.wkEngine != nil { | 544 | + | log.Debug().Msg("engine not available, skipping accent handler registration") |
| 575 | - | router = a.wkEngine.InternalMessageRouter() | |||
| 576 | - | } | |||
| 577 | - | if router == nil { | |||
| 578 | - | log.Debug().Msg("router not available, skipping accent handler registration") | |||
| 579 | return | 545 | return | ||
| 580 | } | 546 | } | ||
| 581 | 547 | ||||
| 582 | - | if err := handlers.RegisterAccentHandlers(ctx, router, a.insertAccentUC); err != nil { | 548 | + | if err := a.engine.RegisterAccentHandlers(ctx, a.insertAccentUC); err != nil { |
| 583 | log.Error().Err(err).Msg("failed to register accent handlers") | 549 | log.Error().Err(err).Msg("failed to register accent handlers") | ||
| 584 | } else { | 550 | } else { | ||
| 585 | log.Info().Msg("registered accent key handlers") | 551 | log.Info().Msg("registered accent key handlers") | ||
| @@ -589,7 +555,7 @@ func (a *App) registerAccentHandlers(ctx context.Context) { | |||||
| 589 | func (a *App) initDownloadHandler(ctx context.Context) { | 555 | func (a *App) initDownloadHandler(ctx context.Context) { | ||
| 590 | log := logging.FromContext(ctx) | 556 | log := logging.FromContext(ctx) | ||
| 591 | 557 | ||||
| 592 | - | if a.deps == nil || a.wkEngine == nil { | 558 | + | if a.deps == nil || a.engine == nil { |
| 593 | log.Debug().Msg("WebContext not available, skipping download handler") | 559 | log.Debug().Msg("WebContext not available, skipping download handler") | ||
| 594 | return | 560 | return | ||
| 595 | } | 561 | } | ||
| @@ -623,9 +589,10 @@ func (a *App) initDownloadHandler(ctx context.Context) { | |||||
| 623 | // Create use case for preparing download destinations with file deduplication. | 589 | // Create use case for preparing download destinations with file deduplication. | ||
| 624 | prepareDownloadUC := usecase.NewPrepareDownloadUseCase(filesystem.New()) | 590 | prepareDownloadUC := usecase.NewPrepareDownloadUseCase(filesystem.New()) | ||
| 625 | 591 | ||||
| 626 | - | // Create and wire the download handler. | 592 | + | if err := a.engine.ConfigureDownloads(ctx, downloadPath, eventAdapter, prepareDownloadUC); err != nil { |
| 627 | - | handler := webkit.NewDownloadHandler(downloadPath, eventAdapter, prepareDownloadUC) | 593 | + | log.Error().Err(err).Msg("failed to configure downloads") |
| 628 | - | a.wkEngine.InternalContext().SetDownloadHandler(ctx, handler) | 594 | + | return |
| 595 | + | } | |||
| 629 | 596 | ||||
| 630 | log.Info().Str("path", downloadPath).Msg("download handler initialized") | 597 | log.Info().Str("path", downloadPath).Msg("download handler initialized") | ||
| 631 | } | 598 | } | ||
| @@ -1618,11 +1585,9 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1618 | 1585 | ||||
| 1619 | // Wire popup handling | 1586 | // Wire popup handling | ||
| 1620 | // Set theme background color on the engine's popup factory to eliminate white flash. | 1587 | // Set theme background color on the engine's popup factory to eliminate white flash. | ||
| 1621 | - | if a.wkEngine != nil && a.deps.Theme != nil { | 1588 | + | if a.engine != nil && a.deps.Theme != nil { |
| 1622 | r, g, b, alpha := a.deps.Theme.GetBackgroundRGBA() | 1589 | r, g, b, alpha := a.deps.Theme.GetBackgroundRGBA() | ||
| 1623 | - | if f := a.wkEngine.InternalFactory(); f != nil { | 1590 | + | _ = a.engine.UpdateAppearance(context.Background(), float64(r), float64(g), float64(b), float64(alpha)) |
| 1624 | - | f.SetBackgroundColor(r, g, b, alpha) | |||
| 1625 | - | } | |||
| 1626 | } | 1591 | } | ||
| 1627 | a.contentCoord.SetPopupConfig( | 1592 | a.contentCoord.SetPopupConfig( | ||
| 1628 | a.engine.Factory(), | 1593 | a.engine.Factory(), | ||
| @@ -2145,12 +2110,10 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget { | |||||
| 2145 | return nil | 2110 | return nil | ||
| 2146 | } | 2111 | } | ||
| 2147 | 2112 | ||||
| 2148 | - | // Get the underlying webkit.WebView for the text input target | 2113 | + | if provider, ok := wv.(port.TextInputTargetProvider); ok { |
| 2149 | - | wkWV, ok := wv.(*webkit.WebView) | 2114 | + | return provider.TextInputTarget() |
| 2150 | - | if !ok { | |||
| 2151 | - | return nil | |||
| 2152 | } | 2115 | } | ||
| 2153 | - | return textinput.NewWebViewTarget(wkWV.Widget()) | 2116 | + | return nil |
| 2154 | } | 2117 | } | ||
| 2155 | 2118 | ||||
| 2156 | // attachPopupToTab attaches a popup WebView to a newly created tab. | 2119 | // attachPopupToTab attaches a popup WebView to a newly created tab. | ||
| @@ -3100,11 +3063,8 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||||
| 3100 | *a.deps.Config = *cfg | 3063 | *a.deps.Config = *cfg | ||
| 3101 | } | 3064 | } | ||
| 3102 | 3065 | ||||
| 3103 | - | // Update WebKit settings manager | 3066 | + | if a.engine != nil { |
| 3104 | - | if a.wkEngine != nil { | 3067 | + | _ = a.engine.UpdateSettings(ctx, cfg) |
| 3105 | - | if settings := a.wkEngine.InternalSettings(); settings != nil { | |||
| 3106 | - | settings.UpdateFromConfig(ctx, cfg) | |||
| 3107 | - | } | |||
| 3108 | } | 3068 | } | ||
| 3109 | 3069 | ||||
| 3110 | // Apply settings to existing webviews via coordinator | 3070 | // Apply settings to existing webviews via coordinator | ||
| @@ -3120,28 +3080,25 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||||
| 3120 | } | 3080 | } | ||
| 3121 | a.deps.Theme.UpdateFromConfig(ctx, cfg, display) | 3081 | a.deps.Theme.UpdateFromConfig(ctx, cfg, display) | ||
| 3122 | 3082 | ||||
| 3123 | - | // Resolve injector once for all webkit-specific theme updates below. | 3083 | + | var inj port.ContentInjector |
| 3124 | - | var injector *webkit.ContentInjector | 3084 | + | if a.engine != nil { |
| 3125 | - | if a.wkEngine != nil { | 3085 | + | inj = a.engine.ContentInjector() |
| 3126 | - | injector = a.wkEngine.InternalInjector() | |||
| 3127 | } | 3086 | } | ||
| 3128 | 3087 | ||||
| 3129 | - | // Update find highlight CSS for future navigations | 3088 | + | if inj != nil { |
| 3130 | - | if injector != nil { | |||
| 3131 | findCSS := theme.GenerateFindHighlightCSS(a.deps.Theme.GetCurrentPalette()) | 3089 | findCSS := theme.GenerateFindHighlightCSS(a.deps.Theme.GetCurrentPalette()) | ||
| 3132 | - | if err := injector.InjectFindHighlightCSS(ctx, findCSS); err != nil { | 3090 | + | if err := inj.InjectFindHighlightCSS(ctx, findCSS); err != nil { |
| 3133 | log.Warn().Err(err).Msg("failed to update find highlight CSS") | 3091 | log.Warn().Err(err).Msg("failed to update find highlight CSS") | ||
| 3134 | } | 3092 | } | ||
| 3135 | } | 3093 | } | ||
| 3136 | 3094 | ||||
| 3137 | - | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | 3095 | + | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(inj) |
| 3138 | cssText := a.deps.Theme.GetWebUIThemeCSS() | 3096 | cssText := a.deps.Theme.GetWebUIThemeCSS() | ||
| 3139 | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: cssText}); err != nil { | 3097 | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: cssText}); err != nil { | ||
| 3140 | log.Warn().Err(err).Msg("failed to update WebUI theme CSS") | 3098 | log.Warn().Err(err).Msg("failed to update WebUI theme CSS") | ||
| 3141 | } | 3099 | } | ||
| 3142 | 3100 | ||||
| 3143 | - | // Refresh injected scripts so future navigations use the latest theme. | 3101 | + | if a.contentCoord != nil && inj != nil { |
| 3144 | - | if a.contentCoord != nil && injector != nil { | |||
| 3145 | a.contentCoord.RefreshInjectedScriptsToAll(ctx) | 3102 | a.contentCoord.RefreshInjectedScriptsToAll(ctx) | ||
| 3146 | } | 3103 | } | ||
| 3147 | 3104 | ||||