1package config
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync"
11
12 "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/infrastructure/fonts"
14 "github.com/bnema/dumber/internal/logging"
15 "github.com/spf13/viper"
16)
17
18// Manager handles configuration loading, watching, and reloading.
19type Manager struct {
20 config *Config
21 viper *viper.Viper
22 mu sync.RWMutex
23 callbacks []func(*Config)
24 watching bool
25 skipNextReload bool // Set by Save() to prevent fsnotify reload from overwriting in-memory config
26}
27
28// NewManager creates a new configuration manager.
29func NewManager() (*Manager, error) {
30 v := viper.New()
31
32 // Configure Viper for TOML as default format
33 v.SetConfigName("config") // Name without extension
34 v.SetConfigType("toml") // TOML as default format
35
36 // Add config paths
37 configDir, err := GetConfigDir()
38 if err != nil {
39 return nil, fmt.Errorf("failed to determine config directory: %w\nCheck XDG_CONFIG_HOME environment variable or HOME directory", err)
40 }
41 v.AddConfigPath(configDir)
42 v.AddConfigPath(".") // Current directory for development
43
44 // Set up environment variable support
45 v.SetEnvPrefix("DUMBER")
46 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
47 v.AutomaticEnv()
48
49 // Note: Most environment variables are handled automatically via AutomaticEnv()
50 // with the DUMBER_ prefix (e.g., DUMBER_DATABASE_PATH, DUMBER_LOGGING_LEVEL).
51 // The explicit bindings below are only for special cases with different naming patterns.
52
53 // Explicit bindings for legacy or system env vars
54 if err := v.BindEnv("default_webpage_zoom", "DUMBER_DEFAULT_WEBPAGE_ZOOM"); err != nil {
55 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_WEBPAGE_ZOOM: %w", err)
56 }
57 if err := v.BindEnv("default_ui_scale", "DUMBER_DEFAULT_UI_SCALE"); err != nil {
58 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_UI_SCALE: %w", err)
59 }
60
61 // Logging environment variable bindings
62 if err := v.BindEnv("logging.level", "DUMBER_LOG_LEVEL"); err != nil {
63 return nil, fmt.Errorf("failed to bind DUMBER_LOG_LEVEL: %w", err)
64 }
65 if err := v.BindEnv("logging.format", "DUMBER_LOG_FORMAT"); err != nil {
66 return nil, fmt.Errorf("failed to bind DUMBER_LOG_FORMAT: %w", err)
67 }
68
69 return &Manager{
70 viper: v,
71 callbacks: make([]func(*Config), 0),
72 }, nil
73}
74
75// Load loads the configuration from file and environment variables.
76func (m *Manager) Load() error {
77 m.mu.Lock()
78 defer m.mu.Unlock()
79
80 if err := EnsureDirectories(); err != nil {
81 return fmt.Errorf("failed to ensure directories: %w", err)
82 }
83
84 m.setDefaults()
85
86 if err := m.readConfigFile(); err != nil {
87 return err
88 }
89
90 if err := m.checkLegacyFormat(); err != nil {
91 return err
92 }
93
94 config, err := m.unmarshalConfig()
95 if err != nil {
96 return err
97 }
98 if err := ensureDatabasePath(config); err != nil {
99 return err
100 }
101 normalizeConfig(config)
102
103 if err := validateConfig(config); err != nil {
104 return fmt.Errorf("configuration validation failed: %w", err)
105 }
106
107 m.config = config
108 return nil
109}
110
111func (m *Manager) readConfigFile() error {
112 if err := m.viper.ReadInConfig(); err != nil {
113 var configFileNotFoundError viper.ConfigFileNotFoundError
114 if errors.As(err, &configFileNotFoundError) {
115 if createErr := m.createDefaultConfig(); createErr != nil {
116 configDir, _ := GetConfigDir()
117 return fmt.Errorf(
118 "failed to create default config at %s: %w\nTry creating the directory manually or check permissions",
119 configDir,
120 createErr,
121 )
122 }
123 if rereadErr := m.viper.ReadInConfig(); rereadErr != nil {
124 return fmt.Errorf(
125 "failed to read newly created config file: %w\nThe config file was created but couldn't be read. Please check the file format",
126 rereadErr,
127 )
128 }
129 } else {
130 configFile := m.viper.ConfigFileUsed()
131 if configFile == "" {
132 configDir, _ := GetConfigDir()
133 configFile = filepath.Join(configDir, "config.toml")
134 }
135 return fmt.Errorf("failed to read config file at %s: %w\nCheck the file format (must be valid TOML) and permissions", configFile, err)
136 }
137 }
138
139 // Transform legacy action bindings before unmarshaling
140 m.transformLegacyActionBindings()
141
142 return nil
143}
144
145// transformLegacyActionBindings converts old-format action bindings to new format.
146// This is called after reading config but before unmarshaling.
147func (m *Manager) transformLegacyActionBindings() {
148 rawConfig := m.viper.AllSettings()
149 transformer := NewLegacyConfigTransformer()
150 transformer.TransformLegacyActions(rawConfig)
151
152 // Apply transformed config back to viper
153 for key, value := range rawConfig {
154 m.viper.Set(key, value)
155 }
156}
157
158// checkLegacyFormat detects old config format and returns an error directing user to migrate.
159func (m *Manager) checkLegacyFormat() error {
160 // Check if old sections exist by looking for known keys.
161 // IsSet works for legacy keys because they do not have defaults, while
162 // engine.type has a default and must be checked with InConfig.
163 hasOldSections := m.viper.IsSet("rendering.mode") ||
164 m.viper.IsSet("rendering.disable_dmabuf_renderer") ||
165 m.viper.IsSet("performance.profile") ||
166 m.viper.IsSet("privacy.cookie_policy") ||
167 m.viper.IsSet("runtime.prefix")
168
169 hasEngineSection := m.viper.InConfig("engine.type")
170
171 if hasOldSections && !hasEngineSection {
172 return fmt.Errorf(
173 "config format outdated: [rendering], [performance], [privacy] sections " +
174 "have moved to [engine]/[engine.webkit] — " +
175 "run \"dumber config migrate\" to update your config file",
176 )
177 }
178 return nil
179}
180
181func (m *Manager) unmarshalConfig() (*Config, error) {
182 config := &Config{}
183 if err := m.viper.Unmarshal(config); err != nil {
184 configFile := m.viper.ConfigFileUsed()
185 return nil, fmt.Errorf(
186 "failed to parse config file at %s: %w\nCheck for syntax errors, invalid values, or type mismatches",
187 configFile,
188 err,
189 )
190 }
191 return config, nil
192}
193
194func ensureDatabasePath(config *Config) error {
195 if config.Database.Path != "" {
196 return nil
197 }
198 dbPath, err := GetDatabaseFile()
199 if err != nil {
200 return fmt.Errorf("failed to get database path: %w", err)
201 }
202 config.Database.Path = dbPath
203 return nil
204}
205
206func normalizeConfig(config *Config) {
207 normalizeAppearance(config)
208 normalizeMedia(config)
209 normalizeEngineConfig(config)
210}
211
212func normalizeEngineConfig(config *Config) {
213 // Normalize GSK renderer (engine.webkit)
214 switch strings.ToLower(string(config.Engine.WebKit.GSKRenderer)) {
215 case "", string(GSKRendererAuto):
216 config.Engine.WebKit.GSKRenderer = GSKRendererAuto
217 case string(GSKRendererOpenGL):
218 config.Engine.WebKit.GSKRenderer = GSKRendererOpenGL
219 case string(GSKRendererVulkan):
220 config.Engine.WebKit.GSKRenderer = GSKRendererVulkan
221 case string(GSKRendererCairo):
222 config.Engine.WebKit.GSKRenderer = GSKRendererCairo
223 default:
224 config.Engine.WebKit.GSKRenderer = GSKRendererAuto
225 }
226
227 // Normalize cookie policy (engine).
228 // When no value (or an invalid value) is set, respect the ITP setting: if ITP is
229 // disabled the effective default is to block third-party cookies; if ITP is enabled
230 // all cookies are accepted by default. An explicit "always" value always wins.
231 itpDefault := func() CookiePolicy {
232 if config.Engine.WebKit.ITPEnabled {
233 return CookiePolicyAlways
234 }
235 return CookiePolicyNoThirdParty
236 }
237 switch strings.ToLower(string(config.Engine.CookiePolicy)) {
238 case "":
239 config.Engine.CookiePolicy = itpDefault()
240 case string(CookiePolicyAlways):
241 config.Engine.CookiePolicy = CookiePolicyAlways
242 case string(CookiePolicyNoThirdParty):
243 config.Engine.CookiePolicy = CookiePolicyNoThirdParty
244 case string(CookiePolicyNever):
245 config.Engine.CookiePolicy = CookiePolicyNever
246 default:
247 config.Engine.CookiePolicy = itpDefault()
248 }
249
250 // Normalize performance profile (engine)
251 switch strings.ToLower(string(config.Engine.Profile)) {
252 case "", string(ProfileDefault):
253 config.Engine.Profile = ProfileDefault
254 case string(ProfileLite):
255 config.Engine.Profile = ProfileLite
256 case string(ProfileMax):
257 config.Engine.Profile = ProfileMax
258 case string(ProfileCustom):
259 config.Engine.Profile = ProfileCustom
260 default:
261 config.Engine.Profile = ProfileDefault
262 }
263
264 // Trim runtime prefix (engine.webkit)
265 config.Engine.WebKit.Prefix = strings.TrimSpace(config.Engine.WebKit.Prefix)
266}
267
268func normalizeAppearance(config *Config) {
269 switch strings.ToLower(strings.TrimSpace(config.Appearance.ColorScheme)) {
270 case strings.ToLower(ThemePreferDark):
271 config.Appearance.ColorScheme = ThemePreferDark
272 case strings.ToLower(ThemePreferLight):
273 config.Appearance.ColorScheme = ThemePreferLight
274 case strings.ToLower(ThemeDefault):
275 config.Appearance.ColorScheme = ThemeDefault
276 default:
277 config.Appearance.ColorScheme = ThemeDefault
278 }
279}
280
281func normalizeMedia(config *Config) {
282 switch strings.ToLower(string(config.Media.HardwareDecodingMode)) {
283 case "", string(HardwareDecodingAuto):
284 config.Media.HardwareDecodingMode = HardwareDecodingAuto
285 case string(HardwareDecodingForce):
286 config.Media.HardwareDecodingMode = HardwareDecodingForce
287 case string(HardwareDecodingDisable):
288 config.Media.HardwareDecodingMode = HardwareDecodingDisable
289 default:
290 config.Media.HardwareDecodingMode = HardwareDecodingAuto
291 }
292}
293
294// Get returns the current configuration (thread-safe).
295func (m *Manager) Get() *Config {
296 m.mu.RLock()
297 defer m.mu.RUnlock()
298
299 // Return a copy to prevent external modification
300 configCopy := *m.config
301 return &configCopy
302}
303
304// Save saves the provided configuration to disk with ordered sections.
305func (m *Manager) Save(cfg *Config) error {
306 m.mu.Lock()
307 defer m.mu.Unlock()
308
309 if cfg == nil {
310 return fmt.Errorf("config is nil")
311 }
312
313 // Validate before writing so UI gets immediate errors.
314 if err := validateConfig(cfg); err != nil {
315 return fmt.Errorf("configuration validation failed: %w", err)
316 }
317
318 // Write config with ordered sections (bypasses Viper's random map ordering)
319 configFile := m.viper.ConfigFileUsed()
320 if configFile == "" {
321 var err error
322 configFile, err = GetConfigFile()
323 if err != nil {
324 return fmt.Errorf("failed to determine config file path: %w", err)
325 }
326 }
327
328 // Set flag BEFORE writing to prevent fsnotify-triggered reload from
329 // overwriting our in-memory config with stale viper cache
330 m.skipNextReload = true
331
332 if err := WriteConfigOrdered(cfg, configFile); err != nil {
333 m.skipNextReload = false // Reset on error
334 return fmt.Errorf("failed to write config: %w", err)
335 }
336
337 // Update in-memory config immediately (don't wait for file watcher)
338 // This ensures subsequent Get() calls return the new values
339 m.config = cfg
340
341 // Also reload from disk to sync Viper's internal state (if not watching)
342 if !m.watching {
343 m.skipNextReload = false // Clear flag since we're doing manual reload
344 if err := m.reload(); err != nil {
345 return err
346 }
347 }
348
349 return nil
350}
351
352// GetConfigFile returns the path to the configuration file being used.
353func (m *Manager) GetConfigFile() string {
354 return m.viper.ConfigFileUsed()
355}
356
357// createDefaultConfig creates a default configuration file with ordered sections.
358func (m *Manager) createDefaultConfig() error {
359 configFile, err := GetConfigFile()
360 if err != nil {
361 return err
362 }
363
364 // Create config directory if it doesn't exist
365 if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
366 return err
367 }
368
369 // Start with default config
370 cfg := DefaultConfig()
371
372 // Detect available fonts and override defaults
373 m.detectAndSetFontsOnConfig(cfg)
374
375 // Write config with ordered sections
376 if err := WriteConfigOrdered(cfg, configFile); err != nil {
377 return fmt.Errorf("failed to write config file: %w", err)
378 }
379
380 fmt.Printf("Created default configuration file: %s (TOML format)\n", configFile)
381
382 return nil
383}
384
385// detectAndSetFontsOnConfig detects available system fonts and sets the best available
386// fonts from the fallback chains directly on the Config struct.
387func (*Manager) detectAndSetFontsOnConfig(cfg *Config) {
388 // Create context with logger for debugging first-run font detection.
389 logger := logging.NewFromEnv()
390 ctx := logging.WithContext(context.Background(), logger)
391
392 detector := fonts.NewDetector()
393
394 if !detector.IsAvailable(ctx) {
395 // fc-list not available, keep hardcoded defaults
396 return
397 }
398
399 cfg.Appearance.SansFont = detector.SelectBestFont(ctx, port.FontCategorySansSerif, fonts.SansSerifFallbackChain())
400 cfg.Appearance.SerifFont = detector.SelectBestFont(ctx, port.FontCategorySerif, fonts.SerifFallbackChain())
401 cfg.Appearance.MonospaceFont = detector.SelectBestFont(ctx, port.FontCategoryMonospace, fonts.MonospaceFallbackChain())
402}
403
404// setDefaults sets default configuration values in Viper.
405func (m *Manager) setDefaults() {
406 defaults := DefaultConfig()
407
408 // Note: Database.Path is set dynamically in Load(), no defaults needed
409
410 m.setHistoryDefaults(defaults)
411 m.setSearchDefaults(defaults)
412 m.setDmenuDefaults(defaults)
413 m.setLoggingDefaults(defaults)
414 m.setDebugDefaults(defaults)
415 m.setAppearanceDefaults(defaults)
416 m.setEngineDefaults(defaults)
417 m.setZoomAndScaleDefaults(defaults)
418 m.setWorkspaceDefaults(defaults)
419 m.setContentFilteringDefaults(defaults)
420 m.setClipboardDefaults(defaults)
421 m.setOmniboxDefaults(defaults)
422 m.setMediaDefaults(defaults)
423 m.setSessionDefaults(defaults)
424 m.setUpdateDefaults(defaults)
425 m.setDownloadsDefaults(defaults)
426}
427
428func (m *Manager) setHistoryDefaults(defaults *Config) {
429 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
430 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
431 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
432}
433
434func (m *Manager) setSearchDefaults(defaults *Config) {
435 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
436 m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
437}
438
439func (m *Manager) setDmenuDefaults(defaults *Config) {
440 m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays)
441 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
442 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
443 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
444 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
445 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
446 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
447 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
448}
449
450func (m *Manager) setLoggingDefaults(defaults *Config) {
451 m.viper.SetDefault("logging.level", defaults.Logging.Level)
452 m.viper.SetDefault("logging.format", defaults.Logging.Format)
453 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
454 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
455 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
456 m.viper.SetDefault("logging.capture_console", defaults.Logging.CaptureConsole)
457 m.viper.SetDefault("logging.capture_gtk_logs", defaults.Logging.CaptureGTKLogs)
458}
459
460func (m *Manager) setDebugDefaults(defaults *Config) {
461 m.viper.SetDefault("debug.enable_devtools", defaults.Debug.EnableDevTools)
462}
463
464func (m *Manager) setAppearanceDefaults(defaults *Config) {
465 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
466 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
467 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
468 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
469 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
470 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
471 m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
472}
473
474func (m *Manager) setZoomAndScaleDefaults(defaults *Config) {
475 m.viper.SetDefault("default_webpage_zoom", defaults.DefaultWebpageZoom)
476 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
477}
478
479func (m *Manager) setWorkspaceDefaults(defaults *Config) {
480 m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
481 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
482 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
483 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
484 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
485 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
486 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
487 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
488 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
489 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
490 m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
491 m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions)
492 m.viper.SetDefault("workspace.shortcuts.actions", defaults.Workspace.Shortcuts.Actions)
493 m.viper.SetDefault("workspace.floating_pane.width_pct", defaults.Workspace.FloatingPane.WidthPct)
494 m.viper.SetDefault("workspace.floating_pane.height_pct", defaults.Workspace.FloatingPane.HeightPct)
495 m.viper.SetDefault("workspace.floating_pane.profiles", defaults.Workspace.FloatingPane.Profiles)
496 m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
497 m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
498 m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove)
499 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
500 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
501 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
502 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
503 m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
504 m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
505 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
506 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
507 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
508 m.viper.SetDefault("workspace.styling.mode_border_width", defaults.Workspace.Styling.ModeBorderWidth)
509 m.viper.SetDefault("workspace.styling.pane_mode_color", defaults.Workspace.Styling.PaneModeColor)
510 m.viper.SetDefault("workspace.styling.tab_mode_color", defaults.Workspace.Styling.TabModeColor)
511 m.viper.SetDefault("workspace.styling.session_mode_color", defaults.Workspace.Styling.SessionModeColor)
512 m.viper.SetDefault("workspace.styling.resize_mode_color", defaults.Workspace.Styling.ResizeModeColor)
513 m.viper.SetDefault("workspace.styling.mode_indicator_toaster_enabled", defaults.Workspace.Styling.ModeIndicatorToasterEnabled)
514 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
515}
516
517func (m *Manager) setContentFilteringDefaults(defaults *Config) {
518 m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
519 m.viper.SetDefault("content_filtering.auto_update", defaults.ContentFiltering.AutoUpdate)
520}
521
522func (m *Manager) setClipboardDefaults(defaults *Config) {
523 m.viper.SetDefault("clipboard.auto_copy_on_selection", defaults.Clipboard.AutoCopyOnSelection)
524}
525
526func (m *Manager) setOmniboxDefaults(defaults *Config) {
527 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
528 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
529}
530
531func (m *Manager) setMediaDefaults(defaults *Config) {
532 // GStreamer fields (force_vsync, gl_rendering_mode, gstreamer_debug_level)
533 // have moved to [engine.webkit] — only non-migrated media fields remain here.
534 m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
535 m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
536 m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup)
537}
538
539// setRuntimeDefaults removed — runtime.prefix moved to [engine.webkit].
540
541func (m *Manager) setSessionDefaults(defaults *Config) {
542 m.viper.SetDefault("session.auto_restore", defaults.Session.AutoRestore)
543 m.viper.SetDefault("session.snapshot_interval_ms", defaults.Session.SnapshotIntervalMs)
544 m.viper.SetDefault("session.max_exited_sessions", defaults.Session.MaxExitedSessions)
545 m.viper.SetDefault("session.max_exited_session_age_days", defaults.Session.MaxExitedSessionAgeDays)
546 m.viper.SetDefault("session.session_mode.activation_shortcut", defaults.Session.SessionMode.ActivationShortcut)
547 m.viper.SetDefault("session.session_mode.timeout_ms", defaults.Session.SessionMode.TimeoutMilliseconds)
548 m.viper.SetDefault("session.session_mode.actions", defaults.Session.SessionMode.Actions)
549}
550
551func (m *Manager) setUpdateDefaults(defaults *Config) {
552 m.viper.SetDefault("update.enable_on_startup", defaults.Update.EnableOnStartup)
553 m.viper.SetDefault("update.auto_download", defaults.Update.AutoDownload)
554 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
555}
556
557// setPerformanceDefaults removed — all fields moved to [engine]/[engine.webkit].
558
559func (m *Manager) setDownloadsDefaults(defaults *Config) {
560 m.viper.SetDefault("downloads.path", defaults.Downloads.Path)
561}
562
563func (m *Manager) setEngineDefaults(defaults *Config) {
564 e := defaults.Engine
565 m.viper.SetDefault("engine.type", e.Type)
566 m.viper.SetDefault("engine.profile", string(e.Profile))
567 m.viper.SetDefault("engine.pool_prewarm_count", e.PoolPrewarmCount)
568 m.viper.SetDefault("engine.zoom_cache_size", e.ZoomCacheSize)
569 m.viper.SetDefault("engine.cookie_policy", string(e.CookiePolicy))
570
571 wk := e.WebKit
572 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)
573 m.viper.SetDefault("engine.webkit.skia_cpu_painting_threads", wk.SkiaCPUPaintingThreads)
574 m.viper.SetDefault("engine.webkit.skia_gpu_painting_threads", wk.SkiaGPUPaintingThreads)
575 m.viper.SetDefault("engine.webkit.skia_enable_cpu_rendering", wk.SkiaEnableCPURendering)
576 m.viper.SetDefault("engine.webkit.disable_dmabuf_renderer", wk.DisableDMABufRenderer)
577 m.viper.SetDefault("engine.webkit.force_compositing_mode", wk.ForceCompositingMode)
578 m.viper.SetDefault("engine.webkit.disable_compositing_mode", wk.DisableCompositingMode)
579 m.viper.SetDefault("engine.webkit.gsk_renderer", string(wk.GSKRenderer))
580 m.viper.SetDefault("engine.webkit.disable_mipmaps", wk.DisableMipmaps)
581 m.viper.SetDefault("engine.webkit.prefer_gl", wk.PreferGL)
582 m.viper.SetDefault("engine.webkit.draw_compositing_indicators", wk.DrawCompositingIndicators)
583 m.viper.SetDefault("engine.webkit.show_fps", wk.ShowFPS)
584 m.viper.SetDefault("engine.webkit.sample_memory", wk.SampleMemory)
585 m.viper.SetDefault("engine.webkit.debug_frames", wk.DebugFrames)
586 m.viper.SetDefault("engine.webkit.force_vsync", wk.ForceVSync)
587 m.viper.SetDefault("engine.webkit.gl_rendering_mode", string(wk.GLRenderingMode))
588 m.viper.SetDefault("engine.webkit.gstreamer_debug_level", wk.GStreamerDebugLevel)
589 m.viper.SetDefault("engine.webkit.prefix", wk.Prefix)
590 m.viper.SetDefault("engine.webkit.web_process_memory_limit_mb", wk.WebProcessMemoryLimitMB)
591 m.viper.SetDefault("engine.webkit.web_process_memory_poll_interval_sec", wk.WebProcessMemoryPollIntervalSec)
592 m.viper.SetDefault("engine.webkit.web_process_memory_conservative_threshold", wk.WebProcessMemoryConservativeThreshold)
593 m.viper.SetDefault("engine.webkit.web_process_memory_strict_threshold", wk.WebProcessMemoryStrictThreshold)
594 m.viper.SetDefault("engine.webkit.network_process_memory_limit_mb", wk.NetworkProcessMemoryLimitMB)
595 m.viper.SetDefault("engine.webkit.network_process_memory_poll_interval_sec", wk.NetworkProcessMemoryPollIntervalSec)
596 m.viper.SetDefault("engine.webkit.network_process_memory_conservative_threshold", wk.NetworkProcessMemoryConservativeThreshold)
597 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)
598}
599
600// New returns a new default configuration instance.
601// This is a convenience function for getting default config without the full manager.
602func New() *Config {
603 return DefaultConfig()
604}
605
606// Global configuration manager instance
607var globalManager *Manager
608var globalManagerOnce sync.Once
609
610// Init initializes the global configuration manager.
611func Init() error {
612 var err error
613 globalManagerOnce.Do(func() {
614 globalManager, err = NewManager()
615 if err != nil {
616 return
617 }
618 err = globalManager.Load()
619 })
620 return err
621}
622
623// Get returns the global configuration.
624func Get() *Config {
625 if globalManager == nil {
626 // Return defaults if not initialized
627 return DefaultConfig()
628 }
629 return globalManager.Get()
630}
631
632// GetManager returns the global configuration manager.
633// This is useful for accessing watcher functionality.
634func GetManager() *Manager {
635 return globalManager
636}