detached cfe4a9c

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
99 // The css parameter should contain CSS custom property declarations.
1010 InjectThemeCSS(ctx context.Context, css string) error
1111
12+ // InjectFindHighlightCSS injects CSS used to style in-page find highlights.
13+ InjectFindHighlightCSS(ctx context.Context, css string) error
14+
1215 // RefreshScripts clears and re-injects user scripts for a single WebView.
1316 // Called when appearance settings change so future navigations pick up latest values.
1417 // 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) error10 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 {
122122
123123 // Close releases all resources held by the engine.
124124 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)
125152 }
126153
127154 // 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() error124 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 {
332332 // Close triggers a programmatic close of the WebView (equivalent to window.close()).
333333 Close()
334334 }
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 (
55 "fmt"
66
77 "github.com/bnema/dumber/internal/application/port"
8+ "github.com/bnema/dumber/internal/application/usecase"
89 "github.com/bnema/dumber/internal/infrastructure/config"
910 "github.com/bnema/dumber/internal/infrastructure/webkit"
11+ "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"
1012 "github.com/bnema/dumber/internal/ui/theme"
1113 "github.com/rs/zerolog"
1214 )
@@ -41,6 +43,41 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
4143 return webkit.NewEngine(
4244 input.Ctx, cfg, opts, wkCfg,
4345 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+ },
4481 )
4582 case "cef":
4683 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
22
33 import (
44 "context"
5+ "fmt"
56
67 "github.com/bnema/dumber/internal/application/port"
8+ "github.com/bnema/dumber/internal/application/usecase"
9+ "github.com/bnema/dumber/internal/infrastructure/config"
710 "github.com/bnema/dumber/internal/infrastructure/filtering"
811 "github.com/rs/zerolog"
912 )
@@ -21,6 +24,10 @@ type Engine struct {
2124 schemeHandler *DumbSchemeHandler
2225 schemePath string
2326 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
2431 }
2532
2633 // Compile-time check that Engine implements port.Engine.
@@ -92,3 +99,70 @@ func (e *Engine) InternalMessageRouter() *MessageRouter { return e.messageRo
9299 func (e *Engine) InternalPool() *WebViewPool { return e.pool }
93100 func (e *Engine) InternalFactory() *WebViewFactory { return e.factory }
94101 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 *DumbSchemeHandler24 schemeHandler *DumbSchemeHandler
22 schemePath string25 schemePath string
23 logger zerolog.Logger26 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(
2626 themeManager *theme.Manager,
2727 colorResolver port.ColorSchemeResolver,
2828 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,
2931 ) (*Engine, error) {
3032 // --- Hardware survey and performance profile resolution ---
3133 perfSettings := engineSurveyHardwareAndResolveProfile(ctx, cfg, logger)
@@ -104,17 +106,19 @@ func NewEngine(
104106
105107 // --- Assemble Engine ---
106108 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,
118122 }
119123
120124 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, nil124 return engine, nil
M internal/ui/app.go +57 -100
@@ -20,9 +20,7 @@ import (
2020 "github.com/bnema/dumber/internal/infrastructure/filtering"
2121 "github.com/bnema/dumber/internal/infrastructure/snapshot"
2222 "github.com/bnema/dumber/internal/infrastructure/textinput"
23- "github.com/bnema/dumber/internal/infrastructure/webkit"
2423
25- "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"
2624 "github.com/bnema/dumber/internal/logging"
2725 "github.com/bnema/dumber/internal/ui/adapter"
2826 "github.com/bnema/dumber/internal/ui/component"
@@ -100,9 +98,8 @@ type App struct {
10098 // Floating pane sessions keyed by tab and profile session ID.
10199 floatingSessions map[floatingSessionKey]*floatingWorkspaceSession
102100
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
106103
107104 // Web content (managed by content.Coordinator)
108105 faviconAdapter *adapter.FaviconAdapter
@@ -183,45 +180,28 @@ func New(deps *Dependencies) (*App, error) {
183180 configManager: config.GetManager(),
184181 cancel: cancel,
185182 }
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
225205 }
226206
227207 return app, nil
@@ -246,10 +226,8 @@ func (a *App) Run(ctx context.Context, args []string) int {
246226
247227 // Refresh scripts in pooled WebViews that were prewarmed before adw.Init().
248228 // 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)
253231 }
254232 }
255233
@@ -371,38 +349,30 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
371349
372350 func (a *App) setupPoolBackgroundColor(ctx context.Context) {
373351 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")
384354 return
385355 }
386356 if a.deps == nil || a.deps.Theme == nil {
387357 log.Debug().Msg("theme not available; skipping webview pool background color setup")
388358 return
389359 }
390-
391360 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+ }
393364 log.Debug().Msg("configured webview pool background color")
394365 }
395366
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 {
399369 return
400370 }
401- pool := a.wkEngine.InternalPool()
371+ pool := a.engine.Pool()
402372 if pool == nil {
403373 return
404374 }
405- pool.PrewarmAsync(ctx, 0)
375+ pool.Prewarm(0)
406376 }
407377
408378 func (a *App) createMainWindow(ctx context.Context) error {
@@ -570,16 +540,12 @@ func (a *App) registerAccentHandlers(ctx context.Context) {
570540 log.Debug().Msg("insertAccentUC not available, skipping accent handler registration")
571541 return
572542 }
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")
579545 return
580546 }
581547
582- if err := handlers.RegisterAccentHandlers(ctx, router, a.insertAccentUC); err != nil {
548+ if err := a.engine.RegisterAccentHandlers(ctx, a.insertAccentUC); err != nil {
583549 log.Error().Err(err).Msg("failed to register accent handlers")
584550 } else {
585551 log.Info().Msg("registered accent key handlers")
@@ -589,7 +555,7 @@ func (a *App) registerAccentHandlers(ctx context.Context) {
589555 func (a *App) initDownloadHandler(ctx context.Context) {
590556 log := logging.FromContext(ctx)
591557
592- if a.deps == nil || a.wkEngine == nil {
558+ if a.deps == nil || a.engine == nil {
593559 log.Debug().Msg("WebContext not available, skipping download handler")
594560 return
595561 }
@@ -623,9 +589,10 @@ func (a *App) initDownloadHandler(ctx context.Context) {
623589 // Create use case for preparing download destinations with file deduplication.
624590 prepareDownloadUC := usecase.NewPrepareDownloadUseCase(filesystem.New())
625591
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+ }
629596
630597 log.Info().Str("path", downloadPath).Msg("download handler initialized")
631598 }
@@ -1618,11 +1585,9 @@ func (a *App) initCoordinators(ctx context.Context) {
16181585
16191586 // Wire popup handling
16201587 // 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 {
16221589 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))
16261591 }
16271592 a.contentCoord.SetPopupConfig(
16281593 a.engine.Factory(),
@@ -2145,12 +2110,10 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget {
21452110 return nil
21462111 }
21472112
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()
21522115 }
2153- return textinput.NewWebViewTarget(wkWV.Widget())
2116+ return nil
21542117 }
21552118
21562119 // attachPopupToTab attaches a popup WebView to a newly created tab.
@@ -3100,11 +3063,8 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
31003063 *a.deps.Config = *cfg
31013064 }
31023065
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)
31083068 }
31093069
31103070 // Apply settings to existing webviews via coordinator
@@ -3120,28 +3080,25 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
31203080 }
31213081 a.deps.Theme.UpdateFromConfig(ctx, cfg, display)
31223082
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()
31273086 }
31283087
3129- // Update find highlight CSS for future navigations
3130- if injector != nil {
3088+ if inj != nil {
31313089 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 {
31333091 log.Warn().Err(err).Msg("failed to update find highlight CSS")
31343092 }
31353093 }
31363094
3137- prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
3095+ prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(inj)
31383096 cssText := a.deps.Theme.GetWebUIThemeCSS()
31393097 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: cssText}); err != nil {
31403098 log.Warn().Err(err).Msg("failed to update WebUI theme CSS")
31413099 }
31423100
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 {
31453102 a.contentCoord.RefreshInjectedScriptsToAll(ctx)
31463103 }
31473104
@@ -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]*floatingWorkspaceSession99 floatingSessions map[floatingSessionKey]*floatingWorkspaceSession
102 100
103- // Engine (port-level and concrete for webkit-specific operations)101+ // Engine
104- engine port.Engine102+ 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.FaviconAdapter105 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 = wke185+ 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.MessageRouter189+ 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 handlers197+ )
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 onActivate204+ 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, nil207 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 return354 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 return358 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 return369 return
400 }370 }
401- pool := a.wkEngine.InternalPool()371+ pool := a.engine.Pool()
402 if pool == nil {372 if pool == nil {
403 return373 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 return541 return
572 }542 }
573- var router *webkit.MessageRouter543+ 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 return545 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 return560 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 handling1586 // 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 nil2110 return nil
2146 }2111 }
2147 2112
2148- // Get the underlying webkit.WebView for the text input target2113+ 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 = *cfg3063 *a.deps.Config = *cfg
3101 }3064 }
3102 3065
3103- // Update WebKit settings manager3066+ 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 coordinator3070 // 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.ContentInjector3084+ 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 navigations3088+ 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

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