v0.14.0 76d1892

validation.go RawHistoryFolder

1// Package config provides validation utilities for configuration values.
2package config
3
4import (
5	"fmt"
6	"strings"
7)
8
9// validateConfig performs comprehensive validation of configuration values
10func validateConfig(config *Config) error {
11	var validationErrors []string
12
13	// Validate numeric ranges
14	if config.History.MaxEntries < 0 {
15		validationErrors = append(validationErrors, "history.max_entries must be non-negative")
16	}
17	if config.History.RetentionPeriodDays < 0 {
18		validationErrors = append(validationErrors, "history.retention_period_days must be non-negative")
19	}
20	if config.History.CleanupIntervalDays < 0 {
21		validationErrors = append(validationErrors, "history.cleanup_interval_days must be non-negative")
22	}
23
24	if config.Dmenu.MaxHistoryItems < 0 {
25		validationErrors = append(validationErrors, "dmenu.max_history_items must be non-negative")
26	}
27
28	if config.Appearance.DefaultFontSize < 1 || config.Appearance.DefaultFontSize > 72 {
29		validationErrors = append(validationErrors, "appearance.default_font_size must be between 1 and 72")
30	}
31
32	if config.DefaultZoom < 0.1 || config.DefaultZoom > 5.0 {
33		validationErrors = append(validationErrors, "default_zoom must be between 0.1 and 5.0")
34	}
35
36	// Validate default search engine
37	if config.DefaultSearchEngine == "" {
38		validationErrors = append(validationErrors, "default_search_engine cannot be empty")
39	} else if !strings.Contains(config.DefaultSearchEngine, "%s") {
40		validationErrors = append(validationErrors, "default_search_engine must contain %s placeholder for the search query")
41	}
42
43	// Validate popup behavior
44	switch config.Workspace.Popups.Behavior {
45	case PopupBehaviorSplit, PopupBehaviorStacked, PopupBehaviorTabbed, PopupBehaviorWindowed:
46		// Valid
47	default:
48		validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.behavior must be one of: split, stacked, tabbed, windowed (got: %s)", config.Workspace.Popups.Behavior))
49	}
50
51	// Validate popup placement for split behavior
52	if config.Workspace.Popups.Behavior == PopupBehaviorSplit {
53		switch config.Workspace.Popups.Placement {
54		case "right", "left", "top", "bottom":
55			// Valid
56		default:
57			validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.placement must be one of: right, left, top, bottom (got: %s)", config.Workspace.Popups.Placement))
58		}
59	}
60
61	// Validate blank target behavior
62	switch config.Workspace.Popups.BlankTargetBehavior {
63	case "split", "stacked", "tabbed":
64		// Valid
65	default:
66		validationErrors = append(validationErrors, fmt.Sprintf("workspace.popups.blank_target_behavior must be one of: split, stacked, tabbed (got: %s)", config.Workspace.Popups.BlankTargetBehavior))
67	}
68
69	// Validate workspace styling values
70	if config.Workspace.Styling.BorderWidth < 0 {
71		validationErrors = append(validationErrors, "workspace.styling.border_width must be non-negative")
72	}
73	if config.Workspace.Styling.InactiveBorderWidth < 0 {
74		validationErrors = append(validationErrors, "workspace.styling.inactive_border_width must be non-negative")
75	}
76	if config.Workspace.Styling.TransitionDuration < 0 {
77		validationErrors = append(validationErrors, "workspace.styling.transition_duration must be non-negative")
78	}
79	if config.Workspace.Styling.BorderRadius < 0 {
80		validationErrors = append(validationErrors, "workspace.styling.border_radius must be non-negative")
81	}
82	if config.Workspace.Styling.UIScale < 0.5 || config.Workspace.Styling.UIScale > 3.0 {
83		validationErrors = append(validationErrors, "workspace.styling.ui_scale must be between 0.5 and 3.0")
84	}
85
86	// Validate pane mode timeout
87	if config.Workspace.PaneMode.TimeoutMilliseconds < 0 {
88		validationErrors = append(validationErrors, "workspace.pane_mode.timeout_ms must be non-negative")
89	}
90
91	// Validate pane mode actions
92	if len(config.Workspace.PaneMode.Actions) == 0 {
93		validationErrors = append(validationErrors, "workspace.pane_mode.actions cannot be empty")
94	}
95
96	// Check for duplicate keys and empty action key lists in pane mode
97	seenKeys := make(map[string]string)
98	for action, keys := range config.Workspace.PaneMode.Actions {
99		if len(keys) == 0 {
100			validationErrors = append(validationErrors, fmt.Sprintf("workspace.pane_mode.actions.%s must have at least one key binding", action))
101		}
102		for _, key := range keys {
103			if existingAction, exists := seenKeys[key]; exists {
104				validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in pane_mode actions '%s' and '%s'", key, existingAction, action))
105			}
106			seenKeys[key] = action
107		}
108	}
109
110	// Validate tab bar position
111	switch config.Workspace.TabBarPosition {
112	case "top", "bottom":
113		// Valid
114	default:
115		validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_bar_position must be 'top' or 'bottom' (got: %s)", config.Workspace.TabBarPosition))
116	}
117
118	// Validate tab mode timeout
119	if config.Workspace.TabMode.TimeoutMilliseconds < 0 {
120		validationErrors = append(validationErrors, "workspace.tab_mode.timeout_ms must be non-negative")
121	}
122
123	// Validate tab mode actions
124	if len(config.Workspace.TabMode.Actions) == 0 {
125		validationErrors = append(validationErrors, "workspace.tab_mode.actions cannot be empty")
126	}
127
128	// Check for duplicate keys and empty action key lists in tab mode
129	tabSeenKeys := make(map[string]string)
130	for action, keys := range config.Workspace.TabMode.Actions {
131		if len(keys) == 0 {
132			validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action))
133		}
134		for _, key := range keys {
135			if existingAction, exists := tabSeenKeys[key]; exists {
136				validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in tab_mode actions '%s' and '%s'", key, existingAction, action))
137			}
138			tabSeenKeys[key] = action
139		}
140	}
141
142	// Validate codec buffer sizes
143	if config.CodecPreferences.VideoBufferSizeMB < 0 {
144		validationErrors = append(validationErrors, "codec_preferences.video_buffer_size_mb must be non-negative")
145	}
146	if config.CodecPreferences.QueueBufferTimeSec < 0 {
147		validationErrors = append(validationErrors, "codec_preferences.queue_buffer_time_sec must be non-negative")
148	}
149
150	// Validate AV1 max resolution
151	if config.CodecPreferences.AV1MaxResolution != "" {
152		validResolutions := map[string]bool{
153			"720p": true, "1080p": true, "1440p": true, "4k": true, "unlimited": true,
154		}
155		if !validResolutions[config.CodecPreferences.AV1MaxResolution] {
156			validationErrors = append(validationErrors, fmt.Sprintf("codec_preferences.av1_max_resolution must be one of: 720p, 1080p, 1440p, 4k, unlimited (got: %s)", config.CodecPreferences.AV1MaxResolution))
157		}
158	}
159
160	// Validate logging values
161	if config.Logging.MaxSize < 0 {
162		validationErrors = append(validationErrors, "logging.max_size must be non-negative")
163	}
164	if config.Logging.MaxBackups < 0 {
165		validationErrors = append(validationErrors, "logging.max_backups must be non-negative")
166	}
167	if config.Logging.MaxAge < 0 {
168		validationErrors = append(validationErrors, "logging.max_age must be non-negative")
169	}
170
171	// If there are validation errors, return them
172	if len(validationErrors) > 0 {
173		return fmt.Errorf("config validation failed:\n  - %s", strings.Join(validationErrors, "\n  - "))
174	}
175
176	return nil
177}

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