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}