v0.11.0 dc50343

browser.go RawHistoryFolder

1package browser
2
3import (
4	"database/sql"
5	"embed"
6	"fmt"
7	"log"
8	"os"
9	"os/signal"
10	"runtime"
11	"syscall"
12
13	"github.com/bnema/dumber/internal/app/api"
14	"github.com/bnema/dumber/internal/app/control"
15	"github.com/bnema/dumber/internal/app/environment"
16	"github.com/bnema/dumber/internal/app/messaging"
17	"github.com/bnema/dumber/internal/config"
18	"github.com/bnema/dumber/internal/db"
19	"github.com/bnema/dumber/internal/filtering"
20	"github.com/bnema/dumber/internal/logging"
21	"github.com/bnema/dumber/internal/migrations"
22	"github.com/bnema/dumber/internal/services"
23	"github.com/bnema/dumber/pkg/webkit"
24)
25
26// BrowserApp represents the main browser application
27type BrowserApp struct {
28	version   string
29	commit    string
30	buildDate string
31	assets    embed.FS
32
33	// Core components
34	config   *config.Config
35	database *sql.DB
36	queries  *db.Queries
37
38	// Services
39	parserService  *services.ParserService
40	browserService *services.BrowserService
41
42	// WebView and controllers
43	webView              *webkit.WebView
44	zoomController       *control.ZoomController
45	navigationController *control.NavigationController
46	clipboardController  *control.ClipboardController
47
48	// Pane workspace management
49	panes      []*BrowserPane
50	activePane *BrowserPane
51	workspace  *WorkspaceManager
52
53	// Content filtering
54	filterManager *filtering.FilterManager
55
56	// Handlers
57	schemeHandler         *api.SchemeHandler
58	messageHandler        *messaging.Handler
59	shortcutHandler       *ShortcutHandler
60	windowShortcutHandler WindowShortcutHandlerInterface
61}
62
63// Run starts the browser application
64func Run(assets embed.FS, version, commit, buildDate string) {
65	log.Printf("Starting GUI mode (webkit_cgo=%v)", webkit.IsNativeAvailable())
66
67	app := &BrowserApp{
68		version:   version,
69		commit:    commit,
70		buildDate: buildDate,
71		assets:    assets,
72	}
73
74	if err := app.Initialize(); err != nil {
75		log.Printf("Failed to initialize browser: %v", err)
76		if webkit.IsNativeAvailable() {
77			runtime.UnlockOSThread()
78		}
79		os.Exit(1)
80	}
81
82	app.Run()
83}
84
85// Initialize sets up all browser components
86func (app *BrowserApp) Initialize() error {
87	// Setup media pipeline hardening
88	environment.SetupMediaPipeline()
89
90	// Initialize configuration
91	if err := config.Init(); err != nil {
92		return err
93	}
94	app.config = config.Get()
95
96	// Apply environment configurations
97	environment.ApplyCodecConfiguration(app.config.CodecPreferences)
98
99	// Setup crash handler
100	logging.SetupCrashHandler()
101
102	// Setup output capture if configured
103	if err := app.setupOutputCapture(); err != nil {
104		log.Printf("Warning: failed to setup output capture: %v", err)
105	}
106
107	// Initialize WebKit log capture if configured
108	if app.config.Logging.CaptureCOutput {
109		if err := webkit.InitWebKitLogCapture(); err != nil {
110			log.Printf("Warning: failed to initialize WebKit log capture: %v", err)
111		} else {
112			defer webkit.StopWebKitLogCapture()
113			webkit.StartWebKitOutputCapture()
114		}
115	}
116
117	log.Printf("Config initialized")
118
119	// Detect keyboard layout
120	environment.DetectAndSetKeyboardLocale()
121
122	// Initialize database
123	database, err := db.InitDB(app.config.Database.Path)
124	if err != nil {
125		return err
126	}
127	app.database = database
128
129	// Run database migrations
130	if err := migrations.RunEmbeddedMigrations(database); err != nil {
131		return fmt.Errorf("failed to run database migrations: %w", err)
132	}
133
134	app.queries = db.New(database)
135	log.Printf("Database opened at %s", app.config.Database.Path)
136
137	// Initialize services
138	app.parserService = services.NewParserService(app.config, app.queries)
139	app.browserService = services.NewBrowserService(app.config, app.queries)
140
141	// Initialize handlers
142	app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService)
143	app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)
144
145	return nil
146}
147
148// Run executes the main browser loop
149func (app *BrowserApp) Run() {
150	defer app.cleanup()
151	defer logging.SetupPanicRecovery()
152
153	// Register custom scheme resolver
154	webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
155		return app.schemeHandler.Handle(uri, app.config)
156	})
157
158	// Create and setup WebView
159	if err := app.createWebView(); err != nil {
160		log.Printf("Warning: failed to create WebView: %v", err)
161		return
162	}
163
164	// Initialize content blocking
165	if err := app.setupContentBlocking(); err != nil {
166		log.Printf("Warning: failed to setup content blocking: %v", err)
167		// Continue without content blocking
168	}
169
170	// Handle browse command if present
171	app.navigationController.HandleBrowseCommand()
172
173	// Setup signal handling
174	app.setupSignalHandling()
175
176	// Run main loop
177	app.runMainLoop()
178}
179
180// cleanup handles cleanup on shutdown
181func (app *BrowserApp) cleanup() {
182	log.Printf("Starting browser cleanup...")
183
184	// Cleanup window shortcuts first
185	if app.windowShortcutHandler != nil {
186		log.Printf("Cleaning up window shortcuts")
187		app.windowShortcutHandler.Cleanup()
188		app.windowShortcutHandler = nil
189	}
190
191	// Cleanup all panes
192	if app.panes != nil {
193		log.Printf("Cleaning up %d panes", len(app.panes))
194		for i, pane := range app.panes {
195			if pane != nil {
196				log.Printf("Cleaning up pane %d (%s)", i, pane.ID())
197				pane.Cleanup()
198			}
199		}
200		app.panes = nil
201	}
202
203	// Cleanup workspace manager
204	if app.workspace != nil {
205		log.Printf("Cleaning up workspace manager")
206		// WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference
207		app.workspace = nil
208	}
209
210	// Close database last
211	if app.database != nil {
212		log.Printf("Closing database")
213		if closeErr := app.database.Close(); closeErr != nil {
214			log.Printf("Warning: failed to close database: %v", closeErr)
215		}
216	}
217
218	log.Printf("Browser cleanup completed")
219}
220
221// setupOutputCapture initializes stdout/stderr capture if configured
222func (app *BrowserApp) setupOutputCapture() error {
223	if app.config.Logging.CaptureStdout || app.config.Logging.CaptureStderr {
224		// Capturing stdout/stderr works for non-GTK builds, but in native GTK mode
225		// it interferes with WebKit's own pipe management and crashes immediately.
226		if webkit.IsNativeAvailable() {
227			log.Printf("Warning: stdout/stderr capture is not supported in native GTK mode; skipping")
228			return nil
229		}
230
231		log.Printf("Warning: stdout/stderr capture is experimental and may interfere with normal operations")
232		if logger := logging.GetLogger(); logger != nil {
233			outputCapture := logging.NewOutputCapture(logger)
234			if err := outputCapture.Start(); err != nil {
235				return err
236			}
237			// Note: defer outputCapture.Stop() should be handled by the caller
238		}
239	}
240	return nil
241}
242
243// setupSignalHandling configures graceful shutdown on signals
244func (app *BrowserApp) setupSignalHandling() {
245	sigChan := make(chan os.Signal, 1)
246	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
247
248	// Handle signals in a goroutine to quit the main loop
249	go func() {
250		sig := <-sigChan
251		log.Printf("Received signal %v - shutting down gracefully", sig)
252		webkit.QuitMainLoop()
253	}()
254}
255
256// runMainLoop starts the appropriate main loop based on WebKit availability
257func (app *BrowserApp) runMainLoop() {
258	if webkit.IsNativeAvailable() {
259		log.Printf("Entering GTK main loop…")
260		webkit.RunMainLoop()
261		log.Printf("GTK main loop exited")
262	} else {
263		log.Printf("Not entering GUI loop (non-CGO build)")
264		// In non-CGO mode, just wait for signals
265		sigChan := make(chan os.Signal, 1)
266		signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
267		sig := <-sigChan
268		log.Printf("Received signal %v - exiting", sig)
269	}
270}

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