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}