v0.22.0 a85da87

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/usecase"
14	"github.com/bnema/dumber/internal/infrastructure/config"
15	"github.com/bnema/dumber/internal/infrastructure/deps"
16	"github.com/bnema/dumber/internal/infrastructure/media"
17	"github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
18	"github.com/bnema/dumber/internal/logging"
19	"github.com/bnema/dumber/internal/ui/theme"
20	sqlite3 "github.com/ncruces/go-sqlite3"
21)
22
23// DatabaseResult holds database connection and cleanup function.
24type DatabaseResult struct {
25	DB      *sql.DB
26	Cleanup func()
27}
28
29// ParallelInitResult holds the results of parallel initialization phase.
30type ParallelInitResult struct {
31	DataDir      string
32	CacheDir     string
33	ThemeManager *theme.Manager
34	Duration     time.Duration
35}
36
37// DeferredInitResult holds results from deferred initialization checks.
38type DeferredInitResult struct {
39	RuntimeErr error
40	MediaErr   error
41	SQLiteErr  error
42	Duration   time.Duration
43}
44
45// ParallelInitInput holds the input for parallel initialization.
46type ParallelInitInput struct {
47	Ctx    context.Context
48	Config *config.Config
49}
50
51// DeferredInitInput holds the input for deferred initialization.
52type DeferredInitInput struct {
53	Ctx    context.Context
54	Config *config.Config
55}
56
57// RuntimeRequirementsError contains details about missing runtime dependencies.
58type RuntimeRequirementsError struct {
59	Checks []usecase.RuntimeDependencyStatus
60}
61
62func (*RuntimeRequirementsError) Error() string {
63	return "runtime requirements not met"
64}
65
66// LogDetails logs detailed information about missing dependencies.
67func (e *RuntimeRequirementsError) LogDetails(ctx context.Context) {
68	log := logging.FromContext(ctx)
69	for _, c := range e.Checks {
70		if c.Installed {
71			log.Error().
72				Str("dependency", c.PkgConfigName).
73				Str("have", c.Version).
74				Str("need", c.RequiredVersion).
75				Bool("ok", c.MeetsRequirement).
76				Msg("runtime dependency")
77		} else {
78			log.Error().
79				Str("dependency", c.PkgConfigName).
80				Str("need", c.RequiredVersion).
81				Msg("runtime dependency missing")
82		}
83	}
84}
85
86// RunParallelInit runs the essential parallel initialization phase.
87// This includes directory resolution and theme creation.
88// Returns the first fatal error encountered, or nil with the results.
89func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
90	var (
91		dataDir, cacheDir string
92		themeManager      *theme.Manager
93		dirsErr           error
94		wg                sync.WaitGroup
95	)
96
97	start := time.Now()
98	wg.Add(2)
99
100	// Resolve directories
101	go func() {
102		defer wg.Done()
103		dataDir, cacheDir, dirsErr = resolveWebKitDirs()
104	}()
105
106	// Theme manager (CPU-bound, no I/O)
107	go func() {
108		defer wg.Done()
109		themeManager = theme.NewManager(input.Ctx, input.Config)
110	}()
111
112	wg.Wait()
113	duration := time.Since(start)
114
115	// Return first fatal error
116	if dirsErr != nil {
117		return nil, fmt.Errorf("resolve directories: %w", dirsErr)
118	}
119
120	return &ParallelInitResult{
121		DataDir:      dataDir,
122		CacheDir:     cacheDir,
123		ThemeManager: themeManager,
124		Duration:     duration,
125	}, nil
126}
127
128// RunDeferredInit runs deferred initialization checks off the critical path.
129// This includes SQLite WASM precompile, runtime requirements, and media checks.
130func RunDeferredInit(input DeferredInitInput) DeferredInitResult {
131	var (
132		runtimeErr error
133		mediaErr   error
134		sqliteErr  error
135		wg         sync.WaitGroup
136	)
137
138	start := time.Now()
139	wg.Add(3)
140
141	go func() {
142		defer wg.Done()
143		sqliteErr = sqlite3.Initialize()
144	}()
145
146	go func() {
147		defer wg.Done()
148		runtimeErr = CheckRuntimeRequirements(input.Ctx, input.Config)
149	}()
150
151	go func() {
152		defer wg.Done()
153		mediaErr = CheckMediaRequirements(input.Ctx, input.Config)
154	}()
155
156	wg.Wait()
157
158	return DeferredInitResult{
159		RuntimeErr: runtimeErr,
160		MediaErr:   mediaErr,
161		SQLiteErr:  sqliteErr,
162		Duration:   time.Since(start),
163	}
164}
165
166// CheckRuntimeRequirements verifies WebKitGTK and other runtime dependencies.
167// Returns error if requirements are not met; caller should log details and exit.
168func CheckRuntimeRequirements(ctx context.Context, cfg *config.Config) error {
169	probe := deps.NewPkgConfigProbe()
170	checkRuntimeUC := usecase.NewCheckRuntimeDependenciesUseCase(probe)
171	runtimeOut, err := checkRuntimeUC.Execute(ctx, usecase.CheckRuntimeDependenciesInput{
172		Prefix: cfg.Runtime.Prefix,
173	})
174	if err != nil {
175		return fmt.Errorf("runtime check execution failed: %w", err)
176	}
177	if runtimeOut.OK {
178		return nil
179	}
180	return &RuntimeRequirementsError{Checks: runtimeOut.Checks}
181}
182
183// CheckMediaRequirements verifies GStreamer and media codec availability.
184// Returns error if media subsystem initialization fails.
185func CheckMediaRequirements(ctx context.Context, cfg *config.Config) error {
186	mediaDiagAdapter := media.New()
187	checkMediaUC := usecase.NewCheckMediaUseCase(mediaDiagAdapter)
188	if _, err := checkMediaUC.Execute(ctx, usecase.CheckMediaInput{
189		ShowDiagnostics: cfg.Media.ShowDiagnosticsOnStartup,
190	}); err != nil {
191		return fmt.Errorf("media check failed: %w", err)
192	}
193	return nil
194}
195
196// resolveWebKitDirs resolves and creates the data and cache directories for WebKit.
197func resolveWebKitDirs() (dataDir, cacheDir string, err error) {
198	const cacheDirPerm = 0o755
199	dataDir, err = config.GetDataDir()
200	if err != nil {
201		return "", "", fmt.Errorf("resolve data directory: %w", err)
202	}
203	stateDir, err := config.GetStateDir()
204	if err != nil {
205		return "", "", fmt.Errorf("resolve state directory: %w", err)
206	}
207	cacheDir = filepath.Join(stateDir, "webkit-cache")
208	if mkErr := os.MkdirAll(cacheDir, cacheDirPerm); mkErr != nil {
209		return "", "", fmt.Errorf("create cache directory %s: %w", cacheDir, mkErr)
210	}
211	return dataDir, cacheDir, nil
212}
213
214// OpenDatabase opens and initializes the SQLite database using XDG paths.
215// Returns error if database cannot be opened or migrations fail.
216func OpenDatabase(ctx context.Context) (*DatabaseResult, error) {
217	dbPath, err := config.GetDatabaseFile()
218	if err != nil {
219		return nil, fmt.Errorf("resolve database path: %w", err)
220	}
221	db, err := sqlite.NewConnection(ctx, dbPath)
222	if err != nil {
223		return nil, fmt.Errorf("initialize database at %s: %w", dbPath, err)
224	}
225	return &DatabaseResult{
226		DB: db,
227		Cleanup: func() {
228			_ = sqlite.Close(db)
229		},
230	}, nil
231}
232
233// CreateLazyDatabase creates a lazy database provider that defers initialization.
234// The database is initialized on first access, allowing the UI to render faster.
235//
236// Currently unused: the application uses OpenDatabase with RunParallelDBWebKit for
237// eager initialization. This function is kept for potential future use when lazy
238// initialization past first paint becomes beneficial.
239func CreateLazyDatabase() (*sqlite.LazyDB, error) {
240	dbPath, err := config.GetDatabaseFile()
241	if err != nil {
242		return nil, fmt.Errorf("resolve database path: %w", err)
243	}
244	return sqlite.NewLazyDB(dbPath), nil
245}
246
247// ParallelDBWebKitResult holds results from parallel DB and WebKit initialization.
248type ParallelDBWebKitResult struct {
249	DB        *sql.DB
250	DBCleanup func()
251	Stack     WebKitStack
252}
253
254// ParallelDBWebKitInput holds inputs for parallel DB and WebKit initialization.
255type ParallelDBWebKitInput struct {
256	Ctx          context.Context
257	Config       *config.Config
258	DataDir      string // For WebKit context
259	CacheDir     string // For WebKit cache
260	ThemeManager *theme.Manager
261}
262
263// Note: Database path is resolved via config.GetDatabaseFile() internally.
264
265// dbInitResult holds the result of database initialization for channel communication.
266type dbInitResult struct {
267	db      *sql.DB
268	cleanup func()
269	err     error
270}
271
272// RunParallelDBWebKit initializes database in background while WebKit runs on main thread.
273// Database init happens in goroutine (pure Go/WASM), WebKit must stay on main thread (GTK).
274// This saves ~150ms by overlapping DB migrations with WebKit context creation.
275func RunParallelDBWebKit(input ParallelDBWebKitInput) (*ParallelDBWebKitResult, error) {
276	log := logging.FromContext(input.Ctx)
277
278	dbCh := make(chan dbInitResult, 1)
279
280	// Database initialization in background (WASM is thread-safe)
281	go func() {
282		dbResult, err := OpenDatabase(input.Ctx)
283		if err != nil {
284			dbCh <- dbInitResult{err: err}
285			return
286		}
287		dbCh <- dbInitResult{
288			db:      dbResult.DB,
289			cleanup: dbResult.Cleanup,
290		}
291	}()
292
293	// WebKit stack on main thread (GTK requirement)
294	stack := BuildWebKitStack(input.Ctx, input.Config, input.DataDir, input.CacheDir, input.ThemeManager, *log)
295
296	// Wait for database
297	dbRes := <-dbCh
298
299	if dbRes.err != nil {
300		return nil, fmt.Errorf("database initialization: %w", dbRes.err)
301	}
302
303	return &ParallelDBWebKitResult{
304		DB:        dbRes.db,
305		DBCleanup: dbRes.cleanup,
306		Stack:     stack,
307	}, nil
308}

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