v0.24.0 a261e52

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

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