1// Package config provides configuration management for dumber with Viper integration.
2package config
3
4import (
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync"
11 "time"
12
13 "github.com/bnema/dumber/internal/logging"
14 "github.com/bnema/dumber/pkg/gpu"
15 "github.com/fsnotify/fsnotify"
16 "github.com/spf13/viper"
17)
18
19// File permission constants
20const (
21 dirPerm = 0755 // Standard directory permissions (rwxr-xr-x)
22 filePerm = 0644 // Standard file permissions (rw-r--r--)
23)
24
25// Config represents the complete configuration for dumber.
26type Config struct {
27 Database DatabaseConfig `mapstructure:"database" yaml:"database"`
28 History HistoryConfig `mapstructure:"history" yaml:"history"`
29 SearchShortcuts map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`
30 Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
31 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
32 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
33 VideoAcceleration VideoAccelerationConfig `mapstructure:"video_acceleration" yaml:"video_acceleration"`
34 CodecPreferences CodecConfig `mapstructure:"codec_preferences" yaml:"codec_preferences"`
35 WebkitMemory WebkitMemoryConfig `mapstructure:"webkit_memory" yaml:"webkit_memory"`
36 Debug DebugConfig `mapstructure:"debug" yaml:"debug"`
37 APISecurity APISecurityConfig `mapstructure:"api_security" yaml:"api_security"`
38 // RenderingMode controls GPU/CPU rendering selection for WebKit
39 RenderingMode RenderingMode `mapstructure:"rendering_mode" yaml:"rendering_mode"`
40 // UseDomZoom toggles DOM-based zoom instead of native WebKit zoom.
41 UseDomZoom bool `mapstructure:"use_dom_zoom" yaml:"use_dom_zoom"`
42 // DefaultZoom sets the default zoom level for pages without saved zoom settings (1.0 = 100%, 1.2 = 120%)
43 DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"`
44 // Workspace defines workspace, pane, and tab handling behaviour.
45 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"`
46 // ContentFilteringWhitelist domains skip ad blocking (e.g. Twitch breaks with bot detection)
47 ContentFilteringWhitelist []string `mapstructure:"content_filtering_whitelist" yaml:"content_filtering_whitelist"`
48}
49
50// RenderingMode selects GPU vs CPU rendering.
51type RenderingMode string
52
53const (
54 RenderingModeAuto RenderingMode = "auto"
55 RenderingModeGPU RenderingMode = "gpu"
56 RenderingModeCPU RenderingMode = "cpu"
57)
58
59// DatabaseConfig holds database-related configuration.
60type DatabaseConfig struct {
61 Path string `mapstructure:"path" yaml:"path"`
62 MaxConnections int `mapstructure:"max_connections" yaml:"max_connections"`
63 MaxIdleTime time.Duration `mapstructure:"max_idle_time" yaml:"max_idle_time"`
64 QueryTimeout time.Duration `mapstructure:"query_timeout" yaml:"query_timeout"`
65}
66
67// HistoryConfig holds history-related configuration.
68type HistoryConfig struct {
69 MaxEntries int `mapstructure:"max_entries" yaml:"max_entries"`
70 RetentionPeriodDays int `mapstructure:"retention_period_days" yaml:"retention_period_days"`
71 CleanupIntervalDays int `mapstructure:"cleanup_interval_days" yaml:"cleanup_interval_days"`
72}
73
74// SearchShortcut represents a search shortcut configuration.
75type SearchShortcut struct {
76 URL string `mapstructure:"url" yaml:"url" json:"url"`
77 Description string `mapstructure:"description" yaml:"description" json:"description"`
78}
79
80// DmenuConfig holds dmenu/rofi integration configuration.
81type DmenuConfig struct {
82 MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items"`
83 ShowVisitCount bool `mapstructure:"show_visit_count" yaml:"show_visit_count"`
84 ShowLastVisited bool `mapstructure:"show_last_visited" yaml:"show_last_visited"`
85 HistoryPrefix string `mapstructure:"history_prefix" yaml:"history_prefix"`
86 ShortcutPrefix string `mapstructure:"shortcut_prefix" yaml:"shortcut_prefix"`
87 URLPrefix string `mapstructure:"url_prefix" yaml:"url_prefix"`
88 DateFormat string `mapstructure:"date_format" yaml:"date_format"`
89 SortByVisitCount bool `mapstructure:"sort_by_visit_count" yaml:"sort_by_visit_count"`
90}
91
92// LoggingConfig holds logging configuration.
93type LoggingConfig struct {
94 Level string `mapstructure:"level" yaml:"level"`
95 Format string `mapstructure:"format" yaml:"format"`
96 Filename string `mapstructure:"filename" yaml:"filename"`
97 MaxSize int `mapstructure:"max_size" yaml:"max_size"`
98 MaxBackups int `mapstructure:"max_backups" yaml:"max_backups"`
99 MaxAge int `mapstructure:"max_age" yaml:"max_age"`
100 Compress bool `mapstructure:"compress" yaml:"compress"`
101
102 // File output configuration
103 LogDir string `mapstructure:"log_dir" yaml:"log_dir"`
104 EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"`
105
106 // Capture settings
107 CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout" json:"capture_stdout"`
108 CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr" json:"capture_stderr"`
109 CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output" json:"capture_c_output"`
110 CaptureConsole bool `mapstructure:"capture_console" yaml:"capture_console" json:"capture_console"`
111
112 // Debug output
113 DebugFile string `mapstructure:"debug_file" yaml:"debug_file"`
114 VerboseWebKit bool `mapstructure:"verbose_webkit" yaml:"verbose_webkit"`
115}
116
117// AppearanceConfig holds UI/rendering preferences.
118type AppearanceConfig struct {
119 // Default fonts for pages that do not specify fonts.
120 SansFont string `mapstructure:"sans_font" yaml:"sans_font"`
121 SerifFont string `mapstructure:"serif_font" yaml:"serif_font"`
122 MonospaceFont string `mapstructure:"monospace_font" yaml:"monospace_font"`
123 // Default font size in CSS pixels (approx).
124 DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`
125 LightPalette ColorPalette `mapstructure:"light_palette" yaml:"light_palette"`
126 DarkPalette ColorPalette `mapstructure:"dark_palette" yaml:"dark_palette"`
127}
128
129// ColorPalette contains semantic color tokens for light/dark themes.
130type ColorPalette struct {
131 Background string `mapstructure:"background" yaml:"background" json:"background"`
132 Surface string `mapstructure:"surface" yaml:"surface" json:"surface"`
133 SurfaceVariant string `mapstructure:"surface_variant" yaml:"surface_variant" json:"surface_variant"`
134 Text string `mapstructure:"text" yaml:"text" json:"text"`
135 Muted string `mapstructure:"muted" yaml:"muted" json:"muted"`
136 Accent string `mapstructure:"accent" yaml:"accent" json:"accent"`
137 Border string `mapstructure:"border" yaml:"border" json:"border"`
138}
139
140// VideoAccelerationConfig holds video hardware acceleration preferences.
141type VideoAccelerationConfig struct {
142 EnableVAAPI bool `mapstructure:"enable_vaapi" yaml:"enable_vaapi"`
143 AutoDetectGPU bool `mapstructure:"auto_detect_gpu" yaml:"auto_detect_gpu"`
144 VAAPIDriverName string `mapstructure:"vaapi_driver_name" yaml:"vaapi_driver_name"`
145 EnableAllDrivers bool `mapstructure:"enable_all_drivers" yaml:"enable_all_drivers"`
146 LegacyVAAPI bool `mapstructure:"legacy_vaapi" yaml:"legacy_vaapi"`
147}
148
149// CodecConfig holds video codec preferences and handling
150type CodecConfig struct {
151 // Codec preference order (e.g., "av1,h264,vp8")
152 PreferredCodecs string `mapstructure:"preferred_codecs" yaml:"preferred_codecs"`
153
154 // Force specific codec for platforms
155 ForceAV1 bool `mapstructure:"force_av1" yaml:"force_av1"`
156
157 // Block problematic codecs
158 BlockVP9 bool `mapstructure:"block_vp9" yaml:"block_vp9"`
159 BlockVP8 bool `mapstructure:"block_vp8" yaml:"block_vp8"`
160
161 // Hardware acceleration per codec
162 AV1HardwareOnly bool `mapstructure:"av1_hardware_only" yaml:"av1_hardware_only"`
163 DisableVP9Hardware bool `mapstructure:"disable_vp9_hardware" yaml:"disable_vp9_hardware"`
164
165 // Buffer configuration for smooth playback
166 VideoBufferSizeMB int `mapstructure:"video_buffer_size_mb" yaml:"video_buffer_size_mb"`
167 QueueBufferTimeSec int `mapstructure:"queue_buffer_time_sec" yaml:"queue_buffer_time_sec"`
168
169 // Custom User-Agent for codec negotiation
170 CustomUserAgent string `mapstructure:"custom_user_agent" yaml:"custom_user_agent"`
171
172 // Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited)
173 AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"`
174
175 // Site-specific codec control settings
176 DisableTwitchCodecControl bool `mapstructure:"disable_twitch_codec_control" yaml:"disable_twitch_codec_control"`
177}
178
179// WebkitMemoryConfig holds WebKit memory optimization settings
180type WebkitMemoryConfig struct {
181 // Cache model (document_viewer | web_browser | primary_web_browser)
182 CacheModel string `mapstructure:"cache_model" yaml:"cache_model"`
183
184 // Enable page cache for back/forward navigation
185 EnablePageCache bool `mapstructure:"enable_page_cache" yaml:"enable_page_cache"`
186
187 // Memory limit in MB (0 = system default)
188 MemoryLimitMB int `mapstructure:"memory_limit_mb" yaml:"memory_limit_mb"`
189
190 // Memory pressure thresholds (0.0-1.0)
191 ConservativeThreshold float64 `mapstructure:"conservative_threshold" yaml:"conservative_threshold"`
192 StrictThreshold float64 `mapstructure:"strict_threshold" yaml:"strict_threshold"`
193 KillThreshold float64 `mapstructure:"kill_threshold" yaml:"kill_threshold"`
194
195 // Memory monitoring interval in seconds
196 PollIntervalSeconds float64 `mapstructure:"poll_interval_seconds" yaml:"poll_interval_seconds"`
197
198 // Garbage collection interval in seconds (0 = disabled)
199 EnableGCInterval int `mapstructure:"enable_gc_interval" yaml:"enable_gc_interval"`
200
201 // Process recycling threshold (number of page loads)
202 ProcessRecycleThreshold int `mapstructure:"process_recycle_threshold" yaml:"process_recycle_threshold"`
203
204 // Enable memory monitoring logs
205 EnableMemoryMonitoring bool `mapstructure:"enable_memory_monitoring" yaml:"enable_memory_monitoring"`
206}
207
208// DebugConfig holds debug and troubleshooting options
209type DebugConfig struct {
210 // Enable WebKit internal debug logging
211 EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"`
212
213 // WebKit debug categories (comma-separated)
214 // Common values: "Network:preconnectTo", "ContentFilters", "Loading", "JavaScript"
215 WebKitDebugCategories string `mapstructure:"webkit_debug_categories" yaml:"webkit_debug_categories"`
216
217 // Enable content filtering debug logs
218 EnableFilteringDebug bool `mapstructure:"enable_filtering_debug" yaml:"enable_filtering_debug"`
219
220 // Enable detailed WebView state logging
221 EnableWebViewDebug bool `mapstructure:"enable_webview_debug" yaml:"enable_webview_debug"`
222
223 // Log WebKit crashes and errors to file
224 LogWebKitCrashes bool `mapstructure:"log_webkit_crashes" yaml:"log_webkit_crashes"`
225
226 // Enable script injection debug logs
227 EnableScriptDebug bool `mapstructure:"enable_script_debug" yaml:"enable_script_debug"`
228
229 // Enable general debug mode (equivalent to DUMBER_DEBUG env var)
230 EnableGeneralDebug bool `mapstructure:"enable_general_debug" yaml:"enable_general_debug"`
231
232 // Enable workspace navigation and focus debug logs
233 EnableWorkspaceDebug bool `mapstructure:"enable_workspace_debug" yaml:"enable_workspace_debug"`
234
235 // Enable focus state machine debug logs
236 EnableFocusDebug bool `mapstructure:"enable_focus_debug" yaml:"enable_focus_debug"`
237
238 // Enable CSS reconciler debug logs
239 EnableCSSDebug bool `mapstructure:"enable_css_debug" yaml:"enable_css_debug"`
240
241 // Enable focus metrics tracking
242 EnableFocusMetrics bool `mapstructure:"enable_focus_metrics" yaml:"enable_focus_metrics"`
243
244 // Enable detailed pane close instrumentation and tree snapshots
245 EnablePaneCloseDebug bool `mapstructure:"enable_pane_close_debug" yaml:"enable_pane_close_debug"`
246}
247
248// APISecurityConfig holds optional API key protection for dumb://api endpoints
249type APISecurityConfig struct {
250 // If non-empty, token that requests must present via `token` query param
251 Token string `mapstructure:"token" yaml:"token"`
252 // If true, require token for all API endpoints (except a minimal allowlist)
253 RequireToken bool `mapstructure:"require_token" yaml:"require_token"`
254}
255
256// WorkspaceConfig captures layout, pane, and tab behaviour preferences.
257type WorkspaceConfig struct {
258 // EnableZellijControls toggles Zellij-inspired keybindings.
259 EnableZellijControls bool `mapstructure:"enable_zellij_controls" yaml:"enable_zellij_controls" json:"enable_zellij_controls"`
260 // PaneMode defines modal pane behaviour and bindings.
261 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" json:"pane_mode"`
262 // Tabs holds classic browser tab shortcuts.
263 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`
264 // Popups configures default popup placement rules.
265 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`
266 // Styling configures workspace visual appearance.
267 Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" json:"styling"`
268}
269
270// PaneModeConfig defines modal behaviour for pane management.
271type PaneModeConfig struct {
272 ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" json:"activation_shortcut"`
273 TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" json:"timeout_ms"`
274 ActionBindings map[string]string `mapstructure:"action_bindings" yaml:"action_bindings" json:"action_bindings"`
275}
276
277// TabKeyConfig defines Zellij-inspired tab shortcuts.
278type TabKeyConfig struct {
279 NewTab string `mapstructure:"new_tab" yaml:"new_tab" json:"new_tab"`
280 CloseTab string `mapstructure:"close_tab" yaml:"close_tab" json:"close_tab"`
281 NextTab string `mapstructure:"next_tab" yaml:"next_tab" json:"next_tab"`
282 PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" json:"previous_tab"`
283}
284
285// PopupBehavior defines how popup windows should be opened
286type PopupBehavior string
287
288const (
289 // PopupBehaviorSplit opens popups in a split pane (default)
290 PopupBehaviorSplit PopupBehavior = "split"
291 // PopupBehaviorStacked opens popups in a stacked pane
292 PopupBehaviorStacked PopupBehavior = "stacked"
293 // PopupBehaviorTabbed opens popups as a new tab
294 PopupBehaviorTabbed PopupBehavior = "tabbed"
295 // PopupBehaviorWindowed opens popups in a new workspace window
296 PopupBehaviorWindowed PopupBehavior = "windowed"
297)
298
299// PopupBehaviorConfig defines handling for popup windows.
300type PopupBehaviorConfig struct {
301 // Behavior determines how popups are opened (split/stacked/tabbed/windowed)
302 Behavior PopupBehavior `mapstructure:"behavior" yaml:"behavior" json:"behavior"`
303
304 // Placement specifies direction for split behavior ("right", "left", "top", "bottom")
305 // Only used when Behavior is "split"
306 Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`
307
308 // OpenInNewPane controls whether popups are opened in workspace or blocked
309 OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`
310
311 // FollowPaneContext determines if popup placement follows parent pane context
312 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
313
314 // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents
315 // Accepted values: "pane" (default) or "tab" (future support)
316 BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"`
317
318 // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents
319 EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"`
320
321 // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects
322 OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"`
323}
324
325// WorkspaceStylingConfig defines visual styling for workspace panes.
326type WorkspaceStylingConfig struct {
327 // BorderWidth in pixels for pane borders
328 BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`
329 // BorderColor for focused panes (CSS color value or theme variable)
330 BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"`
331 // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)
332 // Defaults to "#FFA500" (orange) if not set
333 PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"`
334 // TransitionDuration in milliseconds for border animations
335 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
336 // BorderRadius in pixels for pane border corners
337 BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"`
338}
339
340// Manager handles configuration loading, watching, and reloading.
341type Manager struct {
342 config *Config
343 viper *viper.Viper
344 mu sync.RWMutex
345 callbacks []func(*Config)
346 watching bool
347}
348
349// NewManager creates a new configuration manager.
350func NewManager() (*Manager, error) {
351 v := viper.New()
352
353 // Configure Viper - supports yaml, json, toml automatically
354 v.SetConfigName("config") // Will find config.yaml, config.json, config.toml, etc.
355
356 // Add config paths
357 configDir, err := GetConfigDir()
358 if err != nil {
359 return nil, fmt.Errorf("failed to get config directory: %w", err)
360 }
361 v.AddConfigPath(configDir)
362 v.AddConfigPath(".") // Current directory for development
363
364 // Set up environment variable support
365 v.SetEnvPrefix("DUMB_BROWSER")
366 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
367 v.AutomaticEnv()
368
369 // Bind specific environment variables
370 bindings := map[string]string{
371 "database.path": "DATABASE_PATH",
372 "database.max_connections": "DATABASE_MAX_CONNECTIONS",
373 "database.max_idle_time": "DATABASE_MAX_IDLE_TIME",
374 "database.query_timeout": "DATABASE_QUERY_TIMEOUT",
375 "browser.command": "BROWSER_COMMAND",
376 "browser.timeout": "BROWSER_TIMEOUT",
377 "browser.detach_process": "BROWSER_DETACH_PROCESS",
378 "history.max_entries": "HISTORY_MAX_ENTRIES",
379 "history.retention_period": "HISTORY_RETENTION_PERIOD",
380 "history.cleanup_interval": "HISTORY_CLEANUP_INTERVAL",
381 "dmenu.max_history_items": "DMENU_MAX_HISTORY_ITEMS",
382 "dmenu.show_visit_count": "DMENU_SHOW_VISIT_COUNT",
383 "dmenu.show_last_visited": "DMENU_SHOW_LAST_VISITED",
384 "dmenu.history_prefix": "DMENU_HISTORY_PREFIX",
385 "dmenu.shortcut_prefix": "DMENU_SHORTCUT_PREFIX",
386 "dmenu.url_prefix": "DMENU_URL_PREFIX",
387 "dmenu.date_format": "DMENU_DATE_FORMAT",
388 "dmenu.sort_by_visit_count": "DMENU_SORT_BY_VISIT_COUNT",
389 "logging.level": "LOGGING_LEVEL",
390 "logging.format": "LOGGING_FORMAT",
391 "logging.filename": "LOGGING_FILENAME",
392 "logging.max_size": "LOGGING_MAX_SIZE",
393 "logging.max_backups": "LOGGING_MAX_BACKUPS",
394 "logging.max_age": "LOGGING_MAX_AGE",
395 "logging.compress": "LOGGING_COMPRESS",
396 // API security
397 "api_security.token": "API_TOKEN",
398 "api_security.require_token": "API_REQUIRE_TOKEN",
399 }
400
401 for key, env := range bindings {
402 if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
403 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
404 }
405 }
406
407 // Explicit binding for rendering mode via dedicated env var
408 if err := v.BindEnv("rendering_mode", "DUMBER_RENDERING_MODE"); err != nil {
409 return nil, fmt.Errorf("failed to bind DUMBER_RENDERING_MODE: %w", err)
410 }
411 if err := v.BindEnv("use_dom_zoom", "DUMBER_USE_DOM_ZOOM"); err != nil {
412 return nil, fmt.Errorf("failed to bind DUMBER_USE_DOM_ZOOM: %w", err)
413 }
414 if err := v.BindEnv("default_zoom", "DUMBER_DEFAULT_ZOOM"); err != nil {
415 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_ZOOM: %w", err)
416 }
417
418 // Video acceleration environment variable bindings
419 videoAccelEnvBindings := map[string]string{
420 "video_acceleration.enable_vaapi": "DUMBER_VIDEO_ACCELERATION_ENABLE",
421 "video_acceleration.auto_detect_gpu": "DUMBER_VIDEO_AUTO_DETECT",
422 "video_acceleration.vaapi_driver_name": "LIBVA_DRIVER_NAME",
423 "video_acceleration.enable_all_drivers": "GST_VAAPI_ALL_DRIVERS",
424 "video_acceleration.legacy_vaapi": "WEBKIT_GST_ENABLE_LEGACY_VAAPI",
425 }
426
427 for key, env := range videoAccelEnvBindings {
428 if err := v.BindEnv(key, env); err != nil {
429 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
430 }
431 }
432
433 // Codec preferences environment variable bindings
434 codecEnvBindings := map[string]string{
435 "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS",
436 "codec_preferences.force_av1": "DUMBER_FORCE_AV1",
437 "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9",
438 "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8",
439 "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY",
440 "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW",
441 "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB",
442 "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC",
443 "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA",
444 "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES",
445 "codec_preferences.disable_twitch_codec_control": "DUMBER_DISABLE_TWITCH_CODEC",
446 }
447
448 for key, env := range codecEnvBindings {
449 if err := v.BindEnv(key, env); err != nil {
450 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
451 }
452 }
453
454 // WebKit memory environment variable bindings
455 memoryEnvBindings := map[string]string{
456 "webkit_memory.cache_model": "DUMBER_CACHE_MODEL",
457 "webkit_memory.enable_page_cache": "DUMBER_ENABLE_PAGE_CACHE",
458 "webkit_memory.memory_limit_mb": "DUMBER_MEMORY_LIMIT_MB",
459 "webkit_memory.conservative_threshold": "DUMBER_MEMORY_CONSERVATIVE",
460 "webkit_memory.strict_threshold": "DUMBER_MEMORY_STRICT",
461 "webkit_memory.kill_threshold": "DUMBER_MEMORY_KILL",
462 "webkit_memory.poll_interval_seconds": "DUMBER_MEMORY_POLL_INTERVAL",
463 "webkit_memory.enable_gc_interval": "DUMBER_GC_INTERVAL",
464 "webkit_memory.process_recycle_threshold": "DUMBER_RECYCLE_THRESHOLD",
465 "webkit_memory.enable_memory_monitoring": "DUMBER_ENABLE_MEMORY_MONITORING",
466 }
467
468 for key, env := range memoryEnvBindings {
469 if err := v.BindEnv(key, env); err != nil {
470 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
471 }
472 }
473
474 return &Manager{
475 viper: v,
476 callbacks: make([]func(*Config), 0),
477 }, nil
478}
479
480// Load loads the configuration from file and environment variables.
481func (m *Manager) Load() error {
482 m.mu.Lock()
483 defer m.mu.Unlock()
484
485 // Ensure directories exist
486 if err := EnsureDirectories(); err != nil {
487 return fmt.Errorf("failed to ensure directories: %w", err)
488 }
489
490 // Set defaults
491 m.setDefaults()
492
493 // Read config file if it exists
494 if err := m.viper.ReadInConfig(); err != nil {
495 var configFileNotFoundError viper.ConfigFileNotFoundError
496 if errors.As(err, &configFileNotFoundError) {
497 // Config file not found, create default one
498 if err := m.createDefaultConfig(); err != nil {
499 return fmt.Errorf("failed to create default config: %w", err)
500 }
501 // Re-read the newly created config file
502 if err := m.viper.ReadInConfig(); err != nil {
503 return fmt.Errorf("failed to read newly created config: %w", err)
504 }
505 } else {
506 return fmt.Errorf("failed to read config file: %w", err)
507 }
508 }
509
510 // Unmarshal into config struct
511 config := &Config{}
512 if err := m.viper.Unmarshal(config); err != nil {
513 return fmt.Errorf("failed to unmarshal config: %w", err)
514 }
515
516 // Set database path if not specified
517 if config.Database.Path == "" {
518 dbPath, err := GetDatabaseFile()
519 if err != nil {
520 return fmt.Errorf("failed to get database path: %w", err)
521 }
522 config.Database.Path = dbPath
523 }
524
525 // Normalize/validate rendering mode
526 switch strings.ToLower(string(config.RenderingMode)) {
527 case "", string(RenderingModeAuto):
528 config.RenderingMode = RenderingModeAuto
529 case string(RenderingModeGPU):
530 config.RenderingMode = RenderingModeGPU
531 case string(RenderingModeCPU):
532 config.RenderingMode = RenderingModeCPU
533 default:
534 config.RenderingMode = RenderingModeAuto
535 }
536
537 // Auto-detect GPU if enabled and driver name is not set
538 if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
539 gpuInfo := gpu.DetectGPU()
540 if gpuInfo.SupportsVAAPI() {
541 config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
542 }
543 }
544
545 // Validate and configure codec preferences
546 config = m.validateAndConfigureCodecPreferences(config)
547
548 m.config = config
549 return nil
550}
551
552// Get returns the current configuration (thread-safe).
553func (m *Manager) Get() *Config {
554 m.mu.RLock()
555 defer m.mu.RUnlock()
556
557 // Return a copy to prevent external modification
558 configCopy := *m.config
559 return &configCopy
560}
561
562// Watch starts watching the config file for changes and reloads automatically.
563func (m *Manager) Watch() error {
564 m.mu.Lock()
565 defer m.mu.Unlock()
566
567 if m.watching {
568 return nil // Already watching
569 }
570
571 m.viper.WatchConfig()
572 m.viper.OnConfigChange(func(_ fsnotify.Event) {
573 // Reload config
574 if err := m.reload(); err != nil {
575 fmt.Fprintf(os.Stderr, "Warning: failed to reload config: %v\n", err)
576 return
577 }
578
579 // Notify callbacks
580 m.mu.RLock()
581 config := m.config
582 callbacks := make([]func(*Config), len(m.callbacks))
583 copy(callbacks, m.callbacks)
584 m.mu.RUnlock()
585
586 for _, callback := range callbacks {
587 callback(config)
588 }
589 })
590
591 m.watching = true
592 return nil
593}
594
595// OnConfigChange registers a callback function to be called when config changes.
596func (m *Manager) OnConfigChange(callback func(*Config)) {
597 m.mu.Lock()
598 defer m.mu.Unlock()
599
600 m.callbacks = append(m.callbacks, callback)
601}
602
603// reload reloads the configuration (internal method, must be called with lock).
604func (m *Manager) reload() error {
605 if err := m.viper.ReadInConfig(); err != nil {
606 return err
607 }
608
609 config := &Config{}
610 if err := m.viper.Unmarshal(config); err != nil {
611 return err
612 }
613
614 // Set database path if not specified
615 if config.Database.Path == "" {
616 dbPath, err := GetDatabaseFile()
617 if err != nil {
618 return fmt.Errorf("failed to get database path: %w", err)
619 }
620 config.Database.Path = dbPath
621 }
622
623 // Normalize/validate rendering mode
624 switch strings.ToLower(string(config.RenderingMode)) {
625 case "", string(RenderingModeAuto):
626 config.RenderingMode = RenderingModeAuto
627 case string(RenderingModeGPU):
628 config.RenderingMode = RenderingModeGPU
629 case string(RenderingModeCPU):
630 config.RenderingMode = RenderingModeCPU
631 default:
632 config.RenderingMode = RenderingModeAuto
633 }
634
635 // Auto-detect GPU if enabled and driver name is not set
636 if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
637 gpuInfo := gpu.DetectGPU()
638 if gpuInfo.SupportsVAAPI() {
639 config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
640 }
641 }
642
643 // Validate and configure codec preferences
644 config = m.validateAndConfigureCodecPreferences(config)
645
646 m.config = config
647 return nil
648}
649
650// setDefaults sets default configuration values in Viper.
651func (m *Manager) setDefaults() {
652 defaults := DefaultConfig()
653
654 // Database defaults
655 m.viper.SetDefault("database.max_connections", defaults.Database.MaxConnections)
656 m.viper.SetDefault("database.max_idle_time", defaults.Database.MaxIdleTime)
657 m.viper.SetDefault("database.query_timeout", defaults.Database.QueryTimeout)
658
659 // Note: Browser config removed - we build our own browser
660
661 // History defaults
662 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
663 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
664 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
665
666 // Search shortcuts defaults
667 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
668
669 // Dmenu defaults
670 m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
671 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
672 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
673 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
674 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
675 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
676 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
677 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
678
679 // Logging defaults
680 m.viper.SetDefault("logging.level", defaults.Logging.Level)
681 m.viper.SetDefault("logging.format", defaults.Logging.Format)
682 m.viper.SetDefault("logging.filename", defaults.Logging.Filename)
683 m.viper.SetDefault("logging.max_size", defaults.Logging.MaxSize)
684 m.viper.SetDefault("logging.max_backups", defaults.Logging.MaxBackups)
685 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
686 m.viper.SetDefault("logging.compress", defaults.Logging.Compress)
687 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
688 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
689 m.viper.SetDefault("logging.capture_stdout", defaults.Logging.CaptureStdout)
690 m.viper.SetDefault("logging.capture_stderr", defaults.Logging.CaptureStderr)
691 m.viper.SetDefault("logging.capture_c_output", defaults.Logging.CaptureCOutput)
692 m.viper.SetDefault("logging.debug_file", defaults.Logging.DebugFile)
693 m.viper.SetDefault("logging.verbose_webkit", defaults.Logging.VerboseWebKit)
694
695 // Debug defaults
696 m.viper.SetDefault("debug.enable_webkit_debug", defaults.Debug.EnableWebKitDebug)
697 m.viper.SetDefault("debug.webkit_debug_categories", defaults.Debug.WebKitDebugCategories)
698 m.viper.SetDefault("debug.enable_filtering_debug", defaults.Debug.EnableFilteringDebug)
699 m.viper.SetDefault("debug.enable_webview_debug", defaults.Debug.EnableWebViewDebug)
700 m.viper.SetDefault("debug.log_webkit_crashes", defaults.Debug.LogWebKitCrashes)
701 m.viper.SetDefault("debug.enable_script_debug", defaults.Debug.EnableScriptDebug)
702 m.viper.SetDefault("debug.enable_general_debug", defaults.Debug.EnableGeneralDebug)
703 m.viper.SetDefault("debug.enable_workspace_debug", defaults.Debug.EnableWorkspaceDebug)
704 m.viper.SetDefault("debug.enable_focus_debug", defaults.Debug.EnableFocusDebug)
705 m.viper.SetDefault("debug.enable_css_debug", defaults.Debug.EnableCSSDebug)
706 m.viper.SetDefault("debug.enable_focus_metrics", defaults.Debug.EnableFocusMetrics)
707 m.viper.SetDefault("debug.enable_pane_close_debug", defaults.Debug.EnablePaneCloseDebug)
708
709 // Appearance defaults
710 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
711 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
712 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
713 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
714 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
715 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
716
717 // Video acceleration defaults
718 m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)
719 m.viper.SetDefault("video_acceleration.auto_detect_gpu", defaults.VideoAcceleration.AutoDetectGPU)
720 m.viper.SetDefault("video_acceleration.vaapi_driver_name", defaults.VideoAcceleration.VAAPIDriverName)
721 m.viper.SetDefault("video_acceleration.enable_all_drivers", defaults.VideoAcceleration.EnableAllDrivers)
722 m.viper.SetDefault("video_acceleration.legacy_vaapi", defaults.VideoAcceleration.LegacyVAAPI)
723
724 // Codec preferences defaults
725 m.viper.SetDefault("codec_preferences.preferred_codecs", defaults.CodecPreferences.PreferredCodecs)
726 m.viper.SetDefault("codec_preferences.force_av1", defaults.CodecPreferences.ForceAV1)
727 m.viper.SetDefault("codec_preferences.block_vp9", defaults.CodecPreferences.BlockVP9)
728 m.viper.SetDefault("codec_preferences.block_vp8", defaults.CodecPreferences.BlockVP8)
729 m.viper.SetDefault("codec_preferences.av1_hardware_only", defaults.CodecPreferences.AV1HardwareOnly)
730 m.viper.SetDefault("codec_preferences.disable_vp9_hardware", defaults.CodecPreferences.DisableVP9Hardware)
731 m.viper.SetDefault("codec_preferences.video_buffer_size_mb", defaults.CodecPreferences.VideoBufferSizeMB)
732 m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec)
733 m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent)
734 m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution)
735 m.viper.SetDefault("codec_preferences.disable_twitch_codec_control", defaults.CodecPreferences.DisableTwitchCodecControl)
736
737 // WebKit memory defaults
738 m.viper.SetDefault("webkit_memory.cache_model", defaults.WebkitMemory.CacheModel)
739 m.viper.SetDefault("webkit_memory.enable_page_cache", defaults.WebkitMemory.EnablePageCache)
740 m.viper.SetDefault("webkit_memory.memory_limit_mb", defaults.WebkitMemory.MemoryLimitMB)
741 m.viper.SetDefault("webkit_memory.conservative_threshold", defaults.WebkitMemory.ConservativeThreshold)
742 m.viper.SetDefault("webkit_memory.strict_threshold", defaults.WebkitMemory.StrictThreshold)
743 m.viper.SetDefault("webkit_memory.kill_threshold", defaults.WebkitMemory.KillThreshold)
744 m.viper.SetDefault("webkit_memory.poll_interval_seconds", defaults.WebkitMemory.PollIntervalSeconds)
745 m.viper.SetDefault("webkit_memory.enable_gc_interval", defaults.WebkitMemory.EnableGCInterval)
746 m.viper.SetDefault("webkit_memory.process_recycle_threshold", defaults.WebkitMemory.ProcessRecycleThreshold)
747 m.viper.SetDefault("webkit_memory.enable_memory_monitoring", defaults.WebkitMemory.EnableMemoryMonitoring)
748
749 // Rendering defaults
750 m.viper.SetDefault("rendering_mode", string(RenderingModeGPU))
751 m.viper.SetDefault("use_dom_zoom", defaults.UseDomZoom)
752 m.viper.SetDefault("default_zoom", defaults.DefaultZoom)
753
754 // Workspace defaults
755 m.viper.SetDefault("workspace.enable_zellij_controls", defaults.Workspace.EnableZellijControls)
756 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
757 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
758 m.viper.SetDefault("workspace.pane_mode.action_bindings", defaults.Workspace.PaneMode.ActionBindings)
759 m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab)
760 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)
761 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)
762 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)
763 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
764 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
765 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
766 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
767 // New popup behaviour defaults
768 m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
769 m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
770 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
771 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
772 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
773 m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor)
774 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
775 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
776}
777
778// createDefaultConfig creates a default configuration file.
779func (m *Manager) createDefaultConfig() error {
780 configFile, err := GetConfigFile()
781 if err != nil {
782 return err
783 }
784
785 // Create config directory if it doesn't exist
786 if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
787 return err
788 }
789
790 // Use Viper's SafeWriteConfigAs to avoid duplicate keys
791 if err := m.viper.SafeWriteConfigAs(configFile); err != nil {
792 return fmt.Errorf("failed to write config file: %w", err)
793 }
794
795 fmt.Printf("Created default configuration file: %s\n", configFile)
796 return nil
797}
798
799// GetConfigFile returns the path to the configuration file being used.
800func (m *Manager) GetConfigFile() string {
801 return m.viper.ConfigFileUsed()
802}
803
804// New returns a new default configuration instance.
805// This is a convenience function for getting default config without the full manager.
806func New() *Config {
807 return DefaultConfig()
808}
809
810// Global configuration manager instance
811var globalManager *Manager
812var globalManagerOnce sync.Once
813
814// Init initializes the global configuration manager.
815func Init() error {
816 var err error
817 globalManagerOnce.Do(func() {
818 globalManager, err = NewManager()
819 if err != nil {
820 return
821 }
822 err = globalManager.Load()
823 })
824 return err
825}
826
827// Get returns the global configuration.
828func Get() *Config {
829 if globalManager == nil {
830 // Return defaults if not initialized
831 return DefaultConfig()
832 }
833 return globalManager.Get()
834}
835
836// Watch starts watching the global configuration for changes.
837func Watch() error {
838 if globalManager == nil {
839 return fmt.Errorf("configuration not initialized")
840 }
841 return globalManager.Watch()
842}
843
844// OnConfigChange registers a callback for global configuration changes.
845func OnConfigChange(callback func(*Config)) {
846 if globalManager == nil {
847 return
848 }
849 globalManager.OnConfigChange(callback)
850}
851
852// validateAndConfigureCodecPreferences validates and auto-configures codec preferences based on GPU capabilities
853func (m *Manager) validateAndConfigureCodecPreferences(config *Config) *Config {
854 // Validate codec preferences
855 if config.CodecPreferences.PreferredCodecs == "" {
856 config.CodecPreferences.PreferredCodecs = "av1,h264,vp8"
857 fmt.Printf("Config: Set default codec preferences: %s\n", config.CodecPreferences.PreferredCodecs)
858 }
859
860 // Auto-configure based on GPU capabilities
861 if config.VideoAcceleration.AutoDetectGPU {
862 gpuInfo := gpu.DetectGPU()
863
864 // Check AV1 hardware support
865 if config.CodecPreferences.AV1HardwareOnly && !gpuInfo.SupportsAV1Hardware() {
866 // Disable AV1 hardware-only if GPU doesn't support it
867 config.CodecPreferences.AV1HardwareOnly = false
868 fmt.Printf("Config: GPU doesn't support AV1 hardware, enabling software fallback\n")
869 }
870
871 // Get detailed AV1 capabilities
872 av1Caps := gpuInfo.GetAV1HardwareCapabilities()
873 if av1Caps["decode"] {
874 logging.Info("GPU supports AV1 hardware decode")
875
876 // If GPU supports AV1, prefer it over VP9
877 if !config.CodecPreferences.ForceAV1 &&
878 !strings.HasPrefix(config.CodecPreferences.PreferredCodecs, "av1") {
879 config.CodecPreferences.PreferredCodecs = "av1," + config.CodecPreferences.PreferredCodecs
880 fmt.Printf("Config: Auto-enabled AV1 preference due to hardware support\n")
881 }
882 } else {
883 // If no AV1 support, ensure VP9 hardware is disabled to prevent issues
884 if !config.CodecPreferences.DisableVP9Hardware {
885 config.CodecPreferences.DisableVP9Hardware = true
886 fmt.Printf("Config: Auto-disabled VP9 hardware acceleration (no AV1 fallback)\n")
887 }
888 }
889
890 // Log GPU-specific codec capabilities
891 logging.Info(fmt.Sprintf("%s GPU codec capabilities - AV1: decode=%t, encode=%t, 10bit=%t",
892 gpuInfo.Vendor, av1Caps["decode"], av1Caps["encode"], av1Caps["10bit"]))
893 }
894
895 return config
896}