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