v0.27.0 dcb4c51

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	normalizeRendering(config)
181	normalizePrivacy(config)
182	normalizeAppearance(config)
183	normalizeMedia(config)
184
185	config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)
186	normalizePerformanceProfile(config)
187}
188
189func normalizeRendering(config *Config) {
190	switch strings.ToLower(string(config.Rendering.Mode)) {
191	case "", string(RenderingModeAuto):
192		config.Rendering.Mode = RenderingModeAuto
193	case string(RenderingModeGPU):
194		config.Rendering.Mode = RenderingModeGPU
195	case string(RenderingModeCPU):
196		config.Rendering.Mode = RenderingModeCPU
197	default:
198		config.Rendering.Mode = RenderingModeAuto
199	}
200
201	switch strings.ToLower(string(config.Rendering.GSKRenderer)) {
202	case "", string(GSKRendererAuto):
203		config.Rendering.GSKRenderer = GSKRendererAuto
204	case string(GSKRendererOpenGL):
205		config.Rendering.GSKRenderer = GSKRendererOpenGL
206	case string(GSKRendererVulkan):
207		config.Rendering.GSKRenderer = GSKRendererVulkan
208	case string(GSKRendererCairo):
209		config.Rendering.GSKRenderer = GSKRendererCairo
210	default:
211		config.Rendering.GSKRenderer = GSKRendererAuto
212	}
213}
214
215func normalizePrivacy(config *Config) {
216	switch strings.ToLower(string(config.Privacy.CookiePolicy)) {
217	case "", string(CookiePolicyNoThirdParty):
218		config.Privacy.CookiePolicy = CookiePolicyNoThirdParty
219	case string(CookiePolicyAlways):
220		config.Privacy.CookiePolicy = CookiePolicyAlways
221	case string(CookiePolicyNever):
222		config.Privacy.CookiePolicy = CookiePolicyNever
223	default:
224		config.Privacy.CookiePolicy = CookiePolicyNoThirdParty
225	}
226}
227
228func normalizeAppearance(config *Config) {
229	switch strings.ToLower(strings.TrimSpace(config.Appearance.ColorScheme)) {
230	case strings.ToLower(ThemePreferDark):
231		config.Appearance.ColorScheme = ThemePreferDark
232	case strings.ToLower(ThemePreferLight):
233		config.Appearance.ColorScheme = ThemePreferLight
234	case strings.ToLower(ThemeDefault):
235		config.Appearance.ColorScheme = ThemeDefault
236	default:
237		config.Appearance.ColorScheme = ThemeDefault
238	}
239}
240
241func normalizeMedia(config *Config) {
242	switch strings.ToLower(string(config.Media.HardwareDecodingMode)) {
243	case "", string(HardwareDecodingAuto):
244		config.Media.HardwareDecodingMode = HardwareDecodingAuto
245	case string(HardwareDecodingForce):
246		config.Media.HardwareDecodingMode = HardwareDecodingForce
247	case string(HardwareDecodingDisable):
248		config.Media.HardwareDecodingMode = HardwareDecodingDisable
249	default:
250		config.Media.HardwareDecodingMode = HardwareDecodingAuto
251	}
252
253	switch strings.ToLower(string(config.Media.GLRenderingMode)) {
254	case "", string(GLRenderingModeAuto):
255		config.Media.GLRenderingMode = GLRenderingModeAuto
256	case string(GLRenderingModeGLES2):
257		config.Media.GLRenderingMode = GLRenderingModeGLES2
258	case string(GLRenderingModeGL3):
259		config.Media.GLRenderingMode = GLRenderingModeGL3
260	case string(GLRenderingModeNone):
261		config.Media.GLRenderingMode = GLRenderingModeNone
262	default:
263		config.Media.GLRenderingMode = GLRenderingModeAuto
264	}
265}
266
267func normalizePerformanceProfile(config *Config) {
268	switch strings.ToLower(string(config.Performance.Profile)) {
269	case "", string(ProfileDefault):
270		config.Performance.Profile = ProfileDefault
271	case string(ProfileLite):
272		config.Performance.Profile = ProfileLite
273	case string(ProfileMax):
274		config.Performance.Profile = ProfileMax
275	case string(ProfileCustom):
276		config.Performance.Profile = ProfileCustom
277	default:
278		config.Performance.Profile = ProfileDefault
279	}
280}
281
282// Get returns the current configuration (thread-safe).
283func (m *Manager) Get() *Config {
284	m.mu.RLock()
285	defer m.mu.RUnlock()
286
287	// Return a copy to prevent external modification
288	configCopy := *m.config
289	return &configCopy
290}
291
292// Save saves the provided configuration to disk with ordered sections.
293func (m *Manager) Save(cfg *Config) error {
294	m.mu.Lock()
295	defer m.mu.Unlock()
296
297	if cfg == nil {
298		return fmt.Errorf("config is nil")
299	}
300
301	// Validate before writing so UI gets immediate errors.
302	if err := validateConfig(cfg); err != nil {
303		return fmt.Errorf("configuration validation failed: %w", err)
304	}
305
306	// Write config with ordered sections (bypasses Viper's random map ordering)
307	configFile := m.viper.ConfigFileUsed()
308	if configFile == "" {
309		var err error
310		configFile, err = GetConfigFile()
311		if err != nil {
312			return fmt.Errorf("failed to determine config file path: %w", err)
313		}
314	}
315
316	// Set flag BEFORE writing to prevent fsnotify-triggered reload from
317	// overwriting our in-memory config with stale viper cache
318	m.skipNextReload = true
319
320	if err := WriteConfigOrdered(cfg, configFile); err != nil {
321		m.skipNextReload = false // Reset on error
322		return fmt.Errorf("failed to write config: %w", err)
323	}
324
325	// Update in-memory config immediately (don't wait for file watcher)
326	// This ensures subsequent Get() calls return the new values
327	m.config = cfg
328
329	// Also reload from disk to sync Viper's internal state (if not watching)
330	if !m.watching {
331		m.skipNextReload = false // Clear flag since we're doing manual reload
332		if err := m.reload(); err != nil {
333			return err
334		}
335	}
336
337	return nil
338}
339
340// GetConfigFile returns the path to the configuration file being used.
341func (m *Manager) GetConfigFile() string {
342	return m.viper.ConfigFileUsed()
343}
344
345// createDefaultConfig creates a default configuration file with ordered sections.
346func (m *Manager) createDefaultConfig() error {
347	configFile, err := GetConfigFile()
348	if err != nil {
349		return err
350	}
351
352	// Create config directory if it doesn't exist
353	if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
354		return err
355	}
356
357	// Start with default config
358	cfg := DefaultConfig()
359
360	// Detect available fonts and override defaults
361	m.detectAndSetFontsOnConfig(cfg)
362
363	// Write config with ordered sections
364	if err := WriteConfigOrdered(cfg, configFile); err != nil {
365		return fmt.Errorf("failed to write config file: %w", err)
366	}
367
368	fmt.Printf("Created default configuration file: %s (TOML format)\n", configFile)
369
370	return nil
371}
372
373// detectAndSetFontsOnConfig detects available system fonts and sets the best available
374// fonts from the fallback chains directly on the Config struct.
375func (*Manager) detectAndSetFontsOnConfig(cfg *Config) {
376	// Create context with logger for debugging first-run font detection.
377	logger := logging.NewFromEnv()
378	ctx := logging.WithContext(context.Background(), logger)
379
380	detector := fonts.NewDetector()
381
382	if !detector.IsAvailable(ctx) {
383		// fc-list not available, keep hardcoded defaults
384		return
385	}
386
387	cfg.Appearance.SansFont = detector.SelectBestFont(ctx, port.FontCategorySansSerif, fonts.SansSerifFallbackChain())
388	cfg.Appearance.SerifFont = detector.SelectBestFont(ctx, port.FontCategorySerif, fonts.SerifFallbackChain())
389	cfg.Appearance.MonospaceFont = detector.SelectBestFont(ctx, port.FontCategoryMonospace, fonts.MonospaceFallbackChain())
390}
391
392// setDefaults sets default configuration values in Viper.
393func (m *Manager) setDefaults() {
394	defaults := DefaultConfig()
395
396	// Note: Database.Path is set dynamically in Load(), no defaults needed
397
398	m.setHistoryDefaults(defaults)
399	m.setSearchDefaults(defaults)
400	m.setDmenuDefaults(defaults)
401	m.setLoggingDefaults(defaults)
402	m.setDebugDefaults(defaults)
403	m.setAppearanceDefaults(defaults)
404	m.setRenderingDefaults(defaults)
405	m.setPrivacyDefaults(defaults)
406	m.setWorkspaceDefaults(defaults)
407	m.setContentFilteringDefaults(defaults)
408	m.setClipboardDefaults(defaults)
409	m.setOmniboxDefaults(defaults)
410	m.setMediaDefaults(defaults)
411	m.setRuntimeDefaults(defaults)
412	m.setSessionDefaults(defaults)
413	m.setUpdateDefaults(defaults)
414	m.setPerformanceDefaults(defaults)
415	m.setDownloadsDefaults(defaults)
416}
417
418func (m *Manager) setHistoryDefaults(defaults *Config) {
419	m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
420	m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
421	m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
422}
423
424func (m *Manager) setSearchDefaults(defaults *Config) {
425	m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
426	m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
427}
428
429func (m *Manager) setDmenuDefaults(defaults *Config) {
430	m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays)
431	m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
432	m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
433	m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
434	m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
435	m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
436	m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
437	m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
438}
439
440func (m *Manager) setLoggingDefaults(defaults *Config) {
441	m.viper.SetDefault("logging.level", defaults.Logging.Level)
442	m.viper.SetDefault("logging.format", defaults.Logging.Format)
443	m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
444	m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
445	m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
446	m.viper.SetDefault("logging.capture_console", defaults.Logging.CaptureConsole)
447	m.viper.SetDefault("logging.capture_gtk_logs", defaults.Logging.CaptureGTKLogs)
448}
449
450func (m *Manager) setDebugDefaults(defaults *Config) {
451	m.viper.SetDefault("debug.enable_devtools", defaults.Debug.EnableDevTools)
452}
453
454func (m *Manager) setAppearanceDefaults(defaults *Config) {
455	m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
456	m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
457	m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
458	m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
459	m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
460	m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
461	m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
462}
463
464func (m *Manager) setRenderingDefaults(defaults *Config) {
465	m.viper.SetDefault("rendering.mode", string(defaults.Rendering.Mode))
466	m.viper.SetDefault("rendering.disable_dmabuf_renderer", defaults.Rendering.DisableDMABufRenderer)
467	m.viper.SetDefault("rendering.force_compositing_mode", defaults.Rendering.ForceCompositingMode)
468	m.viper.SetDefault("rendering.disable_compositing_mode", defaults.Rendering.DisableCompositingMode)
469	m.viper.SetDefault("rendering.gsk_renderer", string(defaults.Rendering.GSKRenderer))
470	m.viper.SetDefault("rendering.disable_mipmaps", defaults.Rendering.DisableMipmaps)
471	m.viper.SetDefault("rendering.prefer_gl", defaults.Rendering.PreferGL)
472	m.viper.SetDefault("rendering.draw_compositing_indicators", defaults.Rendering.DrawCompositingIndicators)
473	m.viper.SetDefault("rendering.show_fps", defaults.Rendering.ShowFPS)
474	m.viper.SetDefault("rendering.sample_memory", defaults.Rendering.SampleMemory)
475	m.viper.SetDefault("rendering.debug_frames", defaults.Rendering.DebugFrames)
476	m.viper.SetDefault("default_webpage_zoom", defaults.DefaultWebpageZoom)
477	m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
478}
479
480func (m *Manager) setPrivacyDefaults(defaults *Config) {
481	m.viper.SetDefault("privacy.cookie_policy", string(defaults.Privacy.CookiePolicy))
482	m.viper.SetDefault("privacy.itp_enabled", defaults.Privacy.ITPEnabled)
483}
484
485func (m *Manager) setWorkspaceDefaults(defaults *Config) {
486	m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
487	m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
488	m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
489	m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
490	m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
491	m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
492	m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
493	m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
494	m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
495	m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
496	m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
497	m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions)
498	m.viper.SetDefault("workspace.shortcuts.actions", defaults.Workspace.Shortcuts.Actions)
499	m.viper.SetDefault("workspace.floating_pane.width_pct", defaults.Workspace.FloatingPane.WidthPct)
500	m.viper.SetDefault("workspace.floating_pane.height_pct", defaults.Workspace.FloatingPane.HeightPct)
501	m.viper.SetDefault("workspace.floating_pane.profiles", defaults.Workspace.FloatingPane.Profiles)
502	m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
503	m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
504	m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove)
505	m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
506	m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
507	m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
508	m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
509	m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
510	m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
511	m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
512	m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
513	m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
514	m.viper.SetDefault("workspace.styling.mode_border_width", defaults.Workspace.Styling.ModeBorderWidth)
515	m.viper.SetDefault("workspace.styling.pane_mode_color", defaults.Workspace.Styling.PaneModeColor)
516	m.viper.SetDefault("workspace.styling.tab_mode_color", defaults.Workspace.Styling.TabModeColor)
517	m.viper.SetDefault("workspace.styling.session_mode_color", defaults.Workspace.Styling.SessionModeColor)
518	m.viper.SetDefault("workspace.styling.resize_mode_color", defaults.Workspace.Styling.ResizeModeColor)
519	m.viper.SetDefault("workspace.styling.mode_indicator_toaster_enabled", defaults.Workspace.Styling.ModeIndicatorToasterEnabled)
520	m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
521}
522
523func (m *Manager) setContentFilteringDefaults(defaults *Config) {
524	m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
525	m.viper.SetDefault("content_filtering.auto_update", defaults.ContentFiltering.AutoUpdate)
526}
527
528func (m *Manager) setClipboardDefaults(defaults *Config) {
529	m.viper.SetDefault("clipboard.auto_copy_on_selection", defaults.Clipboard.AutoCopyOnSelection)
530}
531
532func (m *Manager) setOmniboxDefaults(defaults *Config) {
533	m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
534	m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
535}
536
537func (m *Manager) setMediaDefaults(defaults *Config) {
538	m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
539	m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
540	m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup)
541	m.viper.SetDefault("media.force_vsync", defaults.Media.ForceVSync)
542	m.viper.SetDefault("media.gl_rendering_mode", string(defaults.Media.GLRenderingMode))
543	m.viper.SetDefault("media.gstreamer_debug_level", defaults.Media.GStreamerDebugLevel)
544}
545
546func (m *Manager) setRuntimeDefaults(defaults *Config) {
547	m.viper.SetDefault("runtime.prefix", defaults.Runtime.Prefix)
548}
549
550func (m *Manager) setSessionDefaults(defaults *Config) {
551	m.viper.SetDefault("session.auto_restore", defaults.Session.AutoRestore)
552	m.viper.SetDefault("session.snapshot_interval_ms", defaults.Session.SnapshotIntervalMs)
553	m.viper.SetDefault("session.max_exited_sessions", defaults.Session.MaxExitedSessions)
554	m.viper.SetDefault("session.max_exited_session_age_days", defaults.Session.MaxExitedSessionAgeDays)
555	m.viper.SetDefault("session.session_mode.activation_shortcut", defaults.Session.SessionMode.ActivationShortcut)
556	m.viper.SetDefault("session.session_mode.timeout_ms", defaults.Session.SessionMode.TimeoutMilliseconds)
557	m.viper.SetDefault("session.session_mode.actions", defaults.Session.SessionMode.Actions)
558}
559
560func (m *Manager) setUpdateDefaults(defaults *Config) {
561	m.viper.SetDefault("update.enable_on_startup", defaults.Update.EnableOnStartup)
562	m.viper.SetDefault("update.auto_download", defaults.Update.AutoDownload)
563	m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
564}
565
566func (m *Manager) setPerformanceDefaults(defaults *Config) {
567	m.viper.SetDefault("performance.profile", string(defaults.Performance.Profile))
568	m.viper.SetDefault("performance.zoom_cache_size", defaults.Performance.ZoomCacheSize)
569	m.viper.SetDefault("performance.webview_pool_prewarm_count", defaults.Performance.WebViewPoolPrewarmCount)
570	// Skia threading (only used when profile = "custom")
571	m.viper.SetDefault("performance.skia_cpu_painting_threads", defaults.Performance.SkiaCPUPaintingThreads)
572	m.viper.SetDefault("performance.skia_gpu_painting_threads", defaults.Performance.SkiaGPUPaintingThreads)
573	m.viper.SetDefault("performance.skia_enable_cpu_rendering", defaults.Performance.SkiaEnableCPURendering)
574	// Web process memory pressure
575	m.viper.SetDefault("performance.web_process_memory_limit_mb", defaults.Performance.WebProcessMemoryLimitMB)
576	m.viper.SetDefault("performance.web_process_memory_poll_interval_sec", defaults.Performance.WebProcessMemoryPollIntervalSec)
577	m.viper.SetDefault("performance.web_process_memory_conservative_threshold", defaults.Performance.WebProcessMemoryConservativeThreshold)
578	m.viper.SetDefault("performance.web_process_memory_strict_threshold", defaults.Performance.WebProcessMemoryStrictThreshold)
579	// Network process memory pressure
580	m.viper.SetDefault("performance.network_process_memory_limit_mb", defaults.Performance.NetworkProcessMemoryLimitMB)
581	m.viper.SetDefault("performance.network_process_memory_poll_interval_sec", defaults.Performance.NetworkProcessMemoryPollIntervalSec)
582	netConservativeThreshold := defaults.Performance.NetworkProcessMemoryConservativeThreshold
583	m.viper.SetDefault("performance.network_process_memory_conservative_threshold", netConservativeThreshold)
584	m.viper.SetDefault("performance.network_process_memory_strict_threshold", defaults.Performance.NetworkProcessMemoryStrictThreshold)
585}
586
587func (m *Manager) setDownloadsDefaults(defaults *Config) {
588	m.viper.SetDefault("downloads.path", defaults.Downloads.Path)
589}
590
591// New returns a new default configuration instance.
592// This is a convenience function for getting default config without the full manager.
593func New() *Config {
594	return DefaultConfig()
595}
596
597// Global configuration manager instance
598var globalManager *Manager
599var globalManagerOnce sync.Once
600
601// Init initializes the global configuration manager.
602func Init() error {
603	var err error
604	globalManagerOnce.Do(func() {
605		globalManager, err = NewManager()
606		if err != nil {
607			return
608		}
609		err = globalManager.Load()
610	})
611	return err
612}
613
614// Get returns the global configuration.
615func Get() *Config {
616	if globalManager == nil {
617		// Return defaults if not initialized
618		return DefaultConfig()
619	}
620	return globalManager.Get()
621}
622
623// GetManager returns the global configuration manager.
624// This is useful for accessing watcher functionality.
625func GetManager() *Manager {
626	return globalManager
627}

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