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: "theme",
281 min_mode: Mode::Full,
282 },
283 FieldDef {
284 key: "icons",
285 min_mode: Mode::Full,
286 },
287 FieldDef {
288 key: "cursor",
289 min_mode: Mode::Full,
290 },
291 FieldDef {
292 key: "font",
293 min_mode: Mode::Long,
294 },
295 FieldDef {
296 key: "users",
297 min_mode: Mode::Long,
298 },
299 FieldDef {
300 key: "packages",
301 min_mode: Mode::Long,
302 },
303 FieldDef {
304 key: "weather",
305 min_mode: Mode::Full,
306 },
307];
308
309pub fn fields_for(mode: Mode) -> Vec<String> {
315 FIELDS
316 .iter()
317 .filter(|f| f.min_mode <= mode)
318 .map(|f| f.key.to_string())
319 .collect()
320}
321
322pub fn all_keys() -> Vec<&'static str> {
324 FIELDS.iter().map(|f| f.key).collect()
325}
326
327pub fn config_fields_block() -> String {
334 const PER_LINE: usize = 6;
335 let mut out = String::new();
336 out.push_str("# List of fields to display (leave empty or omit to show all)\n");
337 out.push_str(
338 "# Note: \"phys-mem\" requires running as root (sudo) on Linux to read DMI memory tables.\n",
339 );
340 out.push_str(
341 "# Note: \"weather\" requires network access and is shown in full mode only by default.\n",
342 );
343 out.push_str(
344 "# Note: \"domain-search\" queries resolvectl and is shown in full mode only by default.\n",
345 );
346 out.push_str("# fields = [\n");
347 for chunk in FIELDS.chunks(PER_LINE) {
348 let quoted: Vec<String> = chunk.iter().map(|f| format!("\"{}\"", f.key)).collect();
349 out.push_str("# ");
350 out.push_str("ed.join(", "));
351 out.push_str(",\n");
352 }
353 if let Some(pos) = out.rfind(",\n") {
355 out.replace_range(pos..pos + 2, "\n");
356 }
357 out.push_str("# ]");
358 out
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use std::collections::HashSet;
365
366 #[test]
367 fn test_no_duplicate_keys() {
368 let mut seen = HashSet::new();
369 for f in FIELDS {
370 assert!(seen.insert(f.key), "duplicate field key: {}", f.key);
371 }
372 }
373
374 #[test]
375 fn test_strata_strictly_nested() {
376 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
377 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
378 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
379 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
380
381 assert!(
382 short.is_subset(&standard),
383 "short must be a subset of standard"
384 );
385 assert!(
386 standard.is_subset(&long),
387 "standard must be a subset of long"
388 );
389 assert!(long.is_subset(&full), "long must be a subset of full");
390 }
391
392 #[test]
393 fn test_strata_counts() {
394 assert_eq!(fields_for(Mode::Short).len(), 8, "short field count");
397 assert_eq!(fields_for(Mode::Standard).len(), 19, "standard field count");
398 assert_eq!(fields_for(Mode::Long).len(), 54, "long field count");
399 assert_eq!(fields_for(Mode::Full).len(), 60, "full field count");
400 }
401
402 #[test]
403 fn test_short_set_exact() {
404 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
405 let expected: HashSet<String> = [
406 "os", "kernel", "host", "cpu", "gpu", "memory", "disk", "net",
407 ]
408 .iter()
409 .map(|s| s.to_string())
410 .collect();
411 assert_eq!(short, expected);
412 }
413
414 #[test]
415 fn test_mode_membership_boundaries() {
416 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
418 assert!(standard.contains("phys-mem"));
419 assert!(standard.contains("cpu-cache"));
420 assert!(!standard.contains("bios"), "bios is long+, not standard");
421
422 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
423 assert!(long.contains("bios"));
424 assert!(long.contains("terminal-size"));
425 assert!(long.contains("wm"));
426 assert!(long.contains("login-manager"));
427 assert!(long.contains("brightness"));
428 assert!(long.contains("power-adapter"));
429 assert!(long.contains("keyboard"));
430 assert!(long.contains("mouse"));
431 assert!(long.contains("tpm"));
432 assert!(long.contains("player"));
433 assert!(long.contains("media"));
434 assert!(!standard.contains("keyboard"), "keyboard is long+");
436 assert!(!standard.contains("mouse"), "mouse is long+");
437 assert!(!standard.contains("tpm"), "tpm is long+");
438 assert!(!standard.contains("player"), "player is long+");
439 assert!(!standard.contains("media"), "media is long+");
440 assert!(
442 !standard.contains("brightness"),
443 "brightness is long+, not standard"
444 );
445 assert!(!long.contains("weather"), "weather is full-only");
446 assert!(!long.contains("gamepad"), "gamepad is full-only");
447
448 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
449 assert!(full.contains("weather"));
450 assert!(full.contains("domain-search"));
451 }
452
453 #[test]
454 fn test_config_block_shape() {
455 let block = config_fields_block();
456 assert!(block.contains("# fields = ["));
457 assert!(block.trim_end().ends_with("# ]"));
458 for key in all_keys() {
460 assert!(
461 block.contains(&format!("\"{}\"", key)),
462 "config block missing key: {}",
463 key
464 );
465 }
466 for line in block.lines() {
468 assert!(
469 line.starts_with('#'),
470 "uncommented line in config block: {line:?}"
471 );
472 }
473 assert!(
475 !block.contains(",\n# ]"),
476 "trailing comma before closing bracket"
477 );
478 }
479}