v0.26.0 ae3a77a

loader.go RawHistoryFolder

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

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