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