1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum Mode {
27 Short,
29 Standard,
31 Long,
33 Full,
35}
36
37struct FieldDef {
45 key: &'static str,
47 min_mode: Mode,
49}
50
51const FIELDS: &[FieldDef] = &[
59 FieldDef {
61 key: "os",
62 min_mode: Mode::Short,
63 },
64 FieldDef {
65 key: "kernel",
66 min_mode: Mode::Short,
67 },
68 FieldDef {
69 key: "host",
70 min_mode: Mode::Short,
71 },
72 FieldDef {
73 key: "domain",
74 min_mode: Mode::Long,
75 },
76 FieldDef {
77 key: "domain-search",
78 min_mode: Mode::Full,
79 },
80 FieldDef {
81 key: "chassis",
82 min_mode: Mode::Long,
83 },
84 FieldDef {
85 key: "init",
86 min_mode: Mode::Long,
87 },
88 FieldDef {
89 key: "locale",
90 min_mode: Mode::Long,
91 },
92 FieldDef {
93 key: "arch",
94 min_mode: Mode::Long,
95 },
96 FieldDef {
98 key: "cpu",
99 min_mode: Mode::Short,
100 },
101 FieldDef {
102 key: "cpu-freq",
103 min_mode: Mode::Long,
104 },
105 FieldDef {
106 key: "cpu-cache",
107 min_mode: Mode::Standard,
108 },
109 FieldDef {
110 key: "cpu-usage",
111 min_mode: Mode::Standard,
112 },
113 FieldDef {
115 key: "gpu",
116 min_mode: Mode::Short,
117 },
118 FieldDef {
119 key: "motherboard",
120 min_mode: Mode::Standard,
121 },
122 FieldDef {
123 key: "bios",
124 min_mode: Mode::Long,
125 },
126 FieldDef {
127 key: "bootmgr",
128 min_mode: Mode::Long,
129 },
130 FieldDef {
131 key: "display",
132 min_mode: Mode::Standard,
133 },
134 FieldDef {
135 key: "brightness",
136 min_mode: Mode::Long,
137 },
138 FieldDef {
139 key: "audio",
140 min_mode: Mode::Standard,
141 },
142 FieldDef {
143 key: "camera",
144 min_mode: Mode::Standard,
145 },
146 FieldDef {
147 key: "gamepad",
148 min_mode: Mode::Full,
149 },
150 FieldDef {
152 key: "memory",
153 min_mode: Mode::Short,
154 },
155 FieldDef {
156 key: "phys-mem",
157 min_mode: Mode::Standard,
158 },
159 FieldDef {
160 key: "swap",
161 min_mode: Mode::Standard,
162 },
163 FieldDef {
164 key: "uptime",
165 min_mode: Mode::Standard,
166 },
167 FieldDef {
168 key: "procs",
169 min_mode: Mode::Long,
170 },
171 FieldDef {
172 key: "load",
173 min_mode: Mode::Standard,
174 },
175 FieldDef {
176 key: "disk",
177 min_mode: Mode::Short,
178 },
179 FieldDef {
180 key: "phys-disk",
181 min_mode: Mode::Standard,
182 },
183 FieldDef {
184 key: "btrfs",
185 min_mode: Mode::Long,
186 },
187 FieldDef {
188 key: "zpool",
189 min_mode: Mode::Long,
190 },
191 FieldDef {
192 key: "temp",
193 min_mode: Mode::Long,
194 },
195 FieldDef {
197 key: "net",
198 min_mode: Mode::Short,
199 },
200 FieldDef {
201 key: "public-ip",
202 min_mode: Mode::Long,
203 },
204 FieldDef {
205 key: "wifi",
206 min_mode: Mode::Long,
207 },
208 FieldDef {
209 key: "dns",
210 min_mode: Mode::Long,
211 },
212 FieldDef {
213 key: "bluetooth",
214 min_mode: Mode::Long,
215 },
216 FieldDef {
217 key: "battery",
218 min_mode: Mode::Long,
219 },
220 FieldDef {
221 key: "power-adapter",
222 min_mode: Mode::Long,
223 },
224 FieldDef {
226 key: "shell",
227 min_mode: Mode::Long,
228 },
229 FieldDef {
230 key: "editor",
231 min_mode: Mode::Long,
232 },
233 FieldDef {
234 key: "terminal",
235 min_mode: Mode::Long,
236 },
237 FieldDef {
238 key: "terminal-font",
239 min_mode: Mode::Long,
240 },
241 FieldDef {
242 key: "terminal-size",
243 min_mode: Mode::Long,
244 },
245 FieldDef {
246 key: "desktop",
247 min_mode: Mode::Long,
248 },
249 FieldDef {
250 key: "wm",
251 min_mode: Mode::Long,
252 },
253 FieldDef {
254 key: "login-manager",
255 min_mode: Mode::Long,
256 },
257 FieldDef {
259 key: "theme",
260 min_mode: Mode::Full,
261 },
262 FieldDef {
263 key: "icons",
264 min_mode: Mode::Full,
265 },
266 FieldDef {
267 key: "cursor",
268 min_mode: Mode::Full,
269 },
270 FieldDef {
271 key: "font",
272 min_mode: Mode::Long,
273 },
274 FieldDef {
275 key: "users",
276 min_mode: Mode::Long,
277 },
278 FieldDef {
279 key: "packages",
280 min_mode: Mode::Long,
281 },
282 FieldDef {
283 key: "weather",
284 min_mode: Mode::Full,
285 },
286];
287
288pub fn fields_for(mode: Mode) -> Vec<String> {
294 FIELDS
295 .iter()
296 .filter(|f| f.min_mode <= mode)
297 .map(|f| f.key.to_string())
298 .collect()
299}
300
301pub fn all_keys() -> Vec<&'static str> {
303 FIELDS.iter().map(|f| f.key).collect()
304}
305
306pub fn config_fields_block() -> String {
313 const PER_LINE: usize = 6;
314 let mut out = String::new();
315 out.push_str("# List of fields to display (leave empty or omit to show all)\n");
316 out.push_str(
317 "# Note: \"phys-mem\" requires running as root (sudo) on Linux to read DMI memory tables.\n",
318 );
319 out.push_str(
320 "# Note: \"weather\" requires network access and is shown in full mode only by default.\n",
321 );
322 out.push_str(
323 "# Note: \"domain-search\" queries resolvectl and is shown in full mode only by default.\n",
324 );
325 out.push_str("# fields = [\n");
326 for chunk in FIELDS.chunks(PER_LINE) {
327 let quoted: Vec<String> = chunk.iter().map(|f| format!("\"{}\"", f.key)).collect();
328 out.push_str("# ");
329 out.push_str("ed.join(", "));
330 out.push_str(",\n");
331 }
332 if let Some(pos) = out.rfind(",\n") {
334 out.replace_range(pos..pos + 2, "\n");
335 }
336 out.push_str("# ]");
337 out
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use std::collections::HashSet;
344
345 #[test]
346 fn test_no_duplicate_keys() {
347 let mut seen = HashSet::new();
348 for f in FIELDS {
349 assert!(seen.insert(f.key), "duplicate field key: {}", f.key);
350 }
351 }
352
353 #[test]
354 fn test_strata_strictly_nested() {
355 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
356 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
357 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
358 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
359
360 assert!(
361 short.is_subset(&standard),
362 "short must be a subset of standard"
363 );
364 assert!(
365 standard.is_subset(&long),
366 "standard must be a subset of long"
367 );
368 assert!(long.is_subset(&full), "long must be a subset of full");
369 }
370
371 #[test]
372 fn test_strata_counts() {
373 assert_eq!(fields_for(Mode::Short).len(), 8, "short field count");
376 assert_eq!(fields_for(Mode::Standard).len(), 19, "standard field count");
377 assert_eq!(fields_for(Mode::Long).len(), 49, "long field count");
378 assert_eq!(fields_for(Mode::Full).len(), 55, "full field count");
379 }
380
381 #[test]
382 fn test_short_set_exact() {
383 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
384 let expected: HashSet<String> = [
385 "os", "kernel", "host", "cpu", "gpu", "memory", "disk", "net",
386 ]
387 .iter()
388 .map(|s| s.to_string())
389 .collect();
390 assert_eq!(short, expected);
391 }
392
393 #[test]
394 fn test_mode_membership_boundaries() {
395 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
397 assert!(standard.contains("phys-mem"));
398 assert!(standard.contains("cpu-cache"));
399 assert!(!standard.contains("bios"), "bios is long+, not standard");
400
401 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
402 assert!(long.contains("bios"));
403 assert!(long.contains("terminal-size"));
404 assert!(long.contains("wm"));
405 assert!(long.contains("login-manager"));
406 assert!(long.contains("brightness"));
407 assert!(long.contains("power-adapter"));
408 assert!(
410 !standard.contains("brightness"),
411 "brightness is long+, not standard"
412 );
413 assert!(!long.contains("weather"), "weather is full-only");
414 assert!(!long.contains("gamepad"), "gamepad is full-only");
415
416 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
417 assert!(full.contains("weather"));
418 assert!(full.contains("domain-search"));
419 }
420
421 #[test]
422 fn test_config_block_shape() {
423 let block = config_fields_block();
424 assert!(block.contains("# fields = ["));
425 assert!(block.trim_end().ends_with("# ]"));
426 for key in all_keys() {
428 assert!(
429 block.contains(&format!("\"{}\"", key)),
430 "config block missing key: {}",
431 key
432 );
433 }
434 for line in block.lines() {
436 assert!(
437 line.starts_with('#'),
438 "uncommented line in config block: {line:?}"
439 );
440 }
441 assert!(
443 !block.contains(",\n# ]"),
444 "trailing comma before closing bracket"
445 );
446 }
447}