v0.8.0 0fa6785

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

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