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