v0.25.0 2e6c485

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

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