1package bootstrap
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7
8 "github.com/bnema/dumber/internal/application/dto"
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/application/usecase"
11 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"
12 "github.com/bnema/dumber/internal/infrastructure/cef"
13 clipboardinfra "github.com/bnema/dumber/internal/infrastructure/clipboard"
14 "github.com/bnema/dumber/internal/infrastructure/config"
15 "github.com/bnema/dumber/internal/infrastructure/env"
16 "github.com/bnema/dumber/internal/infrastructure/gtkmenu"
17 "github.com/bnema/dumber/internal/infrastructure/handlers"
18 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
19 "github.com/bnema/dumber/internal/infrastructure/webkit"
20 "github.com/bnema/dumber/internal/ui/theme"
21 "github.com/rs/zerolog"
22)
23
24// EngineInput holds the input for BuildEngine.
25type EngineInput struct {
26 Ctx context.Context
27 Config *config.Config
28 RuntimeProfile runtimeprofile.Profile
29 ThemeManager *theme.Manager
30 ExternalThemeSource port.ExternalThemeSource
31 ColorResolver port.ColorSchemeResolver
32 Logger zerolog.Logger
33}
34
35// BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.
36func BuildEngine(input EngineInput) (port.Engine, error) {
37 cfg := input.Config
38 systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor())
39 systemviewUC := usecase.NewReadSystemviewConfigUseCase(
40 systemviewReader,
41 usecase.WithSystemviewResolvedAppearance(
42 input.ExternalThemeSource,
43 input.ColorResolver,
44 prefersDarkProvider(input.ThemeManager),
45 ),
46 )
47 buildConfigPayload := func(read func(context.Context) (dto.SystemviewConfigPayload, error)) func() ([]byte, error) {
48 return func() ([]byte, error) {
49 payload, err := read(input.Ctx)
50 if err != nil {
51 return nil, err
52 }
53 return json.Marshal(payload)
54 }
55 }
56 currentConfigPayload := buildConfigPayload(systemviewUC.Current)
57 defaultConfigPayload := buildConfigPayload(systemviewUC.Default)
58 contextMenuBuilder := usecase.NewBuildContextMenuUseCase()
59 contextMenuExecutorFactory := &usecase.ContextMenuActionExecutorFactory{}
60 engineType := cfg.Engine.ResolveEngineType()
61 switch engineType {
62 case config.EngineTypeWebKit:
63 profile, err := requireRuntimeProfile(input.RuntimeProfile, engineType)
64 if err != nil {
65 return nil, err
66 }
67 opts := port.EngineOptions{
68 CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy),
69 }
70 wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit)
71
72 return webkit.NewEngine(
73 input.Ctx, cfg, opts, profile, wkCfg,
74 EngineSettingsPayloadFromConfig(cfg),
75 currentConfigPayload, defaultConfigPayload,
76 input.ThemeManager, input.ColorResolver,
77 contextMenuBuilder, contextMenuExecutorFactory,
78 input.Logger,
79 )
80 case config.EngineTypeCEF:
81 profile, err := requireRuntimeProfile(input.RuntimeProfile, engineType)
82 if err != nil {
83 return nil, err
84 }
85 opts := port.EngineOptions{
86 CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy),
87 }
88 cefCfg := cef.RuntimeConfig{
89 CEFDir: cfg.Engine.CEF.CEFDir,
90 LogFile: cfg.Engine.CEF.LogFile,
91 LogSeverity: cfg.Engine.CEF.LogSeverity,
92 RenderStack: string(cfg.Engine.CEF.CEFRenderStack()),
93 AdaptiveWindowlessFrameRate: cfg.Engine.CEF.CEFAdaptiveWindowlessFrameRate(),
94 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,
95 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),
96 Input: cef.RuntimeInputConfig{
97 ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
98 ScrollPreciseMultiplier: cfg.Engine.CEF.Input.ScrollPreciseMultiplier,
99 ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,
100 ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,
101 ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,
102 ScrollTouchpadInertia: cfg.Engine.CEF.Input.ScrollTouchpadInertia,
103 ScrollWheelSmoothing: cfg.Engine.CEF.Input.ScrollWheelSmoothing,
104 TouchpadNavigationEnabled: cfg.Engine.CEF.Input.TouchpadNavigationEnabled,
105 TouchpadNavigationMinDelta: cfg.Engine.CEF.Input.TouchpadNavigationMinDelta,
106 TouchpadNavigationMaxVerticalRatio: cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio,
107 },
108 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
109 TraceHandlers: cfg.Engine.CEF.TraceHandlers,
110 ApplicationScale: cfg.DefaultUIScale,
111 }
112 deps := cef.EngineDependencies{
113 RegisterHandlers: handlers.RegisterAll,
114 RegisterAccentHandlers: handlers.RegisterAccentHandlers,
115 CurrentConfigPayload: currentConfigPayload,
116 DefaultConfigPayload: defaultConfigPayload,
117 ContextMenuBuilder: contextMenuBuilder,
118 ContextMenuExecutorFactory: contextMenuExecutorFactory,
119 ContextMenuRenderer: gtkmenu.NewRenderer(nil),
120 Clipboard: clipboardinfra.New(),
121 ImageDataResolver: webkit.NewContextMenuResolver(),
122 }
123 audioFactory := audiofactory.NewAudioOutputFactory()
124 paths := cef.RuntimePaths{
125 StateRoot: profile.CEFUserDataDir(),
126 LogFile: profile.CEFLogFile(),
127 ProfileLogDir: profile.Shared.LogDir,
128 }
129 if profile.InstanceRoot != "" {
130 paths.IsolatedRoot = profile.CEFUserDataDir()
131 }
132 return cef.NewEngine(input.Ctx, opts, paths, cefCfg, audioFactory, deps)
133 default:
134 return nil, fmt.Errorf("unknown engine type: %q", engineType)
135 }
136}
137
138func prefersDarkProvider(manager *theme.Manager) func() bool {
139 return func() bool {
140 if manager == nil {
141 return true
142 }
143 return manager.PrefersDark()
144 }
145}
146
147func requireRuntimeProfile(profile runtimeprofile.Profile, engineType string) (runtimeprofile.Profile, error) {
148 if profile.Engine == "" {
149 return runtimeprofile.Profile{}, fmt.Errorf("missing runtime profile for %s engine", engineType)
150 }
151 if profile.Engine != engineType {
152 return runtimeprofile.Profile{}, fmt.Errorf("runtime profile engine %q does not match %q", profile.Engine, engineType)
153 }
154 return profile, nil
155}