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