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