v0.12.1 1453d21

browser.go RawHistoryFolder

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

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