v0.6.0 5c50dd3

config.go RawHistoryFolder

1// Package config provides configuration management for dumber with Viper integration.
2package config
3
4import (
5	"encoding/json"
6	"errors"
7	"fmt"
8	"os"
9	"path/filepath"
10	"strings"
11	"sync"
12	"time"
13
14	"github.com/bnema/dumber/pkg/gpu"
15	"github.com/fsnotify/fsnotify"
16	"github.com/spf13/viper"
17)
18
19// File permission constants
20const (
21	dirPerm  = 0755 // Standard directory permissions (rwxr-xr-x)
22	filePerm = 0644 // Standard file permissions (rw-r--r--)
23)
24
25// Config represents the complete configuration for dumber.
26type Config struct {
27	Database          DatabaseConfig            `mapstructure:"database" yaml:"database"`
28	History           HistoryConfig             `mapstructure:"history" yaml:"history"`
29	SearchShortcuts   map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`
30	Dmenu             DmenuConfig               `mapstructure:"dmenu" yaml:"dmenu"`
31	Logging           LoggingConfig             `mapstructure:"logging" yaml:"logging"`
32	Appearance        AppearanceConfig          `mapstructure:"appearance" yaml:"appearance"`
33	VideoAcceleration VideoAccelerationConfig   `mapstructure:"video_acceleration" yaml:"video_acceleration"`
34	CodecPreferences  CodecConfig               `mapstructure:"codec_preferences" yaml:"codec_preferences"`
35	// RenderingMode controls GPU/CPU rendering selection for WebKit
36	RenderingMode RenderingMode `mapstructure:"rendering_mode" yaml:"rendering_mode"`
37}
38
39// RenderingMode selects GPU vs CPU rendering.
40type RenderingMode string
41
42const (
43	RenderingModeAuto RenderingMode = "auto"
44	RenderingModeGPU  RenderingMode = "gpu"
45	RenderingModeCPU  RenderingMode = "cpu"
46)
47
48// DatabaseConfig holds database-related configuration.
49type DatabaseConfig struct {
50	Path           string        `mapstructure:"path" yaml:"path"`
51	MaxConnections int           `mapstructure:"max_connections" yaml:"max_connections"`
52	MaxIdleTime    time.Duration `mapstructure:"max_idle_time" yaml:"max_idle_time"`
53	QueryTimeout   time.Duration `mapstructure:"query_timeout" yaml:"query_timeout"`
54}
55
56// HistoryConfig holds history-related configuration.
57type HistoryConfig struct {
58	MaxEntries          int `mapstructure:"max_entries" yaml:"max_entries"`
59	RetentionPeriodDays int `mapstructure:"retention_period_days" yaml:"retention_period_days"`
60	CleanupIntervalDays int `mapstructure:"cleanup_interval_days" yaml:"cleanup_interval_days"`
61}
62
63// SearchShortcut represents a search shortcut configuration.
64type SearchShortcut struct {
65	URL         string `mapstructure:"url" yaml:"url" json:"url"`
66	Description string `mapstructure:"description" yaml:"description" json:"description"`
67}
68
69// DmenuConfig holds dmenu/rofi integration configuration.
70type DmenuConfig struct {
71	MaxHistoryItems  int    `mapstructure:"max_history_items" yaml:"max_history_items"`
72	ShowVisitCount   bool   `mapstructure:"show_visit_count" yaml:"show_visit_count"`
73	ShowLastVisited  bool   `mapstructure:"show_last_visited" yaml:"show_last_visited"`
74	HistoryPrefix    string `mapstructure:"history_prefix" yaml:"history_prefix"`
75	ShortcutPrefix   string `mapstructure:"shortcut_prefix" yaml:"shortcut_prefix"`
76	URLPrefix        string `mapstructure:"url_prefix" yaml:"url_prefix"`
77	DateFormat       string `mapstructure:"date_format" yaml:"date_format"`
78	SortByVisitCount bool   `mapstructure:"sort_by_visit_count" yaml:"sort_by_visit_count"`
79}
80
81// LoggingConfig holds logging configuration.
82type LoggingConfig struct {
83	Level      string `mapstructure:"level" yaml:"level"`
84	Format     string `mapstructure:"format" yaml:"format"`
85	Filename   string `mapstructure:"filename" yaml:"filename"`
86	MaxSize    int    `mapstructure:"max_size" yaml:"max_size"`
87	MaxBackups int    `mapstructure:"max_backups" yaml:"max_backups"`
88	MaxAge     int    `mapstructure:"max_age" yaml:"max_age"`
89	Compress   bool   `mapstructure:"compress" yaml:"compress"`
90
91	// File output configuration
92	LogDir        string `mapstructure:"log_dir" yaml:"log_dir"`
93	EnableFileLog bool   `mapstructure:"enable_file_log" yaml:"enable_file_log"`
94
95	// Capture settings
96	CaptureStdout  bool `mapstructure:"capture_stdout" yaml:"capture_stdout"`
97	CaptureStderr  bool `mapstructure:"capture_stderr" yaml:"capture_stderr"`
98	CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"`
99
100	// Debug output
101	DebugFile     string `mapstructure:"debug_file" yaml:"debug_file"`
102	VerboseWebKit bool   `mapstructure:"verbose_webkit" yaml:"verbose_webkit"`
103}
104
105// AppearanceConfig holds UI/rendering preferences.
106type AppearanceConfig struct {
107	// Default fonts for pages that do not specify fonts.
108	SansFont      string `mapstructure:"sans_font" yaml:"sans_font"`
109	SerifFont     string `mapstructure:"serif_font" yaml:"serif_font"`
110	MonospaceFont string `mapstructure:"monospace_font" yaml:"monospace_font"`
111	// Default font size in CSS pixels (approx).
112	DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`
113}
114
115// VideoAccelerationConfig holds video hardware acceleration preferences.
116type VideoAccelerationConfig struct {
117	EnableVAAPI      bool   `mapstructure:"enable_vaapi" yaml:"enable_vaapi"`
118	AutoDetectGPU    bool   `mapstructure:"auto_detect_gpu" yaml:"auto_detect_gpu"`
119	VAAPIDriverName  string `mapstructure:"vaapi_driver_name" yaml:"vaapi_driver_name"`
120	EnableAllDrivers bool   `mapstructure:"enable_all_drivers" yaml:"enable_all_drivers"`
121	LegacyVAAPI      bool   `mapstructure:"legacy_vaapi" yaml:"legacy_vaapi"`
122}
123
124// CodecConfig holds video codec preferences and handling
125type CodecConfig struct {
126	// Codec preference order (e.g., "av1,h264,vp8")
127	PreferredCodecs string `mapstructure:"preferred_codecs" yaml:"preferred_codecs"`
128
129	// Force specific codec for platforms
130	ForceAV1 bool `mapstructure:"force_av1" yaml:"force_av1"`
131
132	// Block problematic codecs
133	BlockVP9 bool `mapstructure:"block_vp9" yaml:"block_vp9"`
134	BlockVP8 bool `mapstructure:"block_vp8" yaml:"block_vp8"`
135
136	// Hardware acceleration per codec
137	AV1HardwareOnly    bool `mapstructure:"av1_hardware_only" yaml:"av1_hardware_only"`
138	DisableVP9Hardware bool `mapstructure:"disable_vp9_hardware" yaml:"disable_vp9_hardware"`
139
140	// Buffer configuration for smooth playback
141	VideoBufferSizeMB  int `mapstructure:"video_buffer_size_mb" yaml:"video_buffer_size_mb"`
142	QueueBufferTimeSec int `mapstructure:"queue_buffer_time_sec" yaml:"queue_buffer_time_sec"`
143
144	// Custom User-Agent for codec negotiation
145	CustomUserAgent string `mapstructure:"custom_user_agent" yaml:"custom_user_agent"`
146
147	// Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited)
148	AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"`
149
150	// Site-specific codec control settings
151	DisableTwitchCodecControl bool `mapstructure:"disable_twitch_codec_control" yaml:"disable_twitch_codec_control"`
152}
153
154// Manager handles configuration loading, watching, and reloading.
155type Manager struct {
156	config    *Config
157	viper     *viper.Viper
158	mu        sync.RWMutex
159	callbacks []func(*Config)
160	watching  bool
161}
162
163// NewManager creates a new configuration manager.
164func NewManager() (*Manager, error) {
165	v := viper.New()
166
167	// Configure Viper - supports yaml, json, toml automatically
168	v.SetConfigName("config") // Will find config.yaml, config.json, config.toml, etc.
169
170	// Add config paths
171	configDir, err := GetConfigDir()
172	if err != nil {
173		return nil, fmt.Errorf("failed to get config directory: %w", err)
174	}
175	v.AddConfigPath(configDir)
176	v.AddConfigPath(".") // Current directory for development
177
178	// Set up environment variable support
179	v.SetEnvPrefix("DUMB_BROWSER")
180	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
181	v.AutomaticEnv()
182
183	// Bind specific environment variables
184	bindings := map[string]string{
185		"database.path":             "DATABASE_PATH",
186		"database.max_connections":  "DATABASE_MAX_CONNECTIONS",
187		"database.max_idle_time":    "DATABASE_MAX_IDLE_TIME",
188		"database.query_timeout":    "DATABASE_QUERY_TIMEOUT",
189		"browser.command":           "BROWSER_COMMAND",
190		"browser.timeout":           "BROWSER_TIMEOUT",
191		"browser.detach_process":    "BROWSER_DETACH_PROCESS",
192		"history.max_entries":       "HISTORY_MAX_ENTRIES",
193		"history.retention_period":  "HISTORY_RETENTION_PERIOD",
194		"history.cleanup_interval":  "HISTORY_CLEANUP_INTERVAL",
195		"dmenu.max_history_items":   "DMENU_MAX_HISTORY_ITEMS",
196		"dmenu.show_visit_count":    "DMENU_SHOW_VISIT_COUNT",
197		"dmenu.show_last_visited":   "DMENU_SHOW_LAST_VISITED",
198		"dmenu.history_prefix":      "DMENU_HISTORY_PREFIX",
199		"dmenu.shortcut_prefix":     "DMENU_SHORTCUT_PREFIX",
200		"dmenu.url_prefix":          "DMENU_URL_PREFIX",
201		"dmenu.date_format":         "DMENU_DATE_FORMAT",
202		"dmenu.sort_by_visit_count": "DMENU_SORT_BY_VISIT_COUNT",
203		"logging.level":             "LOGGING_LEVEL",
204		"logging.format":            "LOGGING_FORMAT",
205		"logging.filename":          "LOGGING_FILENAME",
206		"logging.max_size":          "LOGGING_MAX_SIZE",
207		"logging.max_backups":       "LOGGING_MAX_BACKUPS",
208		"logging.max_age":           "LOGGING_MAX_AGE",
209		"logging.compress":          "LOGGING_COMPRESS",
210	}
211
212	for key, env := range bindings {
213		if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
214			return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
215		}
216	}
217
218	// Explicit binding for rendering mode via dedicated env var
219	if err := v.BindEnv("rendering_mode", "DUMBER_RENDERING_MODE"); err != nil {
220		return nil, fmt.Errorf("failed to bind DUMBER_RENDERING_MODE: %w", err)
221	}
222
223	// Video acceleration environment variable bindings
224	videoAccelEnvBindings := map[string]string{
225		"video_acceleration.enable_vaapi":       "DUMBER_VIDEO_ACCELERATION_ENABLE",
226		"video_acceleration.auto_detect_gpu":    "DUMBER_VIDEO_AUTO_DETECT",
227		"video_acceleration.vaapi_driver_name":  "LIBVA_DRIVER_NAME",
228		"video_acceleration.enable_all_drivers": "GST_VAAPI_ALL_DRIVERS",
229		"video_acceleration.legacy_vaapi":       "WEBKIT_GST_ENABLE_LEGACY_VAAPI",
230	}
231
232	for key, env := range videoAccelEnvBindings {
233		if err := v.BindEnv(key, env); err != nil {
234			return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
235		}
236	}
237
238	// Codec preferences environment variable bindings
239	codecEnvBindings := map[string]string{
240		"codec_preferences.preferred_codecs":             "DUMBER_PREFERRED_CODECS",
241		"codec_preferences.force_av1":                    "DUMBER_FORCE_AV1",
242		"codec_preferences.block_vp9":                    "DUMBER_BLOCK_VP9",
243		"codec_preferences.block_vp8":                    "DUMBER_BLOCK_VP8",
244		"codec_preferences.av1_hardware_only":            "DUMBER_AV1_HW_ONLY",
245		"codec_preferences.disable_vp9_hardware":         "DUMBER_DISABLE_VP9_HW",
246		"codec_preferences.video_buffer_size_mb":         "DUMBER_VIDEO_BUFFER_MB",
247		"codec_preferences.queue_buffer_time_sec":        "DUMBER_QUEUE_BUFFER_SEC",
248		"codec_preferences.custom_user_agent":            "DUMBER_CUSTOM_UA",
249		"codec_preferences.av1_max_resolution":           "DUMBER_AV1_MAX_RES",
250		"codec_preferences.disable_twitch_codec_control": "DUMBER_DISABLE_TWITCH_CODEC",
251	}
252
253	for key, env := range codecEnvBindings {
254		if err := v.BindEnv(key, env); err != nil {
255			return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
256		}
257	}
258
259	return &Manager{
260		viper:     v,
261		callbacks: make([]func(*Config), 0),
262	}, nil
263}
264
265// Load loads the configuration from file and environment variables.
266func (m *Manager) Load() error {
267	m.mu.Lock()
268	defer m.mu.Unlock()
269
270	// Ensure directories exist
271	if err := EnsureDirectories(); err != nil {
272		return fmt.Errorf("failed to ensure directories: %w", err)
273	}
274
275	// Set defaults
276	m.setDefaults()
277
278	// Read config file if it exists
279	if err := m.viper.ReadInConfig(); err != nil {
280		var configFileNotFoundError viper.ConfigFileNotFoundError
281		if errors.As(err, &configFileNotFoundError) {
282			// Config file not found, create default one
283			if err := m.createDefaultConfig(); err != nil {
284				return fmt.Errorf("failed to create default config: %w", err)
285			}
286		} else {
287			return fmt.Errorf("failed to read config file: %w", err)
288		}
289	}
290
291	// Unmarshal into config struct
292	config := &Config{}
293	if err := m.viper.Unmarshal(config); err != nil {
294		return fmt.Errorf("failed to unmarshal config: %w", err)
295	}
296
297	// Set database path if not specified
298	if config.Database.Path == "" {
299		dbPath, err := GetDatabaseFile()
300		if err != nil {
301			return fmt.Errorf("failed to get database path: %w", err)
302		}
303		config.Database.Path = dbPath
304	}
305
306	// Normalize/validate rendering mode
307	switch strings.ToLower(string(config.RenderingMode)) {
308	case "", string(RenderingModeAuto):
309		config.RenderingMode = RenderingModeAuto
310	case string(RenderingModeGPU):
311		config.RenderingMode = RenderingModeGPU
312	case string(RenderingModeCPU):
313		config.RenderingMode = RenderingModeCPU
314	default:
315		config.RenderingMode = RenderingModeAuto
316	}
317
318	// Auto-detect GPU if enabled and driver name is not set
319	if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
320		gpuInfo := gpu.DetectGPU()
321		if gpuInfo.SupportsVAAPI() {
322			config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
323		}
324	}
325
326	// Validate and configure codec preferences
327	config = m.validateAndConfigureCodecPreferences(config)
328
329	m.config = config
330	return nil
331}
332
333// Get returns the current configuration (thread-safe).
334func (m *Manager) Get() *Config {
335	m.mu.RLock()
336	defer m.mu.RUnlock()
337
338	// Return a copy to prevent external modification
339	configCopy := *m.config
340	return &configCopy
341}
342
343// Watch starts watching the config file for changes and reloads automatically.
344func (m *Manager) Watch() error {
345	m.mu.Lock()
346	defer m.mu.Unlock()
347
348	if m.watching {
349		return nil // Already watching
350	}
351
352	m.viper.WatchConfig()
353	m.viper.OnConfigChange(func(_ fsnotify.Event) {
354		// Reload config
355		if err := m.reload(); err != nil {
356			fmt.Fprintf(os.Stderr, "Warning: failed to reload config: %v\n", err)
357			return
358		}
359
360		// Notify callbacks
361		m.mu.RLock()
362		config := m.config
363		callbacks := make([]func(*Config), len(m.callbacks))
364		copy(callbacks, m.callbacks)
365		m.mu.RUnlock()
366
367		for _, callback := range callbacks {
368			callback(config)
369		}
370	})
371
372	m.watching = true
373	return nil
374}
375
376// OnConfigChange registers a callback function to be called when config changes.
377func (m *Manager) OnConfigChange(callback func(*Config)) {
378	m.mu.Lock()
379	defer m.mu.Unlock()
380
381	m.callbacks = append(m.callbacks, callback)
382}
383
384// reload reloads the configuration (internal method, must be called with lock).
385func (m *Manager) reload() error {
386	if err := m.viper.ReadInConfig(); err != nil {
387		return err
388	}
389
390	config := &Config{}
391	if err := m.viper.Unmarshal(config); err != nil {
392		return err
393	}
394
395	// Set database path if not specified
396	if config.Database.Path == "" {
397		dbPath, err := GetDatabaseFile()
398		if err != nil {
399			return fmt.Errorf("failed to get database path: %w", err)
400		}
401		config.Database.Path = dbPath
402	}
403
404	// Normalize/validate rendering mode
405	switch strings.ToLower(string(config.RenderingMode)) {
406	case "", string(RenderingModeAuto):
407		config.RenderingMode = RenderingModeAuto
408	case string(RenderingModeGPU):
409		config.RenderingMode = RenderingModeGPU
410	case string(RenderingModeCPU):
411		config.RenderingMode = RenderingModeCPU
412	default:
413		config.RenderingMode = RenderingModeAuto
414	}
415
416	// Auto-detect GPU if enabled and driver name is not set
417	if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
418		gpuInfo := gpu.DetectGPU()
419		if gpuInfo.SupportsVAAPI() {
420			config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
421		}
422	}
423
424	// Validate and configure codec preferences
425	config = m.validateAndConfigureCodecPreferences(config)
426
427	m.config = config
428	return nil
429}
430
431// setDefaults sets default configuration values in Viper.
432func (m *Manager) setDefaults() {
433	defaults := DefaultConfig()
434
435	// Database defaults
436	m.viper.SetDefault("database.max_connections", defaults.Database.MaxConnections)
437	m.viper.SetDefault("database.max_idle_time", defaults.Database.MaxIdleTime)
438	m.viper.SetDefault("database.query_timeout", defaults.Database.QueryTimeout)
439
440	// Note: Browser config removed - we build our own browser
441
442	// History defaults
443	m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
444	m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
445	m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
446
447	// Search shortcuts defaults
448	m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
449
450	// Dmenu defaults
451	m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
452	m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
453	m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
454	m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
455	m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
456	m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
457	m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
458	m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
459
460	// Logging defaults
461	m.viper.SetDefault("logging.level", defaults.Logging.Level)
462	m.viper.SetDefault("logging.format", defaults.Logging.Format)
463	m.viper.SetDefault("logging.filename", defaults.Logging.Filename)
464	m.viper.SetDefault("logging.max_size", defaults.Logging.MaxSize)
465	m.viper.SetDefault("logging.max_backups", defaults.Logging.MaxBackups)
466	m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
467	m.viper.SetDefault("logging.compress", defaults.Logging.Compress)
468	m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
469	m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
470	m.viper.SetDefault("logging.capture_stdout", defaults.Logging.CaptureStdout)
471	m.viper.SetDefault("logging.capture_stderr", defaults.Logging.CaptureStderr)
472	m.viper.SetDefault("logging.capture_c_output", defaults.Logging.CaptureCOutput)
473	m.viper.SetDefault("logging.debug_file", defaults.Logging.DebugFile)
474	m.viper.SetDefault("logging.verbose_webkit", defaults.Logging.VerboseWebKit)
475
476	// Appearance defaults
477	m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
478	m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
479	m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
480	m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
481
482	// Video acceleration defaults
483	m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)
484	m.viper.SetDefault("video_acceleration.auto_detect_gpu", defaults.VideoAcceleration.AutoDetectGPU)
485	m.viper.SetDefault("video_acceleration.vaapi_driver_name", defaults.VideoAcceleration.VAAPIDriverName)
486	m.viper.SetDefault("video_acceleration.enable_all_drivers", defaults.VideoAcceleration.EnableAllDrivers)
487	m.viper.SetDefault("video_acceleration.legacy_vaapi", defaults.VideoAcceleration.LegacyVAAPI)
488
489	// Codec preferences defaults
490	m.viper.SetDefault("codec_preferences.preferred_codecs", defaults.CodecPreferences.PreferredCodecs)
491	m.viper.SetDefault("codec_preferences.force_av1", defaults.CodecPreferences.ForceAV1)
492	m.viper.SetDefault("codec_preferences.block_vp9", defaults.CodecPreferences.BlockVP9)
493	m.viper.SetDefault("codec_preferences.block_vp8", defaults.CodecPreferences.BlockVP8)
494	m.viper.SetDefault("codec_preferences.av1_hardware_only", defaults.CodecPreferences.AV1HardwareOnly)
495	m.viper.SetDefault("codec_preferences.disable_vp9_hardware", defaults.CodecPreferences.DisableVP9Hardware)
496	m.viper.SetDefault("codec_preferences.video_buffer_size_mb", defaults.CodecPreferences.VideoBufferSizeMB)
497	m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec)
498	m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent)
499	m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution)
500	m.viper.SetDefault("codec_preferences.disable_twitch_codec_control", defaults.CodecPreferences.DisableTwitchCodecControl)
501
502	// Rendering defaults
503	m.viper.SetDefault("rendering_mode", string(RenderingModeAuto))
504}
505
506// createDefaultConfig creates a default configuration file.
507func (m *Manager) createDefaultConfig() error {
508	configFile, err := GetConfigFile()
509	if err != nil {
510		return err
511	}
512
513	// Create config directory if it doesn't exist
514	if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
515		return err
516	}
517
518	// Get the default configuration
519	defaultConfig := DefaultConfig()
520
521	// Marshal to JSON with proper indentation
522	configData, err := json.MarshalIndent(defaultConfig, "", "  ")
523	if err != nil {
524		return fmt.Errorf("failed to marshal default config: %w", err)
525	}
526
527	// Write JSON config file
528	if err := os.WriteFile(configFile, configData, filePerm); err != nil {
529		return fmt.Errorf("failed to write config file: %w", err)
530	}
531
532	fmt.Printf("Created default configuration file: %s\n", configFile)
533	return nil
534}
535
536// GetConfigFile returns the path to the configuration file being used.
537func (m *Manager) GetConfigFile() string {
538	return m.viper.ConfigFileUsed()
539}
540
541// Global configuration manager instance
542var globalManager *Manager
543var globalManagerOnce sync.Once
544
545// Init initializes the global configuration manager.
546func Init() error {
547	var err error
548	globalManagerOnce.Do(func() {
549		globalManager, err = NewManager()
550		if err != nil {
551			return
552		}
553		err = globalManager.Load()
554	})
555	return err
556}
557
558// Get returns the global configuration.
559func Get() *Config {
560	if globalManager == nil {
561		// Return defaults if not initialized
562		return DefaultConfig()
563	}
564	return globalManager.Get()
565}
566
567// Watch starts watching the global configuration for changes.
568func Watch() error {
569	if globalManager == nil {
570		return fmt.Errorf("configuration not initialized")
571	}
572	return globalManager.Watch()
573}
574
575// OnConfigChange registers a callback for global configuration changes.
576func OnConfigChange(callback func(*Config)) {
577	if globalManager == nil {
578		return
579	}
580	globalManager.OnConfigChange(callback)
581}
582
583// validateAndConfigureCodecPreferences validates and auto-configures codec preferences based on GPU capabilities
584func (m *Manager) validateAndConfigureCodecPreferences(config *Config) *Config {
585	// Validate codec preferences
586	if config.CodecPreferences.PreferredCodecs == "" {
587		config.CodecPreferences.PreferredCodecs = "av1,h264,vp8"
588		fmt.Printf("Config: Set default codec preferences: %s\n", config.CodecPreferences.PreferredCodecs)
589	}
590
591	// Auto-configure based on GPU capabilities
592	if config.VideoAcceleration.AutoDetectGPU {
593		gpuInfo := gpu.DetectGPU()
594
595		// Check AV1 hardware support
596		if config.CodecPreferences.AV1HardwareOnly && !gpuInfo.SupportsAV1Hardware() {
597			// Disable AV1 hardware-only if GPU doesn't support it
598			config.CodecPreferences.AV1HardwareOnly = false
599			fmt.Printf("Config: GPU doesn't support AV1 hardware, enabling software fallback\n")
600		}
601
602		// Get detailed AV1 capabilities
603		av1Caps := gpuInfo.GetAV1HardwareCapabilities()
604		if av1Caps["decode"] {
605			fmt.Printf("Config: GPU supports AV1 hardware decode\n")
606
607			// If GPU supports AV1, prefer it over VP9
608			if !config.CodecPreferences.ForceAV1 &&
609				!strings.HasPrefix(config.CodecPreferences.PreferredCodecs, "av1") {
610				config.CodecPreferences.PreferredCodecs = "av1," + config.CodecPreferences.PreferredCodecs
611				fmt.Printf("Config: Auto-enabled AV1 preference due to hardware support\n")
612			}
613		} else {
614			// If no AV1 support, ensure VP9 hardware is disabled to prevent issues
615			if !config.CodecPreferences.DisableVP9Hardware {
616				config.CodecPreferences.DisableVP9Hardware = true
617				fmt.Printf("Config: Auto-disabled VP9 hardware acceleration (no AV1 fallback)\n")
618			}
619		}
620
621		// Log GPU-specific codec capabilities
622		fmt.Printf("Config: %s GPU codec capabilities - AV1: decode=%t, encode=%t, 10bit=%t\n",
623			gpuInfo.Vendor, av1Caps["decode"], av1Caps["encode"], av1Caps["10bit"])
624	}
625
626	return config
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