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: "disk-io",
197 min_mode: Mode::Long,
198 },
199 FieldDef {
200 key: "btrfs",
201 min_mode: Mode::Long,
202 },
203 FieldDef {
204 key: "zpool",
205 min_mode: Mode::Long,
206 },
207 FieldDef {
208 key: "temp",
209 min_mode: Mode::Long,
210 },
211 FieldDef {
213 key: "net",
214 min_mode: Mode::Short,
215 },
216 FieldDef {
217 key: "net-io",
218 min_mode: Mode::Long,
219 },
220 FieldDef {
221 key: "public-ip",
222 min_mode: Mode::Long,
223 },
224 FieldDef {
225 key: "wifi",
226 min_mode: Mode::Long,
227 },
228 FieldDef {
229 key: "dns",
230 min_mode: Mode::Long,
231 },
232 FieldDef {
233 key: "bluetooth",
234 min_mode: Mode::Long,
235 },
236 FieldDef {
237 key: "battery",
238 min_mode: Mode::Long,
239 },
240 FieldDef {
241 key: "power-adapter",
242 min_mode: Mode::Long,
243 },
244 FieldDef {
246 key: "shell",
247 min_mode: Mode::Long,
248 },
249 FieldDef {
250 key: "editor",
251 min_mode: Mode::Long,
252 },
253 FieldDef {
254 key: "terminal",
255 min_mode: Mode::Long,
256 },
257 FieldDef {
258 key: "terminal-font",
259 min_mode: Mode::Long,
260 },
261 FieldDef {
262 key: "terminal-size",
263 min_mode: Mode::Long,
264 },
265 FieldDef {
266 key: "desktop",
267 min_mode: Mode::Long,
268 },
269 FieldDef {
270 key: "wm",
271 min_mode: Mode::Long,
272 },
273 FieldDef {
274 key: "login-manager",
275 min_mode: Mode::Long,
276 },
277 FieldDef {
279 key: "player",
280 min_mode: Mode::Long,
281 },
282 FieldDef {
283 key: "media",
284 min_mode: Mode::Long,
285 },
286 FieldDef {
288 key: "wm-theme",
289 min_mode: Mode::Full,
290 },
291 FieldDef {
292 key: "wallpaper",
293 min_mode: Mode::Full,
294 },
295 FieldDef {
296 key: "terminal-theme",
297 min_mode: Mode::Full,
298 },
299 FieldDef {
300 key: "theme",
301 min_mode: Mode::Full,
302 },
303 FieldDef {
304 key: "icons",
305 min_mode: Mode::Full,
306 },
307 FieldDef {
308 key: "cursor",
309 min_mode: Mode::Full,
310 },
311 FieldDef {
312 key: "font",
313 min_mode: Mode::Long,
314 },
315 FieldDef {
316 key: "users",
317 min_mode: Mode::Long,
318 },
319 FieldDef {
320 key: "packages",
321 min_mode: Mode::Long,
322 },
323 FieldDef {
324 key: "weather",
325 min_mode: Mode::Full,
326 },
327];
328
329pub fn fields_for(mode: Mode) -> Vec<String> {
335 FIELDS
336 .iter()
337 .filter(|f| f.min_mode <= mode)
338 .map(|f| f.key.to_string())
339 .collect()
340}
341
342pub fn all_keys() -> Vec<&'static str> {
344 FIELDS.iter().map(|f| f.key).collect()
345}
346
347pub fn config_fields_block() -> String {
354 const PER_LINE: usize = 6;
355 let mut out = String::new();
356 out.push_str("# List of fields to display (leave empty or omit to show all)\n");
357 out.push_str(
358 "# Note: \"phys-mem\" requires running as root (sudo) on Linux to read DMI memory tables.\n",
359 );
360 out.push_str(
361 "# Note: \"weather\" requires network access and is shown in full mode only by default.\n",
362 );
363 out.push_str(
364 "# Note: \"domain-search\" queries resolvectl and is shown in full mode only by default.\n",
365 );
366 out.push_str("# fields = [\n");
367 for chunk in FIELDS.chunks(PER_LINE) {
368 let quoted: Vec<String> = chunk.iter().map(|f| format!("\"{}\"", f.key)).collect();
369 out.push_str("# ");
370 out.push_str("ed.join(", "));
371 out.push_str(",\n");
372 }
373 if let Some(pos) = out.rfind(",\n") {
375 out.replace_range(pos..pos + 2, "\n");
376 }
377 out.push_str("# ]");
378 out
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use std::collections::HashSet;
385
386 #[test]
387 fn test_no_duplicate_keys() {
388 let mut seen = HashSet::new();
389 for f in FIELDS {
390 assert!(seen.insert(f.key), "duplicate field key: {}", f.key);
391 }
392 }
393
394 #[test]
395 fn test_strata_strictly_nested() {
396 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
397 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
398 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
399 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
400
401 assert!(
402 short.is_subset(&standard),
403 "short must be a subset of standard"
404 );
405 assert!(
406 standard.is_subset(&long),
407 "standard must be a subset of long"
408 );
409 assert!(long.is_subset(&full), "long must be a subset of full");
410 }
411
412 #[test]
413 fn test_strata_counts() {
414 assert_eq!(fields_for(Mode::Short).len(), 8, "short field count");
417 assert_eq!(fields_for(Mode::Standard).len(), 19, "standard field count");
418 assert_eq!(fields_for(Mode::Long).len(), 56, "long field count");
419 assert_eq!(fields_for(Mode::Full).len(), 65, "full field count");
420 }
421
422 #[test]
423 fn test_short_set_exact() {
424 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
425 let expected: HashSet<String> = [
426 "os", "kernel", "host", "cpu", "gpu", "memory", "disk", "net",
427 ]
428 .iter()
429 .map(|s| s.to_string())
430 .collect();
431 assert_eq!(short, expected);
432 }
433
434 #[test]
435 fn test_mode_membership_boundaries() {
436 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
438 assert!(standard.contains("phys-mem"));
439 assert!(standard.contains("cpu-cache"));
440 assert!(!standard.contains("bios"), "bios is long+, not standard");
441
442 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
443 assert!(long.contains("bios"));
444 assert!(long.contains("terminal-size"));
445 assert!(long.contains("wm"));
446 assert!(long.contains("login-manager"));
447 assert!(long.contains("brightness"));
448 assert!(long.contains("power-adapter"));
449 assert!(long.contains("keyboard"));
450 assert!(long.contains("mouse"));
451 assert!(long.contains("tpm"));
452 assert!(long.contains("player"));
453 assert!(long.contains("media"));
454 assert!(!standard.contains("keyboard"), "keyboard is long+");
456 assert!(!standard.contains("mouse"), "mouse is long+");
457 assert!(!standard.contains("tpm"), "tpm is long+");
458 assert!(!standard.contains("player"), "player is long+");
459 assert!(!standard.contains("media"), "media is long+");
460 assert!(
462 !standard.contains("brightness"),
463 "brightness is long+, not standard"
464 );
465 assert!(!long.contains("weather"), "weather is full-only");
466 assert!(!long.contains("gamepad"), "gamepad is full-only");
467 assert!(!long.contains("wm-theme"), "wm-theme is full-only");
468 assert!(!long.contains("wallpaper"), "wallpaper is full-only");
469 assert!(
470 !long.contains("terminal-theme"),
471 "terminal-theme is full-only"
472 );
473
474 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
475 assert!(full.contains("weather"));
476 assert!(full.contains("domain-search"));
477 assert!(full.contains("wm-theme"));
478 assert!(full.contains("wallpaper"));
479 assert!(full.contains("terminal-theme"));
480 }
481
482 #[test]
483 fn test_config_block_shape() {
484 let block = config_fields_block();
485 assert!(block.contains("# fields = ["));
486 assert!(block.trim_end().ends_with("# ]"));
487 for key in all_keys() {
489 assert!(
490 block.contains(&format!("\"{}\"", key)),
491 "config block missing key: {}",
492 key
493 );
494 }
495 for line in block.lines() {
497 assert!(
498 line.starts_with('#'),
499 "uncommented line in config block: {line:?}"
500 );
501 }
502 assert!(
504 !block.contains(",\n# ]"),
505 "trailing comma before closing bracket"
506 );
507 }
508}