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 TouchpadNavigationEnabled: cfg.Engine.CEF.Input.TouchpadNavigationEnabled,
103 TouchpadNavigationMinDelta: cfg.Engine.CEF.Input.TouchpadNavigationMinDelta,
104 TouchpadNavigationMaxVerticalRatio: cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio,
105 },
106 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
107 TraceHandlers: cfg.Engine.CEF.TraceHandlers,
108 ApplicationScale: cfg.DefaultUIScale,
109 }
110 deps := cef.EngineDependencies{
111 RegisterHandlers: handlers.RegisterAll,
112 RegisterAccentHandlers: handlers.RegisterAccentHandlers,
113 CurrentConfigPayload: currentConfigPayload,
114 DefaultConfigPayload: defaultConfigPayload,
115 ContextMenuBuilder: contextMenuBuilder,
116 ContextMenuExecutorFactory: contextMenuExecutorFactory,
117 ContextMenuRenderer: gtkmenu.NewRenderer(nil),
118 Clipboard: clipboardinfra.New(),
119 ImageDataResolver: webkit.NewContextMenuResolver(),
120 }
121 audioFactory := audiofactory.NewAudioOutputFactory()
122 return cef.NewEngine(input.Ctx, opts, cef.RuntimePaths{
123 StateRoot: profile.CEFUserDataDir(),
124 LogFile: profile.CEFLogFile(),
125 ProfileLogDir: profile.Shared.LogDir,
126 }, cefCfg, audioFactory, deps)
127 default:
128 return nil, fmt.Errorf("unknown engine type: %q", engineType)
129 }
130}
131
132func prefersDarkProvider(manager *theme.Manager) func() bool {
133 return func() bool {
134 if manager == nil {
135 return true
136 }
137 return manager.PrefersDark()
138 }
139}
140
141func requireRuntimeProfile(profile runtimeprofile.Profile, engineType string) (runtimeprofile.Profile, error) {
142 if profile.Engine == "" {
143 return runtimeprofile.Profile{}, fmt.Errorf("missing runtime profile for %s engine", engineType)
144 }
145 if profile.Engine != engineType {
146 return runtimeprofile.Profile{}, fmt.Errorf("runtime profile engine %q does not match %q", profile.Engine, engineType)
147 }
148 return profile, nil
149}