v0.28.0 3cb3770

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

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