v0.30.0 eb3068f

gui.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close