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