1// Package bootstrap provides initialization utilities for the browser.
2package bootstrap
3
4import (
5 "context"
6 "database/sql"
7 "fmt"
8 "os"
9 "sync"
10 "time"
11
12 "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/application/usecase"
14 "github.com/bnema/dumber/internal/infrastructure/colorscheme"
15 "github.com/bnema/dumber/internal/infrastructure/config"
16 "github.com/bnema/dumber/internal/infrastructure/deps"
17 "github.com/bnema/dumber/internal/infrastructure/env"
18 "github.com/bnema/dumber/internal/infrastructure/media"
19 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
20 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
21 "github.com/bnema/dumber/internal/logging"
22 "github.com/bnema/dumber/internal/ui/theme"
23)
24
25// DatabaseResult holds database connection and cleanup function.
26type DatabaseResult struct {
27 DB *sql.DB
28 Cleanup func()
29}
30
31// ParallelInitResult holds the results of parallel initialization phase.
32type ParallelInitResult struct {
33 RuntimeProfile runtimeprofile.Profile
34 ThemeManager *theme.Manager
35 ColorResolver port.ColorSchemeResolver
36 AdwaitaDetector *colorscheme.AdwaitaDetector
37 Duration time.Duration
38}
39
40// DeferredInitResult holds results from deferred initialization checks.
41type DeferredInitResult struct {
42 RuntimeErr error
43 MediaErr error
44 Duration time.Duration
45}
46
47// ParallelInitInput holds the input for parallel initialization.
48type ParallelInitInput struct {
49 Ctx context.Context
50 Config *config.Config
51}
52
53// DeferredInitInput holds the input for deferred initialization.
54type DeferredInitInput struct {
55 Ctx context.Context
56 Config *config.Config
57}
58
59// RuntimeRequirementsError contains details about missing runtime dependencies.
60type RuntimeRequirementsError struct {
61 Checks []usecase.RuntimeDependencyStatus
62}
63
64func (*RuntimeRequirementsError) Error() string {
65 return "runtime requirements not met"
66}
67
68// LogDetails logs detailed information about missing dependencies.
69func (e *RuntimeRequirementsError) LogDetails(ctx context.Context) {
70 log := logging.FromContext(ctx)
71 for _, c := range e.Checks {
72 if c.Installed {
73 log.Error().
74 Str("dependency", c.PkgConfigName).
75 Str("have", c.Version).
76 Str("need", c.RequiredVersion).
77 Bool("ok", c.MeetsRequirement).
78 Msg("runtime dependency")
79 } else {
80 log.Error().
81 Str("dependency", c.PkgConfigName).
82 Str("need", c.RequiredVersion).
83 Msg("runtime dependency missing")
84 }
85 }
86}
87
88// RunParallelInit runs the essential parallel initialization phase.
89// This includes directory resolution, color scheme resolver creation, and theme creation.
90// Returns the first fatal error encountered, or nil with the results.
91func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
92 var (
93 dirsErr error
94 wg sync.WaitGroup
95 )
96
97 profile, err := ResolveRuntimeProfile(input.Config)
98 if err != nil {
99 return nil, fmt.Errorf("resolve runtime profile: %w", err)
100 }
101
102 start := time.Now()
103
104 // Create color scheme resolver with initial detectors.
105 // AdwaitaDetector is created but NOT marked available yet (requires adw.Init()).
106 configAdapter := colorscheme.NewConfigAdapter(input.Config)
107 resolver := colorscheme.NewResolver(configAdapter)
108
109 // Register detectors that are available before GTK init
110 resolver.RegisterDetector(colorscheme.NewEnvDetector())
111 resolver.RegisterDetector(colorscheme.NewGsettingsDetector())
112
113 // Create adwaita detector (will be marked available after adw.Init())
114 adwaitaDetector := colorscheme.NewAdwaitaDetector()
115 resolver.RegisterDetector(adwaitaDetector)
116
117 wg.Add(2)
118
119 // Resolve directories
120 go func() {
121 defer wg.Done()
122 dirsErr = resolveWebKitDirs(profile)
123 }()
124
125 // Theme manager (CPU-bound, no I/O)
126 var themeManager *theme.Manager
127 go func() {
128 defer wg.Done()
129 themeManager = theme.NewManager(
130 input.Ctx, &input.Config.Appearance, input.Config.DefaultUIScale,
131 &input.Config.Workspace.Styling, resolver,
132 )
133 }()
134
135 wg.Wait()
136 duration := time.Since(start)
137
138 // Return first fatal error
139 if dirsErr != nil {
140 return nil, fmt.Errorf("resolve directories: %w", dirsErr)
141 }
142
143 return &ParallelInitResult{
144 RuntimeProfile: profile,
145 ThemeManager: themeManager,
146 ColorResolver: resolver,
147 AdwaitaDetector: adwaitaDetector,
148 Duration: duration,
149 }, nil
150}
151
152// RunDeferredInit runs deferred initialization checks off the critical path.
153// This includes runtime requirements and media checks.
154func RunDeferredInit(input DeferredInitInput) DeferredInitResult {
155 var (
156 runtimeErr error
157 mediaErr error
158 wg sync.WaitGroup
159 )
160
161 start := time.Now()
162 wg.Add(2)
163
164 go func() {
165 defer wg.Done()
166 runtimeErr = CheckRuntimeRequirements(input.Ctx, input.Config)
167 }()
168
169 go func() {
170 defer wg.Done()
171 mediaErr = CheckMediaRequirements(input.Ctx, input.Config)
172 }()
173
174 wg.Wait()
175
176 return DeferredInitResult{
177 RuntimeErr: runtimeErr,
178 MediaErr: mediaErr,
179 Duration: time.Since(start),
180 }
181}
182
183// CheckRuntimeRequirements verifies WebKitGTK and other runtime dependencies.
184// Returns error if requirements are not met; caller should log details and exit.
185// Note: When running in a Flatpak sandbox, runtime checks are skipped because
186// the Flatpak runtime provides all required libraries.
187func CheckRuntimeRequirements(ctx context.Context, cfg *config.Config) error {
188 // Skip pkg-config checks in Flatpak - the runtime provides all dependencies
189 if env.IsFlatpak() {
190 log := logging.FromContext(ctx)
191 log.Debug().Msg("running in Flatpak sandbox, skipping runtime dependency checks")
192 return nil
193 }
194
195 probe := deps.NewPkgConfigProbe()
196 checkRuntimeUC := usecase.NewCheckRuntimeDependenciesUseCase(probe)
197 runtimeOut, err := checkRuntimeUC.Execute(ctx, usecase.CheckRuntimeDependenciesInput{
198 Prefix: cfg.Engine.WebKit.Prefix,
199 })
200 if err != nil {
201 return fmt.Errorf("runtime check execution failed: %w", err)
202 }
203 if runtimeOut.OK {
204 return nil
205 }
206 return &RuntimeRequirementsError{Checks: runtimeOut.Checks}
207}
208
209// CheckMediaRequirements verifies GStreamer and media codec availability.
210// Returns error if media subsystem initialization fails.
211func CheckMediaRequirements(ctx context.Context, cfg *config.Config) error {
212 mediaDiagAdapter := media.New()
213 checkMediaUC := usecase.NewCheckMediaUseCase(mediaDiagAdapter)
214 if _, err := checkMediaUC.Execute(ctx, usecase.CheckMediaInput{
215 ShowDiagnostics: cfg.Media.ShowDiagnosticsOnStartup,
216 }); err != nil {
217 return fmt.Errorf("media check failed: %w", err)
218 }
219 return nil
220}
221
222// resolveWebKitDirs resolves and creates the data and cache directories for WebKit.
223func resolveWebKitDirs(profile runtimeprofile.Profile) error {
224 const dirPerm = 0o755
225 dataDir := profile.WebKitDataDir()
226 cacheDir := profile.WebKitCacheDir()
227 if mkErr := os.MkdirAll(dataDir, dirPerm); mkErr != nil {
228 return fmt.Errorf("create data directory %s: %w", dataDir, mkErr)
229 }
230 if mkErr := os.MkdirAll(cacheDir, dirPerm); mkErr != nil {
231 return fmt.Errorf("create cache directory %s: %w", cacheDir, mkErr)
232 }
233 return nil
234}
235
236// OpenDatabase opens and initializes the SQLite database using XDG paths.
237// Returns error if database cannot be opened or migrations fail.
238func OpenDatabase(ctx context.Context) (*DatabaseResult, error) {
239 dbPath, err := config.GetDatabaseFile()
240 if err != nil {
241 return nil, fmt.Errorf("resolve database path: %w", err)
242 }
243 db, err := sqlite.NewConnection(ctx, dbPath)
244 if err != nil {
245 return nil, fmt.Errorf("initialize database at %s: %w", dbPath, err)
246 }
247 return &DatabaseResult{
248 DB: db,
249 Cleanup: func() {
250 _ = sqlite.Close(db)
251 },
252 }, nil
253}
254
255// CreateLazyDatabase creates a lazy database provider that defers initialization.
256// The database is initialized on first access, allowing the UI to render faster.
257//
258// Currently unused: the application uses OpenDatabase with RunParallelDBEngine for
259// eager initialization. This function is kept for potential future use when lazy
260// initialization past first paint becomes beneficial.
261func CreateLazyDatabase() (*sqlite.LazyDB, error) {
262 dbPath, err := config.GetDatabaseFile()
263 if err != nil {
264 return nil, fmt.Errorf("resolve database path: %w", err)
265 }
266 return sqlite.NewLazyDB(dbPath), nil
267}
268
269// ParallelDBEngineResult holds results from parallel DB and engine initialization.
270type ParallelDBEngineResult struct {
271 DB *sql.DB
272 DBCleanup func()
273 Engine port.Engine
274}
275
276// ParallelDBEngineInput holds inputs for parallel DB and engine initialization.
277type ParallelDBEngineInput struct {
278 Ctx context.Context
279 Config *config.Config
280 RuntimeProfile runtimeprofile.Profile
281 ThemeManager *theme.Manager
282 ColorResolver port.ColorSchemeResolver
283}
284
285// Note: Database path is resolved via config.GetDatabaseFile() internally.
286
287// dbInitResult holds the result of database initialization for channel communication.
288type dbInitResult struct {
289 db *sql.DB
290 cleanup func()
291 err error
292}
293
294// RunParallelDBEngine initializes database in background while the engine runs on main thread.
295// Database init happens in goroutine (pure Go/WASM), engine must stay on main thread (GTK).
296// This saves ~150ms by overlapping DB migrations with engine context creation.
297func RunParallelDBEngine(input ParallelDBEngineInput) (*ParallelDBEngineResult, error) {
298 log := logging.FromContext(input.Ctx)
299
300 dbCh := make(chan dbInitResult, 1)
301
302 // Database initialization in background (WASM is thread-safe)
303 go func() {
304 dbResult, err := OpenDatabase(input.Ctx)
305 if err != nil {
306 dbCh <- dbInitResult{err: err}
307 return
308 }
309 dbCh <- dbInitResult{
310 db: dbResult.DB,
311 cleanup: dbResult.Cleanup,
312 }
313 }()
314
315 // Engine on main thread (GTK requirement)
316 engine, err := BuildEngine(EngineInput{
317 Ctx: input.Ctx,
318 Config: input.Config,
319 RuntimeProfile: input.RuntimeProfile,
320 ThemeManager: input.ThemeManager,
321 ColorResolver: input.ColorResolver,
322 Logger: *log,
323 })
324 if err != nil {
325 dbRes := <-dbCh
326 if dbRes.err == nil && dbRes.cleanup != nil {
327 dbRes.cleanup()
328 }
329 return nil, fmt.Errorf("engine initialization: %w", err)
330 }
331
332 // Wait for database
333 dbRes := <-dbCh
334
335 if dbRes.err != nil {
336 return nil, fmt.Errorf("database initialization: %w", dbRes.err)
337 }
338
339 return &ParallelDBEngineResult{
340 DB: dbRes.db,
341 DBCleanup: dbRes.cleanup,
342 Engine: engine,
343 }, nil
344}