1package config
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10
11 "github.com/spf13/viper"
12)
13
14// Manager handles configuration loading, watching, and reloading.
15type Manager struct {
16 config *Config
17 viper *viper.Viper
18 mu sync.RWMutex
19 callbacks []func(*Config)
20 watching bool
21}
22
23// NewManager creates a new configuration manager.
24func NewManager() (*Manager, error) {
25 v := viper.New()
26
27 // Configure Viper for TOML as default format
28 v.SetConfigName("config") // Name without extension
29 v.SetConfigType("toml") // TOML as default format
30
31 // Add config paths
32 configDir, err := GetConfigDir()
33 if err != nil {
34 return nil, fmt.Errorf("failed to determine config directory: %w\nCheck XDG_CONFIG_HOME environment variable or HOME directory", err)
35 }
36 v.AddConfigPath(configDir)
37 v.AddConfigPath(".") // Current directory for development
38
39 // Set up environment variable support
40 v.SetEnvPrefix("DUMBER")
41 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
42 v.AutomaticEnv()
43
44 // Note: Most environment variables are handled automatically via AutomaticEnv()
45 // with the DUMBER_ prefix (e.g., DUMBER_DATABASE_PATH, DUMBER_LOGGING_LEVEL).
46 // The explicit bindings below are only for special cases with different naming patterns.
47
48 // Explicit bindings for legacy or system env vars
49 if err := v.BindEnv("default_webpage_zoom", "DUMBER_DEFAULT_WEBPAGE_ZOOM"); err != nil {
50 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_WEBPAGE_ZOOM: %w", err)
51 }
52 if err := v.BindEnv("default_ui_scale", "DUMBER_DEFAULT_UI_SCALE"); err != nil {
53 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_UI_SCALE: %w", err)
54 }
55
56 // Logging environment variable bindings
57 if err := v.BindEnv("logging.level", "DUMBER_LOG_LEVEL"); err != nil {
58 return nil, fmt.Errorf("failed to bind DUMBER_LOG_LEVEL: %w", err)
59 }
60 if err := v.BindEnv("logging.format", "DUMBER_LOG_FORMAT"); err != nil {
61 return nil, fmt.Errorf("failed to bind DUMBER_LOG_FORMAT: %w", err)
62 }
63
64 return &Manager{
65 viper: v,
66 callbacks: make([]func(*Config), 0),
67 }, nil
68}
69
70// Load loads the configuration from file and environment variables.
71func (m *Manager) Load() error {
72 m.mu.Lock()
73 defer m.mu.Unlock()
74
75 if err := EnsureDirectories(); err != nil {
76 return fmt.Errorf("failed to ensure directories: %w", err)
77 }
78
79 m.setDefaults()
80
81 if err := m.readConfigFile(); err != nil {
82 return err
83 }
84
85 config, err := m.unmarshalConfig()
86 if err != nil {
87 return err
88 }
89 if err := ensureDatabasePath(config); err != nil {
90 return err
91 }
92 normalizeConfig(config)
93
94 if err := validateConfig(config); err != nil {
95 return fmt.Errorf("configuration validation failed: %w", err)
96 }
97
98 m.config = config
99 return nil
100}
101
102func (m *Manager) readConfigFile() error {
103 if err := m.viper.ReadInConfig(); err != nil {
104 var configFileNotFoundError viper.ConfigFileNotFoundError
105 if errors.As(err, &configFileNotFoundError) {
106 if createErr := m.createDefaultConfig(); createErr != nil {
107 configDir, _ := GetConfigDir()
108 return fmt.Errorf(
109 "failed to create default config at %s: %w\nTry creating the directory manually or check permissions",
110 configDir,
111 createErr,
112 )
113 }
114 if rereadErr := m.viper.ReadInConfig(); rereadErr != nil {
115 return fmt.Errorf(
116 "failed to read newly created config file: %w\nThe config file was created but couldn't be read. Please check the file format",
117 rereadErr,
118 )
119 }
120 } else {
121 configFile := m.viper.ConfigFileUsed()
122 if configFile == "" {
123 configDir, _ := GetConfigDir()
124 configFile = filepath.Join(configDir, "config.toml")
125 }
126 return fmt.Errorf("failed to read config file at %s: %w\nCheck the file format (must be valid TOML) and permissions", configFile, err)
127 }
128 }
129 return nil
130}
131
132func (m *Manager) unmarshalConfig() (*Config, error) {
133 config := &Config{}
134 if err := m.viper.Unmarshal(config); err != nil {
135 configFile := m.viper.ConfigFileUsed()
136 return nil, fmt.Errorf(
137 "failed to parse config file at %s: %w\nCheck for syntax errors, invalid values, or type mismatches",
138 configFile,
139 err,
140 )
141 }
142 return config, nil
143}
144
145func ensureDatabasePath(config *Config) error {
146 if config.Database.Path != "" {
147 return nil
148 }
149 dbPath, err := GetDatabaseFile()
150 if err != nil {
151 return fmt.Errorf("failed to get database path: %w", err)
152 }
153 config.Database.Path = dbPath
154 return nil
155}
156
157func normalizeConfig(config *Config) {
158 switch strings.ToLower(string(config.Rendering.Mode)) {
159 case "", string(RenderingModeAuto):
160 config.Rendering.Mode = RenderingModeAuto
161 case string(RenderingModeGPU):
162 config.Rendering.Mode = RenderingModeGPU
163 case string(RenderingModeCPU):
164 config.Rendering.Mode = RenderingModeCPU
165 default:
166 config.Rendering.Mode = RenderingModeAuto
167 }
168
169 switch strings.ToLower(string(config.Rendering.GSKRenderer)) {
170 case "", string(GSKRendererAuto):
171 config.Rendering.GSKRenderer = GSKRendererAuto
172 case string(GSKRendererOpenGL):
173 config.Rendering.GSKRenderer = GSKRendererOpenGL
174 case string(GSKRendererVulkan):
175 config.Rendering.GSKRenderer = GSKRendererVulkan
176 case string(GSKRendererCairo):
177 config.Rendering.GSKRenderer = GSKRendererCairo
178 default:
179 config.Rendering.GSKRenderer = GSKRendererAuto
180 }
181
182 switch config.Appearance.ColorScheme {
183 case ThemePreferDark, ThemePreferLight, ThemeDefault:
184 case "":
185 config.Appearance.ColorScheme = ThemeDefault
186 default:
187 config.Appearance.ColorScheme = ThemeDefault
188 }
189
190 switch strings.ToLower(string(config.Media.HardwareDecodingMode)) {
191 case "", string(HardwareDecodingAuto):
192 config.Media.HardwareDecodingMode = HardwareDecodingAuto
193 case string(HardwareDecodingForce):
194 config.Media.HardwareDecodingMode = HardwareDecodingForce
195 case string(HardwareDecodingDisable):
196 config.Media.HardwareDecodingMode = HardwareDecodingDisable
197 default:
198 config.Media.HardwareDecodingMode = HardwareDecodingAuto
199 }
200
201 switch strings.ToLower(string(config.Media.GLRenderingMode)) {
202 case "", string(GLRenderingModeAuto):
203 config.Media.GLRenderingMode = GLRenderingModeAuto
204 case string(GLRenderingModeGLES2):
205 config.Media.GLRenderingMode = GLRenderingModeGLES2
206 case string(GLRenderingModeGL3):
207 config.Media.GLRenderingMode = GLRenderingModeGL3
208 case string(GLRenderingModeNone):
209 config.Media.GLRenderingMode = GLRenderingModeNone
210 default:
211 config.Media.GLRenderingMode = GLRenderingModeAuto
212 }
213
214 config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)
215 normalizePerformanceProfile(config)
216}
217
218func normalizePerformanceProfile(config *Config) {
219 switch strings.ToLower(string(config.Performance.Profile)) {
220 case "", string(ProfileDefault):
221 config.Performance.Profile = ProfileDefault
222 case string(ProfileLite):
223 config.Performance.Profile = ProfileLite
224 case string(ProfileMax):
225 config.Performance.Profile = ProfileMax
226 case string(ProfileCustom):
227 config.Performance.Profile = ProfileCustom
228 }
229}
230
231// Get returns the current configuration (thread-safe).
232func (m *Manager) Get() *Config {
233 m.mu.RLock()
234 defer m.mu.RUnlock()
235
236 // Return a copy to prevent external modification
237 configCopy := *m.config
238 return &configCopy
239}
240
241// Save saves the provided configuration to disk and updates Viper.
242func (m *Manager) Save(cfg *Config) error {
243 m.mu.Lock()
244 defer m.mu.Unlock()
245
246 if cfg == nil {
247 return fmt.Errorf("config is nil")
248 }
249
250 // Validate before writing so UI gets immediate errors.
251 if err := validateConfig(cfg); err != nil {
252 return fmt.Errorf("configuration validation failed: %w", err)
253 }
254
255 // Update Viper with the new values.
256 // Since we can't easily update Viper from a struct automatically while preserving
257 // all settings, we update the main keys we care about.
258 m.viper.Set("appearance.sans_font", cfg.Appearance.SansFont)
259 m.viper.Set("appearance.serif_font", cfg.Appearance.SerifFont)
260 m.viper.Set("appearance.monospace_font", cfg.Appearance.MonospaceFont)
261 m.viper.Set("appearance.default_font_size", cfg.Appearance.DefaultFontSize)
262 m.viper.Set("appearance.color_scheme", cfg.Appearance.ColorScheme)
263 m.viper.Set("appearance.light_palette", cfg.Appearance.LightPalette)
264 m.viper.Set("appearance.dark_palette", cfg.Appearance.DarkPalette)
265 m.viper.Set("default_webpage_zoom", cfg.DefaultWebpageZoom)
266 m.viper.Set("default_ui_scale", cfg.DefaultUIScale)
267 m.viper.Set("default_search_engine", cfg.DefaultSearchEngine)
268
269 // Performance profile (requires browser restart to take effect)
270 m.viper.Set("performance.profile", string(cfg.Performance.Profile))
271
272 if err := m.viper.WriteConfig(); err != nil {
273 return fmt.Errorf("failed to write config: %w", err)
274 }
275
276 // We don't update m.config here manually because viper.OnConfigChange
277 // will trigger reload() if Watch() is active.
278 // If Watch() is not active, we should call reload() manually.
279 if !m.watching {
280 if err := m.reload(); err != nil {
281 return err
282 }
283 }
284
285 return nil
286}
287
288// GetConfigFile returns the path to the configuration file being used.
289func (m *Manager) GetConfigFile() string {
290 return m.viper.ConfigFileUsed()
291}
292
293// createDefaultConfig creates a default configuration file.
294func (m *Manager) createDefaultConfig() error {
295 configFile, err := GetConfigFile()
296 if err != nil {
297 return err
298 }
299
300 // Create config directory if it doesn't exist
301 if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
302 return err
303 }
304
305 // Ensure TOML format and write config
306 m.viper.SetConfigType("toml")
307 if err := m.viper.SafeWriteConfigAs(configFile); err != nil {
308 return fmt.Errorf("failed to write config file: %w", err)
309 }
310
311 fmt.Printf("Created default configuration file: %s (TOML format)\n", configFile)
312
313 return nil
314}
315
316// setDefaults sets default configuration values in Viper.
317func (m *Manager) setDefaults() {
318 defaults := DefaultConfig()
319
320 // Note: Database.Path is set dynamically in Load(), no defaults needed
321
322 m.setHistoryDefaults(defaults)
323 m.setSearchDefaults(defaults)
324 m.setDmenuDefaults(defaults)
325 m.setLoggingDefaults(defaults)
326 m.setDebugDefaults(defaults)
327 m.setAppearanceDefaults(defaults)
328 m.setRenderingDefaults(defaults)
329 m.setWorkspaceDefaults(defaults)
330 m.setContentFilteringDefaults(defaults)
331 m.setOmniboxDefaults(defaults)
332 m.setMediaDefaults(defaults)
333 m.setRuntimeDefaults(defaults)
334 m.setSessionDefaults(defaults)
335 m.setUpdateDefaults(defaults)
336 m.setPerformanceDefaults(defaults)
337}
338
339func (m *Manager) setHistoryDefaults(defaults *Config) {
340 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
341 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
342 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
343}
344
345func (m *Manager) setSearchDefaults(defaults *Config) {
346 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
347 m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
348}
349
350func (m *Manager) setDmenuDefaults(defaults *Config) {
351 m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays)
352 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
353 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
354 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
355 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
356 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
357 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
358 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
359}
360
361func (m *Manager) setLoggingDefaults(defaults *Config) {
362 m.viper.SetDefault("logging.level", defaults.Logging.Level)
363 m.viper.SetDefault("logging.format", defaults.Logging.Format)
364 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
365 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
366 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
367 m.viper.SetDefault("logging.capture_console", defaults.Logging.CaptureConsole)
368}
369
370func (m *Manager) setDebugDefaults(defaults *Config) {
371 m.viper.SetDefault("debug.enable_devtools", defaults.Debug.EnableDevTools)
372}
373
374func (m *Manager) setAppearanceDefaults(defaults *Config) {
375 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
376 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
377 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
378 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
379 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
380 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
381 m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
382}
383
384func (m *Manager) setRenderingDefaults(defaults *Config) {
385 m.viper.SetDefault("rendering.mode", string(defaults.Rendering.Mode))
386 m.viper.SetDefault("rendering.disable_dmabuf_renderer", defaults.Rendering.DisableDMABufRenderer)
387 m.viper.SetDefault("rendering.force_compositing_mode", defaults.Rendering.ForceCompositingMode)
388 m.viper.SetDefault("rendering.disable_compositing_mode", defaults.Rendering.DisableCompositingMode)
389 m.viper.SetDefault("rendering.gsk_renderer", string(defaults.Rendering.GSKRenderer))
390 m.viper.SetDefault("rendering.disable_mipmaps", defaults.Rendering.DisableMipmaps)
391 m.viper.SetDefault("rendering.prefer_gl", defaults.Rendering.PreferGL)
392 m.viper.SetDefault("rendering.draw_compositing_indicators", defaults.Rendering.DrawCompositingIndicators)
393 m.viper.SetDefault("rendering.show_fps", defaults.Rendering.ShowFPS)
394 m.viper.SetDefault("rendering.sample_memory", defaults.Rendering.SampleMemory)
395 m.viper.SetDefault("rendering.debug_frames", defaults.Rendering.DebugFrames)
396 m.viper.SetDefault("default_webpage_zoom", defaults.DefaultWebpageZoom)
397 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
398}
399
400func (m *Manager) setWorkspaceDefaults(defaults *Config) {
401 m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
402 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
403 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
404 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
405 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
406 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
407 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
408 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
409 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
410 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
411 m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
412 m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions)
413 m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane)
414 m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab)
415 m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab)
416 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_left", defaults.Workspace.Shortcuts.ConsumeOrExpelLeft)
417 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_right", defaults.Workspace.Shortcuts.ConsumeOrExpelRight)
418 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_up", defaults.Workspace.Shortcuts.ConsumeOrExpelUp)
419 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_down", defaults.Workspace.Shortcuts.ConsumeOrExpelDown)
420 m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
421 m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
422 m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove)
423 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
424 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
425 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
426 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
427 m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
428 m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
429 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
430 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
431 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
432 m.viper.SetDefault("workspace.styling.mode_border_width", defaults.Workspace.Styling.ModeBorderWidth)
433 m.viper.SetDefault("workspace.styling.pane_mode_color", defaults.Workspace.Styling.PaneModeColor)
434 m.viper.SetDefault("workspace.styling.tab_mode_color", defaults.Workspace.Styling.TabModeColor)
435 m.viper.SetDefault("workspace.styling.session_mode_color", defaults.Workspace.Styling.SessionModeColor)
436 m.viper.SetDefault("workspace.styling.resize_mode_color", defaults.Workspace.Styling.ResizeModeColor)
437 m.viper.SetDefault("workspace.styling.mode_indicator_toaster_enabled", defaults.Workspace.Styling.ModeIndicatorToasterEnabled)
438 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
439}
440
441func (m *Manager) setContentFilteringDefaults(defaults *Config) {
442 m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
443 m.viper.SetDefault("content_filtering.auto_update", defaults.ContentFiltering.AutoUpdate)
444}
445
446func (m *Manager) setOmniboxDefaults(defaults *Config) {
447 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
448 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
449}
450
451func (m *Manager) setMediaDefaults(defaults *Config) {
452 m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
453 m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
454 m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup)
455 m.viper.SetDefault("media.force_vsync", defaults.Media.ForceVSync)
456 m.viper.SetDefault("media.gl_rendering_mode", string(defaults.Media.GLRenderingMode))
457 m.viper.SetDefault("media.gstreamer_debug_level", defaults.Media.GStreamerDebugLevel)
458 m.viper.SetDefault("media.video_buffer_size_mb", defaults.Media.VideoBufferSizeMB)
459 m.viper.SetDefault("media.queue_buffer_time_sec", defaults.Media.QueueBufferTimeSec)
460}
461
462func (m *Manager) setRuntimeDefaults(defaults *Config) {
463 m.viper.SetDefault("runtime.prefix", defaults.Runtime.Prefix)
464}
465
466func (m *Manager) setSessionDefaults(defaults *Config) {
467 m.viper.SetDefault("session.auto_restore", defaults.Session.AutoRestore)
468 m.viper.SetDefault("session.snapshot_interval_ms", defaults.Session.SnapshotIntervalMs)
469 m.viper.SetDefault("session.max_exited_sessions", defaults.Session.MaxExitedSessions)
470 m.viper.SetDefault("session.max_exited_session_age_days", defaults.Session.MaxExitedSessionAgeDays)
471 m.viper.SetDefault("session.session_mode.activation_shortcut", defaults.Session.SessionMode.ActivationShortcut)
472 m.viper.SetDefault("session.session_mode.timeout_ms", defaults.Session.SessionMode.TimeoutMilliseconds)
473 m.viper.SetDefault("session.session_mode.actions", defaults.Session.SessionMode.Actions)
474}
475
476func (m *Manager) setUpdateDefaults(defaults *Config) {
477 m.viper.SetDefault("update.enable_on_startup", defaults.Update.EnableOnStartup)
478 m.viper.SetDefault("update.auto_download", defaults.Update.AutoDownload)
479 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
480}
481
482func (m *Manager) setPerformanceDefaults(defaults *Config) {
483 m.viper.SetDefault("performance.profile", string(defaults.Performance.Profile))
484 m.viper.SetDefault("performance.zoom_cache_size", defaults.Performance.ZoomCacheSize)
485 m.viper.SetDefault("performance.webview_pool_prewarm_count", defaults.Performance.WebViewPoolPrewarmCount)
486 // Skia threading (only used when profile = "custom")
487 m.viper.SetDefault("performance.skia_cpu_painting_threads", defaults.Performance.SkiaCPUPaintingThreads)
488 m.viper.SetDefault("performance.skia_gpu_painting_threads", defaults.Performance.SkiaGPUPaintingThreads)
489 m.viper.SetDefault("performance.skia_enable_cpu_rendering", defaults.Performance.SkiaEnableCPURendering)
490 // Web process memory pressure
491 m.viper.SetDefault("performance.web_process_memory_limit_mb", defaults.Performance.WebProcessMemoryLimitMB)
492 m.viper.SetDefault("performance.web_process_memory_poll_interval_sec", defaults.Performance.WebProcessMemoryPollIntervalSec)
493 m.viper.SetDefault("performance.web_process_memory_conservative_threshold", defaults.Performance.WebProcessMemoryConservativeThreshold)
494 m.viper.SetDefault("performance.web_process_memory_strict_threshold", defaults.Performance.WebProcessMemoryStrictThreshold)
495 // Network process memory pressure
496 m.viper.SetDefault("performance.network_process_memory_limit_mb", defaults.Performance.NetworkProcessMemoryLimitMB)
497 m.viper.SetDefault("performance.network_process_memory_poll_interval_sec", defaults.Performance.NetworkProcessMemoryPollIntervalSec)
498 netConservativeThreshold := defaults.Performance.NetworkProcessMemoryConservativeThreshold
499 m.viper.SetDefault("performance.network_process_memory_conservative_threshold", netConservativeThreshold)
500 m.viper.SetDefault("performance.network_process_memory_strict_threshold", defaults.Performance.NetworkProcessMemoryStrictThreshold)
501}
502
503// New returns a new default configuration instance.
504// This is a convenience function for getting default config without the full manager.
505func New() *Config {
506 return DefaultConfig()
507}
508
509// Global configuration manager instance
510var globalManager *Manager
511var globalManagerOnce sync.Once
512
513// Init initializes the global configuration manager.
514func Init() error {
515 var err error
516 globalManagerOnce.Do(func() {
517 globalManager, err = NewManager()
518 if err != nil {
519 return
520 }
521 err = globalManager.Load()
522 })
523 return err
524}
525
526// Get returns the global configuration.
527func Get() *Config {
528 if globalManager == nil {
529 // Return defaults if not initialized
530 return DefaultConfig()
531 }
532 return globalManager.Get()
533}
534
535// GetManager returns the global configuration manager.
536// This is useful for accessing watcher functionality.
537func GetManager() *Manager {
538 return globalManager
539}