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/migrations"
21 "github.com/bnema/dumber/internal/logging"
22 "github.com/bnema/dumber/pkg/webkit"
23 "github.com/bnema/dumber/services"
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 // Content filtering
49 filterManager *filtering.FilterManager
50
51 // Handlers
52 schemeHandler *api.SchemeHandler
53 messageHandler *messaging.Handler
54 shortcutHandler *ShortcutHandler
55}
56
57// Run starts the browser application
58func Run(assets embed.FS, version, commit, buildDate string) {
59 log.Printf("Starting GUI mode (webkit_cgo=%v)", webkit.IsNativeAvailable())
60
61 app := &BrowserApp{
62 version: version,
63 commit: commit,
64 buildDate: buildDate,
65 assets: assets,
66 }
67
68 if err := app.Initialize(); err != nil {
69 log.Printf("Failed to initialize browser: %v", err)
70 if webkit.IsNativeAvailable() {
71 runtime.UnlockOSThread()
72 }
73 os.Exit(1)
74 }
75
76 app.Run()
77}
78
79// Initialize sets up all browser components
80func (app *BrowserApp) Initialize() error {
81 // Setup media pipeline hardening
82 environment.SetupMediaPipeline()
83
84 // Initialize configuration
85 if err := config.Init(); err != nil {
86 return err
87 }
88 app.config = config.Get()
89
90 // Apply environment configurations
91 environment.ApplyCodecConfiguration(app.config.CodecPreferences)
92
93 // Setup crash handler
94 logging.SetupCrashHandler()
95
96 // Setup output capture if configured
97 if err := app.setupOutputCapture(); err != nil {
98 log.Printf("Warning: failed to setup output capture: %v", err)
99 }
100
101 // Initialize WebKit log capture if configured
102 if app.config.Logging.CaptureCOutput {
103 if err := webkit.InitWebKitLogCapture(); err != nil {
104 log.Printf("Warning: failed to initialize WebKit log capture: %v", err)
105 } else {
106 defer webkit.StopWebKitLogCapture()
107 }
108 }
109
110 log.Printf("Config initialized")
111
112 // Detect keyboard layout
113 environment.DetectAndSetKeyboardLocale()
114
115 // Initialize database
116 database, err := db.InitDB(app.config.Database.Path)
117 if err != nil {
118 return err
119 }
120 app.database = database
121
122 // Run database migrations
123 if err := migrations.RunEmbeddedMigrations(database); err != nil {
124 return fmt.Errorf("failed to run database migrations: %w", err)
125 }
126
127 app.queries = db.New(database)
128 log.Printf("Database opened at %s", app.config.Database.Path)
129
130 // Initialize services
131 app.parserService = services.NewParserService(app.config, app.queries)
132 app.browserService = services.NewBrowserService(app.config, app.queries)
133
134 // Initialize handlers
135 app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService)
136 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)
137
138 return nil
139}
140
141// Run executes the main browser loop
142func (app *BrowserApp) Run() {
143 defer app.cleanup()
144 defer logging.SetupPanicRecovery()
145
146 // Register custom scheme resolver
147 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
148 return app.schemeHandler.Handle(uri, app.config)
149 })
150
151 // Create and setup WebView
152 if err := app.createWebView(); err != nil {
153 log.Printf("Warning: failed to create WebView: %v", err)
154 return
155 }
156
157 // Initialize content blocking
158 if err := app.setupContentBlocking(); err != nil {
159 log.Printf("Warning: failed to setup content blocking: %v", err)
160 // Continue without content blocking
161 }
162
163 // Handle browse command if present
164 app.navigationController.HandleBrowseCommand()
165
166 // Setup signal handling
167 app.setupSignalHandling()
168
169 // Run main loop
170 app.runMainLoop()
171}
172
173// cleanup handles cleanup on shutdown
174func (app *BrowserApp) cleanup() {
175 if app.database != nil {
176 if closeErr := app.database.Close(); closeErr != nil {
177 log.Printf("Warning: failed to close database: %v", closeErr)
178 }
179 }
180}
181
182// setupOutputCapture initializes stdout/stderr capture if configured
183func (app *BrowserApp) setupOutputCapture() error {
184 if app.config.Logging.CaptureStdout || app.config.Logging.CaptureStderr {
185 log.Printf("Warning: stdout/stderr capture is experimental and may interfere with normal operations")
186 if logger := logging.GetLogger(); logger != nil {
187 outputCapture := logging.NewOutputCapture(logger)
188 if err := outputCapture.Start(); err != nil {
189 return err
190 }
191 // Note: defer outputCapture.Stop() should be handled by the caller
192 }
193 }
194 return nil
195}
196
197// setupSignalHandling configures graceful shutdown on signals
198func (app *BrowserApp) setupSignalHandling() {
199 sigChan := make(chan os.Signal, 1)
200 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
201
202 // Handle signals in a goroutine to quit the main loop
203 go func() {
204 sig := <-sigChan
205 log.Printf("Received signal %v - shutting down gracefully", sig)
206 webkit.QuitMainLoop()
207 }()
208}
209
210// runMainLoop starts the appropriate main loop based on WebKit availability
211func (app *BrowserApp) runMainLoop() {
212 if webkit.IsNativeAvailable() {
213 log.Printf("Entering GTK main loop…")
214 webkit.RunMainLoop()
215 log.Printf("GTK main loop exited")
216 } else {
217 log.Printf("Not entering GUI loop (non-CGO build)")
218 // In non-CGO mode, just wait for signals
219 sigChan := make(chan os.Signal, 1)
220 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
221 sig := <-sigChan
222 log.Printf("Received signal %v - exiting", sig)
223 }
224}