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 automaticEnv automaticEnvConfig
23 mu sync.RWMutex
24 callbacks []func(*Config)
25 watching bool
26 skipNextReload bool // Set by Save() to prevent fsnotify reload from overwriting in-memory config
27}
28
29type automaticEnvConfig struct {
30 enabled bool
31 prefix string
32 keyReplacer *strings.Replacer
33}
34
35// NewManager creates a new configuration manager.
36func NewManager() (*Manager, error) {
37 v := viper.New()
38 m := &Manager{
39 viper: v,
40 callbacks: make([]func(*Config), 0),
41 }
42
43 // Configure Viper for TOML as default format
44 v.SetConfigName("config") // Name without extension
45 v.SetConfigType("toml") // TOML as default format
46
47 // Add config paths
48 configDir, err := GetConfigDir()
49 if err != nil {
50 return nil, fmt.Errorf("failed to determine config directory: %w\nCheck XDG_CONFIG_HOME environment variable or HOME directory", err)
51 }
52 v.AddConfigPath(configDir)
53 v.AddConfigPath(".") // Current directory for development
54
55 // Set up environment variable support
56 m.configureAutomaticEnv()
57
58 // Note: Most environment variables are handled automatically via AutomaticEnv()
59 // with the DUMBER_ prefix (e.g., DUMBER_DATABASE_PATH, DUMBER_LOGGING_LEVEL).
60 // The explicit bindings below are only for special cases with different naming patterns.
61
62 // Explicit bindings for legacy or system env vars
63 if err := v.BindEnv("default_webpage_zoom", "DUMBER_DEFAULT_WEBPAGE_ZOOM"); err != nil {
64 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_WEBPAGE_ZOOM: %w", err)
65 }
66 if err := v.BindEnv("default_ui_scale", "DUMBER_DEFAULT_UI_SCALE"); err != nil {
67 return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_UI_SCALE: %w", err)
68 }
69
70 // Logging environment variable bindings
71 if err := v.BindEnv("logging.level", "DUMBER_LOG_LEVEL"); err != nil {
72 return nil, fmt.Errorf("failed to bind DUMBER_LOG_LEVEL: %w", err)
73 }
74 if err := v.BindEnv("logging.format", "DUMBER_LOG_FORMAT"); err != nil {
75 return nil, fmt.Errorf("failed to bind DUMBER_LOG_FORMAT: %w", err)
76 }
77 if err := bindBrowsingContextEnvAliases(v); err != nil {
78 return nil, err
79 }
80
81 return m, nil
82}
83
84// Load loads the configuration from file and environment variables.
85func (m *Manager) Load() error {
86 m.mu.Lock()
87 defer m.mu.Unlock()
88
89 if err := EnsureDirectories(); err != nil {
90 return fmt.Errorf("failed to ensure directories: %w", err)
91 }
92
93 m.setDefaults()
94
95 if err := m.readConfigFile(); err != nil {
96 return err
97 }
98
99 if err := m.checkLegacyFormat(); err != nil {
100 return err
101 }
102
103 config, err := m.unmarshalConfig()
104 if err != nil {
105 return err
106 }
107 if err := ensureDatabasePath(config); err != nil {
108 return err
109 }
110 normalizeConfig(config)
111
112 if err := validateConfig(config); err != nil {
113 return fmt.Errorf("configuration validation failed: %w", err)
114 }
115
116 m.config = config
117 return nil
118}
119
120func (m *Manager) readConfigFile() error {
121 if err := m.viper.ReadInConfig(); err != nil {
122 var configFileNotFoundError viper.ConfigFileNotFoundError
123 if errors.As(err, &configFileNotFoundError) {
124 if createErr := m.createDefaultConfig(); createErr != nil {
125 configDir, _ := GetConfigDir()
126 return fmt.Errorf(
127 "failed to create default config at %s: %w\nTry creating the directory manually or check permissions",
128 configDir,
129 createErr,
130 )
131 }
132 if rereadErr := m.viper.ReadInConfig(); rereadErr != nil {
133 return fmt.Errorf(
134 "failed to read newly created config file: %w\nThe config file was created but couldn't be read. Please check the file format",
135 rereadErr,
136 )
137 }
138 } else {
139 configFile := m.viper.ConfigFileUsed()
140 if configFile == "" {
141 configDir, _ := GetConfigDir()
142 configFile = filepath.Join(configDir, "config.toml")
143 }
144 return fmt.Errorf("failed to read config file at %s: %w\nCheck the file format (must be valid TOML) and permissions", configFile, err)
145 }
146 }
147
148 // Transform legacy action bindings and engine config before unmarshaling.
149 m.transformLegacyConfig()
150
151 return nil
152}
153
154// transformLegacyConfig converts old-format action bindings and engine config to the new format.
155// This is called after reading config but before unmarshaling.
156func (m *Manager) transformLegacyConfig() {
157 legacyPopupsInConfig := m.viper.InConfig("workspace.popups")
158
159 rawConfig := m.viper.AllSettings()
160 transformer := NewLegacyConfigTransformer()
161 transformer.TransformLegacyActions(rawConfig)
162 transformer.TransformLegacyEngineConfigWithExplicitAdaptive(
163 rawConfig,
164 m.hasExplicitAdaptiveWindowlessFrameRateSetting(),
165 m.viper.GetBool("engine.cef.adaptive_windowless_frame_rate"),
166 )
167
168 // AllSettings includes defaults, so workspace.browsing_contexts may already be
169 // present even when the user only configured legacy workspace.popups. Always
170 // merge legacy popup values field-by-field into the canonical section while
171 // preserving explicit canonical file/env overrides.
172 if legacyPopupsInConfig {
173 m.mergeLegacyPopupsIntoCanonicalBrowsingContexts(rawConfig)
174 } else {
175 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
176 }
177
178 // Apply transformed config back to viper
179 for key, value := range rawConfig {
180 m.viper.Set(key, value)
181 }
182}
183
184func (m *Manager) configureAutomaticEnv() {
185 if m == nil || m.viper == nil {
186 return
187 }
188 replacer := strings.NewReplacer(".", "_")
189 m.viper.SetEnvPrefix("DUMBER")
190 m.viper.SetEnvKeyReplacer(replacer)
191 m.viper.AutomaticEnv()
192 m.automaticEnv = automaticEnvConfig{
193 enabled: true,
194 prefix: "DUMBER",
195 keyReplacer: replacer,
196 }
197}
198
199func (m *Manager) hasExplicitAdaptiveWindowlessFrameRateSetting() bool {
200 if m == nil || m.viper == nil {
201 return false
202 }
203 if m.viper.InConfig("engine.cef.adaptive_windowless_frame_rate") {
204 return true
205 }
206 if !m.automaticEnv.enabled {
207 return false
208 }
209 envKey := "engine.cef.adaptive_windowless_frame_rate"
210 if m.automaticEnv.keyReplacer != nil {
211 envKey = m.automaticEnv.keyReplacer.Replace(envKey)
212 }
213 envKey = strings.ToUpper(envKey)
214 if prefix := strings.TrimSpace(m.automaticEnv.prefix); prefix != "" {
215 envKey = prefix + "_" + envKey
216 }
217 value, ok := os.LookupEnv(envKey)
218 return ok && value != ""
219}
220
221func (m *Manager) mergeLegacyPopupsIntoCanonicalBrowsingContexts(rawConfig map[string]any) {
222 workspace, ok := rawConfig["workspace"].(map[string]any)
223 if !ok {
224 return
225 }
226
227 legacyPopups, ok := workspace["popups"].(map[string]any)
228 if !ok {
229 return
230 }
231
232 canonical, ok := workspace["browsing_contexts"].(map[string]any)
233 if !ok || canonical == nil {
234 canonical = make(map[string]any, len(legacyPopups))
235 workspace["browsing_contexts"] = canonical
236 }
237
238 for key, value := range legacyPopups {
239 if m.hasCanonicalBrowsingContextsOverride(key) {
240 continue
241 }
242 canonical[key] = value
243 }
244
245 delete(workspace, "popups")
246}
247
248func (m *Manager) hasCanonicalBrowsingContextsOverride(field string) bool {
249 if m.viper.InConfig("workspace.browsing_contexts." + field) {
250 return true
251 }
252 return m.hasCanonicalBrowsingContextsEnvOverride(field)
253}
254
255func (*Manager) hasCanonicalBrowsingContextsEnvOverride(field string) bool {
256 suffix := strings.ToUpper(strings.ReplaceAll(field, ".", "_"))
257 for _, envKey := range []string{
258 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_" + suffix,
259 "DUMBER_WORKSPACE_POPUPS_" + suffix,
260 } {
261 if _, ok := os.LookupEnv(envKey); ok {
262 return true
263 }
264 }
265 return false
266}
267
268func bindBrowsingContextEnvAliases(v *viper.Viper) error {
269 bindings := map[string][]string{
270 "workspace.browsing_contexts.behavior": {
271 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_BEHAVIOR",
272 "DUMBER_WORKSPACE_POPUPS_BEHAVIOR",
273 },
274 "workspace.browsing_contexts.placement": {
275 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_PLACEMENT",
276 "DUMBER_WORKSPACE_POPUPS_PLACEMENT",
277 },
278 "workspace.browsing_contexts.open_in_new_pane": {
279 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_OPEN_IN_NEW_PANE",
280 "DUMBER_WORKSPACE_POPUPS_OPEN_IN_NEW_PANE",
281 },
282 "workspace.browsing_contexts.follow_pane_context": {
283 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_FOLLOW_PANE_CONTEXT",
284 "DUMBER_WORKSPACE_POPUPS_FOLLOW_PANE_CONTEXT",
285 },
286 "workspace.browsing_contexts.blank_target_behavior": {
287 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_BLANK_TARGET_BEHAVIOR",
288 "DUMBER_WORKSPACE_POPUPS_BLANK_TARGET_BEHAVIOR",
289 },
290 "workspace.browsing_contexts.enable_smart_detection": {
291 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_ENABLE_SMART_DETECTION",
292 "DUMBER_WORKSPACE_POPUPS_ENABLE_SMART_DETECTION",
293 },
294 "workspace.browsing_contexts.oauth_auto_close": {
295 "DUMBER_WORKSPACE_BROWSING_CONTEXTS_OAUTH_AUTO_CLOSE",
296 "DUMBER_WORKSPACE_POPUPS_OAUTH_AUTO_CLOSE",
297 },
298 }
299 for key, envs := range bindings {
300 args := append([]string{key}, envs...)
301 if err := v.BindEnv(args...); err != nil {
302 return fmt.Errorf("failed to bind browsing-context env aliases for %s: %w", key, err)
303 }
304 }
305 return nil
306}
307
308// checkLegacyFormat detects old config format and returns an error directing user to migrate.
309func (m *Manager) checkLegacyFormat() error {
310 // Check if old sections exist by looking for known keys.
311 // IsSet works for legacy keys because they do not have defaults, while
312 // engine.type has a default and must be checked with InConfig.
313 hasOldSections := m.viper.IsSet("rendering.mode") ||
314 m.viper.IsSet("rendering.disable_dmabuf_renderer") ||
315 m.viper.IsSet("performance.profile") ||
316 m.viper.IsSet("privacy.cookie_policy") ||
317 m.viper.IsSet("runtime.prefix")
318
319 hasEngineSection := m.viper.InConfig("engine.type")
320
321 if hasOldSections && !hasEngineSection {
322 return fmt.Errorf(
323 "config format outdated: [rendering], [performance], [privacy] sections " +
324 "have moved to [engine]/[engine.webkit] — " +
325 "run \"dumber config migrate\" to update your config file",
326 )
327 }
328 return nil
329}
330
331func (m *Manager) unmarshalConfig() (*Config, error) {
332 config := &Config{}
333 if err := m.viper.Unmarshal(config); err != nil {
334 configFile := m.viper.ConfigFileUsed()
335 return nil, fmt.Errorf(
336 "failed to parse config file at %s: %w\nCheck for syntax errors, invalid values, or type mismatches",
337 configFile,
338 err,
339 )
340 }
341 return config, nil
342}
343
344func ensureDatabasePath(config *Config) error {
345 if config.Database.Path != "" {
346 return nil
347 }
348 dbPath, err := GetDatabaseFile()
349 if err != nil {
350 return fmt.Errorf("failed to get database path: %w", err)
351 }
352 config.Database.Path = dbPath
353 return nil
354}
355
356func normalizeConfig(config *Config) {
357 normalizeAppearance(config)
358 normalizeMedia(config)
359 normalizeEngineConfig(config)
360 normalizeBrowsingContexts(config)
361}
362
363// normalizeBrowsingContexts ensures the deprecated Popups field mirrors BrowsingContexts
364// for runtime compatibility. Existing code can keep reading config.Workspace.Popups.
365func normalizeBrowsingContexts(config *Config) {
366 config.Workspace.Popups = config.Workspace.BrowsingContexts
367}
368
369func normalizeEngineConfig(config *Config) {
370 // Normalize GSK renderer (engine.webkit)
371 switch strings.ToLower(string(config.Engine.WebKit.GSKRenderer)) {
372 case "", string(GSKRendererAuto):
373 config.Engine.WebKit.GSKRenderer = GSKRendererAuto
374 case string(GSKRendererOpenGL):
375 config.Engine.WebKit.GSKRenderer = GSKRendererOpenGL
376 case string(GSKRendererVulkan):
377 config.Engine.WebKit.GSKRenderer = GSKRendererVulkan
378 case string(GSKRendererCairo):
379 config.Engine.WebKit.GSKRenderer = GSKRendererCairo
380 default:
381 config.Engine.WebKit.GSKRenderer = GSKRendererAuto
382 }
383
384 // Normalize cookie policy (engine).
385 // When no value (or an invalid value) is set, respect the ITP setting: if ITP is
386 // disabled the effective default is to block third-party cookies; if ITP is enabled
387 // all cookies are accepted by default. An explicit "always" value always wins.
388 itpDefault := func() CookiePolicy {
389 if config.Engine.WebKit.ITPEnabled {
390 return CookiePolicyAlways
391 }
392 return CookiePolicyNoThirdParty
393 }
394 switch strings.ToLower(string(config.Engine.CookiePolicy)) {
395 case "":
396 config.Engine.CookiePolicy = itpDefault()
397 case string(CookiePolicyAlways):
398 config.Engine.CookiePolicy = CookiePolicyAlways
399 case string(CookiePolicyNoThirdParty):
400 config.Engine.CookiePolicy = CookiePolicyNoThirdParty
401 case string(CookiePolicyNever):
402 config.Engine.CookiePolicy = CookiePolicyNever
403 default:
404 config.Engine.CookiePolicy = itpDefault()
405 }
406
407 // Normalize performance profile (engine)
408 switch strings.ToLower(string(config.Engine.Profile)) {
409 case "", string(ProfileDefault):
410 config.Engine.Profile = ProfileDefault
411 case string(ProfileLite):
412 config.Engine.Profile = ProfileLite
413 case string(ProfileBalanced):
414 config.Engine.Profile = ProfileBalanced
415 case string(ProfileMax):
416 config.Engine.Profile = ProfileMax
417 case string(ProfileCustom):
418 config.Engine.Profile = ProfileCustom
419 default:
420 config.Engine.Profile = ProfileDefault
421 }
422
423 // Trim runtime prefix (engine.webkit)
424 config.Engine.WebKit.Prefix = strings.TrimSpace(config.Engine.WebKit.Prefix)
425}
426
427func normalizeAppearance(config *Config) {
428 switch strings.ToLower(strings.TrimSpace(config.Appearance.ColorScheme)) {
429 case strings.ToLower(ThemePreferDark):
430 config.Appearance.ColorScheme = ThemePreferDark
431 case strings.ToLower(ThemePreferLight):
432 config.Appearance.ColorScheme = ThemePreferLight
433 case strings.ToLower(ThemeDefault):
434 config.Appearance.ColorScheme = ThemeDefault
435 default:
436 config.Appearance.ColorScheme = ThemeDefault
437 }
438}
439
440func normalizeMedia(config *Config) {
441 switch strings.ToLower(string(config.Media.HardwareDecodingMode)) {
442 case "", string(HardwareDecodingAuto):
443 config.Media.HardwareDecodingMode = HardwareDecodingAuto
444 case string(HardwareDecodingForce):
445 config.Media.HardwareDecodingMode = HardwareDecodingForce
446 case string(HardwareDecodingDisable):
447 config.Media.HardwareDecodingMode = HardwareDecodingDisable
448 default:
449 config.Media.HardwareDecodingMode = HardwareDecodingAuto
450 }
451}
452
453// Get returns the current configuration (thread-safe).
454func (m *Manager) Get() *Config {
455 m.mu.RLock()
456 defer m.mu.RUnlock()
457
458 // Return a copy to prevent external modification
459 configCopy := *m.config
460 return &configCopy
461}
462
463// Save saves the provided configuration to disk with ordered sections.
464func (m *Manager) Save(cfg *Config) error {
465 m.mu.Lock()
466 defer m.mu.Unlock()
467
468 if cfg == nil {
469 return fmt.Errorf("config is nil")
470 }
471
472 // Normalize a copy before writing/publishing so deprecated runtime aliases
473 // stay in sync even when the watcher skips reload after Save().
474 cfgToSave := *cfg
475 normalizeConfig(&cfgToSave)
476
477 // Validate before writing so UI gets immediate errors.
478 if err := validateConfig(&cfgToSave); err != nil {
479 return fmt.Errorf("configuration validation failed: %w", err)
480 }
481
482 // Write config with ordered sections (bypasses Viper's random map ordering)
483 configFile := m.viper.ConfigFileUsed()
484 if configFile == "" {
485 var err error
486 configFile, err = GetConfigFile()
487 if err != nil {
488 return fmt.Errorf("failed to determine config file path: %w", err)
489 }
490 }
491
492 // Set flag BEFORE writing to prevent fsnotify-triggered reload from
493 // overwriting our in-memory config with stale viper cache
494 m.skipNextReload = true
495
496 if err := WriteConfigOrdered(&cfgToSave, configFile); err != nil {
497 m.skipNextReload = false // Reset on error
498 return fmt.Errorf("failed to write config: %w", err)
499 }
500
501 // Update in-memory config immediately (don't wait for file watcher).
502 // This ensures subsequent Get() calls return normalized values even when
503 // the watcher intentionally skips reload for our own Save().
504 m.config = &cfgToSave
505
506 // Also reload from disk to sync Viper's internal state (if not watching)
507 if !m.watching {
508 m.skipNextReload = false // Clear flag since we're doing manual reload
509 if err := m.reload(); err != nil {
510 return err
511 }
512 }
513
514 return nil
515}
516
517// GetConfigFile returns the path to the configuration file being used.
518func (m *Manager) GetConfigFile() string {
519 return m.viper.ConfigFileUsed()
520}
521
522// createDefaultConfig creates a default configuration file with ordered sections.
523func (m *Manager) createDefaultConfig() error {
524 configFile, err := GetConfigFile()
525 if err != nil {
526 return err
527 }
528
529 // Create config directory if it doesn't exist
530 if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
531 return err
532 }
533
534 // Start with default config
535 cfg := DefaultConfig()
536
537 // Detect available fonts and override defaults
538 m.detectAndSetFontsOnConfig(cfg)
539
540 // Write config with ordered sections
541 if err := WriteConfigOrdered(cfg, configFile); err != nil {
542 return fmt.Errorf("failed to write config file: %w", err)
543 }
544
545 fmt.Printf("Created default configuration file: %s (TOML format)\n", configFile)
546
547 return nil
548}
549
550// detectAndSetFontsOnConfig detects available system fonts and sets the best available
551// fonts from the fallback chains directly on the Config struct.
552func (*Manager) detectAndSetFontsOnConfig(cfg *Config) {
553 // Create context with logger for debugging first-run font detection.
554 logger := logging.NewFromEnv()
555 ctx := logging.WithContext(context.Background(), logger)
556
557 detector := fonts.NewDetector()
558
559 if !detector.IsAvailable(ctx) {
560 // fc-list not available, keep hardcoded defaults
561 return
562 }
563
564 cfg.Appearance.SansFont = detector.SelectBestFont(ctx, port.FontCategorySansSerif, fonts.SansSerifFallbackChain())
565 cfg.Appearance.SerifFont = detector.SelectBestFont(ctx, port.FontCategorySerif, fonts.SerifFallbackChain())
566 cfg.Appearance.MonospaceFont = detector.SelectBestFont(ctx, port.FontCategoryMonospace, fonts.MonospaceFallbackChain())
567}
568
569// setDefaults sets default configuration values in Viper.
570func (m *Manager) setDefaults() {
571 defaults := DefaultConfig()
572
573 // Note: Database.Path is set dynamically in Load(), no defaults needed
574
575 m.setHistoryDefaults(defaults)
576 m.setSearchDefaults(defaults)
577 m.setDmenuDefaults(defaults)
578 m.setLoggingDefaults(defaults)
579 m.setDebugDefaults(defaults)
580 m.setAppearanceDefaults(defaults)
581 m.setEngineDefaults(defaults)
582 m.setZoomAndScaleDefaults(defaults)
583 m.setWorkspaceDefaults(defaults)
584 m.setContentFilteringDefaults(defaults)
585 m.setClipboardDefaults(defaults)
586 m.setOmniboxDefaults(defaults)
587 m.setMediaDefaults(defaults)
588 m.setSessionDefaults(defaults)
589 m.setUpdateDefaults(defaults)
590 m.setDownloadsDefaults(defaults)
591}
592
593func (m *Manager) setHistoryDefaults(defaults *Config) {
594 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
595 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
596 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
597}
598
599func (m *Manager) setSearchDefaults(defaults *Config) {
600 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
601 m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
602}
603
604func (m *Manager) setDmenuDefaults(defaults *Config) {
605 m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays)
606 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
607 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
608 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
609 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
610 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
611 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
612 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
613}
614
615func (m *Manager) setLoggingDefaults(defaults *Config) {
616 m.viper.SetDefault("logging.level", defaults.Logging.Level)
617 m.viper.SetDefault("logging.format", defaults.Logging.Format)
618 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
619 m.viper.SetDefault("logging.max_files", defaults.Logging.MaxFiles)
620 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
621 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
622 m.viper.SetDefault("logging.capture_console", defaults.Logging.CaptureConsole)
623 m.viper.SetDefault("logging.capture_gtk_logs", defaults.Logging.CaptureGTKLogs)
624}
625
626func (m *Manager) setDebugDefaults(defaults *Config) {
627 m.viper.SetDefault("debug.enable_devtools", defaults.Debug.EnableDevTools)
628}
629
630func (m *Manager) setAppearanceDefaults(defaults *Config) {
631 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
632 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
633 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
634 m.viper.SetDefault("appearance.gtk_font", defaults.Appearance.GtkFont)
635 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
636 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
637 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
638 m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
639}
640
641func (m *Manager) setZoomAndScaleDefaults(defaults *Config) {
642 m.viper.SetDefault("default_webpage_zoom", defaults.DefaultWebpageZoom)
643 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
644}
645
646func (m *Manager) setWorkspaceDefaults(defaults *Config) {
647 m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
648 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
649 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
650 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
651 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
652 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
653 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
654 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
655 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
656 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
657 m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
658 m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions)
659 m.viper.SetDefault("workspace.shortcuts.actions", defaults.Workspace.Shortcuts.Actions)
660 m.viper.SetDefault("workspace.floating_pane.width_pct", defaults.Workspace.FloatingPane.WidthPct)
661 m.viper.SetDefault("workspace.floating_pane.height_pct", defaults.Workspace.FloatingPane.HeightPct)
662 m.viper.SetDefault("workspace.floating_pane.profiles", defaults.Workspace.FloatingPane.Profiles)
663 m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
664 m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
665 m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove)
666 m.viper.SetDefault("workspace.browsing_contexts.behavior", string(defaults.Workspace.BrowsingContexts.Behavior))
667 m.viper.SetDefault("workspace.browsing_contexts.placement", defaults.Workspace.BrowsingContexts.Placement)
668 m.viper.SetDefault("workspace.browsing_contexts.open_in_new_pane", defaults.Workspace.BrowsingContexts.OpenInNewPane)
669 m.viper.SetDefault("workspace.browsing_contexts.follow_pane_context", defaults.Workspace.BrowsingContexts.FollowPaneContext)
670 m.viper.SetDefault("workspace.browsing_contexts.blank_target_behavior", defaults.Workspace.BrowsingContexts.BlankTargetBehavior)
671 m.viper.SetDefault("workspace.browsing_contexts.enable_smart_detection", defaults.Workspace.BrowsingContexts.EnableSmartDetection)
672 m.viper.SetDefault("workspace.browsing_contexts.oauth_auto_close", defaults.Workspace.BrowsingContexts.OAuthAutoClose)
673 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
674 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
675 m.viper.SetDefault("workspace.styling.mode_border_width", defaults.Workspace.Styling.ModeBorderWidth)
676 m.viper.SetDefault("workspace.styling.pane_mode_color", defaults.Workspace.Styling.PaneModeColor)
677 m.viper.SetDefault("workspace.styling.tab_mode_color", defaults.Workspace.Styling.TabModeColor)
678 m.viper.SetDefault("workspace.styling.session_mode_color", defaults.Workspace.Styling.SessionModeColor)
679 m.viper.SetDefault("workspace.styling.resize_mode_color", defaults.Workspace.Styling.ResizeModeColor)
680 m.viper.SetDefault("workspace.styling.mode_indicator_toaster_enabled", defaults.Workspace.Styling.ModeIndicatorToasterEnabled)
681 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
682}
683
684func (m *Manager) setContentFilteringDefaults(defaults *Config) {
685 m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
686 m.viper.SetDefault("content_filtering.auto_update", defaults.ContentFiltering.AutoUpdate)
687}
688
689func (m *Manager) setClipboardDefaults(defaults *Config) {
690 m.viper.SetDefault("clipboard.auto_copy_on_selection", defaults.Clipboard.AutoCopyOnSelection)
691}
692
693func (m *Manager) setOmniboxDefaults(defaults *Config) {
694 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
695 m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays)
696 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
697}
698
699func (m *Manager) setMediaDefaults(defaults *Config) {
700 // GStreamer fields (force_vsync, gl_rendering_mode, gstreamer_debug_level)
701 // have moved to [engine.webkit] — only non-migrated media fields remain here.
702 m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
703 m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
704 m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup)
705}
706
707// setRuntimeDefaults removed — runtime.prefix moved to [engine.webkit].
708
709func (m *Manager) setSessionDefaults(defaults *Config) {
710 m.viper.SetDefault("session.auto_restore", defaults.Session.AutoRestore)
711 m.viper.SetDefault("session.snapshot_interval_ms", defaults.Session.SnapshotIntervalMs)
712 m.viper.SetDefault("session.max_exited_sessions", defaults.Session.MaxExitedSessions)
713 m.viper.SetDefault("session.max_exited_session_age_days", defaults.Session.MaxExitedSessionAgeDays)
714 m.viper.SetDefault("session.session_mode.activation_shortcut", defaults.Session.SessionMode.ActivationShortcut)
715 m.viper.SetDefault("session.session_mode.timeout_ms", defaults.Session.SessionMode.TimeoutMilliseconds)
716 m.viper.SetDefault("session.session_mode.actions", defaults.Session.SessionMode.Actions)
717}
718
719func (m *Manager) setUpdateDefaults(defaults *Config) {
720 m.viper.SetDefault("update.enable_on_startup", defaults.Update.EnableOnStartup)
721 m.viper.SetDefault("update.auto_download", defaults.Update.AutoDownload)
722 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
723}
724
725// setPerformanceDefaults removed — all fields moved to [engine]/[engine.webkit].
726
727func (m *Manager) setDownloadsDefaults(defaults *Config) {
728 m.viper.SetDefault("downloads.path", defaults.Downloads.Path)
729}
730
731func (m *Manager) setEngineDefaults(defaults *Config) {
732 e := defaults.Engine
733 m.viper.SetDefault("engine.type", e.Type)
734 m.viper.SetDefault("engine.profile", string(e.Profile))
735 m.viper.SetDefault("engine.pool_prewarm_count", e.PoolPrewarmCount)
736 m.viper.SetDefault("engine.zoom_cache_size", e.ZoomCacheSize)
737 m.viper.SetDefault("engine.cookie_policy", string(e.CookiePolicy))
738
739 ce := e.CEF
740 m.viper.SetDefault("engine.cef.cef_dir", ce.CEFDir)
741 m.viper.SetDefault("engine.cef.render_stack", string(ce.CEFRenderStack()))
742 m.viper.SetDefault("engine.cef.log_file", ce.LogFile)
743 m.viper.SetDefault("engine.cef.log_severity", ce.LogSeverity)
744 m.viper.SetDefault("engine.cef.adaptive_windowless_frame_rate", ce.AdaptiveWindowlessFrameRate)
745 m.viper.SetDefault("engine.cef.windowless_frame_rate", ce.WindowlessFrameRate)
746 m.viper.SetDefault("engine.cef.windowless_frame_rate_max", ce.CEFWindowlessFrameRateMax())
747 m.viper.SetDefault("engine.cef.enable_audio_handler", ce.EnableAudioHandler)
748 m.viper.SetDefault("engine.cef.trace_handlers", ce.TraceHandlers)
749
750 wk := e.WebKit
751 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)
752 m.viper.SetDefault("engine.webkit.skia_cpu_painting_threads", wk.SkiaCPUPaintingThreads)
753 m.viper.SetDefault("engine.webkit.skia_gpu_painting_threads", wk.SkiaGPUPaintingThreads)
754 m.viper.SetDefault("engine.webkit.skia_enable_cpu_rendering", wk.SkiaEnableCPURendering)
755 m.viper.SetDefault("engine.webkit.disable_dmabuf_renderer", wk.DisableDMABufRenderer)
756 m.viper.SetDefault("engine.webkit.force_compositing_mode", wk.ForceCompositingMode)
757 m.viper.SetDefault("engine.webkit.disable_compositing_mode", wk.DisableCompositingMode)
758 m.viper.SetDefault("engine.webkit.gsk_renderer", string(wk.GSKRenderer))
759 m.viper.SetDefault("engine.webkit.disable_mipmaps", wk.DisableMipmaps)
760 m.viper.SetDefault("engine.webkit.prefer_gl", wk.PreferGL)
761 m.viper.SetDefault("engine.webkit.draw_compositing_indicators", wk.DrawCompositingIndicators)
762 m.viper.SetDefault("engine.webkit.show_fps", wk.ShowFPS)
763 m.viper.SetDefault("engine.webkit.sample_memory", wk.SampleMemory)
764 m.viper.SetDefault("engine.webkit.debug_frames", wk.DebugFrames)
765 m.viper.SetDefault("engine.webkit.force_vsync", wk.ForceVSync)
766 m.viper.SetDefault("engine.webkit.gl_rendering_mode", string(wk.GLRenderingMode))
767 m.viper.SetDefault("engine.webkit.gstreamer_debug_level", wk.GStreamerDebugLevel)
768 m.viper.SetDefault("engine.webkit.prefix", wk.Prefix)
769 m.viper.SetDefault("engine.webkit.web_process_memory_limit_mb", wk.WebProcessMemoryLimitMB)
770 m.viper.SetDefault("engine.webkit.web_process_memory_poll_interval_sec", wk.WebProcessMemoryPollIntervalSec)
771 m.viper.SetDefault("engine.webkit.web_process_memory_conservative_threshold", wk.WebProcessMemoryConservativeThreshold)
772 m.viper.SetDefault("engine.webkit.web_process_memory_strict_threshold", wk.WebProcessMemoryStrictThreshold)
773 m.viper.SetDefault("engine.webkit.network_process_memory_limit_mb", wk.NetworkProcessMemoryLimitMB)
774 m.viper.SetDefault("engine.webkit.network_process_memory_poll_interval_sec", wk.NetworkProcessMemoryPollIntervalSec)
775 m.viper.SetDefault("engine.webkit.network_process_memory_conservative_threshold", wk.NetworkProcessMemoryConservativeThreshold)
776 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)
777}
778
779// New returns a new default configuration instance.
780// This is a convenience function for getting default config without the full manager.
781func New() *Config {
782 return DefaultConfig()
783}
784
785// Global configuration manager instance
786var globalManager *Manager
787var globalManagerOnce sync.Once
788
789// Init initializes the global configuration manager.
790func Init() error {
791 var err error
792 globalManagerOnce.Do(func() {
793 globalManager, err = NewManager()
794 if err != nil {
795 return
796 }
797 err = globalManager.Load()
798 })
799 return err
800}
801
802// Get returns the global configuration.
803func Get() *Config {
804 if globalManager == nil {
805 // Return defaults if not initialized
806 return DefaultConfig()
807 }
808 return globalManager.Get()
809}
810
811// GetManager returns the global configuration manager.
812// This is useful for accessing watcher functionality.
813func GetManager() *Manager {
814 return globalManager
815}