v0.21.0 73bf644

loader.go RawHistoryFolder

1package config
2
3import (
4	"errors"
5	"fmt"
6	"os"
7	"path/filepath"
8	"strings"
9	"sync"
10
11	"github.com/spf13/viper"
12)
13
14// Manager handles configuration loading, watching, and reloading.
15type Manager struct {
16	config    *Config
17	viper     *viper.Viper
18	mu        sync.RWMutex
19	callbacks []func(*Config)
20	watching  bool
21}
22
23// NewManager creates a new configuration manager.
24func NewManager() (*Manager, error) {
25	v := viper.New()
26
27	// Configure Viper for TOML as default format
28	v.SetConfigName("config") // Name without extension
29	v.SetConfigType("toml")   // TOML as default format
30
31	// Add config paths
32	configDir, err := GetConfigDir()
33	if err != nil {
34		return nil, fmt.Errorf("failed to determine config directory: %w\nCheck XDG_CONFIG_HOME environment variable or HOME directory", err)
35	}
36	v.AddConfigPath(configDir)
37	v.AddConfigPath(".") // Current directory for development
38
39	// Set up environment variable support
40	v.SetEnvPrefix("DUMBER")
41	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
42	v.AutomaticEnv()
43
44	// Note: Most environment variables are handled automatically via AutomaticEnv()
45	// with the DUMBER_ prefix (e.g., DUMBER_DATABASE_PATH, DUMBER_LOGGING_LEVEL).
46	// The explicit bindings below are only for special cases with different naming patterns.
47
48	// Explicit bindings for legacy or system env vars
49	if err := v.BindEnv("default_webpage_zoom", "DUMBER_DEFAULT_WEBPAGE_ZOOM"); err != nil {
50		return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_WEBPAGE_ZOOM: %w", err)
51	}
52	if err := v.BindEnv("default_ui_scale", "DUMBER_DEFAULT_UI_SCALE"); err != nil {
53		return nil, fmt.Errorf("failed to bind DUMBER_DEFAULT_UI_SCALE: %w", err)
54	}
55
56	// Logging environment variable bindings
57	if err := v.BindEnv("logging.level", "DUMBER_LOG_LEVEL"); err != nil {
58		return nil, fmt.Errorf("failed to bind DUMBER_LOG_LEVEL: %w", err)
59	}
60	if err := v.BindEnv("logging.format", "DUMBER_LOG_FORMAT"); err != nil {
61		return nil, fmt.Errorf("failed to bind DUMBER_LOG_FORMAT: %w", err)
62	}
63
64	return &Manager{
65		viper:     v,
66		callbacks: make([]func(*Config), 0),
67	}, nil
68}
69
70// Load loads the configuration from file and environment variables.
71func (m *Manager) Load() error {
72	m.mu.Lock()
73	defer m.mu.Unlock()
74
75	if err := EnsureDirectories(); err != nil {
76		return fmt.Errorf("failed to ensure directories: %w", err)
77	}
78
79	m.setDefaults()
80
81	if err := m.readConfigFile(); err != nil {
82		return err
83	}
84
85	config, err := m.unmarshalConfig()
86	if err != nil {
87		return err
88	}
89	if err := ensureDatabasePath(config); err != nil {
90		return err
91	}
92	normalizeConfig(config)
93
94	if err := validateConfig(config); err != nil {
95		return fmt.Errorf("configuration validation failed: %w", err)
96	}
97
98	m.config = config
99	return nil
100}
101
102func (m *Manager) readConfigFile() error {
103	if err := m.viper.ReadInConfig(); err != nil {
104		var configFileNotFoundError viper.ConfigFileNotFoundError
105		if errors.As(err, &configFileNotFoundError) {
106			if createErr := m.createDefaultConfig(); createErr != nil {
107				configDir, _ := GetConfigDir()
108				return fmt.Errorf(
109					"failed to create default config at %s: %w\nTry creating the directory manually or check permissions",
110					configDir,
111					createErr,
112				)
113			}
114			if rereadErr := m.viper.ReadInConfig(); rereadErr != nil {
115				return fmt.Errorf(
116					"failed to read newly created config file: %w\nThe config file was created but couldn't be read. Please check the file format",
117					rereadErr,
118				)
119			}
120		} else {
121			configFile := m.viper.ConfigFileUsed()
122			if configFile == "" {
123				configDir, _ := GetConfigDir()
124				configFile = filepath.Join(configDir, "config.toml")
125			}
126			return fmt.Errorf("failed to read config file at %s: %w\nCheck the file format (must be valid TOML) and permissions", configFile, err)
127		}
128	}
129	return nil
130}
131
132func (m *Manager) unmarshalConfig() (*Config, error) {
133	config := &Config{}
134	if err := m.viper.Unmarshal(config); err != nil {
135		configFile := m.viper.ConfigFileUsed()
136		return nil, fmt.Errorf(
137			"failed to parse config file at %s: %w\nCheck for syntax errors, invalid values, or type mismatches",
138			configFile,
139			err,
140		)
141	}
142	return config, nil
143}
144
145func ensureDatabasePath(config *Config) error {
146	if config.Database.Path != "" {
147		return nil
148	}
149	dbPath, err := GetDatabaseFile()
150	if err != nil {
151		return fmt.Errorf("failed to get database path: %w", err)
152	}
153	config.Database.Path = dbPath
154	return nil
155}
156
157func normalizeConfig(config *Config) {
158	switch strings.ToLower(string(config.Rendering.Mode)) {
159	case "", string(RenderingModeAuto):
160		config.Rendering.Mode = RenderingModeAuto
161	case string(RenderingModeGPU):
162		config.Rendering.Mode = RenderingModeGPU
163	case string(RenderingModeCPU):
164		config.Rendering.Mode = RenderingModeCPU
165	default:
166		config.Rendering.Mode = RenderingModeAuto
167	}
168
169	switch strings.ToLower(string(config.Rendering.GSKRenderer)) {
170	case "", string(GSKRendererAuto):
171		config.Rendering.GSKRenderer = GSKRendererAuto
172	case string(GSKRendererOpenGL):
173		config.Rendering.GSKRenderer = GSKRendererOpenGL
174	case string(GSKRendererVulkan):
175		config.Rendering.GSKRenderer = GSKRendererVulkan
176	case string(GSKRendererCairo):
177		config.Rendering.GSKRenderer = GSKRendererCairo
178	default:
179		config.Rendering.GSKRenderer = GSKRendererAuto
180	}
181
182	switch config.Appearance.ColorScheme {
183	case ThemePreferDark, ThemePreferLight, ThemeDefault:
184	case "":
185		config.Appearance.ColorScheme = ThemeDefault
186	default:
187		config.Appearance.ColorScheme = ThemeDefault
188	}
189
190	switch strings.ToLower(string(config.Media.HardwareDecodingMode)) {
191	case "", string(HardwareDecodingAuto):
192		config.Media.HardwareDecodingMode = HardwareDecodingAuto
193	case string(HardwareDecodingForce):
194		config.Media.HardwareDecodingMode = HardwareDecodingForce
195	case string(HardwareDecodingDisable):
196		config.Media.HardwareDecodingMode = HardwareDecodingDisable
197	default:
198		config.Media.HardwareDecodingMode = HardwareDecodingAuto
199	}
200
201	switch strings.ToLower(string(config.Media.GLRenderingMode)) {
202	case "", string(GLRenderingModeAuto):
203		config.Media.GLRenderingMode = GLRenderingModeAuto
204	case string(GLRenderingModeGLES2):
205		config.Media.GLRenderingMode = GLRenderingModeGLES2
206	case string(GLRenderingModeGL3):
207		config.Media.GLRenderingMode = GLRenderingModeGL3
208	case string(GLRenderingModeNone):
209		config.Media.GLRenderingMode = GLRenderingModeNone
210	default:
211		config.Media.GLRenderingMode = GLRenderingModeAuto
212	}
213
214	config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)
215}
216
217// Get returns the current configuration (thread-safe).
218func (m *Manager) Get() *Config {
219	m.mu.RLock()
220	defer m.mu.RUnlock()
221
222	// Return a copy to prevent external modification
223	configCopy := *m.config
224	return &configCopy
225}
226
227// Save saves the provided configuration to disk and updates Viper.
228func (m *Manager) Save(cfg *Config) error {
229	m.mu.Lock()
230	defer m.mu.Unlock()
231
232	if cfg == nil {
233		return fmt.Errorf("config is nil")
234	}
235
236	// Validate before writing so UI gets immediate errors.
237	if err := validateConfig(cfg); err != nil {
238		return fmt.Errorf("configuration validation failed: %w", err)
239	}
240
241	// Update Viper with the new values.
242	// Since we can't easily update Viper from a struct automatically while preserving
243	// all settings, we update the main keys we care about.
244	m.viper.Set("appearance.sans_font", cfg.Appearance.SansFont)
245	m.viper.Set("appearance.serif_font", cfg.Appearance.SerifFont)
246	m.viper.Set("appearance.monospace_font", cfg.Appearance.MonospaceFont)
247	m.viper.Set("appearance.default_font_size", cfg.Appearance.DefaultFontSize)
248	m.viper.Set("appearance.color_scheme", cfg.Appearance.ColorScheme)
249	m.viper.Set("appearance.light_palette", cfg.Appearance.LightPalette)
250	m.viper.Set("appearance.dark_palette", cfg.Appearance.DarkPalette)
251	m.viper.Set("default_webpage_zoom", cfg.DefaultWebpageZoom)
252	m.viper.Set("default_ui_scale", cfg.DefaultUIScale)
253	m.viper.Set("default_search_engine", cfg.DefaultSearchEngine)
254
255	if err := m.viper.WriteConfig(); err != nil {
256		return fmt.Errorf("failed to write config: %w", err)
257	}
258
259	// We don't update m.config here manually because viper.OnConfigChange
260	// will trigger reload() if Watch() is active.
261	// If Watch() is not active, we should call reload() manually.
262	if !m.watching {
263		if err := m.reload(); err != nil {
264			return err
265		}
266	}
267
268	return nil
269}
270
271// GetConfigFile returns the path to the configuration file being used.
272func (m *Manager) GetConfigFile() string {
273	return m.viper.ConfigFileUsed()
274}
275
276// createDefaultConfig creates a default configuration file.
277func (m *Manager) createDefaultConfig() error {
278	configFile, err := GetConfigFile()
279	if err != nil {
280		return err
281	}
282
283	// Create config directory if it doesn't exist
284	if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
285		return err
286	}
287
288	// Ensure TOML format and write config
289	m.viper.SetConfigType("toml")
290	if err := m.viper.SafeWriteConfigAs(configFile); err != nil {
291		return fmt.Errorf("failed to write config file: %w", err)
292	}
293
294	fmt.Printf("Created default configuration file: %s (TOML format)\n", configFile)
295
296	return nil
297}
298
299// setDefaults sets default configuration values in Viper.
300func (m *Manager) setDefaults() {
301	defaults := DefaultConfig()
302
303	// Note: Database.Path is set dynamically in Load(), no defaults needed
304
305	m.setHistoryDefaults(defaults)
306	m.setSearchDefaults(defaults)
307	m.setDmenuDefaults(defaults)
308	m.setLoggingDefaults(defaults)
309	m.setDebugDefaults(defaults)
310	m.setAppearanceDefaults(defaults)
311	m.setRenderingDefaults(defaults)
312	m.setWorkspaceDefaults(defaults)
313	m.setContentFilteringDefaults(defaults)
314	m.setOmniboxDefaults(defaults)
315	m.setMediaDefaults(defaults)
316	m.setRuntimeDefaults(defaults)
317	m.setSessionDefaults(defaults)
318	m.setUpdateDefaults(defaults)
319}
320
321func (m *Manager) setHistoryDefaults(defaults *Config) {
322	m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
323	m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
324	m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
325}
326
327func (m *Manager) setSearchDefaults(defaults *Config) {
328	m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
329	m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
330}
331
332func (m *Manager) setDmenuDefaults(defaults *Config) {
333	m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
334	m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
335	m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
336	m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
337	m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
338	m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
339	m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
340	m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
341}
342
343func (m *Manager) setLoggingDefaults(defaults *Config) {
344	m.viper.SetDefault("logging.level", defaults.Logging.Level)
345	m.viper.SetDefault("logging.format", defaults.Logging.Format)
346	m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
347	m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
348	m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
349	m.viper.SetDefault("logging.capture_console", defaults.Logging.CaptureConsole)
350}
351
352func (m *Manager) setDebugDefaults(defaults *Config) {
353	m.viper.SetDefault("debug.enable_devtools", defaults.Debug.EnableDevTools)
354}
355
356func (m *Manager) setAppearanceDefaults(defaults *Config) {
357	m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
358	m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
359	m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
360	m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
361	m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
362	m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
363	m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
364}
365
366func (m *Manager) setRenderingDefaults(defaults *Config) {
367	m.viper.SetDefault("rendering.mode", string(defaults.Rendering.Mode))
368	m.viper.SetDefault("rendering.disable_dmabuf_renderer", defaults.Rendering.DisableDMABufRenderer)
369	m.viper.SetDefault("rendering.force_compositing_mode", defaults.Rendering.ForceCompositingMode)
370	m.viper.SetDefault("rendering.disable_compositing_mode", defaults.Rendering.DisableCompositingMode)
371	m.viper.SetDefault("rendering.gsk_renderer", string(defaults.Rendering.GSKRenderer))
372	m.viper.SetDefault("rendering.disable_mipmaps", defaults.Rendering.DisableMipmaps)
373	m.viper.SetDefault("rendering.prefer_gl", defaults.Rendering.PreferGL)
374	m.viper.SetDefault("rendering.draw_compositing_indicators", defaults.Rendering.DrawCompositingIndicators)
375	m.viper.SetDefault("rendering.show_fps", defaults.Rendering.ShowFPS)
376	m.viper.SetDefault("rendering.sample_memory", defaults.Rendering.SampleMemory)
377	m.viper.SetDefault("rendering.debug_frames", defaults.Rendering.DebugFrames)
378	m.viper.SetDefault("default_webpage_zoom", defaults.DefaultWebpageZoom)
379	m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
380}
381
382func (m *Manager) setWorkspaceDefaults(defaults *Config) {
383	m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
384	m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
385	m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
386	m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
387	m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
388	m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
389	m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
390	m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
391	m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
392	m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
393	m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
394	m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions)
395	m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane)
396	m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab)
397	m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab)
398	m.viper.SetDefault("workspace.shortcuts.consume_or_expel_left", defaults.Workspace.Shortcuts.ConsumeOrExpelLeft)
399	m.viper.SetDefault("workspace.shortcuts.consume_or_expel_right", defaults.Workspace.Shortcuts.ConsumeOrExpelRight)
400	m.viper.SetDefault("workspace.shortcuts.consume_or_expel_up", defaults.Workspace.Shortcuts.ConsumeOrExpelUp)
401	m.viper.SetDefault("workspace.shortcuts.consume_or_expel_down", defaults.Workspace.Shortcuts.ConsumeOrExpelDown)
402	m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
403	m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
404	m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
405	m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
406	m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
407	m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
408	m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
409	m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
410	m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
411	m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
412	m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
413	m.viper.SetDefault("workspace.styling.pane_mode_border_width", defaults.Workspace.Styling.PaneModeBorderWidth)
414	m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor)
415	m.viper.SetDefault("workspace.styling.tab_mode_border_width", defaults.Workspace.Styling.TabModeBorderWidth)
416	m.viper.SetDefault("workspace.styling.tab_mode_border_color", defaults.Workspace.Styling.TabModeBorderColor)
417	m.viper.SetDefault("workspace.styling.session_mode_border_width", defaults.Workspace.Styling.SessionModeBorderWidth)
418	m.viper.SetDefault("workspace.styling.session_mode_border_color", defaults.Workspace.Styling.SessionModeBorderColor)
419	m.viper.SetDefault("workspace.styling.resize_mode_border_width", defaults.Workspace.Styling.ResizeModeBorderWidth)
420	m.viper.SetDefault("workspace.styling.resize_mode_border_color", defaults.Workspace.Styling.ResizeModeBorderColor)
421	m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
422}
423
424func (m *Manager) setContentFilteringDefaults(defaults *Config) {
425	m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
426	m.viper.SetDefault("content_filtering.auto_update", defaults.ContentFiltering.AutoUpdate)
427}
428
429func (m *Manager) setOmniboxDefaults(defaults *Config) {
430	m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
431	m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
432}
433
434func (m *Manager) setMediaDefaults(defaults *Config) {
435	m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
436	m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
437	m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup)
438	m.viper.SetDefault("media.force_vsync", defaults.Media.ForceVSync)
439	m.viper.SetDefault("media.gl_rendering_mode", string(defaults.Media.GLRenderingMode))
440	m.viper.SetDefault("media.gstreamer_debug_level", defaults.Media.GStreamerDebugLevel)
441	m.viper.SetDefault("media.video_buffer_size_mb", defaults.Media.VideoBufferSizeMB)
442	m.viper.SetDefault("media.queue_buffer_time_sec", defaults.Media.QueueBufferTimeSec)
443}
444
445func (m *Manager) setRuntimeDefaults(defaults *Config) {
446	m.viper.SetDefault("runtime.prefix", defaults.Runtime.Prefix)
447}
448
449func (m *Manager) setSessionDefaults(defaults *Config) {
450	m.viper.SetDefault("session.auto_restore", defaults.Session.AutoRestore)
451	m.viper.SetDefault("session.snapshot_interval_ms", defaults.Session.SnapshotIntervalMs)
452	m.viper.SetDefault("session.max_exited_sessions", defaults.Session.MaxExitedSessions)
453	m.viper.SetDefault("session.max_exited_session_age_days", defaults.Session.MaxExitedSessionAgeDays)
454	m.viper.SetDefault("session.session_mode.activation_shortcut", defaults.Session.SessionMode.ActivationShortcut)
455	m.viper.SetDefault("session.session_mode.timeout_ms", defaults.Session.SessionMode.TimeoutMilliseconds)
456	m.viper.SetDefault("session.session_mode.actions", defaults.Session.SessionMode.Actions)
457}
458
459func (m *Manager) setUpdateDefaults(defaults *Config) {
460	m.viper.SetDefault("update.enable_on_startup", defaults.Update.EnableOnStartup)
461	m.viper.SetDefault("update.auto_download", defaults.Update.AutoDownload)
462}
463
464// New returns a new default configuration instance.
465// This is a convenience function for getting default config without the full manager.
466func New() *Config {
467	return DefaultConfig()
468}
469
470// Global configuration manager instance
471var globalManager *Manager
472var globalManagerOnce sync.Once
473
474// Init initializes the global configuration manager.
475func Init() error {
476	var err error
477	globalManagerOnce.Do(func() {
478		globalManager, err = NewManager()
479		if err != nil {
480			return
481		}
482		err = globalManager.Load()
483	})
484	return err
485}
486
487// Get returns the global configuration.
488func Get() *Config {
489	if globalManager == nil {
490		// Return defaults if not initialized
491		return DefaultConfig()
492	}
493	return globalManager.Get()
494}
495
496// GetManager returns the global configuration manager.
497// This is useful for accessing watcher functionality.
498func GetManager() *Manager {
499	return globalManager
500}

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