v0.23.0 d05d132

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

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