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