detached 826661c

refactor(logging): migrate log.Printf to logging package

commit
826661c824505953081811775cf89b587fc1e071
author
bnema · · 2025-11-29 12:04 UTC
parent
5e0ca2d
signature
GPG Unverified2B82207D370127A0 The key is unknown
50 files · +867 -843
M internal/app/api/scheme.go +11 -11
@@ -3,7 +3,6 @@ package api
33 import (
44 "embed"
55 "fmt"
6- "log"
76 "mime"
87 neturl "net/url"
98 "os"
@@ -12,6 +11,7 @@ import (
1211
1312 "github.com/bnema/dumber/internal/app/constants"
1413 "github.com/bnema/dumber/internal/config"
14+ "github.com/bnema/dumber/internal/logging"
1515 "github.com/bnema/dumber/internal/services"
1616 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1717 "github.com/diamondburned/gotk4/pkg/gio/v2"
@@ -47,7 +47,7 @@ func (s *SchemeHandler) SetConfig(cfg *config.Config) {
4747 // Handle processes dumb:// scheme requests using the new URISchemeRequest API
4848 func (s *SchemeHandler) Handle(req *webkit.URISchemeRequest) {
4949 uri := req.URI()
50- log.Printf("[scheme] request: %s", uri)
50+ logging.Debug(fmt.Sprintf("[scheme] request: %s", uri))
5151
5252 // Known forms:
5353 // - dumb://homepage or dumb:homepage → index.html
@@ -96,7 +96,7 @@ func (s *SchemeHandler) handleAsset(req *webkit.URISchemeRequest, u *neturl.URL)
9696
9797 // Special-case homepage favicon: map .ico request to embedded PNG file
9898 if (u.Host == constants.HomepagePath || u.Opaque == constants.HomepagePath) && strings.EqualFold(rel, "favicon.ico") {
99- log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.png", rel, u.Host, u.Path)
99+ logging.Debug(fmt.Sprintf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.png", rel, u.Host, u.Path))
100100 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", "favicon.png")))
101101 if rerr == nil {
102102 s.finishRequest(req, "image/png", data, "favicon.png")
@@ -104,12 +104,12 @@ func (s *SchemeHandler) handleAsset(req *webkit.URISchemeRequest, u *neturl.URL)
104104 }
105105 }
106106
107- log.Printf("[scheme] asset: rel=%s (host=%s path=%s)", rel, u.Host, u.Path)
107+ logging.Debug(fmt.Sprintf("[scheme] asset: rel=%s (host=%s path=%s)", rel, u.Host, u.Path))
108108
109109 // Try to read the requested asset
110110 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel)))
111111 if rerr != nil {
112- log.Printf("[scheme] not found: %s", rel)
112+ logging.Debug(fmt.Sprintf("[scheme] not found: %s", rel))
113113 req.FinishError(fmt.Errorf("asset not found: %s", rel))
114114 return
115115 }
@@ -118,7 +118,7 @@ func (s *SchemeHandler) handleAsset(req *webkit.URISchemeRequest, u *neturl.URL)
118118 mt := s.getMimeType(rel)
119119
120120 // Finish the request with the data
121- log.Printf("[scheme] serving %s with mime-type: %s", rel, mt)
121+ logging.Debug(fmt.Sprintf("[scheme] serving %s with mime-type: %s", rel, mt))
122122 s.finishRequest(req, mt, data, rel)
123123 }
124124
@@ -131,7 +131,7 @@ func (s *SchemeHandler) finishRequest(req *webkit.URISchemeRequest, mimeType str
131131 stream := gio.NewMemoryInputStreamFromBytes(gbytes)
132132 if stream == nil {
133133 req.FinishError(fmt.Errorf("failed to create input stream for: %s", filename))
134- log.Printf("[scheme] failed to create stream for: %s", filename)
134+ logging.Error(fmt.Sprintf("[scheme] failed to create stream for: %s", filename))
135135 return
136136 }
137137
@@ -204,7 +204,7 @@ func (s *SchemeHandler) handleFavicon(req *webkit.URISchemeRequest, u *neturl.UR
204204 }
205205
206206 if filename == "" {
207- log.Printf("[scheme] favicon: empty filename")
207+ logging.Debug(fmt.Sprintf("[scheme] favicon: empty filename"))
208208 req.FinishError(fmt.Errorf("invalid favicon path"))
209209 return
210210 }
@@ -212,7 +212,7 @@ func (s *SchemeHandler) handleFavicon(req *webkit.URISchemeRequest, u *neturl.UR
212212 // Get the favicon cache directory path
213213 dataDir, err := config.GetDataDir()
214214 if err != nil {
215- log.Printf("[scheme] favicon: failed to get data directory: %v", err)
215+ logging.Error(fmt.Sprintf("[scheme] favicon: failed to get data directory: %v", err))
216216 req.FinishError(fmt.Errorf("failed to get data directory"))
217217 return
218218 }
@@ -222,11 +222,11 @@ func (s *SchemeHandler) handleFavicon(req *webkit.URISchemeRequest, u *neturl.UR
222222 // Read the favicon file
223223 data, err := os.ReadFile(faviconPath)
224224 if err != nil {
225- log.Printf("[scheme] favicon: file not found: %s", faviconPath)
225+ logging.Debug(fmt.Sprintf("[scheme] favicon: file not found: %s", faviconPath))
226226 req.FinishError(fmt.Errorf("favicon not found: %s", filename))
227227 return
228228 }
229229
230- log.Printf("[scheme] favicon: serving %s (%d bytes)", filename, len(data))
230+ logging.Debug(fmt.Sprintf("[scheme] favicon: serving %s (%d bytes)", filename, len(data)))
231231 s.finishRequest(req, "image/png", data, filename)
232232 }
M internal/app/browser/browser.go +42 -43
@@ -5,7 +5,6 @@ import (
55 "database/sql"
66 "embed"
77 "fmt"
8- "log"
98 "os"
109 "os/signal"
1110 "runtime"
@@ -58,8 +57,8 @@ type BrowserApp struct {
5857 workspace *WorkspaceManager
5958
6059 // Content filtering
61- filterManager *filtering.FilterManager
62- contentBlockingService *filtering.ContentBlockingService
60+ filterManager *filtering.FilterManager
61+ contentBlockingService *filtering.ContentBlockingService
6362
6463 // Handlers
6564 schemeHandler *api.SchemeHandler
@@ -71,7 +70,7 @@ type BrowserApp struct {
7170 // Run starts the browser application
7271 func Run(assets embed.FS, version, commit, buildDate string) {
7372 startupStart := time.Now()
74- log.Printf("Starting GUI mode (webkit_cgo=%v)", webkit.IsNativeAvailable())
73+ logging.Info(fmt.Sprintf("Starting GUI mode (webkit_cgo=%v)", webkit.IsNativeAvailable()))
7574
7675 app := &BrowserApp{
7776 version: version,
@@ -81,7 +80,7 @@ func Run(assets embed.FS, version, commit, buildDate string) {
8180 }
8281
8382 if err := app.Initialize(); err != nil {
84- log.Printf("Failed to initialize browser: %v", err)
83+ logging.Error(fmt.Sprintf("Failed to initialize browser: %v", err))
8584 if webkit.IsNativeAvailable() {
8685 runtime.UnlockOSThread()
8786 }
@@ -89,9 +88,9 @@ func Run(assets embed.FS, version, commit, buildDate string) {
8988 }
9089
9190 startupElapsed := time.Since(startupStart)
92- log.Printf("[startup] Application initialized in %v", startupElapsed)
91+ logging.Info(fmt.Sprintf("[startup] Application initialized in %v", startupElapsed))
9392 if startupElapsed > 500*time.Millisecond {
94- log.Printf("[startup] WARNING: Startup took %v (target: <500ms)", startupElapsed)
93+ logging.Warn(fmt.Sprintf("[startup] WARNING: Startup took %v (target: <500ms)", startupElapsed))
9594 }
9695
9796 app.Run()
@@ -116,20 +115,20 @@ func (app *BrowserApp) Initialize() error {
116115
117116 // Setup output capture if configured
118117 if err := app.setupOutputCapture(); err != nil {
119- log.Printf("Warning: failed to setup output capture: %v", err)
118+ logging.Warn(fmt.Sprintf("Warning: failed to setup output capture: %v", err))
120119 }
121120
122121 // Initialize WebKit log capture if configured
123122 if app.config.Logging.CaptureCOutput {
124123 if err := webkit.InitWebKitLogCapture(); err != nil {
125- log.Printf("Warning: failed to initialize WebKit log capture: %v", err)
124+ logging.Warn(fmt.Sprintf("Warning: failed to initialize WebKit log capture: %v", err))
126125 } else {
127126 defer webkit.StopWebKitLogCapture()
128127 webkit.StartWebKitOutputCapture()
129128 }
130129 }
131130
132- log.Printf("Config initialized")
131+ logging.Info(fmt.Sprintf("Config initialized"))
133132
134133 // Detect keyboard layout
135134 environment.DetectAndSetKeyboardLocale()
@@ -141,11 +140,11 @@ func (app *BrowserApp) Initialize() error {
141140 }
142141 app.database = database
143142 app.queries = db.New(database)
144- log.Printf("Database opened at %s", app.config.Database.Path)
143+ logging.Info(fmt.Sprintf("Database opened at %s", app.config.Database.Path))
145144
146145 // Cleanup expired certificate validations
147146 if err := webkit.CleanupExpiredCertificateValidations(); err != nil {
148- log.Printf("Warning: failed to cleanup expired certificate validations: %v", err)
147+ logging.Warn(fmt.Sprintf("Warning: failed to cleanup expired certificate validations: %v", err))
149148 }
150149
151150 // Initialize services
@@ -154,7 +153,7 @@ func (app *BrowserApp) Initialize() error {
154153
155154 // Load all caches in parallel for fast startup (target: <100ms)
156155 if err := app.loadCachesParallel(context.Background()); err != nil {
157- log.Printf("Warning: failed to load caches: %v", err)
156+ logging.Warn(fmt.Sprintf("Warning: failed to load caches: %v", err))
158157 // Non-fatal - caches will fall back to defaults or DB queries
159158 }
160159
@@ -176,7 +175,7 @@ func (app *BrowserApp) loadCachesParallel(ctx context.Context) error {
176175 // Load zoom cache in parallel
177176 g.Go(func() error {
178177 if err := app.browserService.LoadZoomCacheFromDB(ctx); err != nil {
179- log.Printf("[cache] Failed to load zoom cache: %v", err)
178+ logging.Error(fmt.Sprintf("[cache] Failed to load zoom cache: %v", err))
180179 return err
181180 }
182181 return nil
@@ -185,7 +184,7 @@ func (app *BrowserApp) loadCachesParallel(ctx context.Context) error {
185184 // Load certificate validation cache in parallel
186185 g.Go(func() error {
187186 if err := app.browserService.LoadCertCacheFromDB(ctx); err != nil {
188- log.Printf("[cache] Failed to load cert cache: %v", err)
187+ logging.Error(fmt.Sprintf("[cache] Failed to load cert cache: %v", err))
189188 return err
190189 }
191190 return nil
@@ -194,7 +193,7 @@ func (app *BrowserApp) loadCachesParallel(ctx context.Context) error {
194193 // Load favorites cache in parallel
195194 g.Go(func() error {
196195 if err := app.browserService.LoadFavoritesCacheFromDB(ctx); err != nil {
197- log.Printf("[cache] Failed to load favorites cache: %v", err)
196+ logging.Error(fmt.Sprintf("[cache] Failed to load favorites cache: %v", err))
198197 return err
199198 }
200199 return nil
@@ -203,7 +202,7 @@ func (app *BrowserApp) loadCachesParallel(ctx context.Context) error {
203202 // Load fuzzy search cache in parallel for instant dmenu access
204203 g.Go(func() error {
205204 if err := app.browserService.LoadFuzzyCacheFromDB(ctx); err != nil {
206- log.Printf("[cache] Failed to load fuzzy cache: %v", err)
205+ logging.Error(fmt.Sprintf("[cache] Failed to load fuzzy cache: %v", err))
207206 return err
208207 }
209208 return nil
@@ -215,11 +214,11 @@ func (app *BrowserApp) loadCachesParallel(ctx context.Context) error {
215214 }
216215
217216 elapsed := time.Since(startTime)
218- log.Printf("[cache] All caches loaded in %v", elapsed)
217+ logging.Info(fmt.Sprintf("[cache] All caches loaded in %v", elapsed))
219218
220219 // Warn if startup is slower than target
221220 if elapsed > 100*time.Millisecond {
222- log.Printf("[cache] Warning: Cache loading took %v (target: <100ms)", elapsed)
221+ logging.Warn(fmt.Sprintf("[cache] Warning: Cache loading took %v (target: <100ms)", elapsed))
223222 }
224223
225224 return nil
@@ -239,19 +238,19 @@ func (app *BrowserApp) Run() {
239238 // Initialize content blocking BEFORE creating WebViews so the service is ready
240239 // to register WebViews as they are created (tabs, workspaces, popups)
241240 if err := app.setupContentBlocking(); err != nil {
242- log.Printf("Warning: failed to setup content blocking: %v", err)
241+ logging.Warn(fmt.Sprintf("Warning: failed to setup content blocking: %v", err))
243242 // Continue without content blocking
244243 }
245244
246245 // Create and setup WebView (this also creates TabManager which creates more WebViews)
247246 if err := app.createWebView(); err != nil {
248- log.Printf("Warning: failed to create WebView: %v", err)
247+ logging.Warn(fmt.Sprintf("Warning: failed to create WebView: %v", err))
249248 return
250249 }
251250
252251 // Apply URI scheme handlers after WebView creation
253252 if err := webkit.ApplyURISchemeHandlers(app.webView.GetWebView()); err != nil {
254- log.Printf("Warning: failed to register URI scheme handlers: %v", err)
253+ logging.Warn(fmt.Sprintf("Warning: failed to register URI scheme handlers: %v", err))
255254 }
256255
257256 // Handle browse command if present (must use active tab's navigation controller)
@@ -277,52 +276,52 @@ func (app *BrowserApp) Run() {
277276
278277 // cleanup handles cleanup on shutdown
279278 func (app *BrowserApp) cleanup() {
280- log.Printf("Starting browser cleanup...")
279+ logging.Info(fmt.Sprintf("Starting browser cleanup..."))
281280
282281 // Cleanup window shortcuts first
283282 if app.windowShortcutHandler != nil {
284- log.Printf("Cleaning up window shortcuts")
283+ logging.Info(fmt.Sprintf("Cleaning up window shortcuts"))
285284 app.windowShortcutHandler.Cleanup()
286285 app.windowShortcutHandler = nil
287286 }
288287
289288 // Cleanup tab manager (which cleans up all tabs and their workspaces)
290289 if app.tabManager != nil {
291- log.Printf("Cleaning up tab manager")
290+ logging.Info(fmt.Sprintf("Cleaning up tab manager"))
292291 app.tabManager.Cleanup()
293292 app.tabManager = nil
294293 }
295294
296295 // Flush all caches to ensure pending writes complete before database closes
297296 if app.browserService != nil {
298- log.Printf("Flushing all caches...")
297+ logging.Info(fmt.Sprintf("Flushing all caches..."))
299298 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
300299 defer cancel()
301300 if err := app.browserService.FlushAllCaches(ctx); err != nil {
302- log.Printf("Warning: failed to flush caches: %v", err)
301+ logging.Warn(fmt.Sprintf("Warning: failed to flush caches: %v", err))
303302 }
304303 }
305304
306305 // Close database with WAL checkpoint
307306 if app.database != nil {
308- log.Printf("Performing WAL checkpoint and closing database...")
307+ logging.Info(fmt.Sprintf("Performing WAL checkpoint and closing database..."))
309308
310309 // Run WAL checkpoint to commit all pending writes and truncate WAL file
311310 if _, err := app.database.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
312- log.Printf("Warning: WAL checkpoint failed: %v", err)
311+ logging.Warn(fmt.Sprintf("Warning: WAL checkpoint failed: %v", err))
313312 } else {
314- log.Printf("WAL checkpoint completed successfully")
313+ logging.Info(fmt.Sprintf("WAL checkpoint completed successfully"))
315314 }
316315
317316 // Close database connection
318317 if closeErr := app.database.Close(); closeErr != nil {
319- log.Printf("Warning: failed to close database: %v", closeErr)
318+ logging.Warn(fmt.Sprintf("Warning: failed to close database: %v", closeErr))
320319 } else {
321- log.Printf("Database closed successfully")
320+ logging.Info(fmt.Sprintf("Database closed successfully"))
322321 }
323322 }
324323
325- log.Printf("Browser cleanup completed")
324+ logging.Info(fmt.Sprintf("Browser cleanup completed"))
326325 }
327326
328327 // setupOutputCapture initializes stdout/stderr capture if configured
@@ -331,11 +330,11 @@ func (app *BrowserApp) setupOutputCapture() error {
331330 // Capturing stdout/stderr works for non-GTK builds, but in native GTK mode
332331 // it interferes with WebKit's own pipe management and crashes immediately.
333332 if webkit.IsNativeAvailable() {
334- log.Printf("Warning: stdout/stderr capture is not supported in native GTK mode; skipping")
333+ logging.Warn(fmt.Sprintf("Warning: stdout/stderr capture is not supported in native GTK mode; skipping"))
335334 return nil
336335 }
337336
338- log.Printf("Warning: stdout/stderr capture is experimental and may interfere with normal operations")
337+ logging.Warn(fmt.Sprintf("Warning: stdout/stderr capture is experimental and may interfere with normal operations"))
339338 if logger := logging.GetLogger(); logger != nil {
340339 outputCapture := logging.NewOutputCapture(logger)
341340 if err := outputCapture.Start(); err != nil {
@@ -355,7 +354,7 @@ func (app *BrowserApp) setupSignalHandling() {
355354 // Handle signals in a goroutine to quit the main loop
356355 go func() {
357356 sig := <-sigChan
358- log.Printf("Received signal %v - shutting down gracefully", sig)
357+ logging.Info(fmt.Sprintf("Received signal %v - shutting down gracefully", sig))
359358 webkit.QuitMainLoop()
360359 }()
361360 }
@@ -363,29 +362,29 @@ func (app *BrowserApp) setupSignalHandling() {
363362 // runMainLoop starts the appropriate main loop based on WebKit availability
364363 func (app *BrowserApp) runMainLoop() {
365364 if webkit.IsNativeAvailable() {
366- log.Printf("Entering GTK main loop…")
365+ logging.Info(fmt.Sprintf("Entering GTK main loop…"))
367366 webkit.RunMainLoop()
368- log.Printf("GTK main loop exited")
367+ logging.Info(fmt.Sprintf("GTK main loop exited"))
369368
370369 // Flush pending history writes immediately after main loop exit
371370 // This ensures database operations complete while GTK is still in a valid state
372371 // MUST happen before cleanup() deferred call, which happens after this function returns
373372 if app.browserService != nil {
374- log.Printf("Flushing pending history writes...")
373+ logging.Info(fmt.Sprintf("Flushing pending history writes..."))
375374 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
376375 defer cancel()
377376 if err := app.browserService.FlushHistoryQueue(ctx); err != nil {
378- log.Printf("Warning: history queue flush incomplete: %v", err)
377+ logging.Warn(fmt.Sprintf("Warning: history queue flush incomplete: %v", err))
379378 } else {
380- log.Printf("History queue flushed successfully")
379+ logging.Info(fmt.Sprintf("History queue flushed successfully"))
381380 }
382381 }
383382 } else {
384- log.Printf("Not entering GUI loop (non-CGO build)")
383+ logging.Info(fmt.Sprintf("Not entering GUI loop (non-CGO build)"))
385384 // In non-CGO mode, just wait for signals
386385 sigChan := make(chan os.Signal, 1)
387386 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
388387 sig := <-sigChan
389- log.Printf("Received signal %v - exiting", sig)
388+ logging.Info(fmt.Sprintf("Received signal %v - exiting", sig))
390389 }
391390 }
M internal/app/browser/focus_state_machine.go +40 -40
@@ -4,11 +4,11 @@ package browser
44 import (
55 "container/heap"
66 "fmt"
7- "log"
87 "runtime/debug"
98 "sync"
109 "time"
1110
11+ "github.com/bnema/dumber/internal/logging"
1212 "github.com/bnema/dumber/pkg/webkit"
1313 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1414 )
@@ -274,10 +274,10 @@ func (fsm *FocusStateMachine) ConfigureDebug(focusDebug, _ bool, metricsEnabled
274274 fsm.metricsEnabled = metricsEnabled
275275
276276 if focusDebug {
277- log.Printf("[FSM] Focus debug logging enabled")
277+ logging.Info(fmt.Sprintf("[FSM] Focus debug logging enabled"))
278278 }
279279 if metricsEnabled {
280- log.Printf("[FSM] Focus metrics tracking enabled")
280+ logging.Info(fmt.Sprintf("[FSM] Focus metrics tracking enabled"))
281281 }
282282 }
283283
@@ -286,27 +286,27 @@ func (fsm *FocusStateMachine) Initialize() error {
286286 fsm.mu.Lock()
287287 defer fsm.mu.Unlock()
288288
289- log.Printf("[FSM] Initializing focus state machine")
289+ logging.Info(fmt.Sprintf("[FSM] Initializing focus state machine"))
290290 fsm.currentState = StateInitializing
291291
292292 // Determine initial focus
293293 leaves := fsm.wm.collectLeaves()
294294 if len(leaves) == 0 {
295- log.Printf("[FSM] No panes found during initialization")
295+ logging.Debug(fmt.Sprintf("[FSM] No panes found during initialization"))
296296 fsm.currentState = StateIdle
297297 return nil
298298 }
299299
300300 initialPane := fsm.determineInitialFocus(leaves)
301301 if initialPane == nil {
302- log.Printf("[FSM] Could not determine initial focus target")
302+ logging.Warn(fmt.Sprintf("[FSM] Could not determine initial focus target"))
303303 fsm.currentState = StateIdle
304304 return nil
305305 }
306306
307307 // Phase 3: Apply initial focus and CSS
308308 if err := fsm.applyInitialFocus(initialPane); err != nil {
309- log.Printf("[FSM] Failed to apply initial focus: %v", err)
309+ logging.Error(fmt.Sprintf("[FSM] Failed to apply initial focus: %v", err))
310310 fsm.currentState = StateIdle
311311 return err
312312 }
@@ -324,7 +324,7 @@ func (fsm *FocusStateMachine) Initialize() error {
324324 // Phase 7: Transition to idle state
325325 fsm.currentState = StateIdle
326326
327- log.Printf("[FSM] Focus state machine initialized successfully with pane %p", initialPane)
327+ logging.Info(fmt.Sprintf("[FSM] Focus state machine initialized successfully with pane %p", initialPane))
328328 return nil
329329 }
330330
@@ -333,7 +333,7 @@ func (fsm *FocusStateMachine) Shutdown() {
333333 fsm.mu.Lock()
334334 defer fsm.mu.Unlock()
335335
336- log.Printf("[FSM] Shutting down focus state machine")
336+ logging.Info(fmt.Sprintf("[FSM] Shutting down focus state machine"))
337337
338338 // Signal shutdown
339339 close(fsm.shutdownChan)
@@ -346,7 +346,7 @@ func (fsm *FocusStateMachine) Shutdown() {
346346 fsm.settlingTimer.Stop()
347347 }
348348
349- log.Printf("[FSM] Focus state machine shutdown complete")
349+ logging.Info(fmt.Sprintf("[FSM] Focus state machine shutdown complete"))
350350 }
351351
352352 // RequestFocus queues a focus change request with priority and deduplication
@@ -382,7 +382,7 @@ func (fsm *FocusStateMachine) RequestFocus(node *paneNode, source FocusSource) e
382382 fsm.mu.Unlock()
383383 }
384384 if fsm.debugEnabled {
385- log.Printf("[FSM] Duplicate request ignored: %s -> %p", request.Source, request.TargetNode)
385+ logging.Debug(fmt.Sprintf("[FSM] Duplicate request ignored: %s -> %p", request.Source, request.TargetNode))
386386 }
387387 return nil
388388 }
@@ -395,7 +395,7 @@ func (fsm *FocusStateMachine) RequestFocus(node *paneNode, source FocusSource) e
395395 fsm.mu.Unlock()
396396 }
397397 if fsm.debugEnabled {
398- log.Printf("[FSM] Request coalesced: %s -> %p", request.Source, request.TargetNode)
398+ logging.Debug(fmt.Sprintf("[FSM] Request coalesced: %s -> %p", request.Source, request.TargetNode))
399399 }
400400 return nil
401401 }
@@ -443,7 +443,7 @@ func (fsm *FocusStateMachine) InvalidateActivePane(node *paneNode) {
443443 return
444444 }
445445
446- log.Printf("[FSM] Active pane invalidated: %p", node)
446+ logging.Debug(fmt.Sprintf("[FSM] Active pane invalidated: %p", node))
447447 fsm.activePane = nil
448448
449449 if fsm.wm != nil && fsm.wm.app != nil {
@@ -541,7 +541,7 @@ func (fsm *FocusStateMachine) determineInitialFocus(leaves []*paneNode) *paneNod
541541 if leaf.pane != nil && leaf.pane.webView != nil {
542542 url := leaf.pane.webView.GetCurrentURL()
543543 if url != "" && url != "about:blank" {
544- log.Printf("[FSM] Selected pane with content as initial focus: %s", url)
544+ logging.Debug(fmt.Sprintf("[FSM] Selected pane with content as initial focus: %s", url))
545545 return leaf
546546 }
547547 }
@@ -549,12 +549,12 @@ func (fsm *FocusStateMachine) determineInitialFocus(leaves []*paneNode) *paneNod
549549
550550 // Priority 2: Top-left pane (geometrically)
551551 if topLeft := fsm.findTopLeftPane(leaves); topLeft != nil {
552- log.Printf("[FSM] Selected top-left pane as initial focus")
552+ logging.Debug(fmt.Sprintf("[FSM] Selected top-left pane as initial focus"))
553553 return topLeft
554554 }
555555
556556 // Priority 3: First pane in tree order
557- log.Printf("[FSM] Selected first pane as initial focus")
557+ logging.Debug(fmt.Sprintf("[FSM] Selected first pane as initial focus"))
558558 return leaves[0]
559559 }
560560
@@ -587,7 +587,7 @@ func (fsm *FocusStateMachine) applyInitialFocus(node *paneNode) error {
587587 return ErrInvalidTarget
588588 }
589589
590- log.Printf("[FSM] Applying initial focus to pane %p", node)
590+ logging.Debug(fmt.Sprintf("[FSM] Applying initial focus to pane %p", node))
591591
592592 // Apply GTK focus
593593 if err := fsm.applyGTKFocus(node); err != nil {
@@ -646,7 +646,7 @@ func (fsm *FocusStateMachine) applyGTKFocus(node *paneNode) error {
646646 func (fsm *FocusStateMachine) processQueue() {
647647 defer fsm.queueProcessor.Done()
648648
649- log.Printf("[FSM] Focus request queue processor started")
649+ logging.Debug(fmt.Sprintf("[FSM] Focus request queue processor started"))
650650
651651 for {
652652 select {
@@ -657,7 +657,7 @@ func (fsm *FocusStateMachine) processQueue() {
657657 // Legacy channel for backward compatibility
658658 fsm.handleFocusRequest(request)
659659 case <-fsm.shutdownChan:
660- log.Printf("[FSM] Focus request queue processor shutting down")
660+ logging.Debug(fmt.Sprintf("[FSM] Focus request queue processor shutting down"))
661661 return
662662 }
663663 }
@@ -696,7 +696,7 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) {
696696 fsm.mu.Lock()
697697 fsm.metrics.FailedRequests++
698698 fsm.mu.Unlock()
699- log.Printf("[FSM] Invalid focus request %s: %v", request.ID, err)
699+ logging.Error(fmt.Sprintf("[FSM] Invalid focus request %s: %v", request.ID, err))
700700 return
701701 }
702702
@@ -706,13 +706,13 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) {
706706 fsm.mu.RUnlock()
707707
708708 if currentActive == request.TargetNode {
709- log.Printf("[FSM] Focus request %s: already focused on target", request.ID)
709+ logging.Debug(fmt.Sprintf("[FSM] Focus request %s: already focused on target", request.ID))
710710 fsm.recordProcessTime(time.Since(startTime))
711711 return
712712 }
713713
714- log.Printf("[FSM] Processing focus request %s: %s -> %p",
715- request.ID, request.Source, request.TargetNode)
714+ logging.Debug(fmt.Sprintf("[FSM] Processing focus request %s: %s -> %p",
715+ request.ID, request.Source, request.TargetNode))
716716
717717 // GTK operations must run on main thread - always use IdleAdd for simplicity
718718 var err error
@@ -728,7 +728,7 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) {
728728 fsm.mu.Lock()
729729 fsm.metrics.FailedRequests++
730730 fsm.mu.Unlock()
731- log.Printf("[FSM] Focus request %s failed: %v", request.ID, err)
731+ logging.Error(fmt.Sprintf("[FSM] Focus request %s failed: %v", request.ID, err))
732732 } else {
733733 fsm.mu.Lock()
734734 fsm.metrics.SuccessfulRequests++
@@ -805,7 +805,7 @@ func (fsm *FocusStateMachine) executeFocusChange(request FocusRequest) error {
805805 // Start settling timer
806806 fsm.startSettlingTimer()
807807
808- log.Printf("[FSM] Focus change completed: %p -> %p", oldPane, newPane)
808+ logging.Debug(fmt.Sprintf("[FSM] Focus change completed: %p -> %p", oldPane, newPane))
809809 return nil
810810 }
811811
@@ -866,14 +866,14 @@ func (fsm *FocusStateMachine) verifyCSSState() {
866866 }
867867
868868 if fsm.reconcileCount > fsm.maxReconcileAttempts {
869- log.Printf("[FSM] WARNING: Reconciliation loop detected, skipping (attempt %d)", fsm.reconcileCount)
869+ logging.Warn(fmt.Sprintf("[FSM] WARNING: Reconciliation loop detected, skipping (attempt %d)", fsm.reconcileCount))
870870 fsm.mu.Unlock()
871871 return
872872 }
873873 fsm.mu.Unlock()
874874
875- log.Printf("[FSM] CSS inconsistencies found: %v", issues)
876- log.Printf("[FSM] Triggering CSS reconciliation (attempt %d)", fsm.reconcileCount)
875+ logging.Debug(fmt.Sprintf("[FSM] CSS inconsistencies found: %v", issues))
876+ logging.Debug(fmt.Sprintf("[FSM] Triggering CSS reconciliation (attempt %d)", fsm.reconcileCount))
877877
878878 // Track reconciliation in metrics
879879 fsm.mu.Lock()
@@ -896,7 +896,7 @@ func (fsm *FocusStateMachine) shouldCoalesce(request FocusRequest) bool {
896896 for i := 0; i < fsm.priorityQueue.Len(); i++ {
897897 pending := (*fsm.priorityQueue)[i]
898898 if pending.TargetNode == request.TargetNode {
899- log.Printf("[FSM] Coalescing request to same target: %p", request.TargetNode)
899+ logging.Debug(fmt.Sprintf("[FSM] Coalescing request to same target: %p", request.TargetNode))
900900 return true
901901 }
902902 }
@@ -924,7 +924,7 @@ func (fsm *FocusStateMachine) requeue(request FocusRequest) {
924924 defer fsm.mu.Unlock()
925925
926926 heap.Push(fsm.priorityQueue, &request)
927- log.Printf("[FSM] Requeued request due to higher priority: %s", request.ID)
927+ logging.Debug(fmt.Sprintf("[FSM] Requeued request due to higher priority: %s", request.ID))
928928
929929 // Signal that a request is pending
930930 select {
@@ -940,7 +940,7 @@ func (fsm *FocusStateMachine) attachGTKController(node *paneNode) {
940940 }
941941
942942 if node.focusControllerToken != 0 {
943- log.Printf("[FSM] Controller already attached to pane %p", node)
943+ logging.Debug(fmt.Sprintf("[FSM] Controller already attached to pane %p", node))
944944 return
945945 }
946946
@@ -965,7 +965,7 @@ func (fsm *FocusStateMachine) attachGTKController(node *paneNode) {
965965 }
966966 node.lastFocusEnterTime = now
967967
968- log.Printf("[FSM] GTK focus enter: %p", node)
968+ logging.Debug(fmt.Sprintf("[FSM] GTK focus enter: %p", node))
969969 // Don't automatically change focus on GTK enter - let user interactions drive this
970970 // This prevents infinite loops with our own focus changes
971971 })
@@ -978,7 +978,7 @@ func (fsm *FocusStateMachine) attachGTKController(node *paneNode) {
978978 }
979979 node.lastFocusLeaveTime = now
980980
981- log.Printf("[FSM] GTK focus leave: %p", node)
981+ logging.Debug(fmt.Sprintf("[FSM] GTK focus leave: %p", node))
982982 // Similarly, don't react to GTK leave events automatically
983983 })
984984
@@ -988,7 +988,7 @@ func (fsm *FocusStateMachine) attachGTKController(node *paneNode) {
988988 // Store controller pointer as token for later removal
989989 node.focusControllerToken = uintptr(controller.Native())
990990 if node.focusControllerToken != 0 {
991- log.Printf("[FSM] Attached GTK focus controller to pane %p with token %d", node, node.focusControllerToken)
991+ logging.Debug(fmt.Sprintf("[FSM] Attached GTK focus controller to pane %p with token %d", node, node.focusControllerToken))
992992 }
993993 }
994994
@@ -999,7 +999,7 @@ func (fsm *FocusStateMachine) detachGTKController(node *paneNode, token uintptr)
999999 }
10001000
10011001 if node.focusControllerToken != token {
1002- log.Printf("[FSM] Token mismatch, skipping detach for pane %p", node)
1002+ logging.Debug(fmt.Sprintf("[FSM] Token mismatch, skipping detach for pane %p", node))
10031003 return
10041004 }
10051005
@@ -1007,7 +1007,7 @@ func (fsm *FocusStateMachine) detachGTKController(node *paneNode, token uintptr)
10071007
10081008 // Note: In GTK4, controllers are automatically removed when widget is destroyed
10091009 // We just need to clear our token reference
1010- log.Printf("[FSM] Detached GTK focus controller from pane %p", node)
1010+ logging.Debug(fmt.Sprintf("[FSM] Detached GTK focus controller from pane %p", node))
10111011 }
10121012
10131013 // attachGTKControllersToAllPanes attaches focus controllers to all existing panes
@@ -1021,7 +1021,7 @@ func (fsm *FocusStateMachine) attachGTKControllersToAllPanes() {
10211021 fsm.attachGTKController(leaf)
10221022 }
10231023
1024- log.Printf("[FSM] Attached GTK focus controllers to %d panes", len(leaves))
1024+ logging.Debug(fmt.Sprintf("[FSM] Attached GTK focus controllers to %d panes", len(leaves)))
10251025 }
10261026
10271027 // FocusDebugInfo contains comprehensive debug information about the focus system
@@ -1070,7 +1070,7 @@ func (fsm *FocusStateMachine) DumpState() FocusDebugInfo {
10701070
10711071 // ForceValidation manually triggers CSS consistency check and validation
10721072 func (fsm *FocusStateMachine) ForceValidation() []string {
1073- log.Printf("[FSM] Force validation requested")
1073+ logging.Debug(fmt.Sprintf("[FSM] Force validation requested"))
10741074
10751075 // Trigger CSS verification
10761076 fsm.verifyCSSState()
@@ -1096,7 +1096,7 @@ func (fsm *FocusStateMachine) ForceValidation() []string {
10961096 }
10971097 }
10981098
1099- log.Printf("[FSM] Force validation completed: %d issues found", len(issues))
1099+ logging.Debug(fmt.Sprintf("[FSM] Force validation completed: %d issues found", len(issues)))
11001100 return issues
11011101 }
11021102
@@ -1138,7 +1138,7 @@ func (fsm *FocusStateMachine) ClearHistory() {
11381138 defer fsm.mu.Unlock()
11391139
11401140 fsm.historyRing = NewRingBuffer[FocusTransition](50)
1141- log.Printf("[FSM] Focus transition history cleared")
1141+ logging.Debug(fmt.Sprintf("[FSM] Focus transition history cleared"))
11421142 }
11431143
11441144 // GetMetrics returns performance metrics for the focus system
M internal/app/browser/pane.go +6 -5
@@ -1,11 +1,12 @@
11 package browser
22
33 import (
4- "log"
4+ "fmt"
55 "time"
66
77 "github.com/bnema/dumber/internal/app/control"
88 "github.com/bnema/dumber/internal/app/messaging"
9+ "github.com/bnema/dumber/internal/logging"
910 "github.com/bnema/dumber/pkg/webkit"
1011 )
1112
@@ -85,7 +86,7 @@ func (p *BrowserPane) Cleanup() {
8586
8687 if p.shortcutHandler != nil {
8788 p.shortcutHandler.Detach()
88- log.Printf("[pane-%s] Cleaned up shortcuts", p.id)
89+ logging.Debug(fmt.Sprintf("[pane-%s] Cleaned up shortcuts", p.id))
8990 }
9091
9192 if p.navigationController != nil {
@@ -107,9 +108,9 @@ func (p *BrowserPane) Cleanup() {
107108 // Destroy WebView to unregister from global registry and release resources
108109 if p.webView != nil && !p.webView.IsDestroyed() {
109110 if err := p.webView.Destroy(); err != nil {
110- log.Printf("[pane-%s] failed to destroy webview: %v", p.id, err)
111+ logging.Error(fmt.Sprintf("[pane-%s] failed to destroy webview: %v", p.id, err))
111112 } else {
112- log.Printf("[pane-%s] successfully destroyed webview", p.id)
113+ logging.Debug(fmt.Sprintf("[pane-%s] successfully destroyed webview", p.id))
113114 }
114115 }
115116
@@ -132,6 +133,6 @@ func (p *BrowserPane) CleanupFromWorkspace(wm interface{}) {
132133 }); ok {
133134 workspaceManager.removeFromMaps(p.webView)
134135 workspaceManager.removeFromAppPanes(p)
135- log.Printf("[pane-%s] Cleaned up from workspace tracking", p.id)
136+ logging.Debug(fmt.Sprintf("[pane-%s] Cleaned up from workspace tracking", p.id))
136137 }
137138 }
M internal/app/browser/shortcuts.go +6 -6
@@ -3,10 +3,10 @@ package browser
33 import (
44 "encoding/json"
55 "fmt"
6- "log"
76
87 "github.com/bnema/dumber/internal/app/control"
98 "github.com/bnema/dumber/internal/config"
9+ "github.com/bnema/dumber/internal/logging"
1010 "github.com/bnema/dumber/pkg/webkit"
1111 )
1212
@@ -71,7 +71,7 @@ func (s *ShortcutHandler) RegisterShortcuts() {
7171 if !s.isActivePane() {
7272 return
7373 }
74- log.Printf("Shortcut: Reload page")
74+ logging.Debug("Shortcut: Reload page")
7575 _ = s.webView.Reload()
7676 })
7777
@@ -79,7 +79,7 @@ func (s *ShortcutHandler) RegisterShortcuts() {
7979 if !s.isActivePane() {
8080 return
8181 }
82- log.Printf("Shortcut: Hard reload page")
82+ logging.Debug("Shortcut: Hard reload page")
8383 _ = s.webView.ReloadBypassCache()
8484 })
8585
@@ -87,7 +87,7 @@ func (s *ShortcutHandler) RegisterShortcuts() {
8787 if !s.isActivePane() {
8888 return
8989 }
90- log.Printf("Shortcut: F5 reload")
90+ logging.Debug("Shortcut: F5 reload")
9191 _ = s.webView.Reload()
9292 })
9393
@@ -117,7 +117,7 @@ func (s *ShortcutHandler) exposeWorkspaceConfig() {
117117
118118 payload, err := json.Marshal(s.config.Workspace)
119119 if err != nil {
120- log.Printf("Shortcut: failed to marshal workspace config: %v", err)
120+ logging.Error(fmt.Sprintf("Shortcut: failed to marshal workspace config: %v", err))
121121 return
122122 }
123123
@@ -128,7 +128,7 @@ func (s *ShortcutHandler) exposeWorkspaceConfig() {
128128 })();`, string(payload))
129129
130130 if err := s.webView.InjectScript(script); err != nil {
131- log.Printf("Shortcut: failed to expose workspace config: %v", err)
131+ logging.Error(fmt.Sprintf("Shortcut: failed to expose workspace config: %v", err))
132132 }
133133 }
134134
M internal/app/browser/stacked_panes.go +34 -35
@@ -3,14 +3,13 @@ package browser
33 import (
44 "errors"
55 "fmt"
6- "log"
76 "sync/atomic"
87 "time"
98
9+ "github.com/bnema/dumber/internal/logging"
10+ "github.com/bnema/dumber/pkg/webkit"
1011 "github.com/diamondburned/gotk4/pkg/gdk/v4"
1112 "github.com/diamondburned/gotk4/pkg/gtk/v4"
12-
13- "github.com/bnema/dumber/pkg/webkit"
1413 )
1514
1615 const (
@@ -139,8 +138,8 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
139138
140139 // Insert the new pane right after the current pane
141140 insertIndex := currentIndex + 1
142- log.Printf("[workspace] adding to existing stack: currentIndex=%d insertIndex=%d stackSize=%d",
143- currentIndex, insertIndex, len(stackNode.stackedPanes))
141+ logging.Debug(fmt.Sprintf("[workspace] adding to existing stack: currentIndex=%d insertIndex=%d stackSize=%d",
142+ currentIndex, insertIndex, len(stackNode.stackedPanes)))
144143
145144 // Set parent relationship
146145 newLeaf.parent = stackNode
@@ -179,7 +178,7 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
179178 if box, ok := stackNode.stackWrapper.(*gtk.Box); ok && newLeaf.container != nil && newLeaf.titleBar != nil {
180179 box.InsertChildAfter(newLeaf.container, newLeaf.titleBar)
181180 }
182- log.Printf("[workspace] inserted widgets at position %d (after widget %p)", insertIndex, insertAfterWidget)
181+ logging.Debug(fmt.Sprintf("[workspace] inserted widgets at position %d (after widget %p)", insertIndex, insertAfterWidget))
183182 } else {
184183 // Insert at the beginning (insertIndex = 0)
185184 if box, ok := stackNode.stackWrapper.(*gtk.Box); ok && newLeaf.container != nil {
@@ -188,7 +187,7 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
188187 if box, ok := stackNode.stackWrapper.(*gtk.Box); ok && newLeaf.titleBar != nil {
189188 box.Prepend(newLeaf.titleBar)
190189 }
191- log.Printf("[workspace] prepended widgets at position 0")
190+ logging.Debug("[workspace] prepended widgets at position 0")
192191 }
193192
194193 return stackNode, insertIndex, nil
@@ -196,7 +195,7 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
196195
197196 // convertToStackedContainer converts a simple pane to a stacked container
198197 func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNode) (*paneNode, int, error) {
199- log.Printf("[workspace] converting pane to stacked: %p", target)
198+ logging.Debug(fmt.Sprintf("[workspace] converting pane to stacked: %p", target))
200199
201200 // Create the wrapper container - this is what will be used by splitNode
202201 stackWrapperContainer := gtk.NewBox(gtk.OrientationVertical, 0)
@@ -292,11 +291,11 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
292291 if newLeaf.container != nil {
293292 parentWidget := webkit.WidgetGetParent(newLeaf.container)
294293 if parentWidget != nil {
295- log.Printf("[workspace] unparenting new pane container %p from parent %p before stack append", newLeaf.container, parentWidget)
294+ logging.Debug(fmt.Sprintf("[workspace] unparenting new pane container %p from parent %p before stack append", newLeaf.container, parentWidget))
296295 webkit.WidgetUnparent(newLeaf.container)
297296 // Verify unparent succeeded
298297 if finalParent := webkit.WidgetGetParent(newLeaf.container); finalParent != nil {
299- log.Printf("[workspace] WARNING: container %p still has parent %p after unparent", newLeaf.container, finalParent)
298+ logging.Warn(fmt.Sprintf("[workspace] container %p still has parent %p after unparent", newLeaf.container, finalParent))
300299 }
301300 }
302301 }
@@ -369,7 +368,7 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod
369368 currentTitle := currentlyActivePane.pane.webView.GetTitle()
370369 if currentTitle != "" {
371370 spm.updateTitleBarLabel(currentlyActivePane, currentTitle)
372- log.Printf("[workspace] updated title bar for pane becoming inactive during stack creation: %s", currentTitle)
371+ logging.Debug(fmt.Sprintf("[workspace] updated title bar for pane becoming inactive during stack creation: %s", currentTitle))
373372 }
374373 }
375374 }
@@ -381,38 +380,38 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod
381380 // Set focus on the new pane synchronously
382381 spm.wm.SetActivePane(newLeaf, SourceSplit)
383382
384- log.Printf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",
385- stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex)
383+ logging.Debug(fmt.Sprintf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",
384+ stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex))
386385 return newLeaf, nil
387386 }
388387
389388 // UpdateStackVisibility updates the visibility of panes in a stack
390389 func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
391- log.Printf("[workspace] updateStackVisibility called: stackNode=%p", stackNode)
390+ logging.Debug(fmt.Sprintf("[workspace] updateStackVisibility called: stackNode=%p", stackNode))
392391
393392 if stackNode == nil {
394- log.Printf("[workspace] updateStackVisibility aborted: stackNode is nil")
393+ logging.Debug("[workspace] updateStackVisibility aborted: stackNode is nil")
395394 return
396395 }
397396
398397 if !stackNode.isStacked {
399- log.Printf("[workspace] updateStackVisibility aborted: stackNode.isStacked=%v", stackNode.isStacked)
398+ logging.Debug(fmt.Sprintf("[workspace] updateStackVisibility aborted: stackNode.isStacked=%v", stackNode.isStacked))
400399 return
401400 }
402401
403402 if len(stackNode.stackedPanes) == 0 {
404- log.Printf("[workspace] updateStackVisibility aborted: stackedPanes empty, len=%d", len(stackNode.stackedPanes))
403+ logging.Debug(fmt.Sprintf("[workspace] updateStackVisibility aborted: stackedPanes empty, len=%d", len(stackNode.stackedPanes)))
405404 return
406405 }
407406
408407 activeIndex := stackNode.activeStackIndex
409408 if activeIndex < 0 || activeIndex >= len(stackNode.stackedPanes) {
410- log.Printf("[workspace] updateStackVisibility: correcting activeIndex from %d to 0", activeIndex)
409+ logging.Debug(fmt.Sprintf("[workspace] updateStackVisibility: correcting activeIndex from %d to 0", activeIndex))
411410 activeIndex = 0
412411 stackNode.activeStackIndex = activeIndex
413412 }
414413
415- log.Printf("[workspace] updating stack visibility: activeIndex=%d stackSize=%d", activeIndex, len(stackNode.stackedPanes))
414+ logging.Debug(fmt.Sprintf("[workspace] updating stack visibility: activeIndex=%d stackSize=%d", activeIndex, len(stackNode.stackedPanes)))
416415
417416 // CRITICAL: Process ALL panes in a single pass to prevent flickering
418417 for i, pane := range stackNode.stackedPanes {
@@ -424,7 +423,7 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
424423 if pane.titleBar != nil {
425424 webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane
426425 }
427- log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i)
426+ logging.Debug(fmt.Sprintf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i))
428427 } else {
429428 // Inactive panes: hide container, show title bar
430429 if pane.container != nil {
@@ -474,7 +473,7 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
474473 // This is more reliable than trying to match panes
475474 currentIndex := stackNode.activeStackIndex
476475 if currentIndex < 0 || currentIndex >= len(stackNode.stackedPanes) {
477- log.Printf("[workspace] navigateStack: invalid activeStackIndex=%d, resetting to 0", currentIndex)
476+ logging.Debug(fmt.Sprintf("[workspace] navigateStack: invalid activeStackIndex=%d, resetting to 0", currentIndex))
478477 currentIndex = 0
479478 stackNode.activeStackIndex = 0
480479 }
@@ -509,7 +508,7 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
509508 currentTitle := currentActivePane.pane.webView.GetTitle()
510509 if currentTitle != "" {
511510 spm.updateTitleBarLabel(currentActivePane, currentTitle)
512- log.Printf("[workspace] updated title bar for pane transitioning to INACTIVE: %s", currentTitle)
511+ logging.Debug(fmt.Sprintf("[workspace] updated title bar for pane transitioning to INACTIVE: %s", currentTitle))
513512 }
514513 }
515514
@@ -521,8 +520,8 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
521520 newActivePane := stackNode.stackedPanes[newIndex]
522521 spm.wm.SetActivePane(newActivePane, SourceStackNav)
523522
524- log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",
525- direction, currentIndex, newIndex, len(stackNode.stackedPanes))
523+ logging.Debug(fmt.Sprintf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",
524+ direction, currentIndex, newIndex, len(stackNode.stackedPanes)))
526525 return true
527526 }
528527
@@ -586,10 +585,10 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
586585 if paneIndex != -1 && paneIndex != activeIndex {
587586 // This is an INACTIVE pane - safe to update title bar
588587 spm.updateTitleBarLabel(node, title)
589- log.Printf("[workspace] updated title bar for INACTIVE WebView %d: %s", webView.ID(), title)
588+ logging.Debug(fmt.Sprintf("[workspace] updated title bar for INACTIVE WebView %d: %s", webView.ID(), title))
590589 } else {
591590 // This is the ACTIVE pane - title bar should remain hidden
592- log.Printf("[workspace] skipped title bar update for ACTIVE WebView %d: %s", webView.ID(), title)
591+ logging.Debug(fmt.Sprintf("[workspace] skipped title bar update for ACTIVE WebView %d: %s", webView.ID(), title))
593592 }
594593 }
595594 }
@@ -609,7 +608,7 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
609608 // Create new title bar with updated title and favicon
610609 newTitleBar := spm.createTitleBarWithTitle(title, pageURL)
611610 if newTitleBar == nil {
612- log.Printf("[workspace] failed to create new title bar")
611+ logging.Error("[workspace] failed to create new title bar")
613612 return
614613 }
615614
@@ -655,7 +654,7 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
655654 }
656655 }
657656
658- log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node)
657+ logging.Debug(fmt.Sprintf("[workspace] replaced title bar for pane %d: %p", paneIndex, node))
659658 }
660659 }
661660
@@ -680,7 +679,7 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL str
680679 if pageURL != "" && spm.wm != nil && spm.wm.app != nil && spm.wm.app.faviconService != nil {
681680 spm.wm.app.faviconService.GetFaviconTexture(pageURL, func(texture *gdk.Texture, err error) {
682681 if err != nil {
683- log.Printf("[favicon] Failed to load favicon for title bar: %s - %v", pageURL, err)
682+ logging.Warn(fmt.Sprintf("[favicon] Failed to load favicon for title bar: %s - %v", pageURL, err))
684683 return
685684 }
686685
@@ -753,7 +752,7 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
753752 return errors.New("node not found in stack")
754753 }
755754
756- log.Printf("[workspace] closing stacked pane: index=%d stackSize=%d", nodeIndex, len(stackNode.stackedPanes))
755+ logging.Debug(fmt.Sprintf("[workspace] closing stacked pane: index=%d stackSize=%d", nodeIndex, len(stackNode.stackedPanes)))
757756
758757 // Detach hover/focus controllers before tearing down widgets to avoid GTK
759758 // callbacks referencing freed memory during destruction.
@@ -901,7 +900,7 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
901900 stackNode.stackWrapper = nil
902901 stackNode.stackedPanes = nil
903902
904- log.Printf("[workspace] converted stack back to regular pane")
903+ logging.Debug("[workspace] converted stack back to regular pane")
905904 return nil
906905
907906 default:
@@ -922,8 +921,8 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
922921 generation := spm.wm.nextCleanupGeneration()
923922 spm.wm.cleanupPane(node, generation)
924923
925- log.Printf("[workspace] closed pane from stack: remaining=%d activeIndex=%d",
926- len(stackNode.stackedPanes), stackNode.activeStackIndex)
924+ logging.Debug(fmt.Sprintf("[workspace] closed pane from stack: remaining=%d activeIndex=%d",
925+ len(stackNode.stackedPanes), stackNode.activeStackIndex))
927926 return nil
928927 }
929928 }
@@ -942,8 +941,8 @@ func (spm *StackedPaneManager) handleTitleBarClick(titleBarID uint64) {
942941 if p == pane {
943942 // Only switch if it's not already active
944943 if i != stackNode.activeStackIndex {
945- log.Printf("[workspace] title bar clicked: switching from pane %d to pane %d",
946- stackNode.activeStackIndex, i)
944+ logging.Debug(fmt.Sprintf("[workspace] title bar clicked: switching from pane %d to pane %d",
945+ stackNode.activeStackIndex, i))
947946
948947 // Update title bar for the pane transitioning from ACTIVE to INACTIVE
949948 currentActivePane := stackNode.stackedPanes[stackNode.activeStackIndex]
M internal/app/browser/webview.go +41 -41
@@ -4,7 +4,6 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7- "log"
87 "os"
98 "path/filepath"
109 "time"
@@ -14,6 +13,7 @@ import (
1413 "github.com/bnema/dumber/internal/app/messaging"
1514 "github.com/bnema/dumber/internal/config"
1615 "github.com/bnema/dumber/internal/filtering"
16+ "github.com/bnema/dumber/internal/logging"
1717 "github.com/bnema/dumber/internal/services"
1818 "github.com/bnema/dumber/pkg/webkit"
1919 )
@@ -43,19 +43,19 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) {
4343
4444 // Build WebKit configuration with data directories for persistence
4545 cfg := &webkit.Config{
46- UserAgent: buildUserAgent(app.config),
47- EnableJavaScript: true,
48- EnableWebGL: true,
49- EnableMediaStream: true,
50- HardwareAcceleration: true,
51- DefaultFontSize: app.config.Appearance.DefaultFontSize,
52- MinimumFontSize: 8,
53- EnablePageCache: true, // Instant back/forward navigation (bfcache)
54- EnableSmoothScrolling: true, // Smooth scrolling animations
55- DataDir: webkitData,
56- CacheDir: webkitCache,
57- AppearanceConfigJSON: app.buildAppearanceConfigJSON(),
58- CreateWindow: true, // Default to creating a window for standalone WebViews
46+ UserAgent: buildUserAgent(app.config),
47+ EnableJavaScript: true,
48+ EnableWebGL: true,
49+ EnableMediaStream: true,
50+ HardwareAcceleration: true,
51+ DefaultFontSize: app.config.Appearance.DefaultFontSize,
52+ MinimumFontSize: 8,
53+ EnablePageCache: true, // Instant back/forward navigation (bfcache)
54+ EnableSmoothScrolling: true, // Smooth scrolling animations
55+ DataDir: webkitData,
56+ CacheDir: webkitCache,
57+ AppearanceConfigJSON: app.buildAppearanceConfigJSON(),
58+ CreateWindow: true, // Default to creating a window for standalone WebViews
5959 }
6060
6161 return cfg, nil
@@ -102,7 +102,7 @@ func (app *BrowserApp) buildAppearanceConfigJSON() string {
102102
103103 payload, err := json.Marshal(paletteConfig)
104104 if err != nil {
105- log.Printf("[webview] Failed to marshal palette config: %v", err)
105+ logging.Error(fmt.Sprintf("[webview] Failed to marshal palette config: %v", err))
106106 return ""
107107 }
108108
@@ -149,7 +149,7 @@ func (app *BrowserApp) createPaneForView(view *webkit.WebView) (*BrowserPane, er
149149 pane.initializeGUITracking()
150150
151151 // GUI manager will be loaded on-demand when needed
152- log.Printf("[webview] Created pane for webview: %d", view.ID())
152+ logging.Debug(fmt.Sprintf("[webview] Created pane for webview: %d", view.ID()))
153153
154154 app.attachPaneHandlers(pane)
155155 return pane, nil
@@ -173,7 +173,7 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) {
173173 go func(url, title string) {
174174 ctx := context.Background()
175175 if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil {
176- log.Printf("Warning: failed to update page title: %v", err)
176+ logging.Warn(fmt.Sprintf("Warning: failed to update page title: %v", err))
177177 }
178178 }(url, title)
179179
@@ -210,7 +210,7 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) {
210210 node := app.workspace.GetNodeForWebView(pane.webView)
211211 if node != nil {
212212 app.workspace.setupPopupHandling(pane.webView, node)
213- log.Printf("[webview] Setup native popup handling for WebView ID: %d", pane.webView.ID())
213+ logging.Debug(fmt.Sprintf("[webview] Setup native popup handling for WebView ID: %d", pane.webView.ID()))
214214 }
215215 }
216216 }
@@ -245,7 +245,7 @@ func shouldFocusForScriptMessage(payload string) bool {
245245
246246 // createWebView creates and configures the WebView
247247 func (app *BrowserApp) createWebView() error {
248- log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable())
248+ logging.Info(fmt.Sprintf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable()))
249249
250250 cfg, err := app.buildWebkitConfig()
251251 if err != nil {
@@ -281,7 +281,7 @@ func (app *BrowserApp) createWebView() error {
281281
282282 // Initialize tab system (creates root container with tab bar and first tab)
283283 if err := app.tabManager.Initialize(initialURL); err != nil {
284- log.Printf("ERROR: failed to initialize tab manager: %v", err)
284+ logging.Error(fmt.Sprintf("ERROR: failed to initialize tab manager: %v", err))
285285 return err
286286 }
287287
@@ -289,9 +289,9 @@ func (app *BrowserApp) createWebView() error {
289289 rootContainer := app.tabManager.GetRootContainer()
290290 if rootContainer != nil {
291291 window.SetChild(rootContainer)
292- log.Printf("[tabs] Tab system initialized and set as window content")
292+ logging.Info(fmt.Sprintf("[tabs] Tab system initialized and set as window content"))
293293 } else {
294- log.Printf("ERROR: tab manager root container is nil")
294+ logging.Error(fmt.Sprintf("ERROR: tab manager root container is nil"))
295295 return fmt.Errorf("failed to get tab manager root container")
296296 }
297297 }
@@ -300,9 +300,9 @@ func (app *BrowserApp) createWebView() error {
300300 if window != nil {
301301 app.windowShortcutHandler = NewWindowShortcutHandler(window, app)
302302 if app.windowShortcutHandler != nil {
303- log.Printf("Window-level global shortcuts initialized")
303+ logging.Info(fmt.Sprintf("Window-level global shortcuts initialized"))
304304 } else {
305- log.Printf("Warning: failed to initialize window-level shortcuts")
305+ logging.Warn(fmt.Sprintf("Warning: failed to initialize window-level shortcuts"))
306306 }
307307 }
308308
@@ -310,18 +310,18 @@ func (app *BrowserApp) createWebView() error {
310310
311311 // Initialize FaviconService after WebView is created
312312 if err := app.initializeFaviconService(); err != nil {
313- log.Printf("Warning: failed to initialize favicon service: %v", err)
313+ logging.Warn(fmt.Sprintf("Warning: failed to initialize favicon service: %v", err))
314314 // Continue without favicon service - not critical
315315 }
316316
317317 // Apply initial zoom and show window
318318 app.zoomController.ApplyInitialZoom()
319319
320- log.Printf("Showing WebView window…")
320+ logging.Info(fmt.Sprintf("Showing WebView window…"))
321321 if err := view.Show(); err != nil {
322- log.Printf("Warning: failed to show WebView: %v", err)
322+ logging.Warn(fmt.Sprintf("Warning: failed to show WebView: %v", err))
323323 } else if !webkit.IsNativeAvailable() {
324- log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.")
324+ logging.Info(fmt.Sprintf("Notice: running without webkit_cgo tag — no native window will be displayed."))
325325 }
326326
327327 return nil
@@ -362,27 +362,27 @@ func (app *BrowserApp) initializeFaviconService() error {
362362 }
363363
364364 app.faviconService = faviconService
365- log.Printf("[favicon] FaviconService initialized successfully")
365+ logging.Info(fmt.Sprintf("[favicon] FaviconService initialized successfully"))
366366 return nil
367367 }
368368
369369 // setupContentBlocking initializes the content blocking system with proper timing
370370 func (app *BrowserApp) setupContentBlocking() error {
371- log.Printf("Initializing content blocking system...")
371+ logging.Info(fmt.Sprintf("Initializing content blocking system..."))
372372
373373 // Enable WebKit debug logging if requested
374374 webkit.SetupWebKitDebugLogging()
375375
376376 defer func() {
377377 if r := recover(); r != nil {
378- log.Printf("Content blocking setup panic recovered: %v", r)
378+ logging.Error(fmt.Sprintf("Content blocking setup panic recovered: %v", r))
379379 }
380380 }()
381381
382382 // Setup filter system
383383 filterManager, err := filtering.SetupFilterSystem()
384384 if err != nil {
385- log.Printf("Warning: Failed to setup filter system: %v", err)
385+ logging.Warn(fmt.Sprintf("Warning: Failed to setup filter system: %v", err))
386386 return nil // Don't fail browser startup, continue without filters
387387 }
388388
@@ -392,14 +392,14 @@ func (app *BrowserApp) setupContentBlocking() error {
392392 // Get data directory for content blocking service
393393 dataDir, err := config.GetDataDir()
394394 if err != nil {
395- log.Printf("Warning: Failed to get data dir for content blocking: %v", err)
395+ logging.Warn(fmt.Sprintf("Warning: Failed to get data dir for content blocking: %v", err))
396396 return nil
397397 }
398398
399399 // Create content blocking service
400400 cbService, err := filtering.NewContentBlockingService(dataDir+"/webkit", filterManager)
401401 if err != nil {
402- log.Printf("Warning: Failed to create content blocking service: %v", err)
402+ logging.Warn(fmt.Sprintf("Warning: Failed to create content blocking service: %v", err))
403403 return nil
404404 }
405405 app.contentBlockingService = cbService
@@ -407,36 +407,36 @@ func (app *BrowserApp) setupContentBlocking() error {
407407 // Set up callback to apply network filters when they become ready
408408 // This callback will be called from the async filter loading process
409409 filterManager.SetFiltersReadyCallback(func() {
410- log.Printf("[filtering] Filters ready, distributing to all WebViews...")
410+ logging.Info(fmt.Sprintf("[filtering] Filters ready, distributing to all WebViews..."))
411411
412412 // Get the WebKit JSON rules from filter manager
413413 filterJSON, err := filterManager.GetNetworkFilters()
414414 if err != nil {
415- log.Printf("[filtering] Failed to get network filters: %v", err)
415+ logging.Error(fmt.Sprintf("[filtering] Failed to get network filters: %v", err))
416416 return
417417 }
418418
419419 if len(filterJSON) == 0 {
420- log.Printf("[filtering] No network filters to apply")
420+ logging.Info(fmt.Sprintf("[filtering] No network filters to apply"))
421421 return
422422 }
423423
424- log.Printf("[filtering] Got %d bytes of WebKit JSON rules", len(filterJSON))
424+ logging.Debug(fmt.Sprintf("[filtering] Got %d bytes of WebKit JSON rules", len(filterJSON)))
425425
426426 // Signal the service that filters are ready - it will apply to all registered WebViews
427427 cbService.SetFiltersReady(filterJSON)
428- log.Printf("[filtering] Content blocking enabled for all WebViews")
428+ logging.Info(fmt.Sprintf("[filtering] Content blocking enabled for all WebViews"))
429429 })
430430
431431 // Start async filter loading
432432 // This allows filters to compile in the background while the browser starts
433433 go func() {
434434 if err := filtering.InitializeFiltersAsync(filterManager); err != nil {
435- log.Printf("Warning: failed to initialize filters asynchronously: %v", err)
435+ logging.Warn(fmt.Sprintf("Warning: failed to initialize filters asynchronously: %v", err))
436436 }
437437 }()
438438
439- log.Printf("Content blocking system initialization started")
439+ logging.Info(fmt.Sprintf("Content blocking system initialization started"))
440440 return nil
441441 }
442442
M internal/app/browser/window_shortcuts.go +61 -61
@@ -2,11 +2,11 @@ package browser
22
33 import (
44 "fmt"
5- "log"
65 "sync"
76 "time"
87
98 "github.com/bnema/dumber/internal/app/messaging"
9+ "github.com/bnema/dumber/internal/logging"
1010 "github.com/bnema/dumber/pkg/webkit"
1111 "github.com/diamondburned/gotk4/pkg/gdk/v4"
1212 "github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -35,7 +35,7 @@ func NewWindowShortcutHandler(window *webkit.Window, app *BrowserApp) *WindowSho
3535 }
3636
3737 if err := h.initialize(); err != nil {
38- log.Printf("[window-shortcuts] Failed to initialize: %v", err)
38+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to initialize: %v", err))
3939 return nil
4040 }
4141
@@ -51,7 +51,7 @@ func (h *WindowShortcutHandler) initialize() error {
5151
5252 // Register hardware keycode-based shortcuts (for AZERTY/international keyboards)
5353 if err := h.registerKeycodeShortcuts(); err != nil {
54- log.Printf("[window-shortcuts] Failed to register keycode shortcuts: %v", err)
54+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to register keycode shortcuts: %v", err))
5555 }
5656
5757 return h.registerGlobalShortcuts()
@@ -103,8 +103,8 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
103103
104104 for _, shortcut := range shortcuts {
105105 h.shortcuts.RegisterShortcut(shortcut.key, shortcut.handler)
106- log.Printf("[window-shortcuts] Registered global shortcut: %s (%s)",
107- shortcut.key, shortcut.desc)
106+ logging.Debug(fmt.Sprintf("[window-shortcuts] Registered global shortcut: %s (%s)",
107+ shortcut.key, shortcut.desc))
108108 }
109109
110110 // NOTE: Ctrl+P is NOT registered here - it's handled by WebView's keyboard bridge
@@ -164,14 +164,14 @@ func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
164164
165165 // Handle Ctrl+Tab for next tab
166166 if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) {
167- log.Printf("[window-shortcuts] Ctrl+Tab -> next tab")
167+ logging.Debug(fmt.Sprintf("[window-shortcuts] Ctrl+Tab -> next tab"))
168168 h.handleNextTab()
169169 return true // Consume event to prevent focus cycling
170170 }
171171
172172 // Handle Ctrl+Shift+Tab for previous tab
173173 if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && state.Has(gdk.ShiftMask) {
174- log.Printf("[window-shortcuts] Ctrl+Shift+Tab -> previous tab")
174+ logging.Debug(fmt.Sprintf("[window-shortcuts] Ctrl+Shift+Tab -> previous tab"))
175175 h.handlePrevTab()
176176 return true // Consume event to prevent focus cycling
177177 }
@@ -180,7 +180,7 @@ func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
180180 if state.Has(gdk.AltMask) && !state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) {
181181 // Check if this is a number key
182182 if tabIndex, ok := keycodeToTab[keycode]; ok {
183- log.Printf("[window-shortcuts] Hardware keycode shortcut: Alt+keycode(%d) -> tab %d", keycode, tabIndex)
183+ logging.Debug(fmt.Sprintf("[window-shortcuts] Hardware keycode shortcut: Alt+keycode(%d) -> tab %d", keycode, tabIndex))
184184 h.handleDirectTabSwitch(tabIndex)
185185 return true // Handled
186186 }
@@ -236,7 +236,7 @@ func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
236236 }
237237
238238 if handled {
239- log.Printf("[window-shortcuts] Tab mode action: %s", action)
239+ logging.Debug(fmt.Sprintf("[window-shortcuts] Tab mode action: %s", action))
240240 h.handleTabModeAction(action)
241241 return true // Consume event
242242 }
@@ -248,7 +248,7 @@ func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
248248 })
249249
250250 gtkWindow.AddController(keyController)
251- log.Printf("[window-shortcuts] Registered capture phase shortcuts: Ctrl+Tab, Alt+number")
251+ logging.Debug(fmt.Sprintf("[window-shortcuts] Registered capture phase shortcuts: Ctrl+Tab, Alt+number"))
252252
253253 return nil
254254 }
@@ -271,11 +271,11 @@ func (h *WindowShortcutHandler) handleCopyURL() {
271271 h.lastCopyURL = time.Now()
272272
273273 if h.app.activePane == nil || h.app.activePane.webView == nil {
274- log.Printf("[window-shortcuts] No active pane for copy URL")
274+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for copy URL"))
275275 return
276276 }
277277
278- log.Printf("[window-shortcuts] Copy URL -> pane %p", h.app.activePane.webView)
278+ logging.Debug(fmt.Sprintf("[window-shortcuts] Copy URL -> pane %p", h.app.activePane.webView))
279279
280280 // Execute copy URL script directly on active pane
281281 script := `
@@ -299,7 +299,7 @@ func (h *WindowShortcutHandler) handleCopyURL() {
299299 `
300300
301301 if err := h.app.activePane.webView.InjectScript(script); err != nil {
302- log.Printf("[window-shortcuts] Failed to inject copy URL script: %v", err)
302+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to inject copy URL script: %v", err))
303303 }
304304 }
305305
@@ -313,14 +313,14 @@ func (h *WindowShortcutHandler) handleDevTools() {
313313 h.lastDevTools = time.Now()
314314
315315 if h.app.activePane == nil || h.app.activePane.webView == nil {
316- log.Printf("[window-shortcuts] No active pane for devtools")
316+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for devtools"))
317317 return
318318 }
319319
320- log.Printf("[window-shortcuts] DevTools -> pane %p", h.app.activePane.webView)
320+ logging.Debug(fmt.Sprintf("[window-shortcuts] DevTools -> pane %p", h.app.activePane.webView))
321321
322322 if err := h.app.activePane.webView.ShowDevTools(); err != nil {
323- log.Printf("[window-shortcuts] Failed to show devtools: %v", err)
323+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to show devtools: %v", err))
324324 }
325325 }
326326
@@ -334,14 +334,14 @@ func (h *WindowShortcutHandler) handlePrint() {
334334 h.lastPrint = time.Now()
335335
336336 if h.app.activePane == nil || h.app.activePane.webView == nil {
337- log.Printf("[window-shortcuts] No active pane for print")
337+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for print"))
338338 return
339339 }
340340
341- log.Printf("[window-shortcuts] Print -> pane %p", h.app.activePane.webView)
341+ logging.Debug(fmt.Sprintf("[window-shortcuts] Print -> pane %p", h.app.activePane.webView))
342342
343343 if err := h.app.activePane.webView.ShowPrintDialog(); err != nil {
344- log.Printf("[window-shortcuts] Failed to show print dialog: %v", err)
344+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to show print dialog: %v", err))
345345 }
346346 }
347347
@@ -350,14 +350,14 @@ func (h *WindowShortcutHandler) handleReload() {
350350 defer h.mu.Unlock()
351351
352352 if h.app.activePane == nil || h.app.activePane.webView == nil {
353- log.Printf("[window-shortcuts] No active pane for reload")
353+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for reload"))
354354 return
355355 }
356356
357- log.Printf("[window-shortcuts] Reload page -> pane %p", h.app.activePane.webView)
357+ logging.Debug(fmt.Sprintf("[window-shortcuts] Reload page -> pane %p", h.app.activePane.webView))
358358
359359 if err := h.app.activePane.webView.Reload(); err != nil {
360- log.Printf("[window-shortcuts] Failed to reload page: %v", err)
360+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to reload page: %v", err))
361361 }
362362 }
363363
@@ -366,14 +366,14 @@ func (h *WindowShortcutHandler) handleHardReload() {
366366 defer h.mu.Unlock()
367367
368368 if h.app.activePane == nil || h.app.activePane.webView == nil {
369- log.Printf("[window-shortcuts] No active pane for hard reload")
369+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for hard reload"))
370370 return
371371 }
372372
373- log.Printf("[window-shortcuts] Hard reload (bypass cache) -> pane %p", h.app.activePane.webView)
373+ logging.Debug(fmt.Sprintf("[window-shortcuts] Hard reload (bypass cache) -> pane %p", h.app.activePane.webView))
374374
375375 if err := h.app.activePane.webView.ReloadBypassCache(); err != nil {
376- log.Printf("[window-shortcuts] Failed to hard reload page: %v", err)
376+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to hard reload page: %v", err))
377377 }
378378 }
379379
@@ -398,12 +398,12 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
398398 defer h.mu.Unlock()
399399
400400 if h.app.activePane == nil || h.app.activePane.webView == nil {
401- log.Printf("[window-shortcuts] No active pane for zoom %s", action)
401+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for zoom %s", action))
402402 return
403403 }
404404
405405 activeWebView := h.app.activePane.webView
406- log.Printf("[window-shortcuts] Zoom %s -> pane %p", action, activeWebView)
406+ logging.Debug(fmt.Sprintf("[window-shortcuts] Zoom %s -> pane %p", action, activeWebView))
407407
408408 // Get current zoom level
409409 currentZoom := activeWebView.GetZoom()
@@ -423,11 +423,11 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
423423 }
424424 }
425425
426- log.Printf("[window-shortcuts] Zoom %s: %.2f -> %.2f", action, currentZoom, newZoom)
426+ logging.Debug(fmt.Sprintf("[window-shortcuts] Zoom %s: %.2f -> %.2f", action, currentZoom, newZoom))
427427
428428 // Apply zoom to active pane
429429 if err := activeWebView.SetZoom(newZoom); err != nil {
430- log.Printf("[window-shortcuts] Failed to set zoom: %v", err)
430+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to set zoom: %v", err))
431431 return
432432 }
433433
@@ -448,7 +448,7 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
448448 `, newZoom)
449449
450450 if err := activeWebView.InjectScript(toastScript); err != nil {
451- log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err)
451+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to show zoom toast: %v", err))
452452 }
453453 }
454454
@@ -459,7 +459,7 @@ func (h *WindowShortcutHandler) ensureGUIInActivePane(component string) {
459459
460460 pane := h.app.activePane
461461 if !pane.HasGUI() {
462- log.Printf("[window-shortcuts] Injecting GUI into pane %s for %s", pane.ID(), component)
462+ logging.Debug(fmt.Sprintf("[window-shortcuts] Injecting GUI into pane %s for %s", pane.ID(), component))
463463 if h.app.workspace != nil {
464464 h.app.workspace.ensureGUIInPane(pane)
465465 }
@@ -467,7 +467,7 @@ func (h *WindowShortcutHandler) ensureGUIInActivePane(component string) {
467467
468468 // Ensure specific component is available
469469 if !pane.HasGUIComponent(component) {
470- log.Printf("[window-shortcuts] Ensuring %s component in pane %s", component, pane.ID())
470+ logging.Debug(fmt.Sprintf("[window-shortcuts] Ensuring %s component in pane %s", component, pane.ID()))
471471 pane.SetGUIComponent(component, true)
472472 }
473473 }
@@ -484,17 +484,17 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam
484484 defer h.mu.Unlock()
485485
486486 if time.Since(*lastToggle) < 50*time.Millisecond {
487- log.Printf("[window-shortcuts] %s toggle debounced", featureName)
487+ logging.Debug(fmt.Sprintf("[window-shortcuts] %s toggle debounced", featureName))
488488 return
489489 }
490490 *lastToggle = time.Now()
491491
492492 if h.app.activePane == nil || h.app.activePane.webView == nil {
493- log.Printf("[window-shortcuts] No active pane for %s", featureName)
493+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active pane for %s", featureName))
494494 return
495495 }
496496
497- log.Printf("[window-shortcuts] %s toggle -> pane %p", featureName, h.app.activePane.webView)
497+ logging.Debug(fmt.Sprintf("[window-shortcuts] %s toggle -> pane %p", featureName, h.app.activePane.webView))
498498
499499 h.ensureGUIInActivePane("omnibox")
500500
@@ -504,48 +504,48 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam
504504 "timestamp": time.Now().UnixMilli(),
505505 "source": "window-global",
506506 }); err != nil {
507- log.Printf("[window-shortcuts] Failed to dispatch %s toggle: %v", featureName, err)
507+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to dispatch %s toggle: %v", featureName, err))
508508 }
509509 }
510510
511511 func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) {
512512 if h.app == nil || h.app.workspace == nil {
513- log.Printf("[window-shortcuts] No workspace for navigation")
513+ logging.Debug(fmt.Sprintf("[window-shortcuts] No workspace for navigation"))
514514 return
515515 }
516516
517- log.Printf("[window-shortcuts] Workspace navigation: %s", direction)
517+ logging.Debug(fmt.Sprintf("[window-shortcuts] Workspace navigation: %s", direction))
518518
519519 if h.app.workspace.FocusNeighbor(direction) {
520- log.Printf("[window-shortcuts] Workspace navigation %s successful", direction)
520+ logging.Debug(fmt.Sprintf("[window-shortcuts] Workspace navigation %s successful", direction))
521521 } else {
522- log.Printf("[window-shortcuts] Workspace navigation %s failed", direction)
522+ logging.Debug(fmt.Sprintf("[window-shortcuts] Workspace navigation %s failed", direction))
523523 }
524524 }
525525
526526 func (h *WindowShortcutHandler) handleClosePane() {
527527 if h.app == nil || h.app.workspace == nil {
528- log.Printf("[window-shortcuts] No workspace for close pane")
528+ logging.Debug(fmt.Sprintf("[window-shortcuts] No workspace for close pane"))
529529 return
530530 }
531531
532532 // Find the currently active WebView
533533 var activeWebView *webkit.WebView
534- log.Printf("[window-shortcuts] Searching for active WebView among %d WebViews", len(h.app.workspace.viewToNode))
534+ logging.Debug(fmt.Sprintf("[window-shortcuts] Searching for active WebView among %d WebViews", len(h.app.workspace.viewToNode)))
535535 for webView := range h.app.workspace.viewToNode {
536536 if webView != nil {
537537 isActive := webView.IsActive()
538- log.Printf("[window-shortcuts] WebView %d: IsActive=%t", webView.ID(), isActive)
538+ logging.Debug(fmt.Sprintf("[window-shortcuts] WebView %d: IsActive=%t", webView.ID(), isActive))
539539 if isActive {
540540 activeWebView = webView
541- log.Printf("[window-shortcuts] Found active WebView: %d", webView.ID())
541+ logging.Debug(fmt.Sprintf("[window-shortcuts] Found active WebView: %d", webView.ID()))
542542 break
543543 }
544544 }
545545 }
546546
547547 if activeWebView == nil {
548- log.Printf("[window-shortcuts] No active WebView found, using workspace closeCurrentPane")
548+ logging.Debug(fmt.Sprintf("[window-shortcuts] No active WebView found, using workspace closeCurrentPane"))
549549 h.app.workspace.closeCurrentPane()
550550 return
551551 }
@@ -553,7 +553,7 @@ func (h *WindowShortcutHandler) handleClosePane() {
553553 // Check if the active WebView is a popup
554554 node := h.app.workspace.viewToNode[activeWebView]
555555 if node != nil && node.isPopup {
556- log.Printf("[window-shortcuts] Closing popup via OnWorkspaceMessage")
556+ logging.Debug(fmt.Sprintf("[window-shortcuts] Closing popup via OnWorkspaceMessage"))
557557 // Use the proper close-popup message for popups
558558 msg := messaging.Message{
559559 Event: "close-popup",
@@ -562,7 +562,7 @@ func (h *WindowShortcutHandler) handleClosePane() {
562562 }
563563 h.app.workspace.OnWorkspaceMessage(activeWebView, msg)
564564 } else {
565- log.Printf("[window-shortcuts] Closing regular pane via workspace closeCurrentPane")
565+ logging.Debug(fmt.Sprintf("[window-shortcuts] Closing regular pane via workspace closeCurrentPane"))
566566 // Use regular close for non-popup panes
567567 h.app.workspace.closeCurrentPane()
568568 }
@@ -571,61 +571,61 @@ func (h *WindowShortcutHandler) handleClosePane() {
571571 // handlePaneMode enters pane mode for modal pane management.
572572 func (h *WindowShortcutHandler) handlePaneMode() {
573573 if h.app == nil || h.app.workspace == nil {
574- log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available")
574+ logging.Debug(fmt.Sprintf("[window-shortcuts] Cannot enter pane mode: workspace not available"))
575575 return
576576 }
577577
578- log.Printf("[window-shortcuts] Entering pane mode")
578+ logging.Debug(fmt.Sprintf("[window-shortcuts] Entering pane mode"))
579579 h.app.workspace.EnterPaneMode()
580580 }
581581
582582 // handleTabMode enters tab mode for modal tab management.
583583 func (h *WindowShortcutHandler) handleTabMode() {
584584 if h.app == nil || h.app.tabManager == nil {
585- log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available")
585+ logging.Debug(fmt.Sprintf("[window-shortcuts] Cannot enter tab mode: tab manager not available"))
586586 return
587587 }
588588
589- log.Printf("[window-shortcuts] Entering tab mode")
589+ logging.Debug(fmt.Sprintf("[window-shortcuts] Entering tab mode"))
590590 h.app.tabManager.EnterTabMode()
591591 }
592592
593593 // handleNextTab switches to the next tab.
594594 func (h *WindowShortcutHandler) handleNextTab() {
595595 if h.app == nil || h.app.tabManager == nil {
596- log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
596+ logging.Debug(fmt.Sprintf("[window-shortcuts] Cannot switch tab: tab manager not available"))
597597 return
598598 }
599599
600- log.Printf("[window-shortcuts] Switching to next tab")
600+ logging.Debug(fmt.Sprintf("[window-shortcuts] Switching to next tab"))
601601 if err := h.app.tabManager.NextTab(); err != nil {
602- log.Printf("[window-shortcuts] Failed to switch to next tab: %v", err)
602+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to switch to next tab: %v", err))
603603 }
604604 }
605605
606606 // handlePrevTab switches to the previous tab.
607607 func (h *WindowShortcutHandler) handlePrevTab() {
608608 if h.app == nil || h.app.tabManager == nil {
609- log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
609+ logging.Debug(fmt.Sprintf("[window-shortcuts] Cannot switch tab: tab manager not available"))
610610 return
611611 }
612612
613- log.Printf("[window-shortcuts] Switching to previous tab")
613+ logging.Debug(fmt.Sprintf("[window-shortcuts] Switching to previous tab"))
614614 if err := h.app.tabManager.PreviousTab(); err != nil {
615- log.Printf("[window-shortcuts] Failed to switch to previous tab: %v", err)
615+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to switch to previous tab: %v", err))
616616 }
617617 }
618618
619619 // handleDirectTabSwitch switches to a specific tab by index (0-based).
620620 func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) {
621621 if h.app == nil || h.app.tabManager == nil {
622- log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
622+ logging.Debug(fmt.Sprintf("[window-shortcuts] Cannot switch tab: tab manager not available"))
623623 return
624624 }
625625
626- log.Printf("[window-shortcuts] Direct tab switch to index %d", index)
626+ logging.Debug(fmt.Sprintf("[window-shortcuts] Direct tab switch to index %d", index))
627627 if err := h.app.tabManager.SwitchToTab(index); err != nil {
628- log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err)
628+ logging.Error(fmt.Sprintf("[window-shortcuts] Failed to switch to tab %d: %v", index, err))
629629 }
630630 }
631631
@@ -653,7 +653,7 @@ func (h *WindowShortcutHandler) handleTabModeAction(action string) {
653653 }
654654
655655 // Tab mode is active, handle the action
656- log.Printf("[window-shortcuts] Tab mode action: %s", action)
656+ logging.Debug(fmt.Sprintf("[window-shortcuts] Tab mode action: %s", action))
657657 h.app.tabManager.HandleTabAction(action)
658658 }
659659
M internal/app/browser/workspace_css.go +9 -4
@@ -3,10 +3,10 @@ package browser
33
44 import (
55 "fmt"
6- "log"
76 "strings"
87
98 "github.com/bnema/dumber/internal/config"
9+ "github.com/bnema/dumber/internal/logging"
1010 "github.com/bnema/dumber/pkg/webkit"
1111 )
1212
@@ -393,12 +393,17 @@ func (wm *WorkspaceManager) ensureWorkspaceStyles() {
393393 // Only apply CSS if it hasn't been applied globally or if content changed
394394 if !globalCSSInitialized || globalCSSContent != workspaceCSS {
395395 if err := webkit.AddCSSProvider(workspaceCSS); err != nil {
396- log.Printf("[workspace] WARNING: Failed to add CSS provider: %v", err)
396+ logging.Error(fmt.Sprintf("[workspace] WARNING: Failed to add CSS provider: %v", err))
397397 }
398398 globalCSSInitialized = true
399399 globalCSSContent = workspaceCSS
400- log.Printf("[workspace] Applied workspace CSS styles (%d bytes)", len(workspaceCSS))
401- log.Printf("[workspace] CSS preview: %s...", workspaceCSS[:min(200, len(workspaceCSS))])
400+ logging.Info(fmt.Sprintf("[workspace] Applied workspace CSS styles (%d bytes)", len(workspaceCSS)))
401+
402+ previewLen := 200
403+ if len(workspaceCSS) < previewLen {
404+ previewLen = len(workspaceCSS)
405+ }
406+ logging.Info(fmt.Sprintf("[workspace] CSS preview: %s...", workspaceCSS[:previewLen]))
402407 }
403408
404409 wm.cssInitialized = true
M internal/app/browser/workspace_debug.go +6 -2
@@ -1,12 +1,16 @@
11 package browser
22
3-import "log"
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/dumber/internal/logging"
7+)
48
59 func (wm *WorkspaceManager) paneCloseLogf(format string, args ...interface{}) {
610 if wm == nil || !wm.debugPaneClose {
711 return
812 }
9- log.Printf("[pane-close] "+format, args...)
13+ logging.Info(fmt.Sprintf("[pane-close] "+format, args...))
1014 }
1115
1216 func (wm *WorkspaceManager) dumpTreeState(label string) {
M internal/app/browser/workspace_diagnostics.go +3 -2
@@ -2,9 +2,10 @@ package browser
22
33 import (
44 "fmt"
5- "log"
65 "strings"
76 "sync"
7+
8+ "github.com/bnema/dumber/internal/logging"
89 )
910
1011 type TreeSnapshot struct {
@@ -63,7 +64,7 @@ func (d *WorkspaceDiagnostics) Capture(label string, root *paneNode) TreeSnapsho
6364 d.snapshots = append(d.snapshots, snapshot)
6465 d.mu.Unlock()
6566
66- log.Printf("[pane-close] snapshot %s\n%s", snapshot.Label, strings.Join(snapshot.Lines, "\n"))
67+ logging.Info(fmt.Sprintf("[pane-close] snapshot %s\n%s", snapshot.Label, strings.Join(snapshot.Lines, "\n")))
6768 return snapshot
6869 }
6970
M internal/app/browser/workspace_geometry_validator.go +4 -5
@@ -3,8 +3,7 @@ package browser
33
44 import (
55 "fmt"
6- "log"
7-
6+ "github.com/bnema/dumber/internal/logging"
87 "github.com/bnema/dumber/pkg/webkit"
98 )
109
@@ -52,7 +51,7 @@ func (gv *GeometryValidator) GetPaneGeometry(node *paneNode) PaneGeometry {
5251
5352 if node == nil || node.container == nil {
5453 if gv.debugMode {
55- log.Printf("[geometry] Invalid node or missing container")
54+ logging.Error(fmt.Sprintf("[geometry] Invalid node or missing container"))
5655 }
5756 return geometry
5857 }
@@ -67,8 +66,8 @@ func (gv *GeometryValidator) GetPaneGeometry(node *paneNode) PaneGeometry {
6766 geometry.IsValid = true
6867
6968 if gv.debugMode {
70- log.Printf("[geometry] Pane geometry: %dx%d at (%d,%d)",
71- geometry.Width, geometry.Height, geometry.X, geometry.Y)
69+ logging.Info(fmt.Sprintf("[geometry] Pane geometry: %dx%d at (%d,%d)",
70+ geometry.Width, geometry.Height, geometry.X, geometry.Y))
7271 }
7372
7473 return geometry
M internal/app/browser/workspace_hover.go +6 -5
@@ -2,8 +2,9 @@
22 package browser
33
44 import (
5- "log"
5+ "fmt"
66
7+ "github.com/bnema/dumber/internal/logging"
78 "github.com/bnema/dumber/pkg/webkit"
89 "github.com/diamondburned/gotk4/pkg/glib/v2"
910 )
@@ -43,9 +44,9 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) {
4344
4445 if controller != nil {
4546 node.hoverToken = controller
46- log.Printf("[workspace] Attached hover handler to pane %p (controller=%p)", node, controller)
47+ logging.Debug(fmt.Sprintf("[workspace] Attached hover handler to pane %p (controller=%p)", node, controller))
4748 } else {
48- log.Printf("[workspace] Failed to attach hover handler to pane %p", node)
49+ logging.Error(fmt.Sprintf("[workspace] Failed to attach hover handler to pane %p", node))
4950 }
5051 }
5152
@@ -61,7 +62,7 @@ func (wm *WorkspaceManager) detachHover(node *paneNode) {
6162 }
6263
6364 webkit.WidgetRemoveHoverHandler(node.container, node.hoverToken)
64- log.Printf("[workspace] Detached hover handler from pane %p (controller=%p)", node, node.hoverToken)
65+ logging.Debug(fmt.Sprintf("[workspace] Detached hover handler from pane %p (controller=%p)", node, node.hoverToken))
6566 node.hoverToken = nil
6667 }
6768
@@ -76,5 +77,5 @@ func (wm *WorkspaceManager) attachHoverHandlersToAllPanes() {
7677 wm.ensureHover(leaf)
7778 }
7879
79- log.Printf("[workspace] Attached hover handlers to %d panes", len(leaves))
80+ logging.Debug(fmt.Sprintf("[workspace] Attached hover handlers to %d panes", len(leaves)))
8081 }
M internal/app/browser/workspace_manager.go +39 -39
@@ -4,13 +4,13 @@ package browser
44 import (
55 "errors"
66 "fmt"
7- "log"
87 "strings"
98 "sync"
109 "sync/atomic"
1110 "time"
1211
1312 "github.com/bnema/dumber/internal/app/messaging"
13+ "github.com/bnema/dumber/internal/logging"
1414 "github.com/bnema/dumber/pkg/webkit"
1515 "github.com/diamondburned/gotk4/pkg/glib/v2"
1616 "github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -208,7 +208,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
208208
209209 // Initialize focus state machine after all setup is complete
210210 if err := manager.focusStateMachine.Initialize(); err != nil {
211- log.Printf("[workspace] Failed to initialize focus state machine: %v", err)
211+ logging.Error(fmt.Sprintf("[workspace] Failed to initialize focus state machine: %v", err))
212212 }
213213
214214 // Ensure hover controllers are attached to all panes for mouse-driven focus changes
@@ -225,7 +225,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
225225 func (wm *WorkspaceManager) shouldDebounce(source *webkit.WebView, threshold time.Duration) bool {
226226 if last, ok := wm.lastSplitMsg[source]; ok {
227227 if elapsed := time.Since(last); elapsed < threshold {
228- log.Printf("[workspace] operation debounced: %.0fms", elapsed.Seconds()*1000)
228+ logging.Debug(fmt.Sprintf("[workspace] operation debounced: %.0fms", elapsed.Seconds()*1000))
229229 return true
230230 }
231231 }
@@ -236,14 +236,14 @@ func (wm *WorkspaceManager) shouldDebounce(source *webkit.WebView, threshold tim
236236 // withSplitLock executes an operation with atomic splitting lock protection
237237 func (wm *WorkspaceManager) withSplitLock(operation string, fn func() error) error {
238238 if !atomic.CompareAndSwapInt32(&wm.splitting, 0, 1) {
239- log.Printf("[workspace] %s ignored: operation in progress", operation)
239+ logging.Debug(fmt.Sprintf("[workspace] %s ignored: operation in progress", operation))
240240 return fmt.Errorf("operation in progress")
241241 }
242242
243243 defer func() {
244244 atomic.StoreInt32(&wm.splitting, 0)
245245 if r := recover(); r != nil {
246- log.Printf("[workspace] %s panicked: %v", operation, r)
246+ logging.Error(fmt.Sprintf("[workspace] %s panicked: %v", operation, r))
247247 panic(r)
248248 }
249249 }()
@@ -272,9 +272,9 @@ func (wm *WorkspaceManager) cleanupPopupStorage(parentNode, targetNode *paneNode
272272 `, parentWebViewID, requestID, requestID, requestID, requestID, webviewID)
273273
274274 if err := parentNode.pane.WebView().InjectScript(script); err != nil {
275- log.Printf("[workspace] localStorage cleanup failed: %v", err)
275+ logging.Error(fmt.Sprintf("[workspace] localStorage cleanup failed: %v", err))
276276 } else {
277- log.Printf("[workspace] Cleaned localStorage for popup requestId=%s webviewId=%s", requestID, webviewID)
277+ logging.Debug(fmt.Sprintf("[workspace] Cleaned localStorage for popup requestId=%s webviewId=%s", requestID, webviewID))
278278 }
279279 }
280280
@@ -292,7 +292,7 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
292292 }
293293
294294 if node == nil {
295- log.Printf("[workspace] message from unknown webview: event=%s webviewId=%s", msg.Event, msg.WebViewID)
295+ logging.Warn(fmt.Sprintf("[workspace] message from unknown webview: event=%s webviewId=%s", msg.Event, msg.WebViewID))
296296 return
297297 }
298298
@@ -321,7 +321,7 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
321321 })
322322
323323 if err != nil {
324- log.Printf("[workspace] split failed: %v", err)
324+ logging.Error(fmt.Sprintf("[workspace] split failed: %v", err))
325325 }
326326
327327 case "pane-stack":
@@ -344,12 +344,12 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
344344 })
345345
346346 if err != nil {
347- log.Printf("[workspace] stack failed: %v", err)
347+ logging.Error(fmt.Sprintf("[workspace] stack failed: %v", err))
348348 }
349349
350350 case "close-popup":
351351 if msg.WebViewID == "" {
352- log.Printf("[workspace] close-popup: empty webviewId, ignoring")
352+ logging.Warn(fmt.Sprintf("[workspace] close-popup: empty webviewId, ignoring"))
353353 return
354354 }
355355
@@ -363,16 +363,16 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
363363 }
364364
365365 if targetNode == nil {
366- log.Printf("[workspace] close-popup: webview not found: %s", msg.WebViewID)
366+ logging.Warn(fmt.Sprintf("[workspace] close-popup: webview not found: %s", msg.WebViewID))
367367 return
368368 }
369369
370370 if !targetNode.isPopup {
371- log.Printf("[workspace] close-popup: target is not a popup: %s", msg.WebViewID)
371+ logging.Warn(fmt.Sprintf("[workspace] close-popup: target is not a popup: %s", msg.WebViewID))
372372 return
373373 }
374374
375- log.Printf("[workspace] Closing popup due to %s", msg.Reason)
375+ logging.Info(fmt.Sprintf("[workspace] Closing popup due to %s", msg.Reason))
376376
377377 // Remove from parent's active popup list
378378 if targetNode.parentPane != nil {
@@ -380,7 +380,7 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
380380 for i, childID := range parentNode.activePopupChildren {
381381 if childID == msg.WebViewID {
382382 parentNode.activePopupChildren = append(parentNode.activePopupChildren[:i], parentNode.activePopupChildren[i+1:]...)
383- log.Printf("[workspace] Removed popup from parent (remaining: %d)", len(parentNode.activePopupChildren))
383+ logging.Debug(fmt.Sprintf("[workspace] Removed popup from parent (remaining: %d)", len(parentNode.activePopupChildren)))
384384 break
385385 }
386386 }
@@ -389,11 +389,11 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
389389 }
390390
391391 if err := wm.ClosePane(targetNode); err != nil {
392- log.Printf("[workspace] Failed to close popup: %v", err)
392+ logging.Error(fmt.Sprintf("[workspace] Failed to close popup: %v", err))
393393 }
394394
395395 default:
396- log.Printf("[workspace] unhandled workspace event: %s", msg.Event)
396+ logging.Warn(fmt.Sprintf("[workspace] unhandled workspace event: %s", msg.Event))
397397 }
398398 }
399399
@@ -433,7 +433,7 @@ func (wm *WorkspaceManager) SetActivePane(node *paneNode, source FocusSource) {
433433 wm.lastFocusTarget = node
434434
435435 if err := wm.focusStateMachine.RequestFocus(node, source); err != nil {
436- log.Printf("[workspace] Focus request failed: %v", err)
436+ logging.Error(fmt.Sprintf("[workspace] Focus request failed: %v", err))
437437 }
438438 }
439439
@@ -516,12 +516,12 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
516516 return false
517517 }
518518
519- log.Printf("[workspace] Middle-click/Ctrl+click on link, opening in new pane: %s", linkURL)
519+ logging.Info(fmt.Sprintf("[workspace] Middle-click/Ctrl+click on link, opening in new pane: %s", linkURL))
520520
521521 // Get the node for this webview
522522 node := wm.GetNodeForWebView(webView)
523523 if node == nil {
524- log.Printf("[workspace] Cannot find node for middle-clicked link")
524+ logging.Error(fmt.Sprintf("[workspace] Cannot find node for middle-clicked link"))
525525 return false
526526 }
527527
@@ -537,18 +537,18 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
537537 switch behavior {
538538 case "stacked":
539539 // Stack the current pane - this also creates a new pane in the stack
540- log.Printf("[workspace] Using stacked behavior for gesture link")
540+ logging.Debug(fmt.Sprintf("[workspace] Using stacked behavior for gesture link"))
541541 newNode, err = wm.stackedPaneManager.StackPane(node)
542542 if err != nil {
543- log.Printf("[workspace] Failed to stack pane for gesture link: %v", err)
543+ logging.Error(fmt.Sprintf("[workspace] Failed to stack pane for gesture link: %v", err))
544544 return false
545545 }
546546 // newNode is the new pane that was added to the stack
547- log.Printf("[workspace] StackPane created new pane in stack: %p", newNode)
547+ logging.Debug(fmt.Sprintf("[workspace] StackPane created new pane in stack: %p", newNode))
548548
549549 case "split":
550550 // Regular split behavior
551- log.Printf("[workspace] Using split behavior for gesture link")
551+ logging.Debug(fmt.Sprintf("[workspace] Using split behavior for gesture link"))
552552 direction := "right"
553553 if wm.app.config.Workspace.Popups.Placement != "" {
554554 direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement)
@@ -557,7 +557,7 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
557557
558558 case "tabbed":
559559 // Tabbed not yet implemented, fall back to split
560- log.Printf("[workspace] WARNING: Tabbed behavior not yet implemented for gesture links, falling back to split")
560+ logging.Warn(fmt.Sprintf("[workspace] WARNING: Tabbed behavior not yet implemented for gesture links, falling back to split"))
561561 direction := "right"
562562 if wm.app.config.Workspace.Popups.Placement != "" {
563563 direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement)
@@ -566,7 +566,7 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
566566
567567 default:
568568 // Unknown behavior, fall back to split
569- log.Printf("[workspace] WARNING: Unknown behavior '%s' for gesture links, falling back to split", behavior)
569+ logging.Warn(fmt.Sprintf("[workspace] WARNING: Unknown behavior '%s' for gesture links, falling back to split", behavior))
570570 direction := "right"
571571 if wm.app.config.Workspace.Popups.Placement != "" {
572572 direction = strings.ToLower(wm.app.config.Workspace.Popups.Placement)
@@ -575,14 +575,14 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
575575 }
576576
577577 if err != nil {
578- log.Printf("[workspace] Failed to handle gesture link: %v", err)
578+ logging.Error(fmt.Sprintf("[workspace] Failed to handle gesture link: %v", err))
579579 return false
580580 }
581581
582582 // Navigate the new pane to the URL
583583 if newNode != nil && newNode.pane != nil && newNode.pane.WebView() != nil {
584584 if err := newNode.pane.WebView().LoadURL(linkURL); err != nil {
585- log.Printf("[workspace] Failed to load URL in new pane: %v", err)
585+ logging.Error(fmt.Sprintf("[workspace] Failed to load URL in new pane: %v", err))
586586 return false
587587 }
588588
@@ -593,7 +593,7 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
593593 return true // Indicate we handled the click
594594 })
595595
596- log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID())
596+ logging.Debug(fmt.Sprintf("[workspace] Registered navigation handler for webview: %d", webView.ID()))
597597 }
598598
599599 // GetAllPanes returns all BrowserPanes in this workspace.
@@ -621,7 +621,7 @@ func (wm *WorkspaceManager) GetActivePane() *BrowserPane {
621621 // RestoreFocus restores focus to the active pane in this workspace.
622622 // Called when switching to a tab to ensure the correct pane has focus.
623623 func (wm *WorkspaceManager) RestoreFocus() {
624- log.Printf("[workspace] RestoreFocus: workspace %p", wm)
624+ logging.Debug(fmt.Sprintf("[workspace] RestoreFocus: workspace %p", wm))
625625
626626 // Get the active pane and explicitly grab GTK focus
627627 activeNode := wm.GetActiveNode()
@@ -630,7 +630,7 @@ func (wm *WorkspaceManager) RestoreFocus() {
630630 // This ensures keyboard events go to the active tab's WebView
631631 // Note: RestoreFocus is called from switchToTabInternal which is already on the main thread
632632 webkit.WidgetGrabFocus(activeNode.pane.webView.AsWidget())
633- log.Printf("[workspace] Grabbed GTK focus for WebView in workspace %p", wm)
633+ logging.Debug(fmt.Sprintf("[workspace] Grabbed GTK focus for WebView in workspace %p", wm))
634634
635635 // Also request focus via FSM
636636 if wm.focusStateMachine != nil {
@@ -647,14 +647,14 @@ func (wm *WorkspaceManager) IsInActiveTab() bool {
647647 }
648648 // The active workspace is the one currently assigned to app.workspace
649649 isActive := wm.app.workspace == wm
650- log.Printf("[workspace] IsInActiveTab check: workspace %p, app.workspace %p, isActive=%v", wm, wm.app.workspace, isActive)
650+ logging.Debug(fmt.Sprintf("[workspace] IsInActiveTab check: workspace %p, app.workspace %p, isActive=%v", wm, wm.app.workspace, isActive))
651651 return isActive
652652 }
653653
654654 // ClearFocus removes GTK focus from all panes in this workspace.
655655 // This prevents inactive tab's WebViews from catching keyboard events.
656656 func (wm *WorkspaceManager) ClearFocus() {
657- log.Printf("[workspace] ClearFocus: workspace %p", wm)
657+ logging.Debug(fmt.Sprintf("[workspace] ClearFocus: workspace %p", wm))
658658 // No state to clear - IsInActiveTab() checks dynamically
659659 }
660660
@@ -675,19 +675,19 @@ func (wm *WorkspaceManager) setRootContainer(container gtk.Widgetter) {
675675 }
676676
677677 if wm.app == nil || wm.app.tabManager == nil || wm.app.tabManager.ContentArea == nil {
678- log.Printf("[workspace] ERROR: Cannot set root - tab manager not initialized")
678+ logging.Error(fmt.Sprintf("[workspace] ERROR: Cannot set root - tab manager not initialized"))
679679 return
680680 }
681681
682682 contentBox, ok := wm.app.tabManager.ContentArea.(*gtk.Box)
683683 if !ok || contentBox == nil {
684- log.Printf("[workspace] ERROR: ContentArea is not a Box")
684+ logging.Error(fmt.Sprintf("[workspace] ERROR: ContentArea is not a Box"))
685685 return
686686 }
687687
688688 contentBox.Append(container)
689689 webkit.WidgetSetVisible(container, true)
690- log.Printf("[workspace] Attached root container %p to ContentArea", container)
690+ logging.Debug(fmt.Sprintf("[workspace] Attached root container %p to ContentArea", container))
691691 }
692692
693693 // clearRootContainer removes the current workspace root from TabManager.ContentArea.
@@ -698,16 +698,16 @@ func (wm *WorkspaceManager) clearRootContainer() {
698698 }
699699
700700 if wm.app == nil || wm.app.tabManager == nil || wm.app.tabManager.ContentArea == nil {
701- log.Printf("[workspace] ERROR: Cannot clear root - tab manager not initialized")
701+ logging.Error(fmt.Sprintf("[workspace] ERROR: Cannot clear root - tab manager not initialized"))
702702 return
703703 }
704704
705705 contentBox, ok := wm.app.tabManager.ContentArea.(*gtk.Box)
706706 if !ok || contentBox == nil {
707- log.Printf("[workspace] ERROR: ContentArea is not a Box")
707+ logging.Error(fmt.Sprintf("[workspace] ERROR: ContentArea is not a Box"))
708708 return
709709 }
710710
711711 contentBox.Remove(wm.root.container)
712- log.Printf("[workspace] Removed root container %p from ContentArea", wm.root.container)
712+ logging.Debug(fmt.Sprintf("[workspace] Removed root container %p from ContentArea", wm.root.container))
713713 }
M internal/app/browser/workspace_navigation.go +30 -30
@@ -3,11 +3,11 @@ package browser
33
44 import (
55 "fmt"
6- "log"
76 "math"
87 "strings"
98 "time"
109
10+ "github.com/bnema/dumber/internal/logging"
1111 "github.com/bnema/dumber/pkg/webkit"
1212 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1313 )
@@ -32,9 +32,9 @@ func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool)
3232 }
3333
3434 if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
35- log.Printf("[workspace] failed to dispatch focus event: %v", err)
35+ logging.Error(fmt.Sprintf("[workspace] failed to dispatch focus event: %v", err))
3636 } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
37- log.Printf("[workspace] dispatched focus event for webview %d (active=%v)", node.pane.webView.ID(), active)
37+ logging.Debug(fmt.Sprintf("[workspace] dispatched focus event for webview %d (active=%v)", node.pane.webView.ID(), active))
3838 }
3939 }
4040
@@ -56,7 +56,7 @@ func (wm *WorkspaceManager) focusRespectingStack(node *paneNode, reason string)
5656 activePane := stack.stackedPanes[activeIndex]
5757 if activePane != nil && activePane != node {
5858 if reason != "" {
59- log.Printf("[workspace] %s: preserving active stacked pane index=%d", reason, activeIndex)
59+ logging.Info(fmt.Sprintf("[workspace] %s: preserving active stacked pane index=%d", reason, activeIndex))
6060 }
6161 wm.SetActivePane(activePane, SourceKeyboard)
6262 return
@@ -122,7 +122,7 @@ func (wm *WorkspaceManager) FocusNeighbor(direction string) bool {
122122 case DirectionLeft, DirectionRight:
123123 return wm.focusAdjacent(strings.ToLower(direction))
124124 default:
125- log.Printf("[workspace] invalid focus direction: %s (expected: up, down, left, right)", direction)
125+ logging.Error(fmt.Sprintf("[workspace] invalid focus direction: %s (expected: up, down, left, right)", direction))
126126 return false
127127 }
128128 }
@@ -162,7 +162,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool {
162162 }
163163
164164 if currentIndex == -1 {
165- log.Printf("[workspace] navigateStack: current pane not found in stack")
165+ logging.Error(fmt.Sprintf("[workspace] navigateStack: current pane not found in stack"))
166166 return false
167167 }
168168
@@ -195,8 +195,8 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool {
195195 newActivePane := stackNode.stackedPanes[newIndex]
196196 wm.SetActivePane(newActivePane, SourceStackNav)
197197
198- log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",
199- direction, currentIndex, newIndex, len(stackNode.stackedPanes))
198+ logging.Debug(fmt.Sprintf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",
199+ direction, currentIndex, newIndex, len(stackNode.stackedPanes)))
200200 return true
201201 }
202202
@@ -208,7 +208,7 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
208208 }
209209
210210 if neighbor := wm.structuralNeighbor(currentFocused, direction); neighbor != nil {
211- log.Printf("[workspace] focusAdjacent: using structural neighbor for direction=%s", direction)
211+ logging.Debug(fmt.Sprintf("[workspace] focusAdjacent: using structural neighbor for direction=%s", direction))
212212 wm.SetActivePane(neighbor, SourceKeyboard)
213213 return true
214214 }
@@ -216,7 +216,7 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
216216 curX, curY, curWidth, curHeight := wm.getNavigationAllocation(currentFocused)
217217 cx := float64(curX) + float64(curWidth)/2.0
218218 cy := float64(curY) + float64(curHeight)/2.0
219- log.Printf("[workspace] focusAdjacent: current pane center=(%.0f, %.0f) direction=%s", cx, cy, direction)
219+ logging.Debug(fmt.Sprintf("[workspace] focusAdjacent: current pane center=(%.0f, %.0f) direction=%s", cx, cy, direction))
220220
221221 leaves := wm.collectLeaves()
222222 bestScore := math.MaxFloat64
@@ -271,13 +271,13 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
271271
272272 if best != nil {
273273 bx, by, _, _ := webkit.WidgetGetAllocation(best.container)
274- log.Printf("[workspace] focusAdjacent: found best candidate at pos=(%d,%d) for direction=%s", bx, by, direction)
274+ logging.Debug(fmt.Sprintf("[workspace] focusAdjacent: found best candidate at pos=(%d,%d) for direction=%s", bx, by, direction))
275275 wm.SetActivePane(best, SourceKeyboard)
276276 return true
277277 }
278278
279279 if len(debugCandidates) > 0 {
280- log.Printf("[workspace] focusAdjacent: NO candidate found for direction=%s current=%p center=(%.0f,%.0f) candidates=[%s]", direction, currentFocused.container, cx, cy, strings.Join(debugCandidates, "; "))
280+ logging.Debug(fmt.Sprintf("[workspace] focusAdjacent: NO candidate found for direction=%s current=%p center=(%.0f,%.0f) candidates=[%s]", direction, currentFocused.container, cx, cy, strings.Join(debugCandidates, "; ")))
281281 }
282282 return false
283283 }
@@ -311,14 +311,14 @@ func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string)
311311 cy := float64(refY) + float64(refHeight)/2.0
312312 axisVertical := direction == DirectionUp || direction == DirectionDown
313313
314- log.Printf("[workspace] structuralNeighbor: node=%p pos=(%d,%d) direction=%s", node.container, refX, refY, direction)
314+ logging.Debug(fmt.Sprintf("[workspace] structuralNeighbor: node=%p pos=(%d,%d) direction=%s", node.container, refX, refY, direction))
315315
316316 // Start from the node's parent, but skip stack containers for vertical navigation
317317 // Stack containers are transparent to vertical navigation - we want to navigate
318318 // from stack to external panes, not within the stack (that's handled by NavigateStack)
319319 startParent := node.parent
320320 if startParent != nil && startParent.isStacked && axisVertical {
321- log.Printf("[workspace] structuralNeighbor: skipping stack container parent, using stack's parent instead")
321+ logging.Debug(fmt.Sprintf("[workspace] structuralNeighbor: skipping stack container parent, using stack's parent instead"))
322322 node = startParent // Treat the stack container as the navigation node
323323 startParent = startParent.parent
324324 }
@@ -326,27 +326,27 @@ func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string)
326326 for parent := startParent; parent != nil; parent = parent.parent {
327327 isLeft := parent.left == node
328328 isRight := parent.right == node
329- log.Printf("[workspace] structuralNeighbor: checking parent orientation=%v isLeft=%v isRight=%v", parent.orientation, isLeft, isRight)
329+ logging.Debug(fmt.Sprintf("[workspace] structuralNeighbor: checking parent orientation=%v isLeft=%v isRight=%v", parent.orientation, isLeft, isRight))
330330
331331 switch direction {
332332 case DirectionUp:
333333 if axisVertical && parent.orientation == gtk.OrientationVertical && parent.right == node {
334- log.Printf("[workspace] structuralNeighbor: DirectionUp - we are RIGHT child, looking in LEFT subtree")
334+ logging.Info(fmt.Sprintf("[workspace] structuralNeighbor: DirectionUp - we are RIGHT child, looking in LEFT subtree"))
335335 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {
336336 lx, ly, _, _ := webkit.WidgetGetAllocation(leaf.container)
337- log.Printf("[workspace] structuralNeighbor: found leaf at pos=(%d,%d)", lx, ly)
337+ logging.Debug(fmt.Sprintf("[workspace] structuralNeighbor: found leaf at pos=(%d,%d)", lx, ly))
338338 return leaf
339339 }
340340 }
341341 case DirectionDown:
342342 if axisVertical && parent.orientation == gtk.OrientationVertical && parent.left == node {
343- log.Printf("[workspace] structuralNeighbor: DirectionDown - we are LEFT child, looking in RIGHT subtree")
343+ logging.Info(fmt.Sprintf("[workspace] structuralNeighbor: DirectionDown - we are LEFT child, looking in RIGHT subtree"))
344344 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {
345345 lx, ly, _, _ := wm.getNavigationAllocation(leaf)
346- log.Printf("[workspace] structuralNeighbor: found leaf at pos=(%d,%d)", lx, ly)
346+ logging.Debug(fmt.Sprintf("[workspace] structuralNeighbor: found leaf at pos=(%d,%d)", lx, ly))
347347 return leaf
348348 } else {
349- log.Printf("[workspace] structuralNeighbor: closestLeafFromSubtree returned nil for DirectionDown")
349+ logging.Error(fmt.Sprintf("[workspace] structuralNeighbor: closestLeafFromSubtree returned nil for DirectionDown"))
350350 }
351351 }
352352 case DirectionLeft:
@@ -370,7 +370,7 @@ func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string)
370370 // closestLeafFromSubtree finds the closest leaf node in a subtree based on direction
371371 func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float64, direction string) *paneNode {
372372 leaves := wm.collectLeavesFromWithDirection(node, direction)
373- log.Printf("[workspace] closestLeafFromSubtree: found %d leaves for direction=%s from cx=%.0f cy=%.0f", len(leaves), direction, cx, cy)
373+ logging.Debug(fmt.Sprintf("[workspace] closestLeafFromSubtree: found %d leaves for direction=%s from cx=%.0f cy=%.0f", len(leaves), direction, cx, cy))
374374 bestScore := math.MaxFloat64
375375 var best *paneNode
376376 for _, leaf := range leaves {
@@ -383,47 +383,47 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6
383383 ty := float64(y) + float64(height)/2.0
384384 dx := tx - cx
385385 dy := ty - cy
386- log.Printf("[workspace] closestLeafFromSubtree: leaf=%p pos=(%d,%d) center=(%.0f,%.0f) dx=%.0f dy=%.0f", leaf.container, x, y, tx, ty, dx, dy)
386+ logging.Info(fmt.Sprintf("[workspace] closestLeafFromSubtree: leaf=%p pos=(%d,%d) center=(%.0f,%.0f) dx=%.0f dy=%.0f", leaf.container, x, y, tx, ty, dx, dy))
387387 var score float64
388388 switch direction {
389389 case DirectionLeft:
390390 if dx >= -focusEpsilon {
391- log.Printf("[workspace] closestLeafFromSubtree: SKIPPED (dx=%.0f not left)", dx)
391+ logging.Warn(fmt.Sprintf("[workspace] closestLeafFromSubtree: SKIPPED (dx=%.0f not left)", dx))
392392 continue
393393 }
394394 score = math.Abs(dx)*1000 + math.Abs(dy)
395395 case DirectionRight:
396396 if dx <= focusEpsilon {
397- log.Printf("[workspace] closestLeafFromSubtree: SKIPPED (dx=%.0f not right)", dx)
397+ logging.Warn(fmt.Sprintf("[workspace] closestLeafFromSubtree: SKIPPED (dx=%.0f not right)", dx))
398398 continue
399399 }
400400 score = math.Abs(dx)*1000 + math.Abs(dy)
401401 case DirectionUp:
402402 if dy >= -focusEpsilon {
403- log.Printf("[workspace] closestLeafFromSubtree: SKIPPED (dy=%.0f not above)", dy)
403+ logging.Warn(fmt.Sprintf("[workspace] closestLeafFromSubtree: SKIPPED (dy=%.0f not above)", dy))
404404 continue
405405 }
406406 score = math.Abs(dy)*1000 + math.Abs(dx)
407407 case DirectionDown:
408408 if dy <= focusEpsilon {
409- log.Printf("[workspace] closestLeafFromSubtree: SKIPPED (dy=%.0f not below)", dy)
409+ logging.Warn(fmt.Sprintf("[workspace] closestLeafFromSubtree: SKIPPED (dy=%.0f not below)", dy))
410410 continue
411411 }
412412 score = math.Abs(dy)*1000 + math.Abs(dx)
413413 default:
414414 continue
415415 }
416- log.Printf("[workspace] closestLeafFromSubtree: ACCEPTED with score=%.0f", score)
416+ logging.Debug(fmt.Sprintf("[workspace] closestLeafFromSubtree: ACCEPTED with score=%.0f", score))
417417 if score < bestScore {
418418 bestScore = score
419419 best = leaf
420420 }
421421 }
422422 if best == nil {
423- log.Printf("[workspace] closestLeafFromSubtree: no match found, trying boundaryFallback")
423+ logging.Warn(fmt.Sprintf("[workspace] closestLeafFromSubtree: no match found, trying boundaryFallback"))
424424 return wm.boundaryFallback(node, direction)
425425 }
426- log.Printf("[workspace] closestLeafFromSubtree: returning best=%p", best.container)
426+ logging.Info(fmt.Sprintf("[workspace] closestLeafFromSubtree: returning best=%p", best.container))
427427 return best
428428 }
429429
@@ -437,7 +437,7 @@ func (wm *WorkspaceManager) boundaryFallbackWithDepth(node *paneNode, direction
437437 // Prevent infinite recursion - max tree depth should be reasonable
438438 const maxDepth = 50
439439 if depth > maxDepth {
440- log.Printf("[workspace] boundaryFallback: max depth exceeded, possible tree corruption")
440+ logging.Error(fmt.Sprintf("[workspace] boundaryFallback: max depth exceeded, possible tree corruption"))
441441 return nil
442442 }
443443
M internal/app/browser/workspace_operations.go +39 -40
@@ -3,8 +3,7 @@ package browser
33
44 import (
55 "fmt"
6- "log"
7-
6+ "github.com/bnema/dumber/internal/logging"
87 "github.com/bnema/dumber/pkg/webkit"
98 )
109
@@ -25,7 +24,7 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
2524 return nil, fmt.Errorf("invalid split direction '%s', expected one of: left, right, up, down", direction)
2625 }
2726
28- log.Printf("[workspace] Starting split operation: target=%p direction=%s", target, direction)
27+ logging.Debug(fmt.Sprintf("[workspace] Starting split operation: target=%p direction=%s", target, direction))
2928
3029 // Step 1: Validate geometry constraints (only in DebugBasic+)
3130 if wm.debugLevel >= DebugBasic {
@@ -34,12 +33,12 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
3433 if wm.debugLevel == DebugFull {
3534 return nil, fmt.Errorf("split validation failed: %s", validation.Reason)
3635 }
37- log.Printf("[workspace] WARNING: Split validation failed but allowing operation: %s", validation.Reason)
36+ logging.Error(fmt.Sprintf("[workspace] WARNING: Split validation failed but allowing operation: %s", validation.Reason))
3837 }
3938
4039 // Log if re-validation will be needed due to pending widget allocation
4140 if validation.RequiresRevalidation {
42- log.Printf("[workspace] Split validation passed with pending allocation - operation will proceed")
41+ logging.Info(fmt.Sprintf("[workspace] Split validation passed with pending allocation - operation will proceed"))
4342 }
4443
4544 // Step 3: Validate tree invariants before operation
@@ -47,13 +46,13 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
4746 if wm.debugLevel == DebugFull {
4847 return nil, fmt.Errorf("tree validation failed before split: %w", err)
4948 }
50- log.Printf("[workspace] WARNING: Tree validation failed before split but allowing operation: %v", err)
49+ logging.Error(fmt.Sprintf("[workspace] WARNING: Tree validation failed before split but allowing operation: %v", err))
5150 }
5251 }
5352
5453 // Step 2: Execute directly if we're already on the GTK main thread
5554 if webkit.IsMainThread() {
56- log.Printf("[workspace] Already on main thread, executing split directly")
55+ logging.Info(fmt.Sprintf("[workspace] Already on main thread, executing split directly"))
5756 newNode, err := wm.splitNode(target, direction, nil)
5857 if err != nil {
5958 return nil, err
@@ -61,18 +60,18 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
6160
6261 if wm.debugLevel >= DebugBasic {
6362 if err := wm.treeValidator.ValidateTree(wm.root, "after_split"); err != nil {
64- log.Printf("[workspace] Tree validation failed after split: %v", err)
63+ logging.Error(fmt.Sprintf("[workspace] Tree validation failed after split: %v", err))
6564 }
6665 }
6766
6867 // Tree rebalancing is only needed after close promotions. Splits have correct allocation from GTK.
6968
70- log.Printf("[workspace] Split operation completed successfully (direct execution): newNode=%p", newNode)
69+ logging.Info(fmt.Sprintf("[workspace] Split operation completed successfully (direct execution): newNode=%p", newNode))
7170 return newNode, nil
7271 }
7372
7473 // Step 3: Not on main thread, marshal via IdleAdd
75- log.Printf("[workspace] Not on main thread, marshalling split via IdleAdd")
74+ logging.Info(fmt.Sprintf("[workspace] Not on main thread, marshalling split via IdleAdd"))
7675 var newNode *paneNode
7776 var splitErr error
7877 done := make(chan struct{})
@@ -82,7 +81,7 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
8281 if splitErr == nil {
8382 if wm.debugLevel >= DebugBasic {
8483 if verr := wm.treeValidator.ValidateTree(wm.root, "after_split"); verr != nil {
85- log.Printf("[workspace] Tree validation failed after split: %v", verr)
84+ logging.Error(fmt.Sprintf("[workspace] Tree validation failed after split: %v", verr))
8685 }
8786 }
8887 }
@@ -96,7 +95,7 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
9695 return nil, splitErr
9796 }
9897
99- log.Printf("[workspace] Split operation completed successfully: newNode=%p", newNode)
98+ logging.Info(fmt.Sprintf("[workspace] Split operation completed successfully: newNode=%p", newNode))
10099 return newNode, nil
101100 }
102101
@@ -106,7 +105,7 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
106105 return fmt.Errorf("workspace manager is nil")
107106 }
108107
109- log.Printf("[workspace] Starting close operation: node=%p", node)
108+ logging.Info(fmt.Sprintf("[workspace] Starting close operation: node=%p", node))
110109 wm.paneCloseLogf("start bulletproof close node=%p", node)
111110 wm.dumpTreeState("before_close")
112111
@@ -116,19 +115,19 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
116115 if wm.debugLevel == DebugFull {
117116 return fmt.Errorf("tree validation failed before close: %w", err)
118117 }
119- log.Printf("[workspace] WARNING: Tree validation failed before close but allowing operation: %v", err)
118+ logging.Error(fmt.Sprintf("[workspace] WARNING: Tree validation failed before close but allowing operation: %v", err))
120119 }
121120 }
122121
123122 // Step 3: Check if this is a stacked pane and use enhanced lifecycle management
124123 if node.parent != nil && node.parent.isStacked {
125- log.Printf("[workspace] Using enhanced stack lifecycle management for close")
124+ logging.Debug(fmt.Sprintf("[workspace] Using enhanced stack lifecycle management for close"))
126125 return wm.stackLifecycleManager.CloseStackedPaneWithLifecycle(node)
127126 }
128127
129128 // Step 4: Execute directly when already on the GTK main thread
130129 if webkit.IsMainThread() {
131- log.Printf("[workspace] Already on main thread, executing close directly")
130+ logging.Info(fmt.Sprintf("[workspace] Already on main thread, executing close directly"))
132131
133132 wm.paneCloseLogf("invoking closePane node=%p", node)
134133 promoted, err := wm.closePane(node)
@@ -140,7 +139,7 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
140139
141140 if wm.debugLevel >= DebugBasic {
142141 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {
143- log.Printf("[workspace] Tree validation failed after close: %v", err)
142+ logging.Error(fmt.Sprintf("[workspace] Tree validation failed after close: %v", err))
144143 }
145144 }
146145 wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root)
@@ -148,16 +147,16 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
148147
149148 if wm.treeRebalancer != nil {
150149 if err := wm.treeRebalancer.RebalanceAfterClose(node, promoted); err != nil {
151- log.Printf("[workspace] Tree rebalancing failed after close: %v", err)
150+ logging.Error(fmt.Sprintf("[workspace] Tree rebalancing failed after close: %v", err))
152151 }
153152 }
154153
155- log.Printf("[workspace] Close operation completed successfully (direct execution)")
154+ logging.Info(fmt.Sprintf("[workspace] Close operation completed successfully (direct execution)"))
156155 return nil
157156 }
158157
159158 // Step 5: Not on main thread, marshal via IdleAdd
160- log.Printf("[workspace] Not on main thread, marshalling close via IdleAdd")
159+ logging.Info(fmt.Sprintf("[workspace] Not on main thread, marshalling close via IdleAdd"))
161160 var promoted *paneNode
162161 var closeErr error
163162 done := make(chan struct{})
@@ -171,7 +170,7 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
171170 } else {
172171 if wm.debugLevel >= DebugBasic {
173172 if verr := wm.treeValidator.ValidateTree(wm.root, "after_close"); verr != nil {
174- log.Printf("[workspace] Tree validation failed after close: %v", verr)
173+ logging.Error(fmt.Sprintf("[workspace] Tree validation failed after close: %v", verr))
175174 }
176175 }
177176 wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root)
@@ -179,7 +178,7 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
179178
180179 if wm.treeRebalancer != nil {
181180 if rerr := wm.treeRebalancer.RebalanceAfterClose(node, promoted); rerr != nil {
182- log.Printf("[workspace] Tree rebalancing failed after close: %v", rerr)
181+ logging.Error(fmt.Sprintf("[workspace] Tree rebalancing failed after close: %v", rerr))
183182 }
184183 }
185184 }
@@ -193,7 +192,7 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
193192 return closeErr
194193 }
195194
196- log.Printf("[workspace] Close operation completed successfully")
195+ logging.Info(fmt.Sprintf("[workspace] Close operation completed successfully"))
197196 return nil
198197 }
199198
@@ -203,7 +202,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
203202 return nil, fmt.Errorf("workspace manager is nil")
204203 }
205204
206- log.Printf("[workspace] Starting stack operation: target=%p", target)
205+ logging.Info(fmt.Sprintf("[workspace] Starting stack operation: target=%p", target))
207206
208207 // Step 1: Validate stack operation constraints (only in DebugBasic+)
209208 if wm.debugLevel >= DebugBasic {
@@ -212,12 +211,12 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
212211 if wm.debugLevel == DebugFull {
213212 return nil, fmt.Errorf("stack validation failed: %s", validation.Reason)
214213 }
215- log.Printf("[workspace] WARNING: Stack validation failed but allowing operation: %s", validation.Reason)
214+ logging.Error(fmt.Sprintf("[workspace] WARNING: Stack validation failed but allowing operation: %s", validation.Reason))
216215 }
217216
218217 // Log if re-validation will be needed due to pending widget allocation
219218 if validation.RequiresRevalidation {
220- log.Printf("[workspace] Stack validation passed with pending allocation - operation will proceed")
219+ logging.Info(fmt.Sprintf("[workspace] Stack validation passed with pending allocation - operation will proceed"))
221220 }
222221 }
223222
@@ -227,13 +226,13 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
227226 if wm.debugLevel == DebugFull {
228227 return nil, fmt.Errorf("tree validation failed before stack: %w", err)
229228 }
230- log.Printf("[workspace] WARNING: Tree validation failed before stack but allowing operation: %v", err)
229+ logging.Error(fmt.Sprintf("[workspace] WARNING: Tree validation failed before stack but allowing operation: %v", err))
231230 }
232231 }
233232
234233 // Step 3: Execute directly if already on the GTK main thread
235234 if webkit.IsMainThread() {
236- log.Printf("[workspace] Already on main thread, executing stack directly")
235+ logging.Info(fmt.Sprintf("[workspace] Already on main thread, executing stack directly"))
237236 newNode, err := wm.stackedPaneManager.StackPane(target)
238237 if err != nil {
239238 return nil, err
@@ -241,16 +240,16 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
241240
242241 if wm.debugLevel >= DebugBasic {
243242 if err := wm.treeValidator.ValidateTree(wm.root, "after_stack"); err != nil {
244- log.Printf("[workspace] Tree validation failed after stack: %v", err)
243+ logging.Error(fmt.Sprintf("[workspace] Tree validation failed after stack: %v", err))
245244 }
246245 }
247246
248- log.Printf("[workspace] Stack operation completed successfully (direct execution): newNode=%p", newNode)
247+ logging.Info(fmt.Sprintf("[workspace] Stack operation completed successfully (direct execution): newNode=%p", newNode))
249248 return newNode, nil
250249 }
251250
252251 // Step 4: Not on main thread, marshal via IdleAdd
253- log.Printf("[workspace] Not on main thread, marshalling stack via IdleAdd")
252+ logging.Info(fmt.Sprintf("[workspace] Not on main thread, marshalling stack via IdleAdd"))
254253 var newNode *paneNode
255254 var stackErr error
256255 done := make(chan struct{})
@@ -260,7 +259,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
260259 if stackErr == nil {
261260 if wm.debugLevel >= DebugBasic {
262261 if verr := wm.treeValidator.ValidateTree(wm.root, "after_stack"); verr != nil {
263- log.Printf("[workspace] Tree validation failed after stack: %v", verr)
262+ logging.Error(fmt.Sprintf("[workspace] Tree validation failed after stack: %v", verr))
264263 }
265264 }
266265 }
@@ -274,7 +273,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
274273 return nil, stackErr
275274 }
276275
277- log.Printf("[workspace] Stack operation completed successfully: newNode=%p", newNode)
276+ logging.Info(fmt.Sprintf("[workspace] Stack operation completed successfully: newNode=%p", newNode))
278277 return newNode, nil
279278 }
280279
@@ -286,7 +285,7 @@ func (wm *WorkspaceManager) EnableEnhancedMode() {
286285 if wm.treeRebalancer != nil {
287286 wm.treeRebalancer.Enable()
288287 }
289- log.Printf("[workspace] Enhanced validation mode enabled")
288+ logging.Info(fmt.Sprintf("[workspace] Enhanced validation mode enabled"))
290289 }
291290
292291 // DisableEnhancedMode disables enhanced features for performance
@@ -297,7 +296,7 @@ func (wm *WorkspaceManager) DisableEnhancedMode() {
297296 if wm.treeRebalancer != nil {
298297 wm.treeRebalancer.Disable()
299298 }
300- log.Printf("[workspace] Enhanced validation mode disabled")
299+ logging.Info(fmt.Sprintf("[workspace] Enhanced validation mode disabled"))
301300 }
302301
303302 // SetEnhancedDebugMode enables/disables debug mode for all enhanced validation components
@@ -308,7 +307,7 @@ func (wm *WorkspaceManager) SetEnhancedDebugMode(debug bool) {
308307 if wm.geometryValidator != nil {
309308 wm.geometryValidator.SetDebugMode(debug)
310309 }
311- log.Printf("[workspace] Enhanced debug mode set to: %v", debug)
310+ logging.Debug(fmt.Sprintf("[workspace] Enhanced debug mode set to: %v", debug))
312311 }
313312
314313 // GetEnhancedStats returns comprehensive statistics about all enhanced validation components
@@ -330,7 +329,7 @@ func (wm *WorkspaceManager) GetEnhancedStats() map[string]interface{} {
330329
331330 // ValidateWorkspaceIntegrity performs a comprehensive validation of the workspace
332331 func (wm *WorkspaceManager) ValidateWorkspaceIntegrity() error {
333- log.Printf("[workspace] Performing comprehensive workspace integrity check")
332+ logging.Info(fmt.Sprintf("[workspace] Performing comprehensive workspace integrity check"))
334333
335334 // Tree structure validation
336335 if wm.treeValidator != nil {
@@ -341,14 +340,14 @@ func (wm *WorkspaceManager) ValidateWorkspaceIntegrity() error {
341340
342341 // Geometry validation simplified - ValidateWorkspaceLayout removed
343342
344- log.Printf("[workspace] Workspace integrity check completed successfully")
343+ logging.Info(fmt.Sprintf("[workspace] Workspace integrity check completed successfully"))
345344 return nil
346345 }
347346
348347 // ShutdownEnhancedComponents gracefully shuts down all enhanced validation components
349348 func (wm *WorkspaceManager) ShutdownEnhancedComponents() {
350- log.Printf("[workspace] Shutting down enhanced validation components")
349+ logging.Info(fmt.Sprintf("[workspace] Shutting down enhanced validation components"))
351350
352351 // Other components don't require explicit shutdown currently
353- log.Printf("[workspace] Enhanced validation components shutdown complete")
352+ logging.Info(fmt.Sprintf("[workspace] Enhanced validation components shutdown complete"))
354353 }
M internal/app/browser/workspace_pane_mode.go +21 -19
@@ -2,7 +2,9 @@
22 package browser
33
44 import (
5- "log"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/logging"
68 )
79
810 // EnterPaneMode activates pane mode on the currently focused pane
@@ -16,17 +18,17 @@ func (wm *WorkspaceManager) EnterPaneMode() {
1618 // Get the currently focused pane
1719 activeNode := wm.GetActiveNode()
1820 if activeNode == nil {
19- log.Printf("[pane-mode] Cannot enter pane mode: no active pane")
21+ logging.Error(fmt.Sprintf("[pane-mode] Cannot enter pane mode: no active pane"))
2022 return
2123 }
2224
2325 // Already in pane mode on this pane?
2426 if wm.paneModeActive && wm.paneModeActivePane == activeNode {
25- log.Printf("[pane-mode] Pane mode already active on this pane")
27+ logging.Info(fmt.Sprintf("[pane-mode] Pane mode already active on this pane"))
2628 return
2729 }
2830
29- log.Printf("[pane-mode] Entering pane mode on pane %p (workspace=%p)", activeNode, wm)
31+ logging.Debug(fmt.Sprintf("[pane-mode] Entering pane mode on pane %p (workspace=%p)", activeNode, wm))
3032
3133 wm.paneModeActive = true
3234 wm.paneModeActivePane = activeNode
@@ -51,7 +53,7 @@ func (wm *WorkspaceManager) ExitPaneMode(reason string) {
5153 return
5254 }
5355
54- log.Printf("[pane-mode] Exiting pane mode: %s", reason)
56+ logging.Info(fmt.Sprintf("[pane-mode] Exiting pane mode: %s", reason))
5557
5658 wm.paneModeActive = false
5759 wm.paneModeActivePane = nil
@@ -74,7 +76,7 @@ func (wm *WorkspaceManager) HandlePaneAction(action string) {
7476
7577 if !wm.paneModeActive {
7678 wm.paneMutex.Unlock()
77- log.Printf("[pane-mode] Ignoring action '%s': pane mode not active", action)
79+ logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': pane mode not active", action))
7880 return
7981 }
8082
@@ -82,12 +84,12 @@ func (wm *WorkspaceManager) HandlePaneAction(action string) {
8284 wm.paneMutex.Unlock()
8385
8486 if activePane == nil {
85- log.Printf("[pane-mode] Ignoring action '%s': no active pane", action)
87+ logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': no active pane", action))
8688 wm.ExitPaneMode("no-active-pane")
8789 return
8890 }
8991
90- log.Printf("[pane-mode] Handling action '%s' on pane %p", action, activePane)
92+ logging.Debug(fmt.Sprintf("[pane-mode] Handling action '%s' on pane %p", action, activePane))
9193
9294 switch action {
9395 case "close":
@@ -97,7 +99,7 @@ func (wm *WorkspaceManager) HandlePaneAction(action string) {
9799 case "stack":
98100 wm.handlePaneStack(activePane)
99101 default:
100- log.Printf("[pane-mode] Unknown action: %s", action)
102+ logging.Warn(fmt.Sprintf("[pane-mode] Unknown action: %s", action))
101103 }
102104
103105 // Exit pane mode after action
@@ -110,14 +112,14 @@ func (wm *WorkspaceManager) handlePaneClose(node *paneNode) {
110112 return
111113 }
112114
113- log.Printf("[pane-mode] Closing pane %p", node)
115+ logging.Debug(fmt.Sprintf("[pane-mode] Closing pane %p", node))
114116
115117 // Notify JavaScript first so it can show toast
116118 wm.dispatchPaneModeEvent("action", "close")
117119
118120 // Close the pane
119121 if err := wm.ClosePane(node); err != nil {
120- log.Printf("[pane-mode] Failed to close pane: %v", err)
122+ logging.Error(fmt.Sprintf("[pane-mode] Failed to close pane: %v", err))
121123 }
122124 }
123125
@@ -139,11 +141,11 @@ func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
139141 case "split-down":
140142 direction = DirectionDown
141143 default:
142- log.Printf("[pane-mode] Invalid split action: %s", action)
144+ logging.Error(fmt.Sprintf("[pane-mode] Invalid split action: %s", action))
143145 return
144146 }
145147
146- log.Printf("[pane-mode] Splitting pane %p in direction %s", node, direction)
148+ logging.Debug(fmt.Sprintf("[pane-mode] Splitting pane %p in direction %s", node, direction))
147149
148150 // Notify JavaScript first so it can show toast
149151 wm.dispatchPaneModeEvent("action", action)
@@ -151,14 +153,14 @@ func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
151153 // Perform the split (pass nil so it creates a fresh pane, not popup mode)
152154 newNode, err := wm.splitNode(node, direction, nil)
153155 if err != nil {
154- log.Printf("[pane-mode] Failed to split pane: %v", err)
156+ logging.Error(fmt.Sprintf("[pane-mode] Failed to split pane: %v", err))
155157 return
156158 }
157159
158160 // Load initial URL in the new pane so scripts execute
159161 wm.clonePaneState(node, newNode)
160162
161- log.Printf("[pane-mode] Split successful: direction=%s", direction)
163+ logging.Debug(fmt.Sprintf("[pane-mode] Split successful: direction=%s", direction))
162164 }
163165
164166 // handlePaneStack creates a stacked pane
@@ -167,7 +169,7 @@ func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
167169 return
168170 }
169171
170- log.Printf("[pane-mode] Stacking pane %p", node)
172+ logging.Debug(fmt.Sprintf("[pane-mode] Stacking pane %p", node))
171173
172174 // Notify JavaScript first so it can show toast
173175 wm.dispatchPaneModeEvent("action", "stack")
@@ -175,14 +177,14 @@ func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
175177 // Perform the stack operation
176178 newNode, err := wm.stackedPaneManager.StackPane(node)
177179 if err != nil {
178- log.Printf("[pane-mode] Failed to stack pane: %v", err)
180+ logging.Error(fmt.Sprintf("[pane-mode] Failed to stack pane: %v", err))
179181 return
180182 }
181183
182184 // Load initial URL in the new pane so scripts execute
183185 wm.clonePaneState(node, newNode)
184186
185- log.Printf("[pane-mode] Stack successful")
187+ logging.Info(fmt.Sprintf("[pane-mode] Stack successful"))
186188 }
187189
188190 // dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
@@ -198,7 +200,7 @@ func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
198200 }
199201
200202 if err := view.DispatchCustomEvent("dumber:pane-mode", data); err != nil {
201- log.Printf("[pane-mode] Failed to dispatch event: %v", err)
203+ logging.Error(fmt.Sprintf("[pane-mode] Failed to dispatch event: %v", err))
202204 }
203205 }
204206
M internal/app/browser/workspace_pane_ops.go +57 -57
@@ -4,11 +4,11 @@ package browser
44 import (
55 "errors"
66 "fmt"
7- "log"
87
98 "github.com/diamondburned/gotk4/pkg/gdk/v4"
109 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1110
11+ "github.com/bnema/dumber/internal/logging"
1212 "github.com/bnema/dumber/pkg/webkit"
1313 )
1414
@@ -67,7 +67,7 @@ func (wm *WorkspaceManager) collectLeavesFromWithDirection(node *paneNode, direc
6767 return
6868 }
6969 if visited[n] {
70- log.Printf("[workspace] collectLeavesFromWithDirection: cycle detected in tree")
70+ logging.Error(fmt.Sprintf("[workspace] collectLeavesFromWithDirection: cycle detected in tree"))
7171 return
7272 }
7373 visited[n] = true
@@ -121,7 +121,7 @@ func (wm *WorkspaceManager) collectLeavesFrom(node *paneNode) []*paneNode {
121121 return
122122 }
123123 if visited[n] {
124- log.Printf("[workspace] collectLeavesFrom: cycle detected in tree")
124+ logging.Error(fmt.Sprintf("[workspace] collectLeavesFrom: cycle detected in tree"))
125125 return
126126 }
127127 visited[n] = true
@@ -180,22 +180,22 @@ func (wm *WorkspaceManager) createPane(view *webkit.WebView) (*BrowserPane, erro
180180 // clonePaneState sets up a new pane with cloned state from another pane
181181 func (wm *WorkspaceManager) clonePaneState(_ *paneNode, target *paneNode) {
182182 if wm == nil || target == nil {
183- log.Printf("[workspace] clonePaneState: wm or target is nil")
183+ logging.Debug(fmt.Sprintf("[workspace] clonePaneState: wm or target is nil"))
184184 return
185185 }
186186 if target.pane == nil || target.pane.webView == nil {
187- log.Printf("[workspace] clonePaneState: target pane or webView is nil")
187+ logging.Debug(fmt.Sprintf("[workspace] clonePaneState: target pane or webView is nil"))
188188 return
189189 }
190190
191191 // Load about:blank and auto-open omnibox
192192 const blankURL = "about:blank"
193193
194- log.Printf("[workspace] clonePaneState: loading %s in pane %p", blankURL, target.pane)
194+ logging.Debug(fmt.Sprintf("[workspace] clonePaneState: loading %s in pane %p", blankURL, target.pane))
195195 if err := target.pane.webView.LoadURL(blankURL); err != nil {
196- log.Printf("[workspace] failed to load blank page in new pane: %v", err)
196+ logging.Error(fmt.Sprintf("[workspace] failed to load blank page in new pane: %v", err))
197197 } else {
198- log.Printf("[workspace] successfully initiated load of %s", blankURL)
198+ logging.Debug(fmt.Sprintf("[workspace] successfully initiated load of %s", blankURL))
199199 }
200200 }
201201
@@ -218,13 +218,13 @@ func (wm *WorkspaceManager) safelyDetachControllersBeforeReparent(node *paneNode
218218 if target.hoverToken != nil {
219219 target.pendingHoverReattach = true
220220 target.hoverToken = nil // Clear reference, don't manually remove
221- log.Printf("[workspace] Marked hover controller for reattach on pane %p (GTK will auto-remove)", target)
221+ logging.Debug(fmt.Sprintf("[workspace] Marked hover controller for reattach on pane %p (GTK will auto-remove)", target))
222222 }
223223
224224 if target.focusControllerToken != 0 {
225225 target.pendingFocusReattach = true
226226 target.focusControllerToken = 0 // Clear reference, don't manually remove
227- log.Printf("[workspace] Marked focus controller for reattach on pane %p (GTK will auto-remove)", target)
227+ logging.Debug(fmt.Sprintf("[workspace] Marked focus controller for reattach on pane %p (GTK will auto-remove)", target))
228228 }
229229 }
230230
@@ -246,7 +246,7 @@ func (wm *WorkspaceManager) closeCurrentPane() {
246246 return
247247 }
248248 if err := wm.ClosePane(wm.GetActiveNode()); err != nil {
249- log.Printf("[workspace] close current pane failed: %v", err)
249+ logging.Error(fmt.Sprintf("[workspace] close current pane failed: %v", err))
250250 }
251251 }
252252
@@ -343,7 +343,7 @@ func (wm *WorkspaceManager) removeFromMaps(webView *webkit.WebView) {
343343 delete(wm.viewToNode, webView)
344344 delete(wm.lastSplitMsg, webView)
345345 delete(wm.lastExitMsg, webView)
346- log.Printf("[workspace] removed webview %p from tracking maps", webView)
346+ logging.Debug(fmt.Sprintf("[workspace] removed webview %p from tracking maps", webView))
347347 }
348348
349349 // removeFromAppPanes removes a BrowserPane from the app.panes slice
@@ -355,7 +355,7 @@ func (wm *WorkspaceManager) removeFromAppPanes(pane *BrowserPane) {
355355 for i, p := range wm.app.panes {
356356 if p == pane {
357357 wm.app.panes = append(wm.app.panes[:i], wm.app.panes[i+1:]...)
358- log.Printf("[workspace] removed pane from app.panes slice, remaining: %d", len(wm.app.panes))
358+ logging.Debug(fmt.Sprintf("[workspace] removed pane from app.panes slice, remaining: %d", len(wm.app.panes)))
359359 return
360360 }
361361 }
@@ -383,7 +383,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
383383 if target.parent != nil && target.parent.isStacked {
384384 // Target is in a stack - we'll create the split around the entire stack
385385 stackContainer = target.parent
386- log.Printf("[workspace] target is in stack, will split around the stack: originalTarget=%p stackContainer=%p", originalTarget, stackContainer)
386+ logging.Debug(fmt.Sprintf("[workspace] target is in stack, will split around the stack: originalTarget=%p stackContainer=%p", originalTarget, stackContainer))
387387 }
388388
389389 // Determine what we're actually splitting
@@ -394,12 +394,12 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
394394 // Case: splitting from inside a stack - we split around the entire stack
395395 splitTarget = stackContainer
396396 splitTargetContainer = stackContainer.container
397- log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget)
397+ logging.Debug(fmt.Sprintf("[workspace] splitting around stack: stackContainer=%p", splitTarget))
398398 } else {
399399 // Case: normal split from a simple pane
400400 splitTarget = target
401401 splitTargetContainer = target.container
402- log.Printf("[workspace] normal split from pane: target=%p", splitTarget)
402+ logging.Info(fmt.Sprintf("[workspace] normal split from pane: target=%p", splitTarget))
403403 }
404404
405405 // Use provided pane or create new one
@@ -408,7 +408,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
408408 if existingPane != nil {
409409 // Reuse existing pane (for popup insertion)
410410 newPane = existingPane
411- log.Printf("[workspace] Using existing pane for split: %p", newPane)
411+ logging.Debug(fmt.Sprintf("[workspace] Using existing pane for split: %p", newPane))
412412 } else {
413413 // Create new WebView and pane (for normal splits)
414414 newView, err := wm.createWebView()
@@ -420,7 +420,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
420420 if err != nil {
421421 return nil, err
422422 }
423- log.Printf("[workspace] Created new pane for split: %p", newPane)
423+ logging.Info(fmt.Sprintf("[workspace] Created new pane for split: %p", newPane))
424424 }
425425
426426 if handler := newPane.MessageHandler(); handler != nil {
@@ -451,13 +451,13 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
451451 if splitTargetContainer != nil {
452452 webkit.WidgetSetHExpand(splitTargetContainer, true)
453453 webkit.WidgetSetVExpand(splitTargetContainer, true)
454- log.Printf("[workspace] configured stack wrapper for split: %p", splitTargetContainer)
454+ logging.Debug(fmt.Sprintf("[workspace] configured stack wrapper for split: %p", splitTargetContainer))
455455 }
456456 }
457457
458458 orientation, existingFirst := mapDirection(direction)
459- log.Printf("[workspace] splitting direction=%s orientation=%v existingFirst=%v splitTarget.parent=%p splitTarget.isStacked=%v isPopup=%v",
460- direction, orientation, existingFirst, splitTarget.parent, splitTarget.isStacked, existingPane != nil)
459+ logging.Debug(fmt.Sprintf("[workspace] splitting direction=%s orientation=%v existingFirst=%v splitTarget.parent=%p splitTarget.isStacked=%v isPopup=%v",
460+ direction, orientation, existingFirst, splitTarget.parent, splitTarget.isStacked, existingPane != nil))
461461
462462 paned := gtk.NewPaned(orientation)
463463 if paned == nil {
@@ -500,7 +500,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
500500 // Remove active border class if present (prevents GTK bloom filter corruption during unparent)
501501 if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) {
502502 webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass)
503- log.Printf("[workspace] removed active class from split target (will transfer to new pane)")
503+ logging.Debug(fmt.Sprintf("[workspace] removed active class from split target (will transfer to new pane)"))
504504 }
505505 }
506506
@@ -510,7 +510,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
510510 // For non-root splits, unparent from parent paned
511511 if parent != nil && parent.container != nil {
512512 // Split target has a parent paned - remove it (automatically unparents in GTK4)
513- log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container)
513+ logging.Debug(fmt.Sprintf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container))
514514 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {
515515 if parent.left == splitTarget {
516516 parentPaned.SetStartChild(nil)
@@ -553,33 +553,33 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
553553 if parent == nil {
554554 // ROOT SPLIT: Manipulate TabManager.ContentArea
555555 if wm.app == nil || wm.app.tabManager == nil || wm.app.tabManager.ContentArea == nil {
556- log.Printf("[workspace] ERROR: Cannot perform root split - tab manager not initialized")
556+ logging.Error(fmt.Sprintf("[workspace] ERROR: Cannot perform root split - tab manager not initialized"))
557557 return nil, fmt.Errorf("tab manager not initialized")
558558 }
559559
560560 contentBox, ok := wm.app.tabManager.ContentArea.(*gtk.Box)
561561 if !ok || contentBox == nil {
562- log.Printf("[workspace] ERROR: ContentArea is not a Box")
562+ logging.Error(fmt.Sprintf("[workspace] ERROR: ContentArea is not a Box"))
563563 return nil, fmt.Errorf("ContentArea is not a Box")
564564 }
565565
566566 // Remove the old workspace root from ContentArea
567567 contentBox.Remove(splitTargetContainer)
568- log.Printf("[workspace] ROOT SPLIT: removed old root %p from ContentArea", splitTargetContainer)
568+ logging.Debug(fmt.Sprintf("[workspace] ROOT SPLIT: removed old root %p from ContentArea", splitTargetContainer))
569569
570570 // Attach the new paned to ContentArea FIRST
571571 contentBox.Append(paned)
572- log.Printf("[workspace] ROOT SPLIT: attached paned %p to ContentArea", paned)
572+ logging.Debug(fmt.Sprintf("[workspace] ROOT SPLIT: attached paned %p to ContentArea", paned))
573573
574574 // Now add children to the paned - GTK will handle reparenting automatically
575575 if existingFirst {
576576 paned.SetStartChild(splitTargetContainer)
577577 paned.SetEndChild(newContainer)
578- log.Printf("[workspace] ROOT SPLIT: added splitTarget=%p (start), new=%p (end)", splitTargetContainer, newContainer)
578+ logging.Debug(fmt.Sprintf("[workspace] ROOT SPLIT: added splitTarget=%p (start), new=%p (end)", splitTargetContainer, newContainer))
579579 } else {
580580 paned.SetStartChild(newContainer)
581581 paned.SetEndChild(splitTargetContainer)
582- log.Printf("[workspace] ROOT SPLIT: added new=%p (start), splitTarget=%p (end)", newContainer, splitTargetContainer)
582+ logging.Debug(fmt.Sprintf("[workspace] ROOT SPLIT: added new=%p (start), splitTarget=%p (end)", newContainer, splitTargetContainer))
583583 }
584584 } else {
585585 // NON-ROOT SPLIT: Standard approach
@@ -589,13 +589,13 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
589589 if splitTargetContainer != nil {
590590 paned.SetStartChild(splitTargetContainer)
591591 paned.SetEndChild(newContainer)
592- log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer)
592+ logging.Debug(fmt.Sprintf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer))
593593 }
594594 } else {
595595 if splitTargetContainer != nil {
596596 paned.SetStartChild(newContainer)
597597 paned.SetEndChild(splitTargetContainer)
598- log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer)
598+ logging.Debug(fmt.Sprintf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer))
599599 }
600600 }
601601
@@ -608,7 +608,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
608608 }
609609 webkit.WidgetQueueAllocate(parent.container)
610610 paned.QueueAllocate()
611- log.Printf("[workspace] paned inserted into parent=%p", parent.container)
611+ logging.Info(fmt.Sprintf("[workspace] paned inserted into parent=%p", parent.container))
612612 }
613613 }
614614
@@ -637,7 +637,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
637637 wm.app.panes = append(wm.app.panes, newPane)
638638 } else {
639639 // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow
640- log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)")
640+ logging.Info(fmt.Sprintf("[workspace] Skipping app.panes append for existing popup pane (already added)"))
641641 }
642642
643643 if newPane.zoomController != nil {
@@ -755,26 +755,26 @@ func (wm *WorkspaceManager) swapContainers(grand *paneNode, parent *paneNode, si
755755 // so it has no GTK parent now.
756756
757757 if wm.app == nil || wm.app.tabManager == nil || wm.app.tabManager.ContentArea == nil {
758- log.Printf("[workspace] ERROR: Cannot promote sibling to root - tab manager not initialized")
758+ logging.Error(fmt.Sprintf("[workspace] ERROR: Cannot promote sibling to root - tab manager not initialized"))
759759 return
760760 }
761761
762762 contentBox, ok := wm.app.tabManager.ContentArea.(*gtk.Box)
763763 if !ok || contentBox == nil {
764- log.Printf("[workspace] ERROR: ContentArea is not a Box")
764+ logging.Error(fmt.Sprintf("[workspace] ERROR: ContentArea is not a Box"))
765765 return
766766 }
767767
768768 // CRITICAL: Remove the old paned from ContentArea first
769769 if parent != nil && parent.container != nil {
770770 contentBox.Remove(parent.container)
771- log.Printf("[workspace] Removed old paned %p from ContentArea", parent.container)
771+ logging.Debug(fmt.Sprintf("[workspace] Removed old paned %p from ContentArea", parent.container))
772772 }
773773
774774 // Now add sibling to ContentArea as the new root
775775 contentBox.Append(sibling.container)
776776 webkit.WidgetSetVisible(sibling.container, true)
777- log.Printf("[workspace] Promoted sibling %p to root in ContentArea", sibling.container)
777+ logging.Debug(fmt.Sprintf("[workspace] Promoted sibling %p to root in ContentArea", sibling.container))
778778 }
779779 return
780780 }
@@ -789,15 +789,15 @@ func (wm *WorkspaceManager) swapContainers(grand *paneNode, parent *paneNode, si
789789 // GTK4 PanedSetStartChild/EndChild auto-unparent from current parent
790790 // Focus was already cleared before unparenting closed pane
791791 if parent := webkit.WidgetGetParent(sibling.container); parent != nil && parent != grand.container {
792- log.Printf("[workspace] widget %p reparenting from %p to %p (GTK4 will auto-unparent)", sibling.container, parent, grand.container)
792+ logging.Debug(fmt.Sprintf("[workspace] widget %p reparenting from %p to %p (GTK4 will auto-unparent)", sibling.container, parent, grand.container))
793793 }
794794
795795 if grand.left == sibling {
796796 grandPaned.SetStartChild(sibling.container)
797- log.Printf("[workspace] swapContainers: set widget %p as start child of paned %p", sibling.container, grand.container)
797+ logging.Debug(fmt.Sprintf("[workspace] swapContainers: set widget %p as start child of paned %p", sibling.container, grand.container))
798798 } else {
799799 grandPaned.SetEndChild(sibling.container)
800- log.Printf("[workspace] swapContainers: set widget %p as end child of paned %p", sibling.container, grand.container)
800+ logging.Debug(fmt.Sprintf("[workspace] swapContainers: set widget %p as end child of paned %p", sibling.container, grand.container))
801801 }
802802 webkit.WidgetQueueAllocate(grand.container)
803803 }
@@ -821,7 +821,7 @@ func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
821821 return
822822 }
823823
824- log.Printf("[workspace] Cascading promotion: replacing paned %p with its only child %p", singleChildPaned, onlyChild)
824+ logging.Info(fmt.Sprintf("[workspace] Cascading promotion: replacing paned %p with its only child %p", singleChildPaned, onlyChild))
825825
826826 greatGrandparent := singleChildPaned.parent
827827
@@ -830,7 +830,7 @@ func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
830830 if greatGrandparent == nil {
831831 // Single child becomes new root
832832 wm.root = onlyChild
833- log.Printf("[workspace] Only child %p promoted to root", onlyChild)
833+ logging.Debug(fmt.Sprintf("[workspace] Only child %p promoted to root", onlyChild))
834834
835835 // Attach to window
836836 if onlyChild.container != nil {
@@ -847,7 +847,7 @@ func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
847847 greatGrandparent.right = onlyChild
848848 }
849849
850- log.Printf("[workspace] Only child %p attached to great-grandparent %p", onlyChild, greatGrandparent)
850+ logging.Debug(fmt.Sprintf("[workspace] Only child %p attached to great-grandparent %p", onlyChild, greatGrandparent))
851851
852852 // Reparent widget in GTK
853853 if greatGrandPaned, ok := greatGrandparent.container.(*gtk.Paned); ok && greatGrandPaned != nil && onlyChild.container != nil {
@@ -880,7 +880,7 @@ func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
880880
881881 // Check if great-grandparent now also has only one child (recursive case)
882882 if greatGrandparent != nil && ((greatGrandparent.left == nil) != (greatGrandparent.right == nil)) {
883- log.Printf("[workspace] Great-grandparent %p also has only one child, continuing cascade", greatGrandparent)
883+ logging.Info(fmt.Sprintf("[workspace] Great-grandparent %p also has only one child, continuing cascade", greatGrandparent))
884884 wm.cascadePromotion(greatGrandparent)
885885 }
886886 }
@@ -900,7 +900,7 @@ func (wm *WorkspaceManager) attachRoot(root *paneNode) {
900900 // Detach from previous container (paned, stack, etc.) before attaching to ContentArea
901901 // While GTK4 auto-unparents for paned operations, Box.Append requires manual unparent
902902 if parent := webkit.WidgetGetParent(root.container); parent != nil {
903- log.Printf("[workspace] unparenting widget %p from parent %p before root attach", root.container, parent)
903+ logging.Debug(fmt.Sprintf("[workspace] unparenting widget %p from parent %p before root attach", root.container, parent))
904904 webkit.WidgetUnparent(root.container)
905905 // Note: In gotk4, the Go object reference remains valid after unparent
906906 // Only the GTK parent relationship is cleared
@@ -913,13 +913,13 @@ func (wm *WorkspaceManager) attachRoot(root *paneNode) {
913913
914914 // Add to ContentArea (the old root will be orphaned and garbage collected)
915915 if wm.app == nil || wm.app.tabManager == nil || wm.app.tabManager.ContentArea == nil {
916- log.Printf("[workspace] ERROR: Cannot attach root - tab manager not initialized")
916+ logging.Error(fmt.Sprintf("[workspace] ERROR: Cannot attach root - tab manager not initialized"))
917917 return
918918 }
919919
920920 contentBox, ok := wm.app.tabManager.ContentArea.(*gtk.Box)
921921 if !ok || contentBox == nil {
922- log.Printf("[workspace] ERROR: ContentArea is not a Box")
922+ logging.Error(fmt.Sprintf("[workspace] ERROR: ContentArea is not a Box"))
923923 return
924924 }
925925
@@ -945,9 +945,9 @@ func (wm *WorkspaceManager) attachRoot(root *paneNode) {
945945
946946 // Verify attachment succeeded
947947 if finalParent := webkit.WidgetGetParent(root.container); finalParent == nil {
948- log.Printf("[workspace] WARNING: widget %p has no parent after SetChild", root.container)
948+ logging.Warn(fmt.Sprintf("[workspace] WARNING: widget %p has no parent after SetChild", root.container))
949949 } else {
950- log.Printf("[workspace] attachRoot successful: widget %p now child of %p", root.container, finalParent)
950+ logging.Debug(fmt.Sprintf("[workspace] attachRoot successful: widget %p now child of %p", root.container, finalParent))
951951 }
952952 }
953953
@@ -974,7 +974,7 @@ func (wm *WorkspaceManager) cleanupPane(node *paneNode, generation uint) {
974974 pane.Cleanup()
975975 pane.CleanupFromWorkspace(wm)
976976 if pane.webView != nil {
977- log.Printf("[workspace] releasing WebView reference: %p", pane.webView)
977+ logging.Debug(fmt.Sprintf("[workspace] releasing WebView reference: %p", pane.webView))
978978 pane.webView = nil
979979 }
980980 node.pane = nil
@@ -1002,7 +1002,7 @@ func (wm *WorkspaceManager) decommissionParent(parent *paneNode, generation uint
10021002 if !parent.isLeaf && parent.container != nil {
10031003 // In gotk4, we don't need to manually destroy - GTK handles this when unparented
10041004 // Just mark it for cleanup
1005- log.Printf("[workspace] marked paned widget for cleanup: %p", parent.container)
1005+ logging.Debug(fmt.Sprintf("[workspace] marked paned widget for cleanup: %p", parent.container))
10061006 }
10071007
10081008 wm.cleanupPane(parent, generation)
@@ -1031,7 +1031,7 @@ func (wm *WorkspaceManager) promoteNewRoot(ctx closeContext, oldRoot *paneNode)
10311031
10321032 // cleanupAndExit handles the final pane cleanup and application exit
10331033 func (wm *WorkspaceManager) cleanupAndExit(node *paneNode) (*paneNode, error) {
1034- log.Printf("[workspace] closing final pane; exiting browser")
1034+ logging.Info(fmt.Sprintf("[workspace] closing final pane; exiting browser"))
10351035 wm.cleanupPane(node, wm.nextCleanupGeneration())
10361036 wm.detachHover(node)
10371037
@@ -1098,7 +1098,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
10981098 } else if parent.right == node {
10991099 parentPaned.SetEndChild(nil)
11001100 }
1101- log.Printf("[workspace] unparented closed pane container: %p", node.container)
1101+ logging.Debug(fmt.Sprintf("[workspace] unparented closed pane container: %p", node.container))
11021102 }
11031103 }
11041104 }
@@ -1112,7 +1112,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
11121112 } else if parent.right == sibling {
11131113 parentPaned.SetEndChild(nil)
11141114 }
1115- log.Printf("[workspace] unparented sibling container %p from parent paned %p", sibling.container, parent.container)
1115+ logging.Debug(fmt.Sprintf("[workspace] unparented sibling container %p from parent paned %p", sibling.container, parent.container))
11161116 }
11171117 }
11181118
@@ -1142,7 +1142,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
11421142 // STEP 8: Check if grandparent now has only one child (cascade promotion needed)
11431143 if grandparent != nil && ((grandparent.left == nil) != (grandparent.right == nil)) {
11441144 // Grandparent has exactly one child - it should be promoted too
1145- log.Printf("[workspace] Grandparent %p now has only one child, cascading promotion", grandparent)
1145+ logging.Info(fmt.Sprintf("[workspace] Grandparent %p now has only one child, cascading promotion", grandparent))
11461146 wm.cascadePromotion(grandparent)
11471147 }
11481148
@@ -1278,7 +1278,7 @@ func (wm *WorkspaceManager) syncPanedDivider(paned *gtk.Paned, orientation gtk.O
12781278 }
12791279
12801280 paned.SetPosition(pos)
1281- log.Printf("[workspace] Set paned position to 50%%: %d (orientation=%d, size=%dx%d)", pos, orientation, alloc.Width(), alloc.Height())
1281+ logging.Debug(fmt.Sprintf("[workspace] Set paned position to 50%%: %d (orientation=%d, size=%dx%d)", pos, orientation, alloc.Width(), alloc.Height()))
12821282 return true
12831283 }
12841284
@@ -1305,7 +1305,7 @@ func (wm *WorkspaceManager) syncPanedDivider(paned *gtk.Paned, orientation gtk.O
13051305
13061306 if frames >= maxFrames {
13071307 alloc := paned.Allocation()
1308- log.Printf("[workspace] Unable to stabilize paned position after %d frames (size=%dx%d)", frames, alloc.Width(), alloc.Height())
1308+ logging.Error(fmt.Sprintf("[workspace] Unable to stabilize paned position after %d frames (size=%dx%d)", frames, alloc.Width(), alloc.Height()))
13091309 return false
13101310 }
13111311
M internal/app/browser/workspace_popup.go +73 -72
@@ -2,11 +2,12 @@
22 package browser
33
44 import (
5- "log"
5+ "fmt"
66 "strings"
77 "time"
88
99 "github.com/bnema/dumber/internal/config"
10+ "github.com/bnema/dumber/internal/logging"
1011 "github.com/bnema/dumber/pkg/webkit"
1112 webkitgtk "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1213 )
@@ -24,8 +25,8 @@ const (
2425 // registerOAuthAutoClose sets up OAuth auto-close functionality for popups
2526 // Note: OAuth detection is now handled by the main-world.js injection script
2627 func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) {
27- log.Printf("[workspace] OAuth auto-close enabled for popup with URL: %s", url)
28- log.Printf("[workspace] OAuth detection will be handled by main-world.js injection script")
28+ logging.Info(fmt.Sprintf("[workspace] OAuth auto-close enabled for popup with URL: %s", url))
29+ logging.Info(fmt.Sprintf("[workspace] OAuth detection will be handled by main-world.js injection script"))
2930 }
3031
3132 // insertIndependentPane inserts a new independent pane next to the source
@@ -59,7 +60,7 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
5960 related := webView
6061 newPane, err := wm.createPane(related)
6162 if err != nil {
62- log.Printf("[workspace] failed to create related popup pane: %v", err)
63+ logging.Error(fmt.Sprintf("[workspace] failed to create related popup pane: %v", err))
6364 return
6465 }
6566 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
@@ -68,7 +69,7 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
6869 }
6970 // Use splitNode with existing pane instead of insertPopupPane
7071 if _, err := wm.splitNode(sourceNode, direction, newPane); err != nil {
71- log.Printf("[workspace] failed to insert related popup pane: %v", err)
72+ logging.Error(fmt.Sprintf("[workspace] failed to insert related popup pane: %v", err))
7273 return
7374 }
7475 node := wm.viewToNode[related]
@@ -99,7 +100,7 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
99100 sourceNode.activePopupChildren = make([]string, 0)
100101 }
101102 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID)
102- log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren))
103+ logging.Debug(fmt.Sprintf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)))
103104
104105 // Register navigation policy handler for parent to block script-initiated navigations
105106 // when it has active popup children (prevents OAuth from hijacking parent pane)
@@ -112,27 +113,27 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
112113
113114 // Block script-initiated navigations if this pane has active popup children
114115 if len(sourceNode.activePopupChildren) > 0 {
115- log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url)
116+ logging.Info(fmt.Sprintf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url))
116117 return false
117118 }
118119
119120 // Allow if no active popups
120121 return true
121122 })
122- log.Printf("[workspace] Registered navigation policy handler for parent pane")
123+ logging.Debug(fmt.Sprintf("[workspace] Registered navigation policy handler for parent pane"))
123124 }
124125 }
125126
126127 // Pipe into existing auto-close flow only for popups (confirmed by detection)
127128 related.RegisterCloseHandler(func() {
128- log.Printf("[workspace] Popup requesting close via window.close()")
129+ logging.Info(fmt.Sprintf("[workspace] Popup requesting close via window.close()"))
129130
130131 // Remove this popup from parent's activePopupChildren
131132 if sourceNode != nil && popupWebViewID != "" {
132133 for i, childID := range sourceNode.activePopupChildren {
133134 if childID == popupWebViewID {
134135 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...)
135- log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren))
136+ logging.Debug(fmt.Sprintf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)))
136137 break
137138 }
138139 }
@@ -145,7 +146,7 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
145146 return false
146147 }
147148 if err := wm.ClosePane(n); err != nil {
148- log.Printf("[workspace] Failed to close popup pane: %v", err)
149+ logging.Error(fmt.Sprintf("[workspace] Failed to close popup pane: %v", err))
149150 }
150151 return false
151152 }, n)
@@ -179,32 +180,32 @@ func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, widt
179180 if container := view.RootWidget(); container != nil {
180181 webkit.WidgetSetSizeRequest(container, minWidth, minHeight)
181182 webkit.WidgetQueueResize(container)
182- log.Printf("[workspace] Applied minimum size %dx%d to popup container=%p", minWidth, minHeight, container)
183+ logging.Debug(fmt.Sprintf("[workspace] Applied minimum size %dx%d to popup container=%p", minWidth, minHeight, container))
183184 }
184185
185186 // Apply to WebView widget
186187 if webViewWidget := view.Widget(); webViewWidget != nil {
187188 webkit.WidgetSetSizeRequest(webViewWidget, minWidth, minHeight)
188189 webkit.WidgetQueueResize(webViewWidget)
189- log.Printf("[workspace] Applied minimum size %dx%d to popup webview=%p", minWidth, minHeight, webViewWidget)
190+ logging.Debug(fmt.Sprintf("[workspace] Applied minimum size %dx%d to popup webview=%p", minWidth, minHeight, webViewWidget))
190191 }
191192 }
192193
193194 // shouldAutoClose checks simple OAuth-like URL patterns and config flag
194195 func (wm *WorkspaceManager) shouldAutoClose(url string) bool {
195- log.Printf("[workspace] shouldAutoClose called for URL: %s", url)
196+ logging.Info(fmt.Sprintf("[workspace] shouldAutoClose called for URL: %s", url))
196197
197198 if wm == nil || wm.app == nil || wm.app.config == nil {
198- log.Printf("[workspace] shouldAutoClose: missing config, returning true")
199+ logging.Error(fmt.Sprintf("[workspace] shouldAutoClose: missing config, returning true"))
199200 return true
200201 }
201202 if !wm.app.config.Workspace.Popups.OAuthAutoClose {
202- log.Printf("[workspace] shouldAutoClose: OAuthAutoClose disabled in config, returning false")
203+ logging.Info(fmt.Sprintf("[workspace] shouldAutoClose: OAuthAutoClose disabled in config, returning false"))
203204 return false
204205 }
205206
206207 u := strings.ToLower(url)
207- log.Printf("[workspace] shouldAutoClose: checking lowercase URL: %s", u)
208+ logging.Debug(fmt.Sprintf("[workspace] shouldAutoClose: checking lowercase URL: %s", u))
208209
209210 // RFC 6749 compliant OAuth 2.0 URL patterns
210211 oauthPatterns := []string{
@@ -225,12 +226,12 @@ func (wm *WorkspaceManager) shouldAutoClose(url string) bool {
225226 // Check for OAuth patterns in URL
226227 for _, pattern := range oauthPatterns {
227228 if strings.Contains(u, pattern) {
228- log.Printf("[workspace] shouldAutoClose: MATCHED pattern '%s' in URL, returning true", pattern)
229+ logging.Info(fmt.Sprintf("[workspace] shouldAutoClose: MATCHED pattern '%s' in URL, returning true", pattern))
229230 return true
230231 }
231232 }
232233
233- log.Printf("[workspace] shouldAutoClose: no OAuth patterns matched, returning false")
234+ logging.Info(fmt.Sprintf("[workspace] shouldAutoClose: no OAuth patterns matched, returning false"))
234235 return false
235236 }
236237
@@ -247,14 +248,14 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi
247248 return // Already placed
248249 }
249250
250- log.Printf("[workspace] Smart detection result: type=%d url=%s", windowType, url)
251+ logging.Info(fmt.Sprintf("[workspace] Smart detection result: type=%d url=%s", windowType, url))
251252
252253 switch windowType {
253254 case webkit.WindowTypeTab:
254255 // For tabs, create a NEW independent WebView (can't use the related one)
255256 webkitCfg, err := wm.app.buildWebkitConfig()
256257 if err != nil {
257- log.Printf("[workspace] failed to build webkit config for tab: %v", err)
258+ logging.Error(fmt.Sprintf("[workspace] failed to build webkit config for tab: %v", err))
258259 return
259260 }
260261 // Tabs are embedded in workspace paned containers - no separate window needed
@@ -263,7 +264,7 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi
263264 // Create independent WebView matching our standard tab insertion path
264265 independentView, err := webkit.NewWebView(webkitCfg)
265266 if err != nil {
266- log.Printf("[workspace] failed to create independent tab WebView: %v", err)
267+ logging.Error(fmt.Sprintf("[workspace] failed to create independent tab WebView: %v", err))
267268 return
268269 }
269270
@@ -273,7 +274,7 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi
273274 // The related webView was just for detection - we don't use it for tabs
274275 // Insert the new independent view as a tab
275276 if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil {
276- log.Printf("[workspace] Failed to insert independent pane: %v", err)
277+ logging.Error(fmt.Sprintf("[workspace] Failed to insert independent pane: %v", err))
277278 }
278279
279280 case webkit.WindowTypePopup:
@@ -296,16 +297,16 @@ func (wm *WorkspaceManager) setupPopupHandling(webView *webkit.WebView, parentNo
296297 // Get URL from navigation action
297298 req := navAction.Request()
298299 if req == nil {
299- log.Printf("[workspace] create signal: no request in navigation action")
300+ logging.Info(fmt.Sprintf("[workspace] create signal: no request in navigation action"))
300301 return nil
301302 }
302303 url := req.URI()
303304
304- log.Printf("[workspace] create signal received for URL: %s", url)
305+ logging.Info(fmt.Sprintf("[workspace] create signal received for URL: %s", url))
305306
306307 // Check if popups are enabled in config
307308 if wm.app.config != nil && !wm.app.config.Workspace.Popups.OpenInNewPane {
308- log.Printf("[workspace] Popup creation disabled in config - blocking popup")
309+ logging.Info(fmt.Sprintf("[workspace] Popup creation disabled in config - blocking popup"))
309310 return nil
310311 }
311312
@@ -313,59 +314,59 @@ func (wm *WorkspaceManager) setupPopupHandling(webView *webkit.WebView, parentNo
313314 // WebKit requires the related-view property to be set during construction for proper lifecycle
314315 // This ensures session/cookie sharing and proper WindowFeatures initialization
315316 // All initialization (wrapping, container, scripts) happens in ready-to-show
316- log.Printf("[workspace] Step 1: Creating bare gotk4 WebView with related-view property")
317+ logging.Debug(fmt.Sprintf("[workspace] Step 1: Creating bare gotk4 WebView with related-view property"))
317318 barePopupView := webkit.NewBareRelatedWebView(webView.GtkWebView())
318319 if barePopupView == nil {
319- log.Printf("[workspace] Failed to create bare related popup WebView - blocking popup")
320+ logging.Error(fmt.Sprintf("[workspace] Failed to create bare related popup WebView - blocking popup"))
320321 return nil
321322 }
322- log.Printf("[workspace] Step 1 complete: bare related gotk4 WebView created=%p (parent=%p)", barePopupView, webView.GtkWebView())
323+ logging.Debug(fmt.Sprintf("[workspace] Step 1 complete: bare related gotk4 WebView created=%p (parent=%p)", barePopupView, webView.GtkWebView()))
323324
324325 // Wrap the bare view immediately to get a proper ID
325326 // BUT don't initialize it yet - that happens in ready-to-show
326- log.Printf("[workspace] Step 2: Wrapping bare view (no initialization)")
327+ logging.Info(fmt.Sprintf("[workspace] Step 2: Wrapping bare view (no initialization)"))
327328 wrappedView := webkit.WrapBareWebView(barePopupView)
328329 if wrappedView == nil {
329- log.Printf("[workspace] Failed to wrap bare popup WebView - blocking popup")
330+ logging.Error(fmt.Sprintf("[workspace] Failed to wrap bare popup WebView - blocking popup"))
330331 return nil
331332 }
332333
333334 // Use the wrapper's ID for tracking
334335 popupID := wrappedView.ID()
335- log.Printf("[workspace] Step 2 complete: wrapped view created with ID=%d", popupID)
336+ logging.Debug(fmt.Sprintf("[workspace] Step 2 complete: wrapped view created with ID=%d", popupID))
336337
337338 // Store wrapped view (not initialized) for initialization in ready-to-show
338- log.Printf("[workspace] Step 3: Storing in pendingPopups map")
339+ logging.Info(fmt.Sprintf("[workspace] Step 3: Storing in pendingPopups map"))
339340 wm.pendingPopups[popupID] = &pendingPopup{
340341 wrappedView: wrappedView,
341342 parentView: webView,
342343 parentNode: parentNode,
343344 url: url,
344345 }
345- log.Printf("[workspace] Step 3 complete: stored in pendingPopups")
346+ logging.Info(fmt.Sprintf("[workspace] Step 3 complete: stored in pendingPopups"))
346347
347348 // Connect signals immediately BEFORE returning to WebKit
348349 // This is safe because we're only connecting signal handlers, not accessing properties
349350 // WebKit will configure WindowFeatures AFTER this callback returns, which is fine
350- log.Printf("[workspace] Step 4: Connecting signals immediately before returning to WebKit")
351+ logging.Info(fmt.Sprintf("[workspace] Step 4: Connecting signals immediately before returning to WebKit"))
351352
352353 // Connect ready-to-show signal
353354 barePopupView.ConnectReadyToShow(func() {
354- log.Printf("[workspace] ready-to-show signal received for popup ID=%d", popupID)
355+ logging.Info(fmt.Sprintf("[workspace] ready-to-show signal received for popup ID=%d", popupID))
355356 wm.handlePopupReadyToShow(popupID)
356357 })
357- log.Printf("[workspace] Connected ready-to-show signal for popup ID=%d", popupID)
358+ logging.Debug(fmt.Sprintf("[workspace] Connected ready-to-show signal for popup ID=%d", popupID))
358359
359360 // Connect close handler
360361 barePopupView.ConnectClose(func() {
361- log.Printf("[workspace] close signal received for popup ID=%d", popupID)
362+ logging.Info(fmt.Sprintf("[workspace] close signal received for popup ID=%d", popupID))
362363 wm.handlePopupClose(popupID)
363364 })
364- log.Printf("[workspace] Connected close signal for popup ID=%d", popupID)
365+ logging.Debug(fmt.Sprintf("[workspace] Connected close signal for popup ID=%d", popupID))
365366
366- log.Printf("[workspace] Step 4 complete: signals connected")
367+ logging.Debug(fmt.Sprintf("[workspace] Step 4 complete: signals connected"))
367368
368- log.Printf("[workspace] Step 5: Returning wrapped view to WebKit (will extract bare view in ConnectCreate)")
369+ logging.Debug(fmt.Sprintf("[workspace] Step 5: Returning wrapped view to WebKit (will extract bare view in ConnectCreate)"))
369370
370371 // Return the wrapped view - WebKit will extract the underlying gotk4 view and configure it
371372 return wrappedView
@@ -376,12 +377,12 @@ func (wm *WorkspaceManager) setupPopupHandling(webView *webkit.WebView, parentNo
376377 func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
377378 info, ok := wm.pendingPopups[popupID]
378379 if !ok {
379- log.Printf("[workspace] ready-to-show for unknown popup ID: %d", popupID)
380+ logging.Warn(fmt.Sprintf("[workspace] ready-to-show for unknown popup ID: %d", popupID))
380381 return
381382 }
382383 delete(wm.pendingPopups, popupID)
383384
384- log.Printf("[workspace] ready-to-show received for popup ID=%d, starting full initialization", popupID)
385+ logging.Info(fmt.Sprintf("[workspace] ready-to-show received for popup ID=%d, starting full initialization", popupID))
385386
386387 // NOW it's safe to initialize (WebKit has configured WindowProperties)
387388 // We already have the wrapped view from setupPopupHandling
@@ -390,30 +391,30 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
390391 // Build webkit config for initialization
391392 webkitCfg, err := wm.app.buildWebkitConfig()
392393 if err != nil {
393- log.Printf("[workspace] Failed to build webkit config for popup initialization: %v", err)
394+ logging.Error(fmt.Sprintf("[workspace] Failed to build webkit config for popup initialization: %v", err))
394395 return
395396 }
396397 webkitCfg.CreateWindow = false // Embedded in workspace
397398
398399 // Initialize the wrapped view with full configuration
399400 if err := wrappedView.InitializeFromBare(webkitCfg); err != nil {
400- log.Printf("[workspace] Failed to initialize popup WebView: %v", err)
401+ logging.Error(fmt.Sprintf("[workspace] Failed to initialize popup WebView: %v", err))
401402 return
402403 }
403404
404405 // Register with content blocking service for network/cosmetic filtering
405406 wm.app.RegisterWebViewForFiltering(wrappedView)
406407
407- log.Printf("[workspace] Successfully initialized popup WebView ID %d", wrappedView.ID())
408+ logging.Debug(fmt.Sprintf("[workspace] Successfully initialized popup WebView ID %d", wrappedView.ID()))
408409
409410 // Create pane for the fully initialized WebView
410411 popupPane, err := wm.createPane(wrappedView)
411412 if err != nil {
412- log.Printf("[workspace] Failed to create popup pane: %v", err)
413+ logging.Error(fmt.Sprintf("[workspace] Failed to create popup pane: %v", err))
413414 return
414415 }
415416
416- log.Printf("[workspace] Created popup pane, now inserting into workspace")
417+ logging.Info(fmt.Sprintf("[workspace] Created popup pane, now inserting into workspace"))
417418
418419 // Determine window type from node (set during setupPopupHandling)
419420 node := wm.viewToNode[wrappedView]
@@ -428,10 +429,10 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
428429 var behaviorStr string
429430 if windowType == webkit.WindowTypeTab {
430431 behaviorStr = wm.app.config.Workspace.Popups.BlankTargetBehavior
431- log.Printf("[workspace] WindowTypeTab (_blank link) detected, using BlankTargetBehavior: %s", behaviorStr)
432+ logging.Debug(fmt.Sprintf("[workspace] WindowTypeTab (_blank link) detected, using BlankTargetBehavior: %s", behaviorStr))
432433 } else {
433434 behaviorStr = string(wm.app.config.Workspace.Popups.Behavior)
434- log.Printf("[workspace] WindowTypePopup detected, using Behavior: %s", behaviorStr)
435+ logging.Debug(fmt.Sprintf("[workspace] WindowTypePopup detected, using Behavior: %s", behaviorStr))
435436 }
436437
437438 // Map string to PopupBehavior enum
@@ -447,7 +448,7 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
447448 behavior = config.PopupBehaviorSplit // Fallback
448449 }
449450
450- log.Printf("[workspace] Final behavior: %s", behavior)
451+ logging.Info(fmt.Sprintf("[workspace] Final behavior: %s", behavior))
451452
452453 switch behavior {
453454 case config.PopupBehaviorSplit:
@@ -457,7 +458,7 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
457458 direction = DirectionRight
458459 }
459460 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
460- log.Printf("[workspace] Failed to insert split popup pane: %v", err)
461+ logging.Error(fmt.Sprintf("[workspace] Failed to insert split popup pane: %v", err))
461462 return
462463 }
463464
@@ -465,7 +466,7 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
465466 // Stacked pane behavior
466467 _, err := wm.stackedPaneManager.StackPane(info.parentNode)
467468 if err != nil {
468- log.Printf("[workspace] Failed to stack existing pane: %v", err)
469+ logging.Error(fmt.Sprintf("[workspace] Failed to stack existing pane: %v", err))
469470 return
470471 }
471472 // Insert the popup pane in the same container - use splitNode with existing popup pane
@@ -474,44 +475,44 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
474475 direction = DirectionRight
475476 }
476477 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
477- log.Printf("[workspace] Failed to insert stacked popup pane: %v", err)
478+ logging.Error(fmt.Sprintf("[workspace] Failed to insert stacked popup pane: %v", err))
478479 return
479480 }
480481
481482 case config.PopupBehaviorTabbed:
482483 // Tabbed behavior - TODO: implement proper tabbed interface
483- log.Printf("[workspace] WARNING: Tabbed popup behavior not yet implemented, falling back to split")
484+ logging.Warn(fmt.Sprintf("[workspace] WARNING: Tabbed popup behavior not yet implemented, falling back to split"))
484485 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
485486 if direction == "" {
486487 direction = DirectionRight
487488 }
488489 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
489- log.Printf("[workspace] Failed to insert tabbed popup pane: %v", err)
490+ logging.Error(fmt.Sprintf("[workspace] Failed to insert tabbed popup pane: %v", err))
490491 return
491492 }
492493
493494 case config.PopupBehaviorWindowed:
494495 // Windowed behavior - opens in new workspace/window
495496 // TODO: implement new window creation with GTK
496- log.Printf("[workspace] WARNING: Windowed popup behavior not yet implemented, falling back to split")
497+ logging.Warn(fmt.Sprintf("[workspace] WARNING: Windowed popup behavior not yet implemented, falling back to split"))
497498 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
498499 if direction == "" {
499500 direction = DirectionRight
500501 }
501502 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
502- log.Printf("[workspace] Failed to insert windowed popup pane: %v", err)
503+ logging.Error(fmt.Sprintf("[workspace] Failed to insert windowed popup pane: %v", err))
503504 return
504505 }
505506
506507 default:
507508 // Fallback to split if unknown behavior
508- log.Printf("[workspace] Unknown popup behavior '%s', falling back to split", behavior)
509+ logging.Warn(fmt.Sprintf("[workspace] Unknown popup behavior '%s', falling back to split", behavior))
509510 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
510511 if direction == "" {
511512 direction = DirectionRight
512513 }
513514 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
514- log.Printf("[workspace] Failed to insert popup pane: %v", err)
515+ logging.Error(fmt.Sprintf("[workspace] Failed to insert popup pane: %v", err))
515516 return
516517 }
517518 }
@@ -534,8 +535,8 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
534535 info.parentNode.activePopupChildren = make([]string, 0)
535536 }
536537 info.parentNode.activePopupChildren = append(info.parentNode.activePopupChildren, popupWebViewID)
537- log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)",
538- popupWebViewID, len(info.parentNode.activePopupChildren))
538+ logging.Debug(fmt.Sprintf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)",
539+ popupWebViewID, len(info.parentNode.activePopupChildren)))
539540
540541 // Register navigation policy handler for parent
541542 if info.parentNode.pane != nil && info.parentNode.pane.WebView() != nil {
@@ -544,8 +545,8 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
544545 return true
545546 }
546547 if len(info.parentNode.activePopupChildren) > 0 {
547- log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s",
548- len(info.parentNode.activePopupChildren), url)
548+ logging.Info(fmt.Sprintf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s",
549+ len(info.parentNode.activePopupChildren), url))
549550 return false
550551 }
551552 return true
@@ -564,17 +565,17 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
564565 // Widgets are already shown by splitNode - no need to show again
565566 // GTK4 handles visibility automatically during reparenting
566567
567- log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url)
568+ logging.Info(fmt.Sprintf("[workspace] Popup pane inserted and visible for URL: %s", info.url))
568569 }
569570
570571 // handlePopupClose responds to WebKit's close signal
571572 func (wm *WorkspaceManager) handlePopupClose(popupID uint64) {
572- log.Printf("[workspace] close signal received for popup native=%d", popupID)
573+ logging.Info(fmt.Sprintf("[workspace] close signal received for popup native=%d", popupID))
573574
574575 // Check if it's in pending (not yet shown)
575576 if info, ok := wm.pendingPopups[popupID]; ok {
576577 delete(wm.pendingPopups, popupID)
577- log.Printf("[workspace] Popup closed before ready-to-show, cleaning up")
578+ logging.Info(fmt.Sprintf("[workspace] Popup closed before ready-to-show, cleaning up"))
578579 // Just remove from pending, WebKit will handle cleanup
579580 _ = info // avoid unused variable
580581 return
@@ -590,7 +591,7 @@ func (wm *WorkspaceManager) handlePopupClose(popupID uint64) {
590591 }
591592
592593 if targetNode == nil {
593- log.Printf("[workspace] close signal for unknown popup ID=%d, already closed?", popupID)
594+ logging.Warn(fmt.Sprintf("[workspace] close signal for unknown popup ID=%d, already closed?", popupID))
594595 return
595596 }
596597
@@ -610,16 +611,16 @@ func (wm *WorkspaceManager) handlePopupClose(popupID uint64) {
610611 targetNode.parentPane.activePopupChildren = append(
611612 targetNode.parentPane.activePopupChildren[:i],
612613 targetNode.parentPane.activePopupChildren[i+1:]...)
613- log.Printf("[workspace] Removed popup from parent's activePopupChildren (remaining: %d)",
614- len(targetNode.parentPane.activePopupChildren))
614+ logging.Debug(fmt.Sprintf("[workspace] Removed popup from parent's activePopupChildren (remaining: %d)",
615+ len(targetNode.parentPane.activePopupChildren)))
615616 break
616617 }
617618 }
618619 }
619620
620621 // Clean close via existing ClosePane (WebKit already stopped)
621- log.Printf("[workspace] Closing popup pane via ClosePane")
622+ logging.Info(fmt.Sprintf("[workspace] Closing popup pane via ClosePane"))
622623 if err := wm.ClosePane(targetNode); err != nil {
623- log.Printf("[workspace] Error closing popup pane: %v", err)
624+ logging.Error(fmt.Sprintf("[workspace] Error closing popup pane: %v", err))
624625 }
625626 }
M internal/app/browser/workspace_stack_lifecycle.go +6 -6
@@ -4,7 +4,7 @@ package browser
44 import (
55 "errors"
66 "fmt"
7- "log"
7+ "github.com/bnema/dumber/internal/logging"
88 )
99
1010 // StackLifecycleManager manages the lifecycle hooks around stacked pane operations.
@@ -34,8 +34,8 @@ func (slm *StackLifecycleManager) CloseStackedPaneWithLifecycle(node *paneNode)
3434 return errors.New("node is not part of a stacked pane")
3535 }
3636
37- log.Printf("[stack-lifecycle] Closing stacked pane: node=%p stack=%p stackSize=%d",
38- node, stackNode, len(stackNode.stackedPanes))
37+ logging.Info(fmt.Sprintf("[stack-lifecycle] Closing stacked pane: node=%p stack=%p stackSize=%d",
38+ node, stackNode, len(stackNode.stackedPanes)))
3939
4040 if err := slm.validateStackState(stackNode, "before close"); err != nil {
4141 return fmt.Errorf("invalid stack state before close: %w", err)
@@ -51,7 +51,7 @@ func (slm *StackLifecycleManager) CloseStackedPaneWithLifecycle(node *paneNode)
5151
5252 if slm.treeValidator != nil {
5353 if err := slm.treeValidator.ValidateTree(slm.wm.root, "after stack close"); err != nil {
54- log.Printf("[stack-lifecycle] Tree validation failed after stack close: %v", err)
54+ logging.Error(fmt.Sprintf("[stack-lifecycle] Tree validation failed after stack close: %v", err))
5555 }
5656 }
5757
@@ -86,7 +86,7 @@ func (slm *StackLifecycleManager) validateStackState(stackNode *paneNode, operat
8686 }
8787 }
8888
89- log.Printf("[stack-lifecycle] Stack state valid for operation '%s': size=%d activeIndex=%d",
90- operation, len(stackNode.stackedPanes), stackNode.activeStackIndex)
89+ logging.Info(fmt.Sprintf("[stack-lifecycle] Stack state valid for operation '%s': size=%d activeIndex=%d",
90+ operation, len(stackNode.stackedPanes), stackNode.activeStackIndex))
9191 return nil
9292 }
M internal/app/browser/workspace_utils.go +3 -4
@@ -3,8 +3,7 @@ package browser
33
44 import (
55 "fmt"
6- "log"
7-
6+ "github.com/bnema/dumber/internal/logging"
87 "github.com/bnema/dumber/pkg/webkit"
98 "github.com/diamondburned/gotk4/pkg/glib/v2"
109 "github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -182,7 +181,7 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
182181 }
183182
184183 webkit.WidgetSetVisible(node.borderOverlay, true)
185- log.Printf("[workspace] Showing border overlay for pane %p", node)
184+ logging.Debug(fmt.Sprintf("[workspace] Showing border overlay for pane %p", node))
186185 }
187186
188187 // removeActivePaneBorder hides the border overlay for a pane.
@@ -192,7 +191,7 @@ func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
192191 }
193192
194193 webkit.WidgetSetVisible(node.borderOverlay, false)
195- log.Printf("[workspace] Hiding border overlay for pane %p", node)
194+ logging.Debug(fmt.Sprintf("[workspace] Hiding border overlay for pane %p", node))
196195 }
197196
198197 // Widget cleanup utilities
M internal/app/control/clipboard.go +7 -6
@@ -1,8 +1,9 @@
11 package control
22
33 import (
4- "log"
4+ "fmt"
55
6+ "github.com/bnema/dumber/internal/logging"
67 "github.com/bnema/dumber/pkg/clipboard"
78 "github.com/bnema/dumber/pkg/webkit"
89 )
@@ -21,19 +22,19 @@ func NewClipboardController(webView *webkit.WebView) *ClipboardController {
2122
2223 // CopyCurrentURL copies the current WebView URL to the clipboard
2324 func (c *ClipboardController) CopyCurrentURL() {
24- log.Printf("Shortcut: Copy URL")
25+ logging.Debug(fmt.Sprintf("Shortcut: Copy URL"))
2526 currentURL := c.webView.GetCurrentURL()
2627 if currentURL == "" {
27- log.Printf("No URL to copy")
28+ logging.Warn(fmt.Sprintf("No URL to copy"))
2829 c.showToast("No URL to copy", "error")
2930 return
3031 }
3132
3233 if err := clipboard.CopyToClipboard(currentURL); err != nil {
33- log.Printf("Failed to copy URL to clipboard: %v", err)
34+ logging.Error(fmt.Sprintf("Failed to copy URL to clipboard: %v", err))
3435 c.showToast("Failed to copy URL", "error")
3536 } else {
36- log.Printf("URL copied to clipboard: %s", currentURL)
37+ logging.Debug(fmt.Sprintf("URL copied to clipboard: %s", currentURL))
3738 c.showToast("URL copied to clipboard", "success")
3839 }
3940 }
@@ -52,7 +53,7 @@ func (c *ClipboardController) showToast(message, toastType string) {
5253 }
5354
5455 if err := c.webView.DispatchCustomEvent("dumber:toast", detail); err != nil {
55- log.Printf("Failed to dispatch toast event: %v", err)
56+ logging.Error(fmt.Sprintf("Failed to dispatch toast event: %v", err))
5657 }
5758 }
5859
M internal/app/control/navigation.go +14 -13
@@ -2,9 +2,10 @@ package control
22
33 import (
44 "context"
5- "log"
5+ "fmt"
66 "os"
77
8+ "github.com/bnema/dumber/internal/logging"
89 "github.com/bnema/dumber/internal/services"
910 "github.com/bnema/dumber/pkg/webkit"
1011 )
@@ -42,7 +43,7 @@ func (n *NavigationController) NavigateToURL(input string) error {
4243
4344 // Record navigation in browser service
4445 if _, navErr := n.browserService.Navigate(ctx, result.URL); navErr != nil {
45- log.Printf("Warning: failed to navigate to %s: %v", result.URL, navErr)
46+ logging.Warn(fmt.Sprintf("Warning: failed to navigate to %s: %v", result.URL, navErr))
4647 }
4748
4849 var (
@@ -56,18 +57,18 @@ func (n *NavigationController) NavigateToURL(input string) error {
5657 if n.webView != nil {
5758 if n.webView.UsesDomZoom() {
5859 if err := n.webView.SeedDomZoom(zoomLevel); err != nil {
59- log.Printf("Warning: failed to seed DOM zoom for %s: %v", result.URL, err)
60+ logging.Warn(fmt.Sprintf("Warning: failed to seed DOM zoom for %s: %v", result.URL, err))
6061 }
6162 } else {
6263 n.webView.RunOnMainThread(func() {
6364 if err := n.webView.SetZoom(zoomLevel); err != nil {
64- log.Printf("Warning: failed to prime native zoom for %s: %v", result.URL, err)
65+ logging.Warn(fmt.Sprintf("Warning: failed to prime native zoom for %s: %v", result.URL, err))
6566 }
6667 })
6768 }
6869 }
6970 } else {
70- log.Printf("Warning: failed to lookup zoom for %s: %v", result.URL, err)
71+ logging.Warn(fmt.Sprintf("Warning: failed to lookup zoom for %s: %v", result.URL, err))
7172 }
7273 }
7374
@@ -91,16 +92,16 @@ func (n *NavigationController) NavigateToURL(input string) error {
9192 // HandleBrowseCommand processes the browse command line argument
9293 func (n *NavigationController) HandleBrowseCommand() {
9394 if len(os.Args) >= 3 && os.Args[1] == "browse" {
94- log.Printf("Browse command detected: %s", os.Args[2])
95+ logging.Debug(fmt.Sprintf("Browse command detected: %s", os.Args[2]))
9596 ctx := context.Background()
9697 result, err := n.parserService.ParseInput(ctx, os.Args[2])
9798 if err == nil {
98- log.Printf("Parsed input → URL: %s", result.URL)
99+ logging.Debug(fmt.Sprintf("Parsed input → URL: %s", result.URL))
99100 if _, navErr := n.browserService.Navigate(ctx, result.URL); navErr != nil {
100- log.Printf("Warning: failed to navigate to %s: %v", result.URL, navErr)
101+ logging.Warn(fmt.Sprintf("Warning: failed to navigate to %s: %v", result.URL, navErr))
101102 }
102103 if n.webView != nil {
103- log.Printf("Loading URL in WebView: %s", result.URL)
104+ logging.Debug(fmt.Sprintf("Loading URL in WebView: %s", result.URL))
104105 var (
105106 zoomLevel float64
106107 haveZoom bool
@@ -111,21 +112,21 @@ func (n *NavigationController) HandleBrowseCommand() {
111112 haveZoom = true
112113 if n.webView.UsesDomZoom() {
113114 if err := n.webView.SeedDomZoom(zoomLevel); err != nil {
114- log.Printf("Warning: failed to seed DOM zoom for %s: %v", result.URL, err)
115+ logging.Warn(fmt.Sprintf("Warning: failed to seed DOM zoom for %s: %v", result.URL, err))
115116 }
116117 } else {
117118 n.webView.RunOnMainThread(func() {
118119 if err := n.webView.SetZoom(zoomLevel); err != nil {
119- log.Printf("Warning: failed to prime native zoom for %s: %v", result.URL, err)
120+ logging.Warn(fmt.Sprintf("Warning: failed to prime native zoom for %s: %v", result.URL, err))
120121 }
121122 })
122123 }
123124 } else {
124- log.Printf("Warning: failed to lookup zoom for %s: %v", result.URL, err)
125+ logging.Warn(fmt.Sprintf("Warning: failed to lookup zoom for %s: %v", result.URL, err))
125126 }
126127 }
127128 if err := n.webView.LoadURL(result.URL); err != nil {
128- log.Printf("Warning: failed to load URL: %v", err)
129+ logging.Warn(fmt.Sprintf("Warning: failed to load URL: %v", err))
129130 }
130131 if n.zoomController != nil {
131132 if haveZoom {
M internal/app/control/zoom.go +9 -8
@@ -2,9 +2,10 @@ package control
22
33 import (
44 "context"
5- "log"
5+ "fmt"
66 "time"
77
8+ "github.com/bnema/dumber/internal/logging"
89 "github.com/bnema/dumber/internal/services"
910 "github.com/bnema/dumber/pkg/webkit"
1011 )
@@ -74,11 +75,11 @@ func (z *ZoomController) handleZoomChange(level float64) {
7475 go func(url string, level float64) {
7576 ctx := context.Background()
7677 if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
77- log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
78+ logging.Error(fmt.Sprintf("[zoom] failed to save level %.2f for %s: %v", level, url, err))
7879 return
7980 }
8081 key := services.ZoomKeyForLog(url)
81- log.Printf("[zoom] saved %.2f for %s", level, key)
82+ logging.Debug(fmt.Sprintf("[zoom] saved %.2f for %s", level, key))
8283 }(url, level)
8384
8485 // Only show toast for user-initiated zoom changes, not during init or programmatic changes
@@ -89,15 +90,15 @@ func (z *ZoomController) handleZoomChange(level float64) {
8990
9091 // showZoomToast displays a zoom level notification using TypeScript toast system
9192 func (z *ZoomController) showZoomToast(level float64) {
92- log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
93+ logging.Debug(fmt.Sprintf("[zoom] Attempting to show zoom toast for level %.2f", level))
9394
9495 if z.webView == nil {
95- log.Printf("[zoom] webview unavailable for zoom toast")
96+ logging.Debug(fmt.Sprintf("[zoom] webview unavailable for zoom toast"))
9697 return
9798 }
9899
99100 if err := z.webView.DispatchCustomEvent("dumber:toast:zoom", map[string]any{"level": level}); err != nil {
100- log.Printf("[zoom] failed to dispatch zoom toast: %v", err)
101+ logging.Error(fmt.Sprintf("[zoom] failed to dispatch zoom toast: %v", err))
101102 }
102103 }
103104
@@ -172,11 +173,11 @@ func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, a
172173 })
173174
174175 if err := z.webView.SetZoom(zoomLevel); err != nil {
175- log.Printf("Warning: failed to set zoom: %v", err)
176+ logging.Warn(fmt.Sprintf("Warning: failed to set zoom: %v", err))
176177 return
177178 }
178179
179- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain)
180+ logging.Debug(fmt.Sprintf("[zoom] loaded %.2f for %s", zoomLevel, domain))
180181 if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" {
181182 z.showZoomToast(zoomLevel)
182183 }
M internal/app/environment/codec.go +17 -16
@@ -1,18 +1,19 @@
11 package environment
22
33 import (
4- "log"
4+ "fmt"
55 "os"
66 "strconv"
77 "strings"
88
99 "github.com/bnema/dumber/internal/config"
10+ "github.com/bnema/dumber/internal/logging"
1011 )
1112
1213 // ApplyCodecConfiguration applies codec preferences to environment variables and GStreamer settings
1314 func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
14- log.Printf("[codec] Applying codec preferences: preferred=%s, force_av1=%t, block_vp9=%t",
15- codecPrefs.PreferredCodecs, codecPrefs.ForceAV1, codecPrefs.BlockVP9)
15+ logging.Info(fmt.Sprintf("[codec] Applying codec preferences: preferred=%s, force_av1=%t, block_vp9=%t",
16+ codecPrefs.PreferredCodecs, codecPrefs.ForceAV1, codecPrefs.BlockVP9))
1617
1718 // Build comprehensive GStreamer plugin rankings
1819 var rankSettings []string
@@ -32,7 +33,7 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
3233 "avdec_av1:256", // libav AV1 decoder
3334 }
3435 rankSettings = append(rankSettings, av1Ranks...)
35- log.Printf("[codec] Promoted AV1 decoders for maximum priority")
36+ logging.Info(fmt.Sprintf("[codec] Promoted AV1 decoders for maximum priority"))
3637 }
3738
3839 // H.264 decoder management
@@ -58,7 +59,7 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
5859 vp9Demotions = append(vp9Demotions, "vp9dec:64", "avdec_vp9:64")
5960 }
6061 rankSettings = append(rankSettings, vp9Demotions...)
61- log.Printf("[codec] Demoted/blocked VP9 decoders")
62+ logging.Info(fmt.Sprintf("[codec] Demoted/blocked VP9 decoders"))
6263 }
6364
6465 // VP8 decoder blocking
@@ -69,16 +70,16 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
6970 "avdec_vp8:0",
7071 }
7172 rankSettings = append(rankSettings, vp8Demotions...)
72- log.Printf("[codec] Blocked VP8 decoders")
73+ logging.Info(fmt.Sprintf("[codec] Blocked VP8 decoders"))
7374 }
7475
7576 // Apply the complete ranking system
7677 if len(rankSettings) > 0 {
7778 finalRank := strings.Join(rankSettings, ",")
7879 if err := os.Setenv("GST_PLUGIN_FEATURE_RANK", finalRank); err != nil {
79- log.Printf("[codec] Warning: failed to set GST_PLUGIN_FEATURE_RANK: %v", err)
80+ logging.Warn(fmt.Sprintf("[codec] Warning: failed to set GST_PLUGIN_FEATURE_RANK: %v", err))
8081 } else {
81- log.Printf("[codec] Set comprehensive GST_PLUGIN_FEATURE_RANK: %s", finalRank)
82+ logging.Info(fmt.Sprintf("[codec] Set comprehensive GST_PLUGIN_FEATURE_RANK: %s", finalRank))
8283 }
8384 }
8485
@@ -87,14 +88,14 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
8788 bufferSize := strconv.Itoa(codecPrefs.VideoBufferSizeMB * 1024 * 1024)
8889
8990 if err := os.Setenv("GST_BUFFER_SIZE", bufferSize); err != nil {
90- log.Printf("[codec] Warning: failed to set GST_BUFFER_SIZE: %v", err)
91+ logging.Warn(fmt.Sprintf("[codec] Warning: failed to set GST_BUFFER_SIZE: %v", err))
9192 }
9293
9394 if err := os.Setenv("GST_QUEUE2_MAX_SIZE_BYTES", bufferSize); err != nil {
94- log.Printf("[codec] Warning: failed to set GST_QUEUE2_MAX_SIZE_BYTES: %v", err)
95+ logging.Warn(fmt.Sprintf("[codec] Warning: failed to set GST_QUEUE2_MAX_SIZE_BYTES: %v", err))
9596 }
9697
97- log.Printf("[codec] Set video buffer size to %dMB", codecPrefs.VideoBufferSizeMB)
98+ logging.Info(fmt.Sprintf("[codec] Set video buffer size to %dMB", codecPrefs.VideoBufferSizeMB))
9899 }
99100
100101 // Set queue buffer time if specified
@@ -103,9 +104,9 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
103104 bufferTime := strconv.Itoa(codecPrefs.QueueBufferTimeSec * 1000000000)
104105
105106 if err := os.Setenv("GST_QUEUE2_MAX_SIZE_TIME", bufferTime); err != nil {
106- log.Printf("[codec] Warning: failed to set GST_QUEUE2_MAX_SIZE_TIME: %v", err)
107+ logging.Warn(fmt.Sprintf("[codec] Warning: failed to set GST_QUEUE2_MAX_SIZE_TIME: %v", err))
107108 } else {
108- log.Printf("[codec] Set queue buffer time to %ds", codecPrefs.QueueBufferTimeSec)
109+ logging.Info(fmt.Sprintf("[codec] Set queue buffer time to %ds", codecPrefs.QueueBufferTimeSec))
109110 }
110111 }
111112
@@ -113,16 +114,16 @@ func ApplyCodecConfiguration(codecPrefs config.CodecConfig) {
113114 if codecPrefs.ForceAV1 {
114115 // Enable AV1 hardware decoding if available
115116 if err := os.Setenv("GST_AV1_DECODER_ENABLE_HW", "1"); err != nil {
116- log.Printf("[codec] Warning: failed to set GST_AV1_DECODER_ENABLE_HW: %v", err)
117+ logging.Warn(fmt.Sprintf("[codec] Warning: failed to set GST_AV1_DECODER_ENABLE_HW: %v", err))
117118 }
118119 }
119120
120121 // Disable video post-processing to avoid VA-API issues
121122 if err := os.Setenv("GST_VAAPI_DISABLE_VPP", "1"); err != nil {
122- log.Printf("[codec] Warning: failed to set GST_VAAPI_DISABLE_VPP: %v", err)
123+ logging.Warn(fmt.Sprintf("[codec] Warning: failed to set GST_VAAPI_DISABLE_VPP: %v", err))
123124 }
124125
125- log.Printf("[codec] Codec configuration applied successfully")
126+ logging.Info(fmt.Sprintf("[codec] Codec configuration applied successfully"))
126127 }
127128
128129 // BuildBlockedCodecsList converts config codec blocking preferences to a string slice
M internal/app/environment/locale.go +8 -6
@@ -1,10 +1,12 @@
11 package environment
22
33 import (
4- "log"
4+ "fmt"
55 "os"
66 "os/exec"
77 "strings"
8+
9+ "github.com/bnema/dumber/internal/logging"
810 )
911
1012 // DetectAndSetKeyboardLocale attempts to determine the current keyboard layout
@@ -38,7 +40,7 @@ func DetectAndSetKeyboardLocale() {
3840 cand := strings.TrimSpace(parts[1])
3941 if cand != "" {
4042 locale = cand
41- log.Printf("[locale] keyboard layout from localectl: %s", locale)
43+ logging.Debug(fmt.Sprintf("[locale] keyboard layout from localectl: %s", locale))
4244 }
4345 }
4446 break
@@ -57,7 +59,7 @@ func DetectAndSetKeyboardLocale() {
5759 cand := strings.TrimSpace(parts[1])
5860 if cand != "" {
5961 locale = cand
60- log.Printf("[locale] keyboard layout from setxkbmap: %s", locale)
62+ logging.Debug(fmt.Sprintf("[locale] keyboard layout from setxkbmap: %s", locale))
6163 }
6264 }
6365 break
@@ -87,13 +89,13 @@ func DetectAndSetKeyboardLocale() {
8789 if i := strings.IndexByte(locale, '_'); i > 0 {
8890 locale = locale[:i]
8991 }
90- log.Printf("[locale] keyboard layout from system locale: %s", locale)
92+ logging.Debug(fmt.Sprintf("[locale] keyboard layout from system locale: %s", locale))
9193 }
9294
9395 if locale == "" {
9496 locale = "en"
95- log.Printf("[locale] keyboard layout defaulted to: %s", locale)
97+ logging.Debug(fmt.Sprintf("[locale] keyboard layout defaulted to: %s", locale))
9698 }
9799
98- log.Printf("[locale] keyboard locale detected: %s", locale)
100+ logging.Info(fmt.Sprintf("[locale] keyboard locale detected: %s", locale))
99101 }
M internal/app/environment/media.go +6 -4
@@ -1,8 +1,10 @@
11 package environment
22
33 import (
4- "log"
4+ "fmt"
55 "os"
6+
7+ "github.com/bnema/dumber/internal/logging"
68 )
79
810 // SetupMediaPipeline proactively hardens media pipeline to avoid WebProcess crashes from buggy VAAPI/plugins.
@@ -12,15 +14,15 @@ func SetupMediaPipeline() {
1214 // Demote VAAPI-related elements so they won't be auto-picked
1315 if os.Getenv("GST_PLUGIN_FEATURE_RANK") == "" {
1416 if err := os.Setenv("GST_PLUGIN_FEATURE_RANK", "vaapisink:0,vaapidecodebin:0,vaapih264dec:0,vaapivideoconvert:0"); err != nil {
15- log.Printf("Warning: failed to set GST_PLUGIN_FEATURE_RANK: %v", err)
17+ logging.Warn(fmt.Sprintf("Warning: failed to set GST_PLUGIN_FEATURE_RANK: %v", err))
1618 }
1719 }
1820 // Optionally disable media stream features that can engage complex pipelines (webcam/mic)
1921 if os.Getenv("WEBKIT_DISABLE_MEDIA_STREAM") == "" {
2022 if err := os.Setenv("WEBKIT_DISABLE_MEDIA_STREAM", "1"); err != nil {
21- log.Printf("Warning: failed to set WEBKIT_DISABLE_MEDIA_STREAM: %v", err)
23+ logging.Warn(fmt.Sprintf("Warning: failed to set WEBKIT_DISABLE_MEDIA_STREAM: %v", err))
2224 }
2325 }
24- log.Printf("[media] Safe mode enabled: VAAPI demoted, webcam/mic disabled (override with DUMBER_MEDIA_SAFE=0)")
26+ logging.Info(fmt.Sprintf("[media] Safe mode enabled: VAAPI demoted, webcam/mic disabled (override with DUMBER_MEDIA_SAFE=0)"))
2527 }
2628 }
M internal/app/messaging/handler.go +55 -56
@@ -5,7 +5,6 @@ import (
55 "context"
66 "encoding/json"
77 "fmt"
8- "log"
98 "strconv"
109 "time"
1110
@@ -79,7 +78,7 @@ func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
7978 func (h *Handler) Handle(payload string) {
8079 msg, err := parseIncomingMessage(payload)
8180 if err != nil {
82- log.Printf("[ERROR] Failed to unmarshal message: %v", err)
81+ logging.Error(fmt.Sprintf("[ERROR] Failed to unmarshal message: %v", err))
8382 return
8483 }
8584
@@ -202,7 +201,7 @@ func (h *Handler) SetWebView(webView *webkit.WebView) {
202201 func (h *Handler) handleNavigation(msg Message) {
203202 if h.navigationController != nil {
204203 if err := h.navigationController.NavigateToURL(msg.URL); err != nil {
205- log.Printf("[messaging] Navigation controller failed for input %q: %v", msg.URL, err)
204+ logging.Error(fmt.Sprintf("[messaging] Navigation controller failed for input %q: %v", msg.URL, err))
206205 h.legacyNavigate(msg)
207206 }
208207 return
@@ -215,12 +214,12 @@ func (h *Handler) legacyNavigate(msg Message) {
215214 ctx := context.Background()
216215 res, err := h.parserService.ParseInput(ctx, msg.URL)
217216 if err != nil {
218- log.Printf("[messaging] Legacy navigation parse failed for %q: %v", msg.URL, err)
217+ logging.Error(fmt.Sprintf("[messaging] Legacy navigation parse failed for %q: %v", msg.URL, err))
219218 return
220219 }
221220
222221 if _, navErr := h.browserService.Navigate(ctx, res.URL); navErr != nil {
223- log.Printf("Warning: failed to navigate to %s: %v", res.URL, navErr)
222+ logging.Warn(fmt.Sprintf("Warning: failed to navigate to %s: %v", res.URL, navErr))
224223 }
225224
226225 if h.webView == nil {
@@ -228,12 +227,12 @@ func (h *Handler) legacyNavigate(msg Message) {
228227 }
229228
230229 if err := h.webView.LoadURL(res.URL); err != nil {
231- log.Printf("[messaging] Legacy LoadURL failed for %s: %v", res.URL, err)
230+ logging.Error(fmt.Sprintf("[messaging] Legacy LoadURL failed for %s: %v", res.URL, err))
232231 }
233232
234233 if z, zerr := h.browserService.GetZoomLevel(ctx, res.URL); zerr == nil {
235234 if err := h.webView.SetZoom(z); err != nil {
236- log.Printf("[messaging] Legacy SetZoom failed for %s: %v", res.URL, err)
235+ logging.Error(fmt.Sprintf("[messaging] Legacy SetZoom failed for %s: %v", res.URL, err))
237236 }
238237 }
239238 }
@@ -242,26 +241,26 @@ func (h *Handler) legacyNavigate(msg Message) {
242241 // handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch
243242 func (h *Handler) handleWorkspace(msg Message) {
244243 if h.workspaceObserver == nil {
245- log.Printf("[workspace] Received workspace event %q but no observer registered", msg.Event)
244+ logging.Warn(fmt.Sprintf("[workspace] Received workspace event %q but no observer registered", msg.Event))
246245 return
247246 }
248247 if h.webView == nil {
249- log.Printf("[workspace] Ignoring workspace event %q: webview not attached", msg.Event)
248+ logging.Debug(fmt.Sprintf("[workspace] Ignoring workspace event %q: webview not attached", msg.Event))
250249 return
251250 }
252- log.Printf("[workspace] Forwarding workspace event: event=%s direction=%s action=%s webviewId=%s", msg.Event, msg.Direction, msg.Action, msg.WebViewID)
251+ logging.Debug(fmt.Sprintf("[workspace] Forwarding workspace event: event=%s direction=%s action=%s webviewId=%s", msg.Event, msg.Direction, msg.Action, msg.WebViewID))
253252 h.workspaceObserver.OnWorkspaceMessage(h.webView, msg)
254253 }
255254
256255 func (h *Handler) handleClosePopup(msg Message) {
257- log.Printf("[messaging] Received close-popup request: webviewId=%s reason=%s", msg.WebViewID, msg.Reason)
256+ logging.Debug(fmt.Sprintf("[messaging] Received close-popup request: webviewId=%s reason=%s", msg.WebViewID, msg.Reason))
258257
259258 if h.workspaceObserver == nil {
260- log.Printf("[messaging] No workspace observer registered for close-popup request")
259+ logging.Warn(fmt.Sprintf("[messaging] No workspace observer registered for close-popup request"))
261260 return
262261 }
263262 if h.webView == nil {
264- log.Printf("[messaging] No webview attached for close-popup request")
263+ logging.Debug(fmt.Sprintf("[messaging] No webview attached for close-popup request"))
265264 return
266265 }
267266
@@ -273,7 +272,7 @@ func (h *Handler) handleClosePopup(msg Message) {
273272 Reason: msg.Reason,
274273 }
275274
276- log.Printf("[messaging] Forwarding close-popup to workspace: webviewId=%s reason=%s", msg.WebViewID, msg.Reason)
275+ logging.Debug(fmt.Sprintf("[messaging] Forwarding close-popup to workspace: webviewId=%s reason=%s", msg.WebViewID, msg.Reason))
277276 h.workspaceObserver.OnWorkspaceMessage(h.webView, closeMsg)
278277 }
279278
@@ -342,7 +341,7 @@ func (h *Handler) handleQuery(msg Message) {
342341 // Prefer unified page-world API; fallback to legacy global function
343342 script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))"
344343 if err := h.webView.InjectScript(script); err != nil {
345- log.Printf("[ERROR] Failed to inject omnibox suggestions: %v", err)
344+ logging.Error(fmt.Sprintf("[ERROR] Failed to inject omnibox suggestions: %v", err))
346345 }
347346 }
348347 }
@@ -362,7 +361,7 @@ func (h *Handler) handleOmniboxInitialHistory(msg Message) {
362361 // Get config to determine behavior
363362 cfg, err := h.browserService.GetConfig(ctx)
364363 if err != nil {
365- log.Printf("[ERROR] Failed to get config: %v", err)
364+ logging.Error(fmt.Sprintf("[ERROR] Failed to get config: %v", err))
366365 return
367366 }
368367 behavior := cfg.Omnibox.InitialBehavior
@@ -396,7 +395,7 @@ func (h *Handler) handleOmniboxInitialHistory(msg Message) {
396395 }
397396
398397 if err != nil {
399- log.Printf("[ERROR] Failed to fetch initial history: %v", err)
398+ logging.Error(fmt.Sprintf("[ERROR] Failed to fetch initial history: %v", err))
400399 return
401400 }
402401
@@ -437,7 +436,7 @@ func (h *Handler) handleOmniboxInitialHistory(msg Message) {
437436 if b, err := json.Marshal(results); err == nil {
438437 script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))"
439438 if err := h.webView.InjectScript(script); err != nil {
440- log.Printf("[ERROR] Failed to inject initial history: %v", err)
439+ logging.Error(fmt.Sprintf("[ERROR] Failed to inject initial history: %v", err))
441440 }
442441 }
443442 }
@@ -495,7 +494,7 @@ func (h *Handler) handleWailsBridge(msg Message) {
495494 // handleTheme processes theme-related messages
496495 func (h *Handler) handleTheme(msg Message) {
497496 if msg.Value != "" && msg.Value != h.lastTheme {
498- log.Printf("[theme] color-scheme changed: %s", msg.Value)
497+ logging.Debug(fmt.Sprintf("[theme] color-scheme changed: %s", msg.Value))
499498 h.lastTheme = msg.Value
500499 }
501500 }
@@ -541,7 +540,7 @@ func (h *Handler) handleHistoryRecent(msg Message) {
541540 }
542541
543542 if err != nil {
544- log.Printf("[messaging] Failed to get recent history: %v", err)
543+ logging.Error(fmt.Sprintf("[messaging] Failed to get recent history: %v", err))
545544 requestIdParam := ""
546545 if msg.RequestID != "" {
547546 requestIdParam = ", '" + msg.RequestID + "'"
@@ -567,7 +566,7 @@ func (h *Handler) handleHistoryStats(_ Message) {
567566 ctx := context.Background()
568567 stats, err := h.browserService.GetHistoryStats(ctx)
569568 if err != nil {
570- log.Printf("[messaging] Failed to get history stats: %v", err)
569+ logging.Error(fmt.Sprintf("[messaging] Failed to get history stats: %v", err))
571570 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to load history stats')")
572571 return
573572 }
@@ -591,7 +590,7 @@ func (h *Handler) handleHistorySearch(msg Message) {
591590
592591 entries, err := h.browserService.SearchHistory(ctx, query, limit)
593592 if err != nil {
594- log.Printf("[messaging] Failed to search history: %v", err)
593+ logging.Error(fmt.Sprintf("[messaging] Failed to search history: %v", err))
595594 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to search history')")
596595 return
597596 }
@@ -607,7 +606,7 @@ func (h *Handler) handleHistoryDelete(msg Message) {
607606 }
608607
609608 if msg.HistoryID == "" {
610- log.Printf("[messaging] History delete: missing historyId")
609+ logging.Warn(fmt.Sprintf("[messaging] History delete: missing historyId"))
611610 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Missing history ID')")
612611 return
613612 }
@@ -615,7 +614,7 @@ func (h *Handler) handleHistoryDelete(msg Message) {
615614 // Convert string ID to int64
616615 id, err := strconv.ParseInt(msg.HistoryID, 10, 64)
617616 if err != nil {
618- log.Printf("[messaging] History delete: invalid ID format: %v", err)
617+ logging.Error(fmt.Sprintf("[messaging] History delete: invalid ID format: %v", err))
619618 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Invalid history ID format')")
620619 return
621620 }
@@ -623,7 +622,7 @@ func (h *Handler) handleHistoryDelete(msg Message) {
623622 ctx := context.Background()
624623 err = h.browserService.DeleteHistoryEntry(ctx, id)
625624 if err != nil {
626- log.Printf("[messaging] Failed to delete history entry: %v", err)
625+ logging.Error(fmt.Sprintf("[messaging] Failed to delete history entry: %v", err))
627626 _ = h.webView.InjectScript("window.__dumber_history_error && window.__dumber_history_error('Failed to delete history entry')")
628627 return
629628 }
@@ -650,7 +649,7 @@ func (h *Handler) handleConsoleMessage(msg Message) {
650649 }
651650
652651 if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil {
653- log.Printf("[messaging] Failed to unmarshal console-message: %v", err)
652+ logging.Error(fmt.Sprintf("[messaging] Failed to unmarshal console-message: %v", err))
654653 return
655654 }
656655
@@ -661,44 +660,44 @@ func (h *Handler) handleConsoleMessage(msg Message) {
661660 // handleWebViewIDRequest responds to JavaScript requests for the webview ID
662661 func (h *Handler) handleWebViewIDRequest(msg Message) {
663662 if h.webView == nil {
664- log.Printf("[messaging] Cannot provide webview ID - no webview available")
663+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide webview ID - no webview available"))
665664 return
666665 }
667666
668667 // Check if WebView is destroyed before attempting any operations
669668 if h.webView.IsDestroyed() {
670- log.Printf("[messaging] Cannot provide webview ID - webview is destroyed")
669+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide webview ID - webview is destroyed"))
671670 return
672671 }
673672
674673 webViewID := h.webView.ID()
675- log.Printf("[messaging] Sending webview ID %d to JavaScript", webViewID)
674+ logging.Debug(fmt.Sprintf("[messaging] Sending webview ID %d to JavaScript", webViewID))
676675
677676 // Send the webview ID back to JavaScript via custom event
678677 if err := h.webView.DispatchCustomEvent("dumber:webview-id", map[string]any{
679678 "webviewId": webViewID,
680679 "timestamp": time.Now().UnixMilli(),
681680 }); err != nil {
682- log.Printf("[messaging] Failed to send webview ID: %v", err)
681+ logging.Error(fmt.Sprintf("[messaging] Failed to send webview ID: %v", err))
683682 }
684683 }
685684
686685 // handleGetSearchShortcuts sends search shortcuts configuration to JavaScript
687686 func (h *Handler) handleGetSearchShortcuts(msg Message) {
688687 if h.webView == nil {
689- log.Printf("[messaging] Cannot provide search shortcuts - no webview available")
688+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide search shortcuts - no webview available"))
690689 return
691690 }
692691
693692 if h.webView.IsDestroyed() {
694- log.Printf("[messaging] Cannot provide search shortcuts - webview is destroyed")
693+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide search shortcuts - webview is destroyed"))
695694 return
696695 }
697696
698697 // Get search shortcuts from browser service config
699698 shortcuts, err := h.browserService.GetSearchShortcuts(context.Background())
700699 if err != nil {
701- log.Printf("[messaging] Failed to get search shortcuts: %v", err)
700+ logging.Error(fmt.Sprintf("[messaging] Failed to get search shortcuts: %v", err))
702701 _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to get search shortcuts')")
703702 return
704703 }
@@ -706,25 +705,25 @@ func (h *Handler) handleGetSearchShortcuts(msg Message) {
706705 // Marshal to JSON
707706 b, err := json.Marshal(shortcuts)
708707 if err != nil {
709- log.Printf("[messaging] Failed to marshal search shortcuts: %v", err)
708+ logging.Error(fmt.Sprintf("[messaging] Failed to marshal search shortcuts: %v", err))
710709 _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to load search shortcuts')")
711710 return
712711 }
713712
714713 // Inject the search shortcuts into the page
715- log.Printf("[messaging] Sending search shortcuts to JavaScript")
714+ logging.Debug(fmt.Sprintf("[messaging] Sending search shortcuts to JavaScript"))
716715 _ = h.webView.InjectScript("window.__dumber_search_shortcuts && window.__dumber_search_shortcuts(" + string(b) + ")")
717716 }
718717
719718 // handleGetColorPalettes sends color palettes configuration to JavaScript
720719 func (h *Handler) handleGetColorPalettes(msg Message) {
721720 if h.webView == nil {
722- log.Printf("[messaging] Cannot provide color palettes - no webview available")
721+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide color palettes - no webview available"))
723722 return
724723 }
725724
726725 if h.webView.IsDestroyed() {
727- log.Printf("[messaging] Cannot provide color palettes - webview is destroyed")
726+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide color palettes - webview is destroyed"))
728727 return
729728 }
730729
@@ -734,32 +733,32 @@ func (h *Handler) handleGetColorPalettes(msg Message) {
734733 // Marshal to JSON
735734 b, err := json.Marshal(palettes)
736735 if err != nil {
737- log.Printf("[messaging] Failed to marshal color palettes: %v", err)
736+ logging.Error(fmt.Sprintf("[messaging] Failed to marshal color palettes: %v", err))
738737 _ = h.webView.InjectScript("window.__dumber_color_palettes_error && window.__dumber_color_palettes_error('Failed to load color palettes')")
739738 return
740739 }
741740
742741 // Inject the color palettes into the page
743- log.Printf("[messaging] Sending color palettes to JavaScript")
742+ logging.Debug(fmt.Sprintf("[messaging] Sending color palettes to JavaScript"))
744743 _ = h.webView.InjectScript("window.__dumber_color_palettes && window.__dumber_color_palettes(" + string(b) + ")")
745744 }
746745
747746 // handleGetFavorites sends all favorites to JavaScript
748747 func (h *Handler) handleGetFavorites(msg Message) {
749748 if h.webView == nil {
750- log.Printf("[messaging] Cannot provide favorites - no webview available")
749+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide favorites - no webview available"))
751750 return
752751 }
753752
754753 if h.webView.IsDestroyed() {
755- log.Printf("[messaging] Cannot provide favorites - webview is destroyed")
754+ logging.Warn(fmt.Sprintf("[messaging] Cannot provide favorites - webview is destroyed"))
756755 return
757756 }
758757
759758 // Get favorites from browser service
760759 favorites, err := h.browserService.GetFavorites(context.Background())
761760 if err != nil {
762- log.Printf("[messaging] Failed to get favorites: %v", err)
761+ logging.Error(fmt.Sprintf("[messaging] Failed to get favorites: %v", err))
763762 _ = h.webView.InjectScript("window.__dumber_favorites_error && window.__dumber_favorites_error('Failed to get favorites')")
764763 return
765764 }
@@ -767,37 +766,37 @@ func (h *Handler) handleGetFavorites(msg Message) {
767766 // Marshal to JSON
768767 b, err := json.Marshal(favorites)
769768 if err != nil {
770- log.Printf("[messaging] Failed to marshal favorites: %v", err)
769+ logging.Error(fmt.Sprintf("[messaging] Failed to marshal favorites: %v", err))
771770 _ = h.webView.InjectScript("window.__dumber_favorites_error && window.__dumber_favorites_error('Failed to load favorites')")
772771 return
773772 }
774773
775774 // Inject the favorites into the page
776- log.Printf("[messaging] Sending %d favorites to JavaScript", len(favorites))
775+ logging.Debug(fmt.Sprintf("[messaging] Sending %d favorites to JavaScript", len(favorites)))
777776 _ = h.webView.InjectScript("window.__dumber_favorites && window.__dumber_favorites(" + string(b) + ")")
778777 }
779778
780779 // handleToggleFavorite adds or removes a URL from favorites
781780 func (h *Handler) handleToggleFavorite(msg Message) {
782781 if h.webView == nil {
783- log.Printf("[messaging] Cannot toggle favorite - no webview available")
782+ logging.Warn(fmt.Sprintf("[messaging] Cannot toggle favorite - no webview available"))
784783 return
785784 }
786785
787786 if h.webView.IsDestroyed() {
788- log.Printf("[messaging] Cannot toggle favorite - webview is destroyed")
787+ logging.Warn(fmt.Sprintf("[messaging] Cannot toggle favorite - webview is destroyed"))
789788 return
790789 }
791790
792791 if msg.URL == "" {
793- log.Printf("[messaging] Cannot toggle favorite - URL is empty")
792+ logging.Warn(fmt.Sprintf("[messaging] Cannot toggle favorite - URL is empty"))
794793 return
795794 }
796795
797796 // Toggle the favorite
798797 added, err := h.browserService.ToggleFavorite(context.Background(), msg.URL, msg.Title, msg.FaviconURL)
799798 if err != nil {
800- log.Printf("[messaging] Failed to toggle favorite for %s: %v", msg.URL, err)
799+ logging.Error(fmt.Sprintf("[messaging] Failed to toggle favorite for %s: %v", msg.URL, err))
801800 _ = h.webView.InjectScript("window.__dumber_favorite_toggled_error && window.__dumber_favorite_toggled_error('Failed to toggle favorite')")
802801 return
803802 }
@@ -809,35 +808,35 @@ func (h *Handler) handleToggleFavorite(msg Message) {
809808 }
810809 b, err := json.Marshal(result)
811810 if err != nil {
812- log.Printf("[messaging] Failed to marshal toggle result: %v", err)
811+ logging.Error(fmt.Sprintf("[messaging] Failed to marshal toggle result: %v", err))
813812 return
814813 }
815814
816- log.Printf("[messaging] Favorite toggled for %s (added: %v)", msg.URL, added)
815+ logging.Debug(fmt.Sprintf("[messaging] Favorite toggled for %s (added: %v)", msg.URL, added))
817816 _ = h.webView.InjectScript("window.__dumber_favorite_toggled && window.__dumber_favorite_toggled(" + string(b) + ")")
818817 }
819818
820819 // handleIsFavorite checks if a URL is favorited
821820 func (h *Handler) handleIsFavorite(msg Message) {
822821 if h.webView == nil {
823- log.Printf("[messaging] Cannot check favorite - no webview available")
822+ logging.Warn(fmt.Sprintf("[messaging] Cannot check favorite - no webview available"))
824823 return
825824 }
826825
827826 if h.webView.IsDestroyed() {
828- log.Printf("[messaging] Cannot check favorite - webview is destroyed")
827+ logging.Warn(fmt.Sprintf("[messaging] Cannot check favorite - webview is destroyed"))
829828 return
830829 }
831830
832831 if msg.URL == "" {
833- log.Printf("[messaging] Cannot check favorite - URL is empty")
832+ logging.Warn(fmt.Sprintf("[messaging] Cannot check favorite - URL is empty"))
834833 return
835834 }
836835
837836 // Check if the URL is favorited
838837 isFavorite, err := h.browserService.IsFavorite(context.Background(), msg.URL)
839838 if err != nil {
840- log.Printf("[messaging] Failed to check if favorite for %s: %v", msg.URL, err)
839+ logging.Error(fmt.Sprintf("[messaging] Failed to check if favorite for %s: %v", msg.URL, err))
841840 return
842841 }
843842
@@ -848,7 +847,7 @@ func (h *Handler) handleIsFavorite(msg Message) {
848847 }
849848 b, err := json.Marshal(result)
850849 if err != nil {
851- log.Printf("[messaging] Failed to marshal is_favorite result: %v", err)
850+ logging.Error(fmt.Sprintf("[messaging] Failed to marshal is_favorite result: %v", err))
852851 return
853852 }
854853
@@ -869,6 +868,6 @@ func (h *Handler) handleKeyboardBlocking(msg Message) {
869868 }
870869
871870 if err := h.webView.InjectScript(script); err != nil {
872- log.Printf("[ERROR] Failed to inject keyboard blocking script: %v", err)
871+ logging.Error(fmt.Sprintf("[ERROR] Failed to inject keyboard blocking script: %v", err))
873872 }
874873 }
M internal/cache/binary_format.go +2 -3
@@ -4,7 +4,6 @@ package cache
44 import (
55 "encoding/binary"
66 "fmt"
7- "log"
87 "os"
98 "syscall"
109 "unsafe"
@@ -214,7 +213,7 @@ func (c *DmenuFuzzyCache) writeHeader(data []byte, offset int, totalSize int) in
214213 logging.Warn(fmt.Sprintf("too many entries for binary format: %d, truncating", len(c.entries)))
215214 }
216215 if indexOffset < 0 {
217- log.Printf("Warning: negative index offset: %d, using 0", indexOffset)
216+ logging.Warn(fmt.Sprintf("Warning: negative index offset: %d, using 0", indexOffset))
218217 indexOffset = 0
219218 }
220219
@@ -446,7 +445,7 @@ func IsValidCacheFile(filename string) bool {
446445 }
447446 defer func() {
448447 if closeErr := file.Close(); closeErr != nil {
449- log.Printf("Warning: failed to close cache validation file: %v", closeErr)
448+ logging.Warn(fmt.Sprintf("Warning: failed to close cache validation file: %v", closeErr))
450449 }
451450 }()
452451
M internal/cache/filter_cache.go +3 -4
@@ -4,7 +4,6 @@ import (
44 "encoding/binary"
55 "fmt"
66 "io"
7- "log"
87 "os"
98 "syscall"
109 "time"
@@ -116,7 +115,7 @@ func (fc *FilterCache) LoadMapped() (*FilterData, error) {
116115 header, err := fc.parseHeader()
117116 if err != nil {
118117 if unmapErr := fc.Unmap(); unmapErr != nil {
119- log.Printf("[filter-cache] failed to unmap after header parse error: %v", unmapErr)
118+ logging.Warn(fmt.Sprintf("[filter-cache] failed to unmap after header parse error: %v", unmapErr))
120119 }
121120 return nil, fmt.Errorf("invalid cache header: %w", err)
122121 }
@@ -124,14 +123,14 @@ func (fc *FilterCache) LoadMapped() (*FilterData, error) {
124123 // Validate magic and version
125124 if header.Magic != cacheMagic {
126125 if unmapErr := fc.Unmap(); unmapErr != nil {
127- log.Printf("[filter-cache] failed to unmap after magic validation: %v", unmapErr)
126+ logging.Warn(fmt.Sprintf("[filter-cache] failed to unmap after magic validation: %v", unmapErr))
128127 }
129128 return nil, fmt.Errorf("invalid cache magic bytes")
130129 }
131130
132131 if header.Version != cacheFormatVersion {
133132 if unmapErr := fc.Unmap(); unmapErr != nil {
134- log.Printf("[filter-cache] failed to unmap after version validation: %v", unmapErr)
133+ logging.Warn(fmt.Sprintf("[filter-cache] failed to unmap after version validation: %v", unmapErr))
135134 }
136135 return nil, fmt.Errorf("unsupported cache version: %d", header.Version)
137136 }
M internal/cache/generic/cache.go +5 -3
@@ -2,8 +2,10 @@ package generic
22
33 import (
44 "context"
5- "log"
5+ "fmt"
66 "sync"
7+
8+ "github.com/bnema/dumber/internal/logging"
79 )
810
911 // Cache provides a generic interface for caching any type of data.
@@ -109,7 +111,7 @@ func (c *GenericCache[K, V]) Set(key K, value V) error {
109111
110112 ctx := context.Background()
111113 if err := c.dbOps.Persist(ctx, key, value); err != nil {
112- log.Printf("Warning: async persist failed for key %v: %v", key, err)
114+ logging.Warn(fmt.Sprintf("Warning: async persist failed for key %v: %v", key, err))
113115 }
114116 }()
115117
@@ -129,7 +131,7 @@ func (c *GenericCache[K, V]) Delete(key K) error {
129131
130132 ctx := context.Background()
131133 if err := c.dbOps.Delete(ctx, key); err != nil {
132- log.Printf("Warning: async delete failed for key %v: %v", key, err)
134+ logging.Warn(fmt.Sprintf("Warning: async delete failed for key %v: %v", key, err))
133135 }
134136 }()
135137
M internal/db/init.go +5 -5
@@ -3,10 +3,10 @@ package db
33 import (
44 "database/sql"
55 "fmt"
6- "log"
76 "os"
87 "path/filepath"
98
9+ "github.com/bnema/dumber/internal/logging"
1010 "github.com/bnema/dumber/internal/migrations"
1111 _ "github.com/ncruces/go-sqlite3/driver" // SQLite driver
1212 _ "github.com/ncruces/go-sqlite3/embed" // Embed SQLite
@@ -32,7 +32,7 @@ func InitDB(dbPath string) (*sql.DB, error) {
3232 // Test the connection
3333 if err := database.Ping(); err != nil {
3434 if err := database.Close(); err != nil {
35- log.Printf("Warning: failed to close database: %v", err)
35+ logging.Warn(fmt.Sprintf("Warning: failed to close database: %v", err))
3636 }
3737 return nil, fmt.Errorf("failed to connect to database: %w", err)
3838 }
@@ -50,19 +50,19 @@ func InitDB(dbPath string) (*sql.DB, error) {
5050 for _, pragma := range pragmas {
5151 if _, err := database.Exec(pragma); err != nil {
5252 if err := database.Close(); err != nil {
53- log.Printf("Warning: failed to close database: %v", err)
53+ logging.Warn(fmt.Sprintf("Warning: failed to close database: %v", err))
5454 }
5555 return nil, fmt.Errorf("failed to set pragma %q: %w", pragma, err)
5656 }
5757 }
5858
59- log.Printf("SQLite configured with WAL mode and performance optimizations")
59+ logging.Info(fmt.Sprintf("SQLite configured with WAL mode and performance optimizations"))
6060
6161 // Run embedded migrations - single source of truth for schema initialization
6262 // RunEmbeddedMigrations is idempotent and checks if migrations are already applied
6363 if err := migrations.RunEmbeddedMigrations(database); err != nil {
6464 if err := database.Close(); err != nil {
65- log.Printf("Warning: failed to close database: %v", err)
65+ logging.Warn(fmt.Sprintf("Warning: failed to close database: %v", err))
6666 }
6767 return nil, fmt.Errorf("failed to run database migrations: %w", err)
6868 }
M internal/filtering/updater.go +2 -3
@@ -4,7 +4,6 @@ import (
44 "context"
55 "crypto/sha256"
66 "fmt"
7- "log"
87 "net/http"
98 "strings"
109 "sync"
@@ -141,7 +140,7 @@ func (fu *FilterUpdater) checkSingleUpdate(ctx context.Context, url string) (*Fi
141140 }
142141 defer func() {
143142 if err := resp.Body.Close(); err != nil {
144- log.Printf("warning: failed to close response body: %v", err)
143+ logging.Warn(fmt.Sprintf("warning: failed to close response body: %v", err))
145144 }
146145 }()
147146
@@ -190,7 +189,7 @@ func (fu *FilterUpdater) downloadUpdate(ctx context.Context, url, lastModified,
190189 }
191190 defer func() {
192191 if err := resp.Body.Close(); err != nil {
193- log.Printf("warning: failed to close response body: %v", err)
192+ logging.Warn(fmt.Sprintf("warning: failed to close response body: %v", err))
194193 }
195194 }()
196195
M internal/migrations/migrations.go +11 -10
@@ -4,10 +4,11 @@ import (
44 "database/sql"
55 "embed"
66 "fmt"
7- "log"
87 "sort"
98 "strconv"
109 "strings"
10+
11+ "github.com/bnema/dumber/internal/logging"
1112 )
1213
1314 //go:embed *.sql
@@ -36,14 +37,14 @@ func GetMigrations() ([]Migration, error) {
3637 // Parse version from filename (e.g., "001_initial.sql" -> version 1)
3738 parts := strings.SplitN(entry.Name(), "_", 2)
3839 if len(parts) != 2 {
39- log.Printf("Warning: skipping migration file with invalid name format: %s", entry.Name())
40+ logging.Warn(fmt.Sprintf("Warning: skipping migration file with invalid name format: %s", entry.Name()))
4041 continue
4142 }
4243
4344 versionStr := parts[0]
4445 version, err := strconv.Atoi(versionStr)
4546 if err != nil {
46- log.Printf("Warning: skipping migration file with invalid version: %s", entry.Name())
47+ logging.Warn(fmt.Sprintf("Warning: skipping migration file with invalid version: %s", entry.Name()))
4748 continue
4849 }
4950
@@ -123,7 +124,7 @@ func applyMigration(db *sql.DB, migration Migration) error {
123124 return nil
124125 }
125126
126- log.Printf("Applying migration %03d: %s", migration.Version, migration.Name)
127+ logging.Info(fmt.Sprintf("Applying migration %03d: %s", migration.Version, migration.Name))
127128
128129 // Use transaction for safety
129130 tx, err := db.Begin()
@@ -133,7 +134,7 @@ func applyMigration(db *sql.DB, migration Migration) error {
133134 defer func() {
134135 if err != nil {
135136 if rollbackErr := tx.Rollback(); rollbackErr != nil {
136- log.Printf("failed to rollback migration transaction: %v", rollbackErr)
137+ logging.Error(fmt.Sprintf("failed to rollback migration transaction: %v", rollbackErr))
137138 }
138139 }
139140 }()
@@ -155,7 +156,7 @@ func applyMigration(db *sql.DB, migration Migration) error {
155156 return fmt.Errorf("failed to commit migration transaction: %w", err)
156157 }
157158
158- log.Printf("Successfully applied migration %03d: %s", migration.Version, migration.Name)
159+ logging.Info(fmt.Sprintf("Successfully applied migration %03d: %s", migration.Version, migration.Name))
159160 return nil
160161 }
161162
@@ -179,7 +180,7 @@ func GetAppliedMigrations(db *sql.DB) ([]int, error) {
179180 }
180181 defer func() {
181182 if err := rows.Close(); err != nil {
182- log.Printf("failed to close migration query rows: %v", err)
183+ logging.Warn(fmt.Sprintf("failed to close migration query rows: %v", err))
183184 }
184185 }()
185186
@@ -230,13 +231,13 @@ func VerifyAllMigrationsApplied(db *sql.DB) error {
230231
231232 // Report results
232233 if len(missingMigrations) > 0 {
233- log.Printf("WARNING: %d migrations are not applied:", len(missingMigrations))
234+ logging.Warn(fmt.Sprintf("WARNING: %d migrations are not applied:", len(missingMigrations)))
234235 for _, migration := range missingMigrations {
235- log.Printf(" - Migration %03d: %s", migration.Version, migration.Name)
236+ logging.Warn(fmt.Sprintf(" - Migration %03d: %s", migration.Version, migration.Name))
236237 }
237238 return fmt.Errorf("%d migrations are not applied", len(missingMigrations))
238239 }
239240
240- log.Printf("Migration verification: All %d migrations are applied", len(allMigrations))
241+ logging.Info(fmt.Sprintf("Migration verification: All %d migrations are applied", len(allMigrations)))
241242 return nil
242243 }
M internal/services/browser.go +19 -19
@@ -6,7 +6,6 @@ import (
66 "database/sql"
77 "embed"
88 "fmt"
9- "log"
109 "math"
1110 neturl "net/url"
1211 "sync"
@@ -15,6 +14,7 @@ import (
1514 "github.com/bnema/dumber/internal/cache"
1615 "github.com/bnema/dumber/internal/config"
1716 "github.com/bnema/dumber/internal/db"
17+ "github.com/bnema/dumber/internal/logging"
1818 "github.com/bnema/dumber/internal/parser"
1919 "github.com/bnema/dumber/pkg/webkit"
2020 )
@@ -126,14 +126,14 @@ func (s *BrowserService) AttachWebView(view *webkit.WebView) {
126126
127127 // LoadGUIBundle loads the unified GUI bundle from assets
128128 func (s *BrowserService) LoadGUIBundle(assets embed.FS) error {
129- log.Printf("[browser] Attempting to load GUI bundle from assets/gui/gui.min.js")
129+ logging.Debug(fmt.Sprintf("[browser] Attempting to load GUI bundle from assets/gui/gui.min.js"))
130130 bundleBytes, err := assets.ReadFile("assets/gui/gui.min.js")
131131 if err != nil {
132- log.Printf("[browser] ERROR: Failed to load GUI bundle: %v", err)
132+ logging.Error(fmt.Sprintf("[browser] ERROR: Failed to load GUI bundle: %v", err))
133133 return fmt.Errorf("failed to load GUI bundle: %w", err)
134134 }
135135 s.guiBundle = string(bundleBytes)
136- log.Printf("[browser] GUI bundle loaded successfully into browser service, size: %d bytes", len(bundleBytes))
136+ logging.Info(fmt.Sprintf("[browser] GUI bundle loaded successfully into browser service, size: %d bytes", len(bundleBytes)))
137137 return nil
138138 }
139139
@@ -438,7 +438,7 @@ func (s *BrowserService) recordHistory(ctx context.Context, url, title string) e
438438 return nil
439439 default:
440440 // Queue full, write directly (fallback)
441- log.Printf("Warning: history queue full, writing directly")
441+ logging.Warn(fmt.Sprintf("Warning: history queue full, writing directly"))
442442 return s.dbQueries.AddOrUpdateHistory(ctx, normalizedURL, titleNull)
443443 }
444444 }
@@ -463,11 +463,11 @@ func (s *BrowserService) processHistoryQueue() {
463463 // but we can still batch the operations to reduce overhead
464464 for _, update := range batch {
465465 if err := s.dbQueries.AddOrUpdateHistory(ctx, update.url, update.title); err != nil {
466- log.Printf("Warning: failed to write history for %s: %v", update.url, err)
466+ logging.Warn(fmt.Sprintf("Warning: failed to write history for %s: %v", update.url, err))
467467 }
468468 }
469469
470- log.Printf("Flushed %d history updates to database", len(batch))
470+ logging.Debug(fmt.Sprintf("Flushed %d history updates to database", len(batch)))
471471
472472 // Track history entries for fuzzy cache refresh
473473 batchSize := len(batch)
@@ -481,8 +481,8 @@ func (s *BrowserService) processHistoryQueue() {
481481 timeSinceRefresh := time.Since(s.lastCacheRefresh)
482482
483483 if s.historySinceRefresh >= minEntries && timeSinceRefresh >= minInterval {
484- log.Printf("[cache] Triggering async fuzzy cache refresh (%d new entries, %v since last refresh)",
485- s.historySinceRefresh, timeSinceRefresh)
484+ logging.Debug(fmt.Sprintf("[cache] Triggering async fuzzy cache refresh (%d new entries, %v since last refresh)",
485+ s.historySinceRefresh, timeSinceRefresh))
486486
487487 // Async refresh in background (non-blocking)
488488 s.fuzzyCache.InvalidateAndRefresh(ctx)
@@ -524,10 +524,10 @@ func (s *BrowserService) FlushHistoryQueue(ctx context.Context) error {
524524 // Wait for completion with context cancellation support
525525 select {
526526 case <-s.historyFlushDone:
527- log.Printf("History queue flushed and closed")
527+ logging.Info(fmt.Sprintf("History queue flushed and closed"))
528528 case <-ctx.Done():
529529 flushErr = fmt.Errorf("history queue flush cancelled: %w", ctx.Err())
530- log.Printf("Warning: %v", flushErr)
530+ logging.Warn(fmt.Sprintf("Warning: %v", flushErr))
531531 }
532532 })
533533 return flushErr
@@ -581,7 +581,7 @@ func (s *BrowserService) LoadZoomCacheFromDB(ctx context.Context) error {
581581 }
582582
583583 loadedCount := len(s.zoomCache.List())
584- log.Printf("Loaded %d zoom levels into cache", loadedCount)
584+ logging.Info(fmt.Sprintf("Loaded %d zoom levels into cache", loadedCount))
585585 return nil
586586 }
587587
@@ -593,7 +593,7 @@ func (s *BrowserService) LoadCertCacheFromDB(ctx context.Context) error {
593593 }
594594
595595 loadedCount := len(s.certCache.List())
596- log.Printf("Loaded %d certificate validations into cache", loadedCount)
596+ logging.Info(fmt.Sprintf("Loaded %d certificate validations into cache", loadedCount))
597597 return nil
598598 }
599599
@@ -607,8 +607,8 @@ func (s *BrowserService) LoadFuzzyCacheFromDB(ctx context.Context) error {
607607 }
608608
609609 stats := s.fuzzyCache.Stats()
610- log.Printf("Loaded fuzzy cache: %d entries, %d trigrams",
611- stats.EntryCount, stats.TrigramCount)
610+ logging.Info(fmt.Sprintf("Loaded fuzzy cache: %d entries, %d trigrams",
611+ stats.EntryCount, stats.TrigramCount))
612612 return nil
613613 }
614614
@@ -620,7 +620,7 @@ func (s *BrowserService) LoadFavoritesCacheFromDB(ctx context.Context) error {
620620 }
621621
622622 loadedCount := len(s.favoritesCache.List())
623- log.Printf("Loaded %d favorites into cache", loadedCount)
623+ logging.Info(fmt.Sprintf("Loaded %d favorites into cache", loadedCount))
624624 return nil
625625 }
626626
@@ -676,7 +676,7 @@ func (s *BrowserService) ToggleFavorite(ctx context.Context, url, title, favicon
676676 if err := s.favoritesCache.Delete(url); err != nil {
677677 return false, fmt.Errorf("failed to remove favorite: %w", err)
678678 }
679- log.Printf("Removed favorite: %s", url)
679+ logging.Info(fmt.Sprintf("Removed favorite: %s", url))
680680 return false, nil
681681 }
682682
@@ -691,7 +691,7 @@ func (s *BrowserService) ToggleFavorite(ctx context.Context, url, title, favicon
691691 return false, fmt.Errorf("failed to add favorite: %w", err)
692692 }
693693
694- log.Printf("Added favorite: %s", url)
694+ logging.Info(fmt.Sprintf("Added favorite: %s", url))
695695 return true, nil
696696 }
697697
@@ -721,7 +721,7 @@ func (s *BrowserService) FlushAllCaches(ctx context.Context) error {
721721 return fmt.Errorf("failed to flush history queue: %w", err)
722722 }
723723
724- log.Printf("All caches flushed successfully")
724+ logging.Info(fmt.Sprintf("All caches flushed successfully"))
725725 return nil
726726 }
727727
M internal/services/favicon_service.go +13 -13
@@ -6,13 +6,13 @@ import (
66 "database/sql"
77 "errors"
88 "fmt"
9- "log"
109 "os"
1110 "path/filepath"
1211 "sync"
1312 "time"
1413
1514 "github.com/bnema/dumber/internal/db"
15+ "github.com/bnema/dumber/internal/logging"
1616 "github.com/bnema/dumber/internal/parser"
1717 "github.com/bnema/dumber/pkg/webkit"
1818 "github.com/diamondburned/gotk4/pkg/gdk/v4"
@@ -83,11 +83,11 @@ func NewFaviconService(faviconDB *webkit.FaviconDatabase, queries db.DatabaseQue
8383 // The FaviconDatabase is shared across all WebViews (at NetworkSession level)
8484 faviconDB.ConnectFaviconChanged(func(pageURI, faviconURI string) {
8585 if err := fs.OnFaviconChanged(pageURI, faviconURI); err != nil {
86- log.Printf("[favicon] Handler error for %s: %v", pageURI, err)
86+ logging.Error(fmt.Sprintf("[favicon] Handler error for %s: %v", pageURI, err))
8787 }
8888 })
8989
90- log.Printf("[favicon] Registered single shared favicon handler at FaviconService level")
90+ logging.Info(fmt.Sprintf("[favicon] Registered single shared favicon handler at FaviconService level"))
9191
9292 return fs, nil
9393 }
@@ -133,14 +133,14 @@ func (fs *FaviconService) OnFaviconChanged(pageURL, faviconURI string) error {
133133 }
134134
135135 // NEW favicon detected (first time or actual change)
136- log.Printf("[favicon] Favicon changed for %s: %s", pageURL, faviconURI)
136+ logging.Info(fmt.Sprintf("[favicon] Favicon changed for %s: %s", pageURL, faviconURI))
137137
138138 ctx := context.Background()
139139
140140 // Update our history database with the favicon URI
141141 nullString := sql.NullString{String: faviconURI, Valid: faviconURI != ""}
142142 if err := fs.dbQueries.UpdateHistoryFavicon(ctx, nullString, canonicalURL); err != nil {
143- log.Printf("[favicon] Failed to update favicon URI in database for %s: %v", pageURL, err)
143+ logging.Error(fmt.Sprintf("[favicon] Failed to update favicon URI in database for %s: %v", pageURL, err))
144144 return fmt.Errorf("failed to update favicon in database: %w", err)
145145 }
146146
@@ -182,7 +182,7 @@ func (fs *FaviconService) propagateFaviconToDomain(pageURL, faviconURI string) {
182182 domainPattern := sql.NullString{String: "%://" + parsedURL.Host + "%", Valid: true}
183183 entries, err := fs.dbQueries.SearchHistory(ctx, domainPattern, sql.NullString{}, 1000)
184184 if err != nil {
185- log.Printf("[favicon] Failed to search history for domain %s: %v", parsedURL.Host, err)
185+ logging.Error(fmt.Sprintf("[favicon] Failed to search history for domain %s: %v", parsedURL.Host, err))
186186 return
187187 }
188188
@@ -205,9 +205,9 @@ func (fs *FaviconService) propagateFaviconToDomain(pageURL, faviconURI string) {
205205 for _, entry := range entries {
206206 if entry.FaviconUrl.String == "" {
207207 if err := fs.dbQueries.UpdateHistoryFavicon(ctx, faviconNull, entry.Url); err != nil {
208- log.Printf("[favicon] Failed to propagate favicon to %s: %v", entry.Url, err)
208+ logging.Error(fmt.Sprintf("[favicon] Failed to propagate favicon to %s: %v", entry.Url, err))
209209 } else {
210- log.Printf("[favicon] Propagated favicon to %s", entry.Url)
210+ logging.Debug(fmt.Sprintf("[favicon] Propagated favicon to %s", entry.Url))
211211 }
212212 }
213213 }
@@ -222,12 +222,12 @@ func (fs *FaviconService) preloadFavicon(pageURL, canonicalURL, faviconURI strin
222222
223223 texture, err := fs.renderFaviconTexture(ctx, pageURL, faviconURI)
224224 if err != nil {
225- log.Printf("[favicon] Preload failed for %s: %v", pageURL, err)
225+ logging.Warn(fmt.Sprintf("[favicon] Preload failed for %s: %v", pageURL, err))
226226 return
227227 }
228228
229229 fs.maybeExportTexture(canonicalURL, texture)
230- log.Printf("[favicon] Preloaded and exported favicon for %s", pageURL)
230+ logging.Debug(fmt.Sprintf("[favicon] Preloaded and exported favicon for %s", pageURL))
231231 }
232232
233233 // GetFaviconTexture loads a favicon texture asynchronously and calls the callback with the result.
@@ -374,7 +374,7 @@ func (fs *FaviconService) lookupFaviconURI(ctx context.Context, canonicalURL, or
374374 continue
375375 }
376376 if !errors.Is(err, sql.ErrNoRows) {
377- log.Printf("[favicon] failed to query history entry for %s: %v", candidate, err)
377+ logging.Error(fmt.Sprintf("[favicon] failed to query history entry for %s: %v", candidate, err))
378378 }
379379 }
380380
@@ -417,7 +417,7 @@ func (fs *FaviconService) maybeExportTexture(pageURL string, texture *gdk.Textur
417417
418418 _ = webkit.IdleAdd(func() bool {
419419 if err := webkit.SaveTextureAsPNG(texture, exportPath, fs.targetSize); err != nil {
420- log.Printf("[favicon] Failed to export favicon PNG for %s: %v", pageURL, err)
420+ logging.Error(fmt.Sprintf("[favicon] Failed to export favicon PNG for %s: %v", pageURL, err))
421421 }
422422 return false
423423 })
@@ -473,7 +473,7 @@ func (fs *FaviconService) getExportPath(pageURL string) string {
473473 func (fs *FaviconService) shouldProcessURL(pageURL string) bool {
474474 parsedURL, err := webkit.ParseURL(pageURL)
475475 if err != nil {
476- log.Printf("[favicon] Invalid page URL: %s", pageURL)
476+ logging.Warn(fmt.Sprintf("[favicon] Invalid page URL: %s", pageURL))
477477 return false
478478 }
479479
M main.go +6 -4
@@ -2,7 +2,7 @@ package main
22
33 import (
44 "embed"
5- "log"
5+ "fmt"
66 "os"
77 "runtime"
88
@@ -35,7 +35,8 @@ func main() {
3535
3636 // Load config file first, then initialize logging with actual config
3737 if err := config.Init(); err != nil {
38- log.Printf("Warning: failed to load config: %v, using defaults", err)
38+ // Use fmt.Printf for early errors before logging is initialized
39+ fmt.Printf("Warning: failed to load config: %v, using defaults\n", err)
3940 }
4041 cfg := config.Get()
4142
@@ -49,10 +50,11 @@ func main() {
4950 cfg.Logging.MaxAge,
5051 cfg.Logging.Compress,
5152 ); err != nil {
52- log.Printf("Warning: failed to initialize logging: %v", err)
53+ // Use fmt.Printf for early errors before logging is initialized
54+ fmt.Printf("Warning: failed to initialize logging: %v\n", err)
5355 }
5456
55- log.Printf("Starting dumber browser v%s (commit: %s, built: %s)", version, commit, buildDate)
57+ logging.Info(fmt.Sprintf("Starting dumber browser v%s (commit: %s, built: %s)", version, commit, buildDate))
5658
5759 // GTK requires all UI calls to run on the main OS thread
5860 if webkit.IsNativeAvailable() {
M pkg/webkit/content_blocking.go +12 -12
@@ -3,10 +3,10 @@ package webkit
33 import (
44 "context"
55 "fmt"
6- "log"
76 "path/filepath"
87 "sync"
98
9+ "github.com/bnema/dumber/internal/logging"
1010 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1111 "github.com/diamondburned/gotk4/pkg/gio/v2"
1212 "github.com/diamondburned/gotk4/pkg/glib/v2"
@@ -31,7 +31,7 @@ func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, err
3131 return nil, fmt.Errorf("failed to create UserContentFilterStore")
3232 }
3333
34- log.Printf("[webkit] Created UserContentFilterStore at: %s", storagePath)
34+ logging.Debug(fmt.Sprintf("[webkit] Created UserContentFilterStore at: %s", storagePath))
3535
3636 return &ContentBlockingManager{
3737 filterStore: filterStore,
@@ -49,7 +49,7 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [
4949 return
5050 }
5151
52- log.Printf("[webkit] Compiling content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules))
52+ logging.Debug(fmt.Sprintf("[webkit] Compiling content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)))
5353
5454 // Store identifier for later use
5555 cbm.mu.Lock()
@@ -64,7 +64,7 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [
6464 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
6565 filter, err := cbm.filterStore.SaveFinish(result)
6666 if err != nil {
67- log.Printf("[webkit] Failed to compile content filter: %v", err)
67+ logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
6868 if onComplete != nil {
6969 onComplete(fmt.Errorf("failed to compile filter: %w", err))
7070 }
@@ -72,7 +72,7 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [
7272 }
7373
7474 if filter == nil {
75- log.Printf("[webkit] Filter compilation returned nil")
75+ logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
7676 if onComplete != nil {
7777 onComplete(fmt.Errorf("filter compilation returned nil"))
7878 }
@@ -84,7 +84,7 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [
8484 cbm.compiledFilter = filter
8585 cbm.mu.Unlock()
8686
87- log.Printf("[webkit] Filter compilation complete: %s", identifier)
87+ logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))
8888 if onComplete != nil {
8989 onComplete(nil)
9090 }
@@ -131,7 +131,7 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
131131 filter := cbm.compiledFilter
132132 cbm.mu.RUnlock()
133133 ucm.AddFilter(filter)
134- log.Printf("[webkit] Applied cached content filter to UCM")
134+ logging.Debug(fmt.Sprintf("[webkit] Applied cached content filter to UCM"))
135135 return nil
136136 }
137137 cbm.mu.RUnlock()
@@ -140,7 +140,7 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
140140 return fmt.Errorf("empty filter rules")
141141 }
142142
143- log.Printf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules))
143+ logging.Debug(fmt.Sprintf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)))
144144
145145 // Convert []byte to *glib.Bytes for WebKit API
146146 gBytes := glib.NewBytesWithGo(jsonRules)
@@ -151,13 +151,13 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
151151 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
152152 filter, err := cbm.filterStore.SaveFinish(result)
153153 if err != nil {
154- log.Printf("[webkit] Failed to compile content filter: %v", err)
154+ logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
155155 done <- fmt.Errorf("failed to compile filter: %w", err)
156156 return
157157 }
158158
159159 if filter == nil {
160- log.Printf("[webkit] Filter compilation returned nil")
160+ logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
161161 done <- fmt.Errorf("filter compilation returned nil")
162162 return
163163 }
@@ -170,7 +170,7 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
170170
171171 RunOnMainThread(func() {
172172 ucm.AddFilter(filter)
173- log.Printf("[webkit] Successfully added content filter: %s", identifier)
173+ logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
174174 done <- nil
175175 })
176176 })
@@ -182,7 +182,7 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
182182 func (cbm *ContentBlockingManager) RemoveAllFilters(ucm *webkit.UserContentManager) {
183183 if ucm != nil {
184184 ucm.RemoveAllFilters()
185- log.Printf("[webkit] Removed all content filters")
185+ logging.Debug(fmt.Sprintf("[webkit] Removed all content filters"))
186186 }
187187 }
188188
M pkg/webkit/keyboard_bridge.go +10 -10
@@ -3,8 +3,8 @@ package webkit
33 import (
44 "encoding/json"
55 "fmt"
6- "log"
76
7+ "github.com/bnema/dumber/internal/logging"
88 "github.com/diamondburned/gotk4/pkg/gdk/v4"
99 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1010 )
@@ -221,7 +221,7 @@ func (w *WebView) AttachKeyboardBridge() {
221221 handler := w.onPaneModeShortcut
222222 w.mu.RUnlock()
223223 if handler != nil && handler(actionEnter) {
224- log.Printf("[keyboard-bridge] Pane mode: enter")
224+ logging.Debug(fmt.Sprintf("[keyboard-bridge] Pane mode: enter"))
225225 return true
226226 }
227227 return true // Block even if no handler
@@ -233,7 +233,7 @@ func (w *WebView) AttachKeyboardBridge() {
233233 handler := w.onPaneModeShortcut
234234 w.mu.RUnlock()
235235 if handler != nil && handler(actionExit) {
236- log.Printf("[keyboard-bridge] Escape: exited mode")
236+ logging.Debug(fmt.Sprintf("[keyboard-bridge] Escape: exited mode"))
237237 return true
238238 }
239239 return false // Pass through to DOM if no active mode
@@ -247,7 +247,7 @@ func (w *WebView) AttachKeyboardBridge() {
247247 handler := w.onWorkspaceNavigation
248248 w.mu.RUnlock()
249249 if handler != nil && handler(action) {
250- log.Printf("[keyboard-bridge] Navigation: %s", action)
250+ logging.Debug(fmt.Sprintf("[keyboard-bridge] Navigation: %s", action))
251251 return true
252252 }
253253 return true // Block anyway to prevent scrolling
@@ -258,7 +258,7 @@ func (w *WebView) AttachKeyboardBridge() {
258258 handler := w.onPaneModeShortcut
259259 w.mu.RUnlock()
260260 if handler != nil && handler(action) {
261- log.Printf("[keyboard-bridge] Pane mode action: %s", action)
261+ logging.Debug(fmt.Sprintf("[keyboard-bridge] Pane mode action: %s", action))
262262 return true
263263 }
264264 return false // Not in pane mode, pass through
@@ -266,26 +266,26 @@ func (w *WebView) AttachKeyboardBridge() {
266266
267267 // Pane mode active: block all other shortcuts
268268 if isPaneModeActive {
269- log.Printf("[keyboard-bridge] Blocking '%s' during pane mode", shortcut)
269+ logging.Debug(fmt.Sprintf("[keyboard-bridge] Blocking '%s' during pane mode", shortcut))
270270 return true
271271 }
272272
273273 // Forward to JavaScript using proper JSON marshaling to prevent any potential injection
274274 shortcutJSON, err := json.Marshal(shortcut)
275275 if err != nil {
276- log.Printf("[keyboard-bridge] Failed to marshal shortcut: %v", err)
276+ logging.Error(fmt.Sprintf("[keyboard-bridge] Failed to marshal shortcut: %v", err))
277277 return false
278278 }
279279 script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:%s}}));`, shortcutJSON)
280280 if err := w.InjectScript(script); err != nil {
281- log.Printf("[keyboard-bridge] Failed to dispatch: %v", err)
281+ logging.Error(fmt.Sprintf("[keyboard-bridge] Failed to dispatch: %v", err))
282282 }
283283 return false // Allow WebKit to handle
284284 }
285285
286286 // Pane mode active: block ALL non-shortcut keys
287287 if isPaneModeActive {
288- log.Printf("[keyboard-bridge] Blocking key during pane mode: keyval=%d", keyval)
288+ logging.Debug(fmt.Sprintf("[keyboard-bridge] Blocking key during pane mode: keyval=%d", keyval))
289289 return true // Block all other keys
290290 }
291291
@@ -295,5 +295,5 @@ func (w *WebView) AttachKeyboardBridge() {
295295
296296 // Attach controller to WebView
297297 w.view.AddController(keyController)
298- log.Printf("[keyboard-bridge] EventControllerKey attached to WebView ID %d", w.id)
298+ logging.Debug(fmt.Sprintf("[keyboard-bridge] EventControllerKey attached to WebView ID %d", w.id))
299299 }
M pkg/webkit/mouse_gestures.go +8 -7
@@ -1,8 +1,9 @@
11 package webkit
22
33 import (
4- "log"
4+ "fmt"
55
6+ "github.com/bnema/dumber/internal/logging"
67 "github.com/diamondburned/gotk4/pkg/gtk/v4"
78 )
89
@@ -17,7 +18,7 @@ const (
1718 // This enables hardware mouse button navigation (back/forward buttons)
1819 func (w *WebView) AttachMouseGestures() {
1920 if w.view == nil {
20- log.Printf("[mouse-gestures] Cannot attach: WebView is nil")
21+ logging.Error(fmt.Sprintf("[mouse-gestures] Cannot attach: WebView is nil"))
2122 return
2223 }
2324
@@ -43,18 +44,18 @@ func (w *WebView) AttachMouseGestures() {
4344 switch button {
4445 case mouseButtonBack:
4546 if w.view.CanGoBack() {
46- log.Printf("[mouse-gestures] Back button clicked - navigating backward")
47+ logging.Debug(fmt.Sprintf("[mouse-gestures] Back button clicked - navigating backward"))
4748 w.view.GoBack()
4849 } else {
49- log.Printf("[mouse-gestures] Back button clicked but cannot go back")
50+ logging.Debug(fmt.Sprintf("[mouse-gestures] Back button clicked but cannot go back"))
5051 }
5152
5253 case mouseButtonForward:
5354 if w.view.CanGoForward() {
54- log.Printf("[mouse-gestures] Forward button clicked - navigating forward")
55+ logging.Debug(fmt.Sprintf("[mouse-gestures] Forward button clicked - navigating forward"))
5556 w.view.GoForward()
5657 } else {
57- log.Printf("[mouse-gestures] Forward button clicked but cannot go forward")
58+ logging.Debug(fmt.Sprintf("[mouse-gestures] Forward button clicked but cannot go forward"))
5859 }
5960 }
6061 })
@@ -62,5 +63,5 @@ func (w *WebView) AttachMouseGestures() {
6263 // Attach controller to WebView
6364 w.view.AddController(gestureClick)
6465
65- log.Printf("[mouse-gestures] Mouse gesture controller attached to WebView ID %d", w.id)
66+ logging.Debug(fmt.Sprintf("[mouse-gestures] Mouse gesture controller attached to WebView ID %d", w.id))
6667 }
M pkg/webkit/network_session.go +7 -7
@@ -2,8 +2,8 @@ package webkit
22
33 import (
44 "fmt"
5- "log"
65
6+ "github.com/bnema/dumber/internal/logging"
77 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
88 )
99
@@ -25,7 +25,7 @@ func InitPersistentSession(dataDir, cacheDir string) error {
2525 // If session already exists, return success (idempotent)
2626 // This ensures we only create ONE session for the entire application
2727 if globalNetworkSession != nil {
28- log.Printf("[webkit] Using existing persistent network session")
28+ logging.Debug(fmt.Sprintf("[webkit] Using existing persistent network session"))
2929 return nil
3030 }
3131
@@ -40,7 +40,7 @@ func InitPersistentSession(dataDir, cacheDir string) error {
4040 // This is CRITICAL - if the session is GC'd, WebKit falls back to ephemeral storage
4141 globalNetworkSession = session
4242
43- log.Printf("[webkit] Persistent network session created: data=%s, cache=%s", dataDir, cacheDir)
43+ logging.Debug(fmt.Sprintf("[webkit] Persistent network session created: data=%s, cache=%s", dataDir, cacheDir))
4444
4545 // Verify the session is not ephemeral
4646 if session.IsEphemeral() {
@@ -55,7 +55,7 @@ func InitPersistentSession(dataDir, cacheDir string) error {
5555 if websiteDataManager.IsEphemeral() {
5656 return fmt.Errorf("website data manager is ephemeral despite providing data directories")
5757 }
58- log.Printf("[webkit] WebsiteDataManager verified as non-ephemeral")
58+ logging.Debug(fmt.Sprintf("[webkit] WebsiteDataManager verified as non-ephemeral"))
5959
6060 // Configure CookieManager for persistent cookie storage
6161 // This is REQUIRED - without this, cookies are not persisted to disk
@@ -72,7 +72,7 @@ func InitPersistentSession(dataDir, cacheDir string) error {
7272 // Set cookie accept policy to no third-party (default)
7373 cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
7474
75- log.Printf("[webkit] Cookie manager configured: storage=%s, policy=no-third-party", cookiePath)
75+ logging.Debug(fmt.Sprintf("[webkit] Cookie manager configured: storage=%s, policy=no-third-party", cookiePath))
7676
7777 // Enable persistent credential storage (HTTP auth, etc.)
7878 session.SetPersistentCredentialStorageEnabled(true)
@@ -80,7 +80,7 @@ func InitPersistentSession(dataDir, cacheDir string) error {
8080 // Configure TLS errors to emit signals instead of failing silently
8181 // This enables the load-failed-with-tls-errors signal
8282 session.SetTLSErrorsPolicy(webkit.TLSErrorsPolicyFail)
83- log.Printf("[webkit] TLS errors policy set to FAIL (signals enabled)")
83+ logging.Debug(fmt.Sprintf("[webkit] TLS errors policy set to FAIL (signals enabled)"))
8484
8585 // Verify this is indeed the default session now
8686 defaultSession := webkit.NetworkSessionGetDefault()
@@ -93,7 +93,7 @@ func InitPersistentSession(dataDir, cacheDir string) error {
9393 return fmt.Errorf("default network session is ephemeral after creating persistent session")
9494 }
9595
96- log.Printf("[webkit] Network session verified as persistent and set as default")
96+ logging.Debug(fmt.Sprintf("[webkit] Network session verified as persistent and set as default"))
9797 return nil
9898 }
9999
M pkg/webkit/shortcuts.go +6 -5
@@ -1,9 +1,10 @@
11 package webkit
22
33 import (
4- "log"
4+ "fmt"
55 "strings"
66
7+ "github.com/bnema/dumber/internal/logging"
78 "github.com/diamondburned/gotk4/pkg/gdk/v4"
89 "github.com/diamondburned/gotk4/pkg/glib/v2"
910 "github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -62,7 +63,7 @@ func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func())
6263 // Convert to GTK format (e.g., "ctrl+plus" -> "<Control>plus")
6364 gtkFormat := convertToGtkFormat(accelerator)
6465 if gtkFormat == "" {
65- log.Printf("[shortcuts] Invalid key format: %s", accelerator)
66+ logging.Warn(fmt.Sprintf("[shortcuts] Invalid key format: %s", accelerator))
6667 return
6768 }
6869
@@ -70,7 +71,7 @@ func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func())
7071 // ShortcutFunc signature: func(widget Widgetter, args *glib.Variant) (ok bool)
7172 action := gtk.NewCallbackAction(func(widget gtk.Widgetter, args *glib.Variant) bool {
7273 if handler != nil {
73- log.Printf("[shortcuts] Shortcut triggered: %s", accelerator)
74+ logging.Debug(fmt.Sprintf("[shortcuts] Shortcut triggered: %s", accelerator))
7475 handler()
7576 }
7677 // Return false to allow event propagation to WebView
@@ -82,7 +83,7 @@ func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func())
8283 // GTK4 uses gtk.ShortcutTrigger for key combinations
8384 trigger := gtk.NewShortcutTriggerParseString(gtkFormat)
8485 if trigger == nil {
85- log.Printf("[shortcuts] Failed to parse trigger: %s", gtkFormat)
86+ logging.Error(fmt.Sprintf("[shortcuts] Failed to parse trigger: %s", gtkFormat))
8687 return
8788 }
8889
@@ -95,7 +96,7 @@ func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func())
9596 // Store the shortcut for potential cleanup
9697 ws.shortcuts[accelerator] = shortcut
9798
98- log.Printf("[shortcuts] Registered: %s -> %s", accelerator, gtkFormat)
99+ logging.Debug(fmt.Sprintf("[shortcuts] Registered: %s -> %s", accelerator, gtkFormat))
99100 }
100101
101102 // convertToGtkFormat converts common key formats to GTK shortcut format
M pkg/webkit/tls_handler.go +29 -29
@@ -5,13 +5,13 @@ import (
55 "database/sql"
66 "errors"
77 "fmt"
8- "log"
98 "net/url"
109 "strings"
1110 "time"
1211
1312 "github.com/bnema/dumber/internal/config"
1413 "github.com/bnema/dumber/internal/db"
14+ "github.com/bnema/dumber/internal/logging"
1515 "github.com/diamondburned/gotk4/pkg/gio/v2"
1616 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1717 )
@@ -29,12 +29,12 @@ type TLSCertificateInfo struct {
2929 // setupTLSErrorHandler sets up the TLS error handler for the WebView
3030 func (wv *WebView) setupTLSErrorHandler() {
3131 wv.view.ConnectLoadFailedWithTLSErrors(func(failingUri string, certificate gio.TLSCertificater, errors gio.TLSCertificateFlags) bool {
32- log.Printf("[tls] Load failed with TLS errors for: %s, errors: %v", failingUri, errors)
32+ logging.Warn(fmt.Sprintf("[tls] Load failed with TLS errors for: %s, errors: %v", failingUri, errors))
3333
3434 // Extract hostname from URI
3535 hostname := extractHostname(failingUri)
3636 if hostname == "" {
37- log.Printf("[tls] Failed to extract hostname from URI: %s", failingUri)
37+ logging.Error(fmt.Sprintf("[tls] Failed to extract hostname from URI: %s", failingUri))
3838 return false // Let WebKit handle it
3939 }
4040
@@ -44,17 +44,17 @@ func (wv *WebView) setupTLSErrorHandler() {
4444 decision, exists := checkStoredCertificateDecision(hostname)
4545 if exists {
4646 if decision == "accepted" {
47- log.Printf("[tls] Certificate previously accepted for %s, allowing", hostname)
47+ logging.Debug(fmt.Sprintf("[tls] Certificate previously accepted for %s, allowing", hostname))
4848 wv.allowCertificateAndReload(certificate, hostname, failingUri)
4949 return true // Signal handled
5050 } else {
51- log.Printf("[tls] Certificate previously rejected for %s, blocking", hostname)
51+ logging.Debug(fmt.Sprintf("[tls] Certificate previously rejected for %s, blocking", hostname))
5252 return false // Let WebKit show error
5353 }
5454 }
5555
5656 // No stored decision - show dialog to user
57- log.Printf("[tls] No stored decision for %s, showing dialog", hostname)
57+ logging.Debug(fmt.Sprintf("[tls] No stored decision for %s, showing dialog", hostname))
5858 wv.showTLSErrorDialog(failingUri, hostname, certificate, errors)
5959
6060 return true // Signal handled - we're managing the error
@@ -87,24 +87,24 @@ func extractHostname(uri string) string {
8787 func checkStoredCertificateDecision(hostname string) (string, bool) {
8888 cfg := config.Get()
8989 if cfg == nil {
90- log.Printf("[tls] Config not available, cannot check stored decisions")
90+ logging.Error(fmt.Sprintf("[tls] Config not available, cannot check stored decisions"))
9191 return "", false
9292 }
9393
9494 dbPath := cfg.Database.Path
9595 if dbPath == "" {
96- log.Printf("[tls] Database path not configured")
96+ logging.Error(fmt.Sprintf("[tls] Database path not configured"))
9797 return "", false
9898 }
9999
100100 database, err := sql.Open("sqlite3", dbPath)
101101 if err != nil {
102- log.Printf("[tls] Failed to open database: %v", err)
102+ logging.Error(fmt.Sprintf("[tls] Failed to open database: %v", err))
103103 return "", false
104104 }
105105 defer func() {
106106 if err := database.Close(); err != nil {
107- log.Printf("[tls] warning: failed to close database: %v", err)
107+ logging.Warn(fmt.Sprintf("[tls] warning: failed to close database: %v", err))
108108 }
109109 }()
110110
@@ -117,7 +117,7 @@ func checkStoredCertificateDecision(hostname string) (string, bool) {
117117 if errors.Is(err, sql.ErrNoRows) {
118118 return "", false
119119 }
120- log.Printf("[tls] Error checking certificate validation: %v", err)
120+ logging.Error(fmt.Sprintf("[tls] Error checking certificate validation: %v", err))
121121 return "", false
122122 }
123123
@@ -142,23 +142,23 @@ func storeCertificateDecision(hostname, decision string, expiresAt sql.NullTime)
142142 }
143143 defer func() {
144144 if err := database.Close(); err != nil {
145- log.Printf("[tls] warning: failed to close database: %v", err)
145+ logging.Warn(fmt.Sprintf("[tls] warning: failed to close database: %v", err))
146146 }
147147 }()
148148
149149 queries := db.New(database)
150150 ctx := context.Background()
151151
152- log.Printf("[tls] Saving certificate decision to database: hostname=%s, decision=%s, expiresAt=%v",
153- hostname, decision, expiresAt)
152+ logging.Debug(fmt.Sprintf("[tls] Saving certificate decision to database: hostname=%s, decision=%s, expiresAt=%v",
153+ hostname, decision, expiresAt))
154154
155155 // Use empty string for cert hash since we're doing hostname-only matching
156156 if err := queries.StoreCertificateValidation(ctx, hostname, "", decision, expiresAt); err != nil {
157- log.Printf("[tls] Failed to save certificate decision to database: %v", err)
157+ logging.Error(fmt.Sprintf("[tls] Failed to save certificate decision to database: %v", err))
158158 return err
159159 }
160160
161- log.Printf("[tls] Certificate decision saved successfully in database")
161+ logging.Debug(fmt.Sprintf("[tls] Certificate decision saved successfully in database"))
162162 return nil
163163 }
164164
@@ -222,7 +222,7 @@ func (wv *WebView) showTLSErrorDialog(failingUri, hostname string, certificate g
222222 // Create AlertDialog using CGO wrapper
223223 dialog := newAlertDialog(fmt.Sprintf("Certificate Error for %s", hostname))
224224 if dialog == nil {
225- log.Printf("[tls] Failed to create alert dialog")
225+ logging.Error(fmt.Sprintf("[tls] Failed to create alert dialog"))
226226 return
227227 }
228228
@@ -245,32 +245,32 @@ func (wv *WebView) showTLSErrorDialog(failingUri, hostname string, certificate g
245245 dialog.Choose(context.Background(), parentWindow, func(result gio.AsyncResulter) {
246246 buttonIndex, err := dialog.ChooseFinish(result)
247247 if err != nil {
248- log.Printf("[tls] Dialog error: %v", err)
248+ logging.Error(fmt.Sprintf("[tls] Dialog error: %v", err))
249249 return
250250 }
251251
252- log.Printf("[tls] User chose button %d for %s", buttonIndex, hostname)
252+ logging.Debug(fmt.Sprintf("[tls] User chose button %d for %s", buttonIndex, hostname))
253253
254254 switch buttonIndex {
255255 case 0: // Go Back
256- log.Printf("[tls] User chose to go back")
256+ logging.Debug(fmt.Sprintf("[tls] User chose to go back"))
257257 // Do nothing - let the load fail
258258
259259 case 1: // Proceed Once
260- log.Printf("[tls] User chose to proceed once")
260+ logging.Debug(fmt.Sprintf("[tls] User chose to proceed once"))
261261 wv.allowCertificateAndReload(certificate, hostname, failingUri)
262262
263263 case 2: // Always Accept This Site
264- log.Printf("[tls] User chose to always accept for %s", hostname)
264+ logging.Debug(fmt.Sprintf("[tls] User chose to always accept for %s", hostname))
265265 // Store decision in database (expires in 30 days)
266266 expiresAt := sql.NullTime{
267267 Time: time.Now().Add(30 * 24 * time.Hour),
268268 Valid: true,
269269 }
270270 if err := storeCertificateDecision(hostname, "accepted", expiresAt); err != nil {
271- log.Printf("[tls] Failed to store certificate decision: %v", err)
271+ logging.Error(fmt.Sprintf("[tls] Failed to store certificate decision: %v", err))
272272 } else {
273- log.Printf("[tls] Stored certificate acceptance for %s (expires in 30 days)", hostname)
273+ logging.Debug(fmt.Sprintf("[tls] Stored certificate acceptance for %s (expires in 30 days)", hostname))
274274 }
275275 wv.allowCertificateAndReload(certificate, hostname, failingUri)
276276 }
@@ -284,17 +284,17 @@ func (wv *WebView) allowCertificateAndReload(certificate gio.TLSCertificater, ho
284284 // Get the network session
285285 session := wv.view.NetworkSession()
286286 if session == nil {
287- log.Printf("[tls] Failed to get network session")
287+ logging.Error(fmt.Sprintf("[tls] Failed to get network session"))
288288 return
289289 }
290290
291291 // Allow the certificate for this host
292292 session.AllowTLSCertificateForHost(certificate, hostname)
293- log.Printf("[tls] Certificate exception added for host: %s", hostname)
293+ logging.Debug(fmt.Sprintf("[tls] Certificate exception added for host: %s", hostname))
294294
295295 // Reload the page
296296 wv.view.LoadURI(uri)
297- log.Printf("[tls] Reloading %s with certificate exception", uri)
297+ logging.Debug(fmt.Sprintf("[tls] Reloading %s with certificate exception", uri))
298298 })
299299 }
300300
@@ -317,7 +317,7 @@ func CleanupExpiredCertificateValidations() error {
317317 }
318318 defer func() {
319319 if err := database.Close(); err != nil {
320- log.Printf("[tls] warning: failed to close database: %v", err)
320+ logging.Warn(fmt.Sprintf("[tls] warning: failed to close database: %v", err))
321321 }
322322 }()
323323
@@ -328,6 +328,6 @@ func CleanupExpiredCertificateValidations() error {
328328 return fmt.Errorf("failed to delete expired validations: %w", err)
329329 }
330330
331- log.Printf("[tls] Cleaned up expired certificate validations")
331+ logging.Debug(fmt.Sprintf("[tls] Cleaned up expired certificate validations"))
332332 return nil
333333 }
M pkg/webkit/touchpad_gestures.go +10 -9
@@ -1,9 +1,10 @@
11 package webkit
22
33 import (
4- "log"
4+ "fmt"
55 "math"
66
7+ "github.com/bnema/dumber/internal/logging"
78 "github.com/diamondburned/gotk4/pkg/gtk/v4"
89 )
910
@@ -17,7 +18,7 @@ const (
1718 // This enables two-finger swipe navigation (left/right for back/forward)
1819 func (w *WebView) AttachTouchpadGestures() {
1920 if w.view == nil {
20- log.Printf("[touchpad-gestures] Cannot attach: WebView is nil")
21+ logging.Error(fmt.Sprintf("[touchpad-gestures] Cannot attach: WebView is nil"))
2122 return
2223 }
2324
@@ -40,13 +41,13 @@ func (w *WebView) AttachTouchpadGestures() {
4041
4142 // Require horizontal velocity to be dominant
4243 if absVelocityY > absVelocityX {
43- log.Printf("[touchpad-gestures] Swipe is more vertical (X:%.1f Y:%.1f), ignoring", velocityX, velocityY)
44+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Swipe is more vertical (X:%.1f Y:%.1f), ignoring", velocityX, velocityY))
4445 return
4546 }
4647
4748 // Check if swipe velocity meets threshold
4849 if absVelocityX < swipeVelocityThreshold {
49- log.Printf("[touchpad-gestures] Swipe velocity too low (%.1f < %.1f), ignoring", absVelocityX, swipeVelocityThreshold)
50+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Swipe velocity too low (%.1f < %.1f), ignoring", absVelocityX, swipeVelocityThreshold))
5051 return
5152 }
5253
@@ -54,23 +55,23 @@ func (w *WebView) AttachTouchpadGestures() {
5455 if velocityX > 0 {
5556 // Swipe right (positive velocity) - go back
5657 if w.view.CanGoBack() {
57- log.Printf("[touchpad-gestures] Swipe right detected (velocity: %.1f) - navigating backward", velocityX)
58+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Swipe right detected (velocity: %.1f) - navigating backward", velocityX))
5859 w.view.GoBack()
5960 } else {
60- log.Printf("[touchpad-gestures] Swipe right detected but cannot go back")
61+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Swipe right detected but cannot go back"))
6162 }
6263 } else {
6364 // Swipe left (negative velocity) - go forward
6465 if w.view.CanGoForward() {
65- log.Printf("[touchpad-gestures] Swipe left detected (velocity: %.1f) - navigating forward", velocityX)
66+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Swipe left detected (velocity: %.1f) - navigating forward", velocityX))
6667 w.view.GoForward()
6768 } else {
68- log.Printf("[touchpad-gestures] Swipe left detected but cannot go forward")
69+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Swipe left detected but cannot go forward"))
6970 }
7071 }
7172 })
7273
7374 // Attach controller to WebView
7475 w.view.AddController(gestureSwipe)
75- log.Printf("[touchpad-gestures] Touchpad gesture controller attached to WebView ID %d", w.id)
76+ logging.Debug(fmt.Sprintf("[touchpad-gestures] Touchpad gesture controller attached to WebView ID %d", w.id))
7677 }
M pkg/webkit/types.go +7 -7
@@ -70,13 +70,13 @@ type Config struct {
7070 // GetDefaultConfig returns a Config with sensible defaults
7171 func GetDefaultConfig() *Config {
7272 return &Config{
73- UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // WebKitGTK default (same as Epiphany)
74- EnableJavaScript: true,
75- EnableWebGL: true,
76- EnableMediaStream: true,
77- HardwareAcceleration: true,
78- DefaultFontSize: 16,
79- MinimumFontSize: 8,
73+ UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // WebKitGTK default (same as Epiphany)
74+ EnableJavaScript: true,
75+ EnableWebGL: true,
76+ EnableMediaStream: true,
77+ HardwareAcceleration: true,
78+ DefaultFontSize: 16,
79+ MinimumFontSize: 8,
8080 EnablePageCache: true, // Instant back/forward navigation
8181 EnableSmoothScrolling: true, // Better UX
8282 CreateWindow: true, // Default to standalone window
M pkg/webkit/uri_scheme.go +3 -3
@@ -2,8 +2,8 @@ package webkit
22
33 import (
44 "fmt"
5- "log"
65
6+ "github.com/bnema/dumber/internal/logging"
77 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
88 )
99
@@ -25,7 +25,7 @@ var (
2525 // In WebKitGTK 6.0, this must be called BEFORE creating any WebViews
2626 // The actual registration happens in InitPersistentSession
2727 func RegisterURIScheme(scheme string, callback URISchemeRequestCallback) {
28- log.Printf("[webkit] Registering URI scheme handler for: %s", scheme)
28+ logging.Debug(fmt.Sprintf("[webkit] Registering URI scheme handler for: %s", scheme))
2929 pendingURISchemeHandlers = append(pendingURISchemeHandlers, &URISchemeHandler{
3030 scheme: scheme,
3131 callback: callback,
@@ -45,7 +45,7 @@ func ApplyURISchemeHandlers(view *webkit.WebView) error {
4545 }
4646
4747 for _, handler := range pendingURISchemeHandlers {
48- log.Printf("[webkit] Registering URI scheme on WebContext: %s", handler.scheme)
48+ logging.Debug(fmt.Sprintf("[webkit] Registering URI scheme on WebContext: %s", handler.scheme))
4949
5050 // Capture handler in closure
5151 callback := handler.callback
M pkg/webkit/user_content.go +14 -14
@@ -2,10 +2,10 @@ package webkit
22
33 import (
44 "fmt"
5- "log"
65
76 "github.com/bnema/dumber/assets"
87 "github.com/bnema/dumber/internal/config"
8+ "github.com/bnema/dumber/internal/logging"
99 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1010 )
1111
@@ -26,7 +26,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
2626 // Get the UserContentManager from the WebView
2727 ucm := view.UserContentManager()
2828 if ucm == nil {
29- log.Printf("[webkit] UserContentManager is nil, skipping script injection")
29+ logging.Warn(fmt.Sprintf("[webkit] UserContentManager is nil, skipping script injection"))
3030 return nil
3131 }
3232
@@ -56,7 +56,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
5656 nil,
5757 nil,
5858 ))
59- log.Printf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID)
59+ logging.Debug(fmt.Sprintf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID))
6060
6161 // Inject GTK theme detection in BOTH worlds
6262 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set
@@ -66,14 +66,14 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
6666 switch cfg.Appearance.ColorScheme {
6767 case "prefer-dark":
6868 prefersDark = true
69- log.Printf("[webkit] Using config-forced dark theme (color_scheme: prefer-dark)")
69+ logging.Debug(fmt.Sprintf("[webkit] Using config-forced dark theme (color_scheme: prefer-dark)"))
7070 case "prefer-light":
7171 prefersDark = false
72- log.Printf("[webkit] Using config-forced light theme (color_scheme: prefer-light)")
72+ logging.Debug(fmt.Sprintf("[webkit] Using config-forced light theme (color_scheme: prefer-light)"))
7373 default:
7474 // "default" or empty - follow system GTK preference
7575 prefersDark = PrefersDarkTheme()
76- log.Printf("[webkit] Using system GTK theme preference (color_scheme: %s)", cfg.Appearance.ColorScheme)
76+ logging.Debug(fmt.Sprintf("[webkit] Using system GTK theme preference (color_scheme: %s)", cfg.Appearance.ColorScheme))
7777 }
7878
7979 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)
@@ -96,7 +96,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
9696 nil,
9797 nil,
9898 ))
99- log.Printf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark)
99+ logging.Debug(fmt.Sprintf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark))
100100
101101 // Inject palette config in BOTH worlds
102102 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }
@@ -121,7 +121,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
121121 nil,
122122 nil,
123123 ))
124- log.Printf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript))
124+ logging.Debug(fmt.Sprintf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript)))
125125 }
126126
127127 // Inject color-scheme script in BOTH worlds
@@ -146,7 +146,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
146146 nil,
147147 nil,
148148 ))
149- log.Printf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript))
149+ logging.Debug(fmt.Sprintf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript)))
150150 }
151151
152152 // Inject main-world script in MAIN world
@@ -160,7 +160,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
160160 nil,
161161 nil,
162162 ))
163- log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript))
163+ logging.Debug(fmt.Sprintf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript)))
164164 }
165165
166166 // Inject GUI controls script in ISOLATED world at document-start
@@ -175,7 +175,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
175175 nil,
176176 nil,
177177 ))
178- log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world, document-start)", len(assets.GUIScript))
178+ logging.Debug(fmt.Sprintf("[webkit] Injected GUI controls script (%d bytes, isolated world, document-start)", len(assets.GUIScript)))
179179 }
180180
181181 // Inject component CSS styles as a JavaScript variable in ISOLATED world at document-start
@@ -194,16 +194,16 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
194194 nil,
195195 nil,
196196 ))
197- log.Printf("[webkit] Injected component styles string (%d bytes, isolated world, document-start)", len(assets.ComponentStyles))
197+ logging.Debug(fmt.Sprintf("[webkit] Injected component styles string (%d bytes, isolated world, document-start)", len(assets.ComponentStyles)))
198198 }
199199
200200 // Register script message handler "dumber" in the MAIN world
201201 // Isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler
202202 // This architecture is required because webkit.messageHandlers is only available in main world
203203 if !ucm.RegisterScriptMessageHandler("dumber", "") {
204- log.Printf("[webkit] Warning: failed to register 'dumber' script message handler in main world")
204+ logging.Warn(fmt.Sprintf("[webkit] Warning: failed to register 'dumber' script message handler in main world"))
205205 } else {
206- log.Printf("[webkit] Registered 'dumber' script message handler in main world (default)")
206+ logging.Debug(fmt.Sprintf("[webkit] Registered 'dumber' script message handler in main world (default)"))
207207 }
208208
209209 return nil
M pkg/webkit/webview.go +21 -21
@@ -3,11 +3,11 @@ package webkit
33 import (
44 "encoding/json"
55 "fmt"
6- "log"
76 "strconv"
87 "sync"
98 "sync/atomic"
109
10+ "github.com/bnema/dumber/internal/logging"
1111 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1212 "github.com/diamondburned/gotk4/pkg/gdk/v4"
1313 "github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -61,7 +61,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
6161
6262 // Generate unique ID immediately
6363 id := atomic.AddUint64(&viewIDCounter, 1)
64- log.Printf("[webkit] Generated WebView ID: %d (CreateWindow=%v)", id, cfg.CreateWindow)
64+ logging.Debug(fmt.Sprintf("[webkit] Generated WebView ID: %d (CreateWindow=%v)", id, cfg.CreateWindow))
6565
6666 InitMainThread()
6767
@@ -166,9 +166,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
166166 wv.window = window
167167 // Add container as child of window
168168 window.SetChild(container)
169- log.Printf("[webkit] Created standalone window for WebView ID %d", id)
169+ logging.Debug(fmt.Sprintf("[webkit] Created standalone window for WebView ID %d", id))
170170 } else {
171- log.Printf("[webkit] Created WebView ID %d without window (for workspace embedding)", id)
171+ logging.Debug(fmt.Sprintf("[webkit] Created WebView ID %d without window (for workspace embedding)", id))
172172 }
173173
174174 // Register in global registry
@@ -279,22 +279,22 @@ func (w *WebView) setupEventHandlers() {
279279
280280 // Create signal - for popup lifecycle management
281281 w.view.ConnectCreate(func(navigationAction *webkit.NavigationAction) gtk.Widgetter {
282- log.Printf("[webkit] ConnectCreate callback fired for parent WebView ID=%d", w.id)
282+ logging.Debug(fmt.Sprintf("[webkit] ConnectCreate callback fired for parent WebView ID=%d", w.id))
283283 if w.onPopupCreate != nil {
284- log.Printf("[webkit] Calling onPopupCreate handler")
284+ logging.Debug(fmt.Sprintf("[webkit] Calling onPopupCreate handler"))
285285 newWebView := w.onPopupCreate(navigationAction)
286286 if newWebView != nil {
287- log.Printf("[webkit] onPopupCreate returned WebView wrapper ID=%d", newWebView.id)
288- log.Printf("[webkit] Extracting underlying gotk4 WebView from wrapper")
287+ logging.Debug(fmt.Sprintf("[webkit] onPopupCreate returned WebView wrapper ID=%d", newWebView.id))
288+ logging.Debug(fmt.Sprintf("[webkit] Extracting underlying gotk4 WebView from wrapper"))
289289 underlyingView := newWebView.view
290- log.Printf("[webkit] Extracted gotk4 WebView=%p, about to return to WebKit", underlyingView)
290+ logging.Debug(fmt.Sprintf("[webkit] Extracted gotk4 WebView=%p, about to return to WebKit", underlyingView))
291291 // Return the underlying WebView widget to WebKit
292292 return underlyingView
293293 }
294- log.Printf("[webkit] onPopupCreate returned nil, blocking popup")
294+ logging.Debug(fmt.Sprintf("[webkit] onPopupCreate returned nil, blocking popup"))
295295 }
296296 // Return nil to cancel popup creation
297- log.Printf("[webkit] No onPopupCreate handler, blocking popup")
297+ logging.Debug(fmt.Sprintf("[webkit] No onPopupCreate handler, blocking popup"))
298298 return nil
299299 })
300300
@@ -379,7 +379,7 @@ func (w *WebView) setupNavigationPolicyHandler() {
379379 if isCtrlClick {
380380 clickType = "Ctrl+click"
381381 }
382- log.Printf("[navigation-policy] Detected %s on link: %s", clickType, linkURL)
382+ logging.Debug(fmt.Sprintf("[navigation-policy] Detected %s on link: %s", clickType, linkURL))
383383
384384 // Call the registered handler
385385 w.mu.RLock()
@@ -389,7 +389,7 @@ func (w *WebView) setupNavigationPolicyHandler() {
389389 if handler != nil {
390390 handled := handler(linkURL)
391391 if handled {
392- log.Printf("[navigation-policy] %s handled by workspace, blocking navigation", clickType)
392+ logging.Debug(fmt.Sprintf("[navigation-policy] %s handled by workspace, blocking navigation", clickType))
393393 // Block the navigation by calling Ignore()
394394 navDecision.Ignore()
395395 return true // Prevent default behavior
@@ -399,7 +399,7 @@ func (w *WebView) setupNavigationPolicyHandler() {
399399 return false // Let WebKit handle if not handled
400400 })
401401
402- log.Printf("[webkit] Navigation policy handler attached to WebView ID %d", w.id)
402+ logging.Debug(fmt.Sprintf("[webkit] Navigation policy handler attached to WebView ID %d", w.id))
403403 }
404404
405405 // LoadURL loads the given URL in the WebView
@@ -800,7 +800,7 @@ func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error
800800 return fmt.Errorf("failed to marshal event data: %w", err)
801801 }
802802
803- log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData))
803+ logging.Debug(fmt.Sprintf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData)))
804804
805805 // Use document.dispatchEvent (not window) so events cross JavaScript world boundaries
806806 // The GUI scripts run in an isolated world but share the same Document object
@@ -925,7 +925,7 @@ func (w *WebView) UpdateContentFilters(rules string) error {
925925
926926 // Note: Content filtering is handled via InitializeContentBlocking()
927927 // This method is kept for backward compatibility but is deprecated
928- log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
928+ logging.Warn(fmt.Sprintf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead"))
929929 return nil
930930 }
931931
@@ -979,21 +979,21 @@ func (w *WebView) setupFaviconHandlers() {
979979 // Get the NetworkSession from the WebView
980980 session := w.view.NetworkSession()
981981 if session == nil {
982- log.Printf("[webkit] Warning: No NetworkSession available for favicon handling")
982+ logging.Warn(fmt.Sprintf("[webkit] Warning: No NetworkSession available for favicon handling"))
983983 return
984984 }
985985
986986 // Get the WebsiteDataManager from the NetworkSession
987987 dataManager := session.WebsiteDataManager()
988988 if dataManager == nil {
989- log.Printf("[webkit] Warning: No WebsiteDataManager available for favicon handling")
989+ logging.Warn(fmt.Sprintf("[webkit] Warning: No WebsiteDataManager available for favicon handling"))
990990 return
991991 }
992992
993993 // Enable favicons if not already enabled
994994 if !dataManager.FaviconsEnabled() {
995995 dataManager.SetFaviconsEnabled(true)
996- log.Printf("[webkit] Enabled favicons for WebView ID %d", w.id)
996+ logging.Debug(fmt.Sprintf("[webkit] Enabled favicons for WebView ID %d", w.id))
997997 }
998998
999999 }
@@ -1016,7 +1016,7 @@ func WrapBareWebView(bareView *webkit.WebView) *WebView {
10161016
10171017 // Generate unique ID
10181018 id := atomic.AddUint64(&viewIDCounter, 1)
1019- log.Printf("[webkit] Created minimal wrapper for bare WebView (ID: %d)", id)
1019+ logging.Debug(fmt.Sprintf("[webkit] Created minimal wrapper for bare WebView (ID: %d)", id))
10201020
10211021 // Create minimal wrapper - NO initialization yet
10221022 wv := &WebView{
@@ -1093,7 +1093,7 @@ func (w *WebView) InitializeFromBare(cfg *Config) error {
10931093 // Attach touchpad gesture controls
10941094 w.AttachTouchpadGestures()
10951095
1096- log.Printf("[webkit] Completed initialization for WebView ID %d", w.id)
1096+ logging.Debug(fmt.Sprintf("[webkit] Completed initialization for WebView ID %d", w.id))
10971097
10981098 return nil
10991099 }
M pkg/webkit/webview_related.go +18 -17
@@ -19,10 +19,11 @@ static inline WebKitWebView* create_related_web_view(WebKitWebView* parent) {
1919 */
2020 import "C"
2121 import (
22- "log"
22+ "fmt"
2323 "runtime"
2424 "unsafe"
2525
26+ "github.com/bnema/dumber/internal/logging"
2627 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
2728 coreglib "github.com/diamondburned/gotk4/pkg/core/glib"
2829 "github.com/diamondburned/gotk4/pkg/glib/v2"
@@ -37,39 +38,39 @@ import (
3738 // we manually construct the WebView struct hierarchy using CGO.
3839 func NewBareRelatedWebView(parentView *webkit.WebView) *webkit.WebView {
3940 if parentView == nil {
40- log.Printf("[webkit] NewBareRelatedWebView: parent is nil, creating unrelated WebView")
41+ logging.Warn(fmt.Sprintf("[webkit] NewBareRelatedWebView: parent is nil, creating unrelated WebView"))
4142 return webkit.NewWebView()
4243 }
4344
4445 // Get the native C pointer from the gotk4 WebView
4546 parentObj := glib.BaseObject(parentView)
4647 if parentObj == nil {
47- log.Printf("[webkit] NewBareRelatedWebView: failed to get parent object, creating unrelated WebView")
48+ logging.Error(fmt.Sprintf("[webkit] NewBareRelatedWebView: failed to get parent object, creating unrelated WebView"))
4849 return webkit.NewWebView()
4950 }
5051 parentNative := (*C.WebKitWebView)(unsafe.Pointer(parentObj.Native()))
5152 if parentNative == nil {
52- log.Printf("[webkit] NewBareRelatedWebView: parent native pointer is nil, creating unrelated WebView")
53+ logging.Error(fmt.Sprintf("[webkit] NewBareRelatedWebView: parent native pointer is nil, creating unrelated WebView"))
5354 return webkit.NewWebView()
5455 }
5556
56- log.Printf("[webkit] Creating bare WebView with related-view property (parent=%p)", parentNative)
57+ logging.Debug(fmt.Sprintf("[webkit] Creating bare WebView with related-view property (parent=%p)", parentNative))
5758
5859 // Create related WebView using CGO helper with g_object_new
59- log.Printf("[webkit] Step 3: About to call C.create_related_web_view")
60+ logging.Debug(fmt.Sprintf("[webkit] Step 3: About to call C.create_related_web_view"))
6061 webViewNative := C.create_related_web_view(parentNative)
61- log.Printf("[webkit] Step 4: C.create_related_web_view returned: %p", webViewNative)
62+ logging.Debug(fmt.Sprintf("[webkit] Step 4: C.create_related_web_view returned: %p", webViewNative))
6263
6364 if webViewNative == nil {
64- log.Printf("[webkit] NewBareRelatedWebView: C.create_related_web_view returned nil")
65+ logging.Error(fmt.Sprintf("[webkit] NewBareRelatedWebView: C.create_related_web_view returned nil"))
6566 return nil
6667 }
6768
6869 // Wrap the C object in a coreglib.Object
6970 // This takes ownership of the floating reference
70- log.Printf("[webkit] Step 5: About to call coreglib.Take")
71+ logging.Debug(fmt.Sprintf("[webkit] Step 5: About to call coreglib.Take"))
7172 obj := coreglib.Take(unsafe.Pointer(webViewNative))
72- log.Printf("[webkit] Step 6: coreglib.Take succeeded, obj=%p", obj)
73+ logging.Debug(fmt.Sprintf("[webkit] Step 6: coreglib.Take succeeded, obj=%p", obj))
7374
7475 // Manually construct the WebView struct hierarchy
7576 // WebView -> WebViewBase -> gtk.Widget -> coreglib.InitiallyUnowned -> coreglib.Object
@@ -78,7 +79,7 @@ func NewBareRelatedWebView(parentView *webkit.WebView) *webkit.WebView {
7879 // manually construct the full struct hierarchy using the same pattern
7980 // that gotk4 uses internally.
8081
81- log.Printf("[webkit] Step 7: About to create gtk.Widget base")
82+ logging.Debug(fmt.Sprintf("[webkit] Step 7: About to create gtk.Widget base"))
8283 // Create the gtk.Widget base
8384 widget := gtk.Widget{
8485 InitiallyUnowned: coreglib.InitiallyUnowned{
@@ -95,25 +96,25 @@ func NewBareRelatedWebView(parentView *webkit.WebView) *webkit.WebView {
9596 Object: obj,
9697 },
9798 }
98- log.Printf("[webkit] Step 8: Created gtk.Widget base")
99+ logging.Debug(fmt.Sprintf("[webkit] Step 8: Created gtk.Widget base"))
99100
100101 // Create the WebViewBase
101- log.Printf("[webkit] Step 9: About to create WebViewBase")
102+ logging.Debug(fmt.Sprintf("[webkit] Step 9: About to create WebViewBase"))
102103 webViewBase := webkit.WebViewBase{
103104 Widget: widget,
104105 }
105- log.Printf("[webkit] Step 10: Created WebViewBase")
106+ logging.Debug(fmt.Sprintf("[webkit] Step 10: Created WebViewBase"))
106107
107108 // Create the final WebView
108- log.Printf("[webkit] Step 11: About to create final WebView")
109+ logging.Debug(fmt.Sprintf("[webkit] Step 11: About to create final WebView"))
109110 relatedView := &webkit.WebView{
110111 WebViewBase: webViewBase,
111112 }
112- log.Printf("[webkit] Step 12: Created final WebView: %p", relatedView)
113+ logging.Debug(fmt.Sprintf("[webkit] Step 12: Created final WebView: %p", relatedView))
113114
114115 // Ensure parentView is kept alive until after the CGO call completes
115116 runtime.KeepAlive(parentView)
116117
117- log.Printf("[webkit] Created bare related WebView (parent=%p)", parentNative)
118+ logging.Debug(fmt.Sprintf("[webkit] Created bare related WebView (parent=%p)", parentNative))
118119 return relatedView
119120 }
M pkg/webkit/window.go +3 -2
@@ -1,8 +1,9 @@
11 package webkit
22
33 import (
4- "log"
4+ "fmt"
55
6+ "github.com/bnema/dumber/internal/logging"
67 "github.com/diamondburned/gotk4/pkg/gtk/v4"
78 )
89
@@ -26,7 +27,7 @@ func NewWindow(title string) (*Window, error) {
2627 // Connect close-request signal to handle Cmd+Q and window close button
2728 // Returning true prevents the default close behavior and allows graceful shutdown
2829 win.ConnectCloseRequest(func() bool {
29- log.Printf("[window] Close request received (Cmd+Q or close button) - initiating graceful shutdown")
30+ logging.Info(fmt.Sprintf("[window] Close request received (Cmd+Q or close button) - initiating graceful shutdown"))
3031 // Quit the main loop gracefully, which will trigger cleanup
3132 QuitMainLoop()
3233 // Return true to prevent GTK from destroying the window immediately

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close