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}