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