1// Package config provides configuration management for dumber with Viper integration.
2package config
3
4import (
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/bnema/dumber/internal/logging"
15 "github.com/bnema/dumber/pkg/gpu"
16 "github.com/fsnotify/fsnotify"
17 "github.com/spf13/viper"
18)
19
20// File permission constants
21const (
22 dirPerm = 0755 // Standard directory permissions (rwxr-xr-x)
23 filePerm = 0644 // Standard file permissions (rw-r--r--)
24)
25
26// Config represents the complete configuration for dumber.
27type Config struct {
28 Database DatabaseConfig `mapstructure:"database" yaml:"database"`
29 History HistoryConfig `mapstructure:"history" yaml:"history"`
30 SearchShortcuts map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`
31 Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
32 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
33 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
34 VideoAcceleration VideoAccelerationConfig `mapstructure:"video_acceleration" yaml:"video_acceleration"`
35 CodecPreferences CodecConfig `mapstructure:"codec_preferences" yaml:"codec_preferences"`
36 WebkitMemory WebkitMemoryConfig `mapstructure:"webkit_memory" yaml:"webkit_memory"`
37 Debug DebugConfig `mapstructure:"debug" yaml:"debug"`
38 APISecurity APISecurityConfig `mapstructure:"api_security" yaml:"api_security"`
39 // RenderingMode controls GPU/CPU rendering selection for WebKit
40 RenderingMode RenderingMode `mapstructure:"rendering_mode" yaml:"rendering_mode"`
41}
42
43// RenderingMode selects GPU vs CPU rendering.
44type RenderingMode string
45
46const (
47 RenderingModeAuto RenderingMode = "auto"
48 RenderingModeGPU RenderingMode = "gpu"
49 RenderingModeCPU RenderingMode = "cpu"
50)
51
52// DatabaseConfig holds database-related configuration.
53type DatabaseConfig struct {
54 Path string `mapstructure:"path" yaml:"path"`
55 MaxConnections int `mapstructure:"max_connections" yaml:"max_connections"`
56 MaxIdleTime time.Duration `mapstructure:"max_idle_time" yaml:"max_idle_time"`
57 QueryTimeout time.Duration `mapstructure:"query_timeout" yaml:"query_timeout"`
58}
59
60// HistoryConfig holds history-related configuration.
61type HistoryConfig struct {
62 MaxEntries int `mapstructure:"max_entries" yaml:"max_entries"`
63 RetentionPeriodDays int `mapstructure:"retention_period_days" yaml:"retention_period_days"`
64 CleanupIntervalDays int `mapstructure:"cleanup_interval_days" yaml:"cleanup_interval_days"`
65}
66
67// SearchShortcut represents a search shortcut configuration.
68type SearchShortcut struct {
69 URL string `mapstructure:"url" yaml:"url" json:"url"`
70 Description string `mapstructure:"description" yaml:"description" json:"description"`
71}
72
73// DmenuConfig holds dmenu/rofi integration configuration.
74type DmenuConfig struct {
75 MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items"`
76 ShowVisitCount bool `mapstructure:"show_visit_count" yaml:"show_visit_count"`
77 ShowLastVisited bool `mapstructure:"show_last_visited" yaml:"show_last_visited"`
78 HistoryPrefix string `mapstructure:"history_prefix" yaml:"history_prefix"`
79 ShortcutPrefix string `mapstructure:"shortcut_prefix" yaml:"shortcut_prefix"`
80 URLPrefix string `mapstructure:"url_prefix" yaml:"url_prefix"`
81 DateFormat string `mapstructure:"date_format" yaml:"date_format"`
82 SortByVisitCount bool `mapstructure:"sort_by_visit_count" yaml:"sort_by_visit_count"`
83}
84
85// LoggingConfig holds logging configuration.
86type LoggingConfig struct {
87 Level string `mapstructure:"level" yaml:"level"`
88 Format string `mapstructure:"format" yaml:"format"`
89 Filename string `mapstructure:"filename" yaml:"filename"`
90 MaxSize int `mapstructure:"max_size" yaml:"max_size"`
91 MaxBackups int `mapstructure:"max_backups" yaml:"max_backups"`
92 MaxAge int `mapstructure:"max_age" yaml:"max_age"`
93 Compress bool `mapstructure:"compress" yaml:"compress"`
94
95 // File output configuration
96 LogDir string `mapstructure:"log_dir" yaml:"log_dir"`
97 EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"`
98
99 // Capture settings
100 CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout"`
101 CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr"`
102 CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"`
103
104 // Debug output
105 DebugFile string `mapstructure:"debug_file" yaml:"debug_file"`
106 VerboseWebKit bool `mapstructure:"verbose_webkit" yaml:"verbose_webkit"`
107}
108
109// AppearanceConfig holds UI/rendering preferences.
110type AppearanceConfig struct {
111 // Default fonts for pages that do not specify fonts.
112 SansFont string `mapstructure:"sans_font" yaml:"sans_font"`
113 SerifFont string `mapstructure:"serif_font" yaml:"serif_font"`
114 MonospaceFont string `mapstructure:"monospace_font" yaml:"monospace_font"`
115 // Default font size in CSS pixels (approx).
116 DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`
117}
118
119// VideoAccelerationConfig holds video hardware acceleration preferences.
120type VideoAccelerationConfig struct {
121 EnableVAAPI bool `mapstructure:"enable_vaapi" yaml:"enable_vaapi"`
122 AutoDetectGPU bool `mapstructure:"auto_detect_gpu" yaml:"auto_detect_gpu"`
123 VAAPIDriverName string `mapstructure:"vaapi_driver_name" yaml:"vaapi_driver_name"`
124 EnableAllDrivers bool `mapstructure:"enable_all_drivers" yaml:"enable_all_drivers"`
125 LegacyVAAPI bool `mapstructure:"legacy_vaapi" yaml:"legacy_vaapi"`
126}
127
128// CodecConfig holds video codec preferences and handling
129type CodecConfig struct {
130 // Codec preference order (e.g., "av1,h264,vp8")
131 PreferredCodecs string `mapstructure:"preferred_codecs" yaml:"preferred_codecs"`
132
133 // Force specific codec for platforms
134 ForceAV1 bool `mapstructure:"force_av1" yaml:"force_av1"`
135
136 // Block problematic codecs
137 BlockVP9 bool `mapstructure:"block_vp9" yaml:"block_vp9"`
138 BlockVP8 bool `mapstructure:"block_vp8" yaml:"block_vp8"`
139
140 // Hardware acceleration per codec
141 AV1HardwareOnly bool `mapstructure:"av1_hardware_only" yaml:"av1_hardware_only"`
142 DisableVP9Hardware bool `mapstructure:"disable_vp9_hardware" yaml:"disable_vp9_hardware"`
143
144 // Buffer configuration for smooth playback
145 VideoBufferSizeMB int `mapstructure:"video_buffer_size_mb" yaml:"video_buffer_size_mb"`
146 QueueBufferTimeSec int `mapstructure:"queue_buffer_time_sec" yaml:"queue_buffer_time_sec"`
147
148 // Custom User-Agent for codec negotiation
149 CustomUserAgent string `mapstructure:"custom_user_agent" yaml:"custom_user_agent"`
150
151 // Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited)
152 AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"`
153
154 // Site-specific codec control settings
155 DisableTwitchCodecControl bool `mapstructure:"disable_twitch_codec_control" yaml:"disable_twitch_codec_control"`
156}
157
158// WebkitMemoryConfig holds WebKit memory optimization settings
159type WebkitMemoryConfig struct {
160 // Cache model (document_viewer | web_browser | primary_web_browser)
161 CacheModel string `mapstructure:"cache_model" yaml:"cache_model"`
162
163 // Enable page cache for back/forward navigation
164 EnablePageCache bool `mapstructure:"enable_page_cache" yaml:"enable_page_cache"`
165
166 // Memory limit in MB (0 = system default)
167 MemoryLimitMB int `mapstructure:"memory_limit_mb" yaml:"memory_limit_mb"`
168
169 // Memory pressure thresholds (0.0-1.0)
170 ConservativeThreshold float64 `mapstructure:"conservative_threshold" yaml:"conservative_threshold"`
171 StrictThreshold float64 `mapstructure:"strict_threshold" yaml:"strict_threshold"`
172 KillThreshold float64 `mapstructure:"kill_threshold" yaml:"kill_threshold"`
173
174 // Memory monitoring interval in seconds
175 PollIntervalSeconds float64 `mapstructure:"poll_interval_seconds" yaml:"poll_interval_seconds"`
176
177 // Garbage collection interval in seconds (0 = disabled)
178 EnableGCInterval int `mapstructure:"enable_gc_interval" yaml:"enable_gc_interval"`
179
180 // Process recycling threshold (number of page loads)
181 ProcessRecycleThreshold int `mapstructure:"process_recycle_threshold" yaml:"process_recycle_threshold"`
182
183 // Enable memory monitoring logs
184 EnableMemoryMonitoring bool `mapstructure:"enable_memory_monitoring" yaml:"enable_memory_monitoring"`
185}
186
187// DebugConfig holds debug and troubleshooting options
188type DebugConfig struct {
189 // Enable WebKit internal debug logging
190 EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"`
191
192 // WebKit debug categories (comma-separated)
193 // Common values: "Network:preconnectTo", "ContentFilters", "Loading", "JavaScript"
194 WebKitDebugCategories string `mapstructure:"webkit_debug_categories" yaml:"webkit_debug_categories"`
195
196 // Enable content filtering debug logs
197 EnableFilteringDebug bool `mapstructure:"enable_filtering_debug" yaml:"enable_filtering_debug"`
198
199 // Enable detailed WebView state logging
200 EnableWebViewDebug bool `mapstructure:"enable_webview_debug" yaml:"enable_webview_debug"`
201
202 // Log WebKit crashes and errors to file
203 LogWebKitCrashes bool `mapstructure:"log_webkit_crashes" yaml:"log_webkit_crashes"`
204
205 // Enable script injection debug logs
206 EnableScriptDebug bool `mapstructure:"enable_script_debug" yaml:"enable_script_debug"`
207
208 // Enable general debug mode (equivalent to DUMBER_DEBUG env var)
209 EnableGeneralDebug bool `mapstructure:"enable_general_debug" yaml:"enable_general_debug"`
210}
211
212// APISecurityConfig holds optional API key protection for dumb://api endpoints
213type APISecurityConfig struct {
214 // If non-empty, token that requests must present via `token` query param
215 Token string `mapstructure:"token" yaml:"token"`
216 // If true, require token for all API endpoints (except a minimal allowlist)
217 RequireToken bool `mapstructure:"require_token" yaml:"require_token"`
218}
219
220// Manager handles configuration loading, watching, and reloading.
221type Manager struct {
222 config *Config
223 viper *viper.Viper
224 mu sync.RWMutex
225 callbacks []func(*Config)
226 watching bool
227}
228
229// NewManager creates a new configuration manager.
230func NewManager() (*Manager, error) {
231 v := viper.New()
232
233 // Configure Viper - supports yaml, json, toml automatically
234 v.SetConfigName("config") // Will find config.yaml, config.json, config.toml, etc.
235
236 // Add config paths
237 configDir, err := GetConfigDir()
238 if err != nil {
239 return nil, fmt.Errorf("failed to get config directory: %w", err)
240 }
241 v.AddConfigPath(configDir)
242 v.AddConfigPath(".") // Current directory for development
243
244 // Set up environment variable support
245 v.SetEnvPrefix("DUMB_BROWSER")
246 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
247 v.AutomaticEnv()
248
249 // Bind specific environment variables
250 bindings := map[string]string{
251 "database.path": "DATABASE_PATH",
252 "database.max_connections": "DATABASE_MAX_CONNECTIONS",
253 "database.max_idle_time": "DATABASE_MAX_IDLE_TIME",
254 "database.query_timeout": "DATABASE_QUERY_TIMEOUT",
255 "browser.command": "BROWSER_COMMAND",
256 "browser.timeout": "BROWSER_TIMEOUT",
257 "browser.detach_process": "BROWSER_DETACH_PROCESS",
258 "history.max_entries": "HISTORY_MAX_ENTRIES",
259 "history.retention_period": "HISTORY_RETENTION_PERIOD",
260 "history.cleanup_interval": "HISTORY_CLEANUP_INTERVAL",
261 "dmenu.max_history_items": "DMENU_MAX_HISTORY_ITEMS",
262 "dmenu.show_visit_count": "DMENU_SHOW_VISIT_COUNT",
263 "dmenu.show_last_visited": "DMENU_SHOW_LAST_VISITED",
264 "dmenu.history_prefix": "DMENU_HISTORY_PREFIX",
265 "dmenu.shortcut_prefix": "DMENU_SHORTCUT_PREFIX",
266 "dmenu.url_prefix": "DMENU_URL_PREFIX",
267 "dmenu.date_format": "DMENU_DATE_FORMAT",
268 "dmenu.sort_by_visit_count": "DMENU_SORT_BY_VISIT_COUNT",
269 "logging.level": "LOGGING_LEVEL",
270 "logging.format": "LOGGING_FORMAT",
271 "logging.filename": "LOGGING_FILENAME",
272 "logging.max_size": "LOGGING_MAX_SIZE",
273 "logging.max_backups": "LOGGING_MAX_BACKUPS",
274 "logging.max_age": "LOGGING_MAX_AGE",
275 "logging.compress": "LOGGING_COMPRESS",
276 // API security
277 "api_security.token": "API_TOKEN",
278 "api_security.require_token": "API_REQUIRE_TOKEN",
279 }
280
281 for key, env := range bindings {
282 if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
283 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
284 }
285 }
286
287 // Explicit binding for rendering mode via dedicated env var
288 if err := v.BindEnv("rendering_mode", "DUMBER_RENDERING_MODE"); err != nil {
289 return nil, fmt.Errorf("failed to bind DUMBER_RENDERING_MODE: %w", err)
290 }
291
292 // Video acceleration environment variable bindings
293 videoAccelEnvBindings := map[string]string{
294 "video_acceleration.enable_vaapi": "DUMBER_VIDEO_ACCELERATION_ENABLE",
295 "video_acceleration.auto_detect_gpu": "DUMBER_VIDEO_AUTO_DETECT",
296 "video_acceleration.vaapi_driver_name": "LIBVA_DRIVER_NAME",
297 "video_acceleration.enable_all_drivers": "GST_VAAPI_ALL_DRIVERS",
298 "video_acceleration.legacy_vaapi": "WEBKIT_GST_ENABLE_LEGACY_VAAPI",
299 }
300
301 for key, env := range videoAccelEnvBindings {
302 if err := v.BindEnv(key, env); err != nil {
303 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
304 }
305 }
306
307 // Codec preferences environment variable bindings
308 codecEnvBindings := map[string]string{
309 "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS",
310 "codec_preferences.force_av1": "DUMBER_FORCE_AV1",
311 "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9",
312 "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8",
313 "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY",
314 "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW",
315 "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB",
316 "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC",
317 "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA",
318 "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES",
319 "codec_preferences.disable_twitch_codec_control": "DUMBER_DISABLE_TWITCH_CODEC",
320 }
321
322 for key, env := range codecEnvBindings {
323 if err := v.BindEnv(key, env); err != nil {
324 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
325 }
326 }
327
328 // WebKit memory environment variable bindings
329 memoryEnvBindings := map[string]string{
330 "webkit_memory.cache_model": "DUMBER_CACHE_MODEL",
331 "webkit_memory.enable_page_cache": "DUMBER_ENABLE_PAGE_CACHE",
332 "webkit_memory.memory_limit_mb": "DUMBER_MEMORY_LIMIT_MB",
333 "webkit_memory.conservative_threshold": "DUMBER_MEMORY_CONSERVATIVE",
334 "webkit_memory.strict_threshold": "DUMBER_MEMORY_STRICT",
335 "webkit_memory.kill_threshold": "DUMBER_MEMORY_KILL",
336 "webkit_memory.poll_interval_seconds": "DUMBER_MEMORY_POLL_INTERVAL",
337 "webkit_memory.enable_gc_interval": "DUMBER_GC_INTERVAL",
338 "webkit_memory.process_recycle_threshold": "DUMBER_RECYCLE_THRESHOLD",
339 "webkit_memory.enable_memory_monitoring": "DUMBER_ENABLE_MEMORY_MONITORING",
340 }
341
342 for key, env := range memoryEnvBindings {
343 if err := v.BindEnv(key, env); err != nil {
344 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
345 }
346 }
347
348 return &Manager{
349 viper: v,
350 callbacks: make([]func(*Config), 0),
351 }, nil
352}
353
354// Load loads the configuration from file and environment variables.
355func (m *Manager) Load() error {
356 m.mu.Lock()
357 defer m.mu.Unlock()
358
359 // Ensure directories exist
360 if err := EnsureDirectories(); err != nil {
361 return fmt.Errorf("failed to ensure directories: %w", err)
362 }
363
364 // Set defaults
365 m.setDefaults()
366
367 // Read config file if it exists
368 if err := m.viper.ReadInConfig(); err != nil {
369 var configFileNotFoundError viper.ConfigFileNotFoundError
370 if errors.As(err, &configFileNotFoundError) {
371 // Config file not found, create default one
372 if err := m.createDefaultConfig(); err != nil {
373 return fmt.Errorf("failed to create default config: %w", err)
374 }
375 } else {
376 return fmt.Errorf("failed to read config file: %w", err)
377 }
378 }
379
380 // Unmarshal into config struct
381 config := &Config{}
382 if err := m.viper.Unmarshal(config); err != nil {
383 return fmt.Errorf("failed to unmarshal config: %w", err)
384 }
385
386 // Set database path if not specified
387 if config.Database.Path == "" {
388 dbPath, err := GetDatabaseFile()
389 if err != nil {
390 return fmt.Errorf("failed to get database path: %w", err)
391 }
392 config.Database.Path = dbPath
393 }
394
395 // Normalize/validate rendering mode
396 switch strings.ToLower(string(config.RenderingMode)) {
397 case "", string(RenderingModeAuto):
398 config.RenderingMode = RenderingModeAuto
399 case string(RenderingModeGPU):
400 config.RenderingMode = RenderingModeGPU
401 case string(RenderingModeCPU):
402 config.RenderingMode = RenderingModeCPU
403 default:
404 config.RenderingMode = RenderingModeAuto
405 }
406
407 // Auto-detect GPU if enabled and driver name is not set
408 if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
409 gpuInfo := gpu.DetectGPU()
410 if gpuInfo.SupportsVAAPI() {
411 config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
412 }
413 }
414
415 // Validate and configure codec preferences
416 config = m.validateAndConfigureCodecPreferences(config)
417
418 m.config = config
419 return nil
420}
421
422// Get returns the current configuration (thread-safe).
423func (m *Manager) Get() *Config {
424 m.mu.RLock()
425 defer m.mu.RUnlock()
426
427 // Return a copy to prevent external modification
428 configCopy := *m.config
429 return &configCopy
430}
431
432// Watch starts watching the config file for changes and reloads automatically.
433func (m *Manager) Watch() error {
434 m.mu.Lock()
435 defer m.mu.Unlock()
436
437 if m.watching {
438 return nil // Already watching
439 }
440
441 m.viper.WatchConfig()
442 m.viper.OnConfigChange(func(_ fsnotify.Event) {
443 // Reload config
444 if err := m.reload(); err != nil {
445 fmt.Fprintf(os.Stderr, "Warning: failed to reload config: %v\n", err)
446 return
447 }
448
449 // Notify callbacks
450 m.mu.RLock()
451 config := m.config
452 callbacks := make([]func(*Config), len(m.callbacks))
453 copy(callbacks, m.callbacks)
454 m.mu.RUnlock()
455
456 for _, callback := range callbacks {
457 callback(config)
458 }
459 })
460
461 m.watching = true
462 return nil
463}
464
465// OnConfigChange registers a callback function to be called when config changes.
466func (m *Manager) OnConfigChange(callback func(*Config)) {
467 m.mu.Lock()
468 defer m.mu.Unlock()
469
470 m.callbacks = append(m.callbacks, callback)
471}
472
473// reload reloads the configuration (internal method, must be called with lock).
474func (m *Manager) reload() error {
475 if err := m.viper.ReadInConfig(); err != nil {
476 return err
477 }
478
479 config := &Config{}
480 if err := m.viper.Unmarshal(config); err != nil {
481 return err
482 }
483
484 // Set database path if not specified
485 if config.Database.Path == "" {
486 dbPath, err := GetDatabaseFile()
487 if err != nil {
488 return fmt.Errorf("failed to get database path: %w", err)
489 }
490 config.Database.Path = dbPath
491 }
492
493 // Normalize/validate rendering mode
494 switch strings.ToLower(string(config.RenderingMode)) {
495 case "", string(RenderingModeAuto):
496 config.RenderingMode = RenderingModeAuto
497 case string(RenderingModeGPU):
498 config.RenderingMode = RenderingModeGPU
499 case string(RenderingModeCPU):
500 config.RenderingMode = RenderingModeCPU
501 default:
502 config.RenderingMode = RenderingModeAuto
503 }
504
505 // Auto-detect GPU if enabled and driver name is not set
506 if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
507 gpuInfo := gpu.DetectGPU()
508 if gpuInfo.SupportsVAAPI() {
509 config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
510 }
511 }
512
513 // Validate and configure codec preferences
514 config = m.validateAndConfigureCodecPreferences(config)
515
516 m.config = config
517 return nil
518}
519
520// setDefaults sets default configuration values in Viper.
521func (m *Manager) setDefaults() {
522 defaults := DefaultConfig()
523
524 // Database defaults
525 m.viper.SetDefault("database.max_connections", defaults.Database.MaxConnections)
526 m.viper.SetDefault("database.max_idle_time", defaults.Database.MaxIdleTime)
527 m.viper.SetDefault("database.query_timeout", defaults.Database.QueryTimeout)
528
529 // Note: Browser config removed - we build our own browser
530
531 // History defaults
532 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
533 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
534 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
535
536 // Search shortcuts defaults
537 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
538
539 // Dmenu defaults
540 m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
541 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
542 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
543 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
544 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
545 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
546 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
547 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
548
549 // Logging defaults
550 m.viper.SetDefault("logging.level", defaults.Logging.Level)
551 m.viper.SetDefault("logging.format", defaults.Logging.Format)
552 m.viper.SetDefault("logging.filename", defaults.Logging.Filename)
553 m.viper.SetDefault("logging.max_size", defaults.Logging.MaxSize)
554 m.viper.SetDefault("logging.max_backups", defaults.Logging.MaxBackups)
555 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
556 m.viper.SetDefault("logging.compress", defaults.Logging.Compress)
557 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
558 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
559 m.viper.SetDefault("logging.capture_stdout", defaults.Logging.CaptureStdout)
560 m.viper.SetDefault("logging.capture_stderr", defaults.Logging.CaptureStderr)
561 m.viper.SetDefault("logging.capture_c_output", defaults.Logging.CaptureCOutput)
562 m.viper.SetDefault("logging.debug_file", defaults.Logging.DebugFile)
563 m.viper.SetDefault("logging.verbose_webkit", defaults.Logging.VerboseWebKit)
564
565 // Appearance defaults
566 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
567 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
568 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
569 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
570
571 // Video acceleration defaults
572 m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)
573 m.viper.SetDefault("video_acceleration.auto_detect_gpu", defaults.VideoAcceleration.AutoDetectGPU)
574 m.viper.SetDefault("video_acceleration.vaapi_driver_name", defaults.VideoAcceleration.VAAPIDriverName)
575 m.viper.SetDefault("video_acceleration.enable_all_drivers", defaults.VideoAcceleration.EnableAllDrivers)
576 m.viper.SetDefault("video_acceleration.legacy_vaapi", defaults.VideoAcceleration.LegacyVAAPI)
577
578 // Codec preferences defaults
579 m.viper.SetDefault("codec_preferences.preferred_codecs", defaults.CodecPreferences.PreferredCodecs)
580 m.viper.SetDefault("codec_preferences.force_av1", defaults.CodecPreferences.ForceAV1)
581 m.viper.SetDefault("codec_preferences.block_vp9", defaults.CodecPreferences.BlockVP9)
582 m.viper.SetDefault("codec_preferences.block_vp8", defaults.CodecPreferences.BlockVP8)
583 m.viper.SetDefault("codec_preferences.av1_hardware_only", defaults.CodecPreferences.AV1HardwareOnly)
584 m.viper.SetDefault("codec_preferences.disable_vp9_hardware", defaults.CodecPreferences.DisableVP9Hardware)
585 m.viper.SetDefault("codec_preferences.video_buffer_size_mb", defaults.CodecPreferences.VideoBufferSizeMB)
586 m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec)
587 m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent)
588 m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution)
589 m.viper.SetDefault("codec_preferences.disable_twitch_codec_control", defaults.CodecPreferences.DisableTwitchCodecControl)
590
591 // WebKit memory defaults
592 m.viper.SetDefault("webkit_memory.cache_model", defaults.WebkitMemory.CacheModel)
593 m.viper.SetDefault("webkit_memory.enable_page_cache", defaults.WebkitMemory.EnablePageCache)
594 m.viper.SetDefault("webkit_memory.memory_limit_mb", defaults.WebkitMemory.MemoryLimitMB)
595 m.viper.SetDefault("webkit_memory.conservative_threshold", defaults.WebkitMemory.ConservativeThreshold)
596 m.viper.SetDefault("webkit_memory.strict_threshold", defaults.WebkitMemory.StrictThreshold)
597 m.viper.SetDefault("webkit_memory.kill_threshold", defaults.WebkitMemory.KillThreshold)
598 m.viper.SetDefault("webkit_memory.poll_interval_seconds", defaults.WebkitMemory.PollIntervalSeconds)
599 m.viper.SetDefault("webkit_memory.enable_gc_interval", defaults.WebkitMemory.EnableGCInterval)
600 m.viper.SetDefault("webkit_memory.process_recycle_threshold", defaults.WebkitMemory.ProcessRecycleThreshold)
601 m.viper.SetDefault("webkit_memory.enable_memory_monitoring", defaults.WebkitMemory.EnableMemoryMonitoring)
602
603 // Rendering defaults
604 m.viper.SetDefault("rendering_mode", string(RenderingModeAuto))
605}
606
607// createDefaultConfig creates a default configuration file.
608func (m *Manager) createDefaultConfig() error {
609 configFile, err := GetConfigFile()
610 if err != nil {
611 return err
612 }
613
614 // Create config directory if it doesn't exist
615 if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
616 return err
617 }
618
619 // Get the default configuration
620 defaultConfig := DefaultConfig()
621
622 // Marshal to JSON with proper indentation
623 configData, err := json.MarshalIndent(defaultConfig, "", " ")
624 if err != nil {
625 return fmt.Errorf("failed to marshal default config: %w", err)
626 }
627
628 // Write JSON config file
629 if err := os.WriteFile(configFile, configData, filePerm); err != nil {
630 return fmt.Errorf("failed to write config file: %w", err)
631 }
632
633 fmt.Printf("Created default configuration file: %s\n", configFile)
634 return nil
635}
636
637// GetConfigFile returns the path to the configuration file being used.
638func (m *Manager) GetConfigFile() string {
639 return m.viper.ConfigFileUsed()
640}
641
642// New returns a new default configuration instance.
643// This is a convenience function for getting default config without the full manager.
644func New() *Config {
645 return DefaultConfig()
646}
647
648// Global configuration manager instance
649var globalManager *Manager
650var globalManagerOnce sync.Once
651
652// Init initializes the global configuration manager.
653func Init() error {
654 var err error
655 globalManagerOnce.Do(func() {
656 globalManager, err = NewManager()
657 if err != nil {
658 return
659 }
660 err = globalManager.Load()
661 })
662 return err
663}
664
665// Get returns the global configuration.
666func Get() *Config {
667 if globalManager == nil {
668 // Return defaults if not initialized
669 return DefaultConfig()
670 }
671 return globalManager.Get()
672}
673
674// Watch starts watching the global configuration for changes.
675func Watch() error {
676 if globalManager == nil {
677 return fmt.Errorf("configuration not initialized")
678 }
679 return globalManager.Watch()
680}
681
682// OnConfigChange registers a callback for global configuration changes.
683func OnConfigChange(callback func(*Config)) {
684 if globalManager == nil {
685 return
686 }
687 globalManager.OnConfigChange(callback)
688}
689
690// validateAndConfigureCodecPreferences validates and auto-configures codec preferences based on GPU capabilities
691func (m *Manager) validateAndConfigureCodecPreferences(config *Config) *Config {
692 // Validate codec preferences
693 if config.CodecPreferences.PreferredCodecs == "" {
694 config.CodecPreferences.PreferredCodecs = "av1,h264,vp8"
695 fmt.Printf("Config: Set default codec preferences: %s\n", config.CodecPreferences.PreferredCodecs)
696 }
697
698 // Auto-configure based on GPU capabilities
699 if config.VideoAcceleration.AutoDetectGPU {
700 gpuInfo := gpu.DetectGPU()
701
702 // Check AV1 hardware support
703 if config.CodecPreferences.AV1HardwareOnly && !gpuInfo.SupportsAV1Hardware() {
704 // Disable AV1 hardware-only if GPU doesn't support it
705 config.CodecPreferences.AV1HardwareOnly = false
706 fmt.Printf("Config: GPU doesn't support AV1 hardware, enabling software fallback\n")
707 }
708
709 // Get detailed AV1 capabilities
710 av1Caps := gpuInfo.GetAV1HardwareCapabilities()
711 if av1Caps["decode"] {
712 logging.Info("GPU supports AV1 hardware decode")
713
714 // If GPU supports AV1, prefer it over VP9
715 if !config.CodecPreferences.ForceAV1 &&
716 !strings.HasPrefix(config.CodecPreferences.PreferredCodecs, "av1") {
717 config.CodecPreferences.PreferredCodecs = "av1," + config.CodecPreferences.PreferredCodecs
718 fmt.Printf("Config: Auto-enabled AV1 preference due to hardware support\n")
719 }
720 } else {
721 // If no AV1 support, ensure VP9 hardware is disabled to prevent issues
722 if !config.CodecPreferences.DisableVP9Hardware {
723 config.CodecPreferences.DisableVP9Hardware = true
724 fmt.Printf("Config: Auto-disabled VP9 hardware acceleration (no AV1 fallback)\n")
725 }
726 }
727
728 // Log GPU-specific codec capabilities
729 logging.Info(fmt.Sprintf("%s GPU codec capabilities - AV1: decode=%t, encode=%t, 10bit=%t",
730 gpuInfo.Vendor, av1Caps["decode"], av1Caps["encode"], av1Caps["10bit"]))
731 }
732
733 return config
734}