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