v0.26.2 3cd9d2e

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

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