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}
369
370func (m *Manager) setHistoryDefaults(defaults *Config) {
371 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
372 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
373 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
374}
375
376func (m *Manager) setSearchDefaults(defaults *Config) {
377 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
378 m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
379}
380
381func (m *Manager) setDmenuDefaults(defaults *Config) {
382 m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays)
383 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
384 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
385 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
386 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
387 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
388 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
389 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
390}
391
392func (m *Manager) setLoggingDefaults(defaults *Config) {
393 m.viper.SetDefault("logging.level", defaults.Logging.Level)
394 m.viper.SetDefault("logging.format", defaults.Logging.Format)
395 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
396 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
397 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
398 m.viper.SetDefault("logging.capture_console", defaults.Logging.CaptureConsole)
399}
400
401func (m *Manager) setDebugDefaults(defaults *Config) {
402 m.viper.SetDefault("debug.enable_devtools", defaults.Debug.EnableDevTools)
403}
404
405func (m *Manager) setAppearanceDefaults(defaults *Config) {
406 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
407 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
408 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
409 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
410 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
411 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
412 m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
413}
414
415func (m *Manager) setRenderingDefaults(defaults *Config) {
416 m.viper.SetDefault("rendering.mode", string(defaults.Rendering.Mode))
417 m.viper.SetDefault("rendering.disable_dmabuf_renderer", defaults.Rendering.DisableDMABufRenderer)
418 m.viper.SetDefault("rendering.force_compositing_mode", defaults.Rendering.ForceCompositingMode)
419 m.viper.SetDefault("rendering.disable_compositing_mode", defaults.Rendering.DisableCompositingMode)
420 m.viper.SetDefault("rendering.gsk_renderer", string(defaults.Rendering.GSKRenderer))
421 m.viper.SetDefault("rendering.disable_mipmaps", defaults.Rendering.DisableMipmaps)
422 m.viper.SetDefault("rendering.prefer_gl", defaults.Rendering.PreferGL)
423 m.viper.SetDefault("rendering.draw_compositing_indicators", defaults.Rendering.DrawCompositingIndicators)
424 m.viper.SetDefault("rendering.show_fps", defaults.Rendering.ShowFPS)
425 m.viper.SetDefault("rendering.sample_memory", defaults.Rendering.SampleMemory)
426 m.viper.SetDefault("rendering.debug_frames", defaults.Rendering.DebugFrames)
427 m.viper.SetDefault("default_webpage_zoom", defaults.DefaultWebpageZoom)
428 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
429}
430
431func (m *Manager) setWorkspaceDefaults(defaults *Config) {
432 m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
433 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
434 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
435 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
436 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
437 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
438 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
439 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
440 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
441 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
442 m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
443 m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions)
444 m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane)
445 m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab)
446 m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab)
447 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_left", defaults.Workspace.Shortcuts.ConsumeOrExpelLeft)
448 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_right", defaults.Workspace.Shortcuts.ConsumeOrExpelRight)
449 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_up", defaults.Workspace.Shortcuts.ConsumeOrExpelUp)
450 m.viper.SetDefault("workspace.shortcuts.consume_or_expel_down", defaults.Workspace.Shortcuts.ConsumeOrExpelDown)
451 m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
452 m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
453 m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove)
454 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
455 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
456 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
457 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
458 m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
459 m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
460 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
461 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
462 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
463 m.viper.SetDefault("workspace.styling.mode_border_width", defaults.Workspace.Styling.ModeBorderWidth)
464 m.viper.SetDefault("workspace.styling.pane_mode_color", defaults.Workspace.Styling.PaneModeColor)
465 m.viper.SetDefault("workspace.styling.tab_mode_color", defaults.Workspace.Styling.TabModeColor)
466 m.viper.SetDefault("workspace.styling.session_mode_color", defaults.Workspace.Styling.SessionModeColor)
467 m.viper.SetDefault("workspace.styling.resize_mode_color", defaults.Workspace.Styling.ResizeModeColor)
468 m.viper.SetDefault("workspace.styling.mode_indicator_toaster_enabled", defaults.Workspace.Styling.ModeIndicatorToasterEnabled)
469 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
470}
471
472func (m *Manager) setContentFilteringDefaults(defaults *Config) {
473 m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
474 m.viper.SetDefault("content_filtering.auto_update", defaults.ContentFiltering.AutoUpdate)
475}
476
477func (m *Manager) setClipboardDefaults(defaults *Config) {
478 m.viper.SetDefault("clipboard.auto_copy_on_selection", defaults.Clipboard.AutoCopyOnSelection)
479}
480
481func (m *Manager) setOmniboxDefaults(defaults *Config) {
482 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
483 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
484}
485
486func (m *Manager) setMediaDefaults(defaults *Config) {
487 m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
488 m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
489 m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup)
490 m.viper.SetDefault("media.force_vsync", defaults.Media.ForceVSync)
491 m.viper.SetDefault("media.gl_rendering_mode", string(defaults.Media.GLRenderingMode))
492 m.viper.SetDefault("media.gstreamer_debug_level", defaults.Media.GStreamerDebugLevel)
493}
494
495func (m *Manager) setRuntimeDefaults(defaults *Config) {
496 m.viper.SetDefault("runtime.prefix", defaults.Runtime.Prefix)
497}
498
499func (m *Manager) setSessionDefaults(defaults *Config) {
500 m.viper.SetDefault("session.auto_restore", defaults.Session.AutoRestore)
501 m.viper.SetDefault("session.snapshot_interval_ms", defaults.Session.SnapshotIntervalMs)
502 m.viper.SetDefault("session.max_exited_sessions", defaults.Session.MaxExitedSessions)
503 m.viper.SetDefault("session.max_exited_session_age_days", defaults.Session.MaxExitedSessionAgeDays)
504 m.viper.SetDefault("session.session_mode.activation_shortcut", defaults.Session.SessionMode.ActivationShortcut)
505 m.viper.SetDefault("session.session_mode.timeout_ms", defaults.Session.SessionMode.TimeoutMilliseconds)
506 m.viper.SetDefault("session.session_mode.actions", defaults.Session.SessionMode.Actions)
507}
508
509func (m *Manager) setUpdateDefaults(defaults *Config) {
510 m.viper.SetDefault("update.enable_on_startup", defaults.Update.EnableOnStartup)
511 m.viper.SetDefault("update.auto_download", defaults.Update.AutoDownload)
512 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
513}
514
515func (m *Manager) setPerformanceDefaults(defaults *Config) {
516 m.viper.SetDefault("performance.profile", string(defaults.Performance.Profile))
517 m.viper.SetDefault("performance.zoom_cache_size", defaults.Performance.ZoomCacheSize)
518 m.viper.SetDefault("performance.webview_pool_prewarm_count", defaults.Performance.WebViewPoolPrewarmCount)
519 // Skia threading (only used when profile = "custom")
520 m.viper.SetDefault("performance.skia_cpu_painting_threads", defaults.Performance.SkiaCPUPaintingThreads)
521 m.viper.SetDefault("performance.skia_gpu_painting_threads", defaults.Performance.SkiaGPUPaintingThreads)
522 m.viper.SetDefault("performance.skia_enable_cpu_rendering", defaults.Performance.SkiaEnableCPURendering)
523 // Web process memory pressure
524 m.viper.SetDefault("performance.web_process_memory_limit_mb", defaults.Performance.WebProcessMemoryLimitMB)
525 m.viper.SetDefault("performance.web_process_memory_poll_interval_sec", defaults.Performance.WebProcessMemoryPollIntervalSec)
526 m.viper.SetDefault("performance.web_process_memory_conservative_threshold", defaults.Performance.WebProcessMemoryConservativeThreshold)
527 m.viper.SetDefault("performance.web_process_memory_strict_threshold", defaults.Performance.WebProcessMemoryStrictThreshold)
528 // Network process memory pressure
529 m.viper.SetDefault("performance.network_process_memory_limit_mb", defaults.Performance.NetworkProcessMemoryLimitMB)
530 m.viper.SetDefault("performance.network_process_memory_poll_interval_sec", defaults.Performance.NetworkProcessMemoryPollIntervalSec)
531 netConservativeThreshold := defaults.Performance.NetworkProcessMemoryConservativeThreshold
532 m.viper.SetDefault("performance.network_process_memory_conservative_threshold", netConservativeThreshold)
533 m.viper.SetDefault("performance.network_process_memory_strict_threshold", defaults.Performance.NetworkProcessMemoryStrictThreshold)
534}
535
536// New returns a new default configuration instance.
537// This is a convenience function for getting default config without the full manager.
538func New() *Config {
539 return DefaultConfig()
540}
541
542// Global configuration manager instance
543var globalManager *Manager
544var globalManagerOnce sync.Once
545
546// Init initializes the global configuration manager.
547func Init() error {
548 var err error
549 globalManagerOnce.Do(func() {
550 globalManager, err = NewManager()
551 if err != nil {
552 return
553 }
554 err = globalManager.Load()
555 })
556 return err
557}
558
559// Get returns the global configuration.
560func Get() *Config {
561 if globalManager == nil {
562 // Return defaults if not initialized
563 return DefaultConfig()
564 }
565 return globalManager.Get()
566}
567
568// GetManager returns the global configuration manager.
569// This is useful for accessing watcher functionality.
570func GetManager() *Manager {
571 return globalManager
572}