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