v0.23.2 bfd5eb4

loader.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close