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 {
114 key: "cpu-usage",
115 min_mode: Mode::Long,
116 },
117 FieldDef {
119 key: "gpu",
120 min_mode: Mode::Short,
121 },
122 FieldDef {
127 key: "vulkan",
128 min_mode: Mode::Full,
129 },
130 FieldDef {
131 key: "opengl",
132 min_mode: Mode::Full,
133 },
134 FieldDef {
135 key: "opencl",
136 min_mode: Mode::Full,
137 },
138 FieldDef {
139 key: "motherboard",
140 min_mode: Mode::Standard,
141 },
142 FieldDef {
143 key: "bios",
144 min_mode: Mode::Long,
145 },
146 FieldDef {
147 key: "bootmgr",
148 min_mode: Mode::Long,
149 },
150 FieldDef {
151 key: "tpm",
152 min_mode: Mode::Long,
153 },
154 FieldDef {
155 key: "display",
156 min_mode: Mode::Standard,
157 },
158 FieldDef {
159 key: "brightness",
160 min_mode: Mode::Long,
161 },
162 FieldDef {
163 key: "audio",
164 min_mode: Mode::Standard,
165 },
166 FieldDef {
167 key: "camera",
168 min_mode: Mode::Standard,
169 },
170 FieldDef {
171 key: "gamepad",
172 min_mode: Mode::Full,
173 },
174 FieldDef {
175 key: "keyboard",
176 min_mode: Mode::Long,
177 },
178 FieldDef {
179 key: "mouse",
180 min_mode: Mode::Long,
181 },
182 FieldDef {
184 key: "memory",
185 min_mode: Mode::Short,
186 },
187 FieldDef {
188 key: "phys-mem",
189 min_mode: Mode::Standard,
190 },
191 FieldDef {
192 key: "swap",
193 min_mode: Mode::Standard,
194 },
195 FieldDef {
196 key: "uptime",
197 min_mode: Mode::Standard,
198 },
199 FieldDef {
200 key: "procs",
201 min_mode: Mode::Long,
202 },
203 FieldDef {
204 key: "load",
205 min_mode: Mode::Standard,
206 },
207 FieldDef {
208 key: "disk",
209 min_mode: Mode::Short,
210 },
211 FieldDef {
212 key: "phys-disk",
213 min_mode: Mode::Standard,
214 },
215 FieldDef {
216 key: "disk-io",
217 min_mode: Mode::Long,
218 },
219 FieldDef {
220 key: "btrfs",
221 min_mode: Mode::Long,
222 },
223 FieldDef {
224 key: "zpool",
225 min_mode: Mode::Long,
226 },
227 FieldDef {
228 key: "temp",
229 min_mode: Mode::Long,
230 },
231 FieldDef {
233 key: "net",
234 min_mode: Mode::Short,
235 },
236 FieldDef {
237 key: "net-io",
238 min_mode: Mode::Long,
239 },
240 FieldDef {
241 key: "public-ip",
242 min_mode: Mode::Long,
243 },
244 FieldDef {
245 key: "wifi",
246 min_mode: Mode::Long,
247 },
248 FieldDef {
249 key: "dns",
250 min_mode: Mode::Long,
251 },
252 FieldDef {
253 key: "bluetooth",
254 min_mode: Mode::Long,
255 },
256 FieldDef {
257 key: "battery",
258 min_mode: Mode::Long,
259 },
260 FieldDef {
261 key: "power-adapter",
262 min_mode: Mode::Long,
263 },
264 FieldDef {
266 key: "shell",
267 min_mode: Mode::Long,
268 },
269 FieldDef {
270 key: "editor",
271 min_mode: Mode::Long,
272 },
273 FieldDef {
274 key: "terminal",
275 min_mode: Mode::Long,
276 },
277 FieldDef {
278 key: "terminal-font",
279 min_mode: Mode::Long,
280 },
281 FieldDef {
282 key: "terminal-size",
283 min_mode: Mode::Long,
284 },
285 FieldDef {
286 key: "desktop",
287 min_mode: Mode::Long,
288 },
289 FieldDef {
290 key: "wm",
291 min_mode: Mode::Long,
292 },
293 FieldDef {
294 key: "login-manager",
295 min_mode: Mode::Long,
296 },
297 FieldDef {
299 key: "player",
300 min_mode: Mode::Long,
301 },
302 FieldDef {
303 key: "media",
304 min_mode: Mode::Long,
305 },
306 FieldDef {
308 key: "wm-theme",
309 min_mode: Mode::Full,
310 },
311 FieldDef {
312 key: "wallpaper",
313 min_mode: Mode::Full,
314 },
315 FieldDef {
316 key: "terminal-theme",
317 min_mode: Mode::Full,
318 },
319 FieldDef {
320 key: "theme",
321 min_mode: Mode::Full,
322 },
323 FieldDef {
324 key: "icons",
325 min_mode: Mode::Full,
326 },
327 FieldDef {
328 key: "cursor",
329 min_mode: Mode::Full,
330 },
331 FieldDef {
332 key: "font",
333 min_mode: Mode::Long,
334 },
335 FieldDef {
336 key: "users",
337 min_mode: Mode::Long,
338 },
339 FieldDef {
340 key: "packages",
341 min_mode: Mode::Long,
342 },
343 FieldDef {
344 key: "weather",
345 min_mode: Mode::Full,
346 },
347];
348
349pub fn fields_for(mode: Mode) -> Vec<String> {
355 FIELDS
356 .iter()
357 .filter(|f| f.min_mode <= mode)
358 .map(|f| f.key.to_string())
359 .collect()
360}
361
362pub fn all_keys() -> Vec<&'static str> {
364 FIELDS.iter().map(|f| f.key).collect()
365}
366
367pub fn config_fields_block() -> String {
374 const PER_LINE: usize = 6;
375 let mut out = String::new();
376 out.push_str("# List of fields to display (leave empty or omit to show all)\n");
377 out.push_str(
378 "# Note: \"phys-mem\" requires running as root (sudo) on Linux to read DMI memory tables.\n",
379 );
380 out.push_str(
381 "# Note: \"weather\" requires network access and is shown in full mode only by default.\n",
382 );
383 out.push_str(
384 "# Note: \"domain-search\" queries resolvectl and is shown in full mode only by default.\n",
385 );
386 out.push_str("# fields = [\n");
387 for chunk in FIELDS.chunks(PER_LINE) {
388 let quoted: Vec<String> = chunk.iter().map(|f| format!("\"{}\"", f.key)).collect();
389 out.push_str("# ");
390 out.push_str("ed.join(", "));
391 out.push_str(",\n");
392 }
393 if let Some(pos) = out.rfind(",\n") {
395 out.replace_range(pos..pos + 2, "\n");
396 }
397 out.push_str("# ]");
398 out
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use std::collections::HashSet;
405
406 #[test]
407 fn test_no_duplicate_keys() {
408 let mut seen = HashSet::new();
409 for f in FIELDS {
410 assert!(seen.insert(f.key), "duplicate field key: {}", f.key);
411 }
412 }
413
414 #[test]
415 fn test_strata_strictly_nested() {
416 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
417 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
418 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
419 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
420
421 assert!(
422 short.is_subset(&standard),
423 "short must be a subset of standard"
424 );
425 assert!(
426 standard.is_subset(&long),
427 "standard must be a subset of long"
428 );
429 assert!(long.is_subset(&full), "long must be a subset of full");
430 }
431
432 #[test]
433 fn test_strata_counts() {
434 assert_eq!(fields_for(Mode::Short).len(), 8, "short field count");
437 assert_eq!(fields_for(Mode::Standard).len(), 18, "standard field count");
438 assert_eq!(fields_for(Mode::Long).len(), 56, "long field count");
439 assert_eq!(fields_for(Mode::Full).len(), 68, "full field count");
440 }
441
442 #[test]
443 fn test_short_set_exact() {
444 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
445 let expected: HashSet<String> = [
446 "os", "kernel", "host", "cpu", "gpu", "memory", "disk", "net",
447 ]
448 .iter()
449 .map(|s| s.to_string())
450 .collect();
451 assert_eq!(short, expected);
452 }
453
454 #[test]
455 fn test_mode_membership_boundaries() {
456 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
458 assert!(standard.contains("phys-mem"));
459 assert!(standard.contains("cpu-cache"));
460 assert!(!standard.contains("bios"), "bios is long+, not standard");
461 assert!(
464 !standard.contains("cpu-usage"),
465 "cpu-usage is long+, not standard"
466 );
467
468 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
469 assert!(long.contains("cpu-usage"));
470 assert!(long.contains("bios"));
471 assert!(long.contains("terminal-size"));
472 assert!(long.contains("wm"));
473 assert!(long.contains("login-manager"));
474 assert!(long.contains("brightness"));
475 assert!(long.contains("power-adapter"));
476 assert!(long.contains("keyboard"));
477 assert!(long.contains("mouse"));
478 assert!(long.contains("tpm"));
479 assert!(long.contains("player"));
480 assert!(long.contains("media"));
481 assert!(!standard.contains("keyboard"), "keyboard is long+");
483 assert!(!standard.contains("mouse"), "mouse is long+");
484 assert!(!standard.contains("tpm"), "tpm is long+");
485 assert!(!standard.contains("player"), "player is long+");
486 assert!(!standard.contains("media"), "media is long+");
487 assert!(
489 !standard.contains("brightness"),
490 "brightness is long+, not standard"
491 );
492 assert!(!long.contains("weather"), "weather is full-only");
493 assert!(!long.contains("gamepad"), "gamepad is full-only");
494 assert!(!long.contains("wm-theme"), "wm-theme is full-only");
495 assert!(!long.contains("wallpaper"), "wallpaper is full-only");
496 assert!(
497 !long.contains("terminal-theme"),
498 "terminal-theme is full-only"
499 );
500
501 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
502 assert!(full.contains("weather"));
503 assert!(full.contains("domain-search"));
504 assert!(full.contains("wm-theme"));
505 assert!(full.contains("wallpaper"));
506 assert!(full.contains("terminal-theme"));
507 }
508
509 #[test]
510 fn test_config_block_shape() {
511 let block = config_fields_block();
512 assert!(block.contains("# fields = ["));
513 assert!(block.trim_end().ends_with("# ]"));
514 for key in all_keys() {
516 assert!(
517 block.contains(&format!("\"{}\"", key)),
518 "config block missing key: {}",
519 key
520 );
521 }
522 for line in block.lines() {
524 assert!(
525 line.starts_with('#'),
526 "uncommented line in config block: {line:?}"
527 );
528 }
529 assert!(
531 !block.contains(",\n# ]"),
532 "trailing comma before closing bracket"
533 );
534 }
535}