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