v0.29.0 21b3150

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/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}

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