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