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: "display",
132 min_mode: Mode::Standard,
133 },
134 FieldDef {
135 key: "audio",
136 min_mode: Mode::Standard,
137 },
138 FieldDef {
139 key: "camera",
140 min_mode: Mode::Standard,
141 },
142 FieldDef {
143 key: "gamepad",
144 min_mode: Mode::Full,
145 },
146 FieldDef {
148 key: "memory",
149 min_mode: Mode::Short,
150 },
151 FieldDef {
152 key: "phys-mem",
153 min_mode: Mode::Standard,
154 },
155 FieldDef {
156 key: "swap",
157 min_mode: Mode::Standard,
158 },
159 FieldDef {
160 key: "uptime",
161 min_mode: Mode::Standard,
162 },
163 FieldDef {
164 key: "procs",
165 min_mode: Mode::Long,
166 },
167 FieldDef {
168 key: "load",
169 min_mode: Mode::Standard,
170 },
171 FieldDef {
172 key: "disk",
173 min_mode: Mode::Short,
174 },
175 FieldDef {
176 key: "phys-disk",
177 min_mode: Mode::Standard,
178 },
179 FieldDef {
180 key: "btrfs",
181 min_mode: Mode::Long,
182 },
183 FieldDef {
184 key: "zpool",
185 min_mode: Mode::Long,
186 },
187 FieldDef {
188 key: "temp",
189 min_mode: Mode::Long,
190 },
191 FieldDef {
193 key: "net",
194 min_mode: Mode::Short,
195 },
196 FieldDef {
197 key: "public-ip",
198 min_mode: Mode::Long,
199 },
200 FieldDef {
201 key: "wifi",
202 min_mode: Mode::Long,
203 },
204 FieldDef {
205 key: "dns",
206 min_mode: Mode::Long,
207 },
208 FieldDef {
209 key: "bluetooth",
210 min_mode: Mode::Long,
211 },
212 FieldDef {
213 key: "battery",
214 min_mode: Mode::Long,
215 },
216 FieldDef {
218 key: "shell",
219 min_mode: Mode::Long,
220 },
221 FieldDef {
222 key: "editor",
223 min_mode: Mode::Long,
224 },
225 FieldDef {
226 key: "terminal",
227 min_mode: Mode::Long,
228 },
229 FieldDef {
230 key: "terminal-font",
231 min_mode: Mode::Long,
232 },
233 FieldDef {
234 key: "terminal-size",
235 min_mode: Mode::Long,
236 },
237 FieldDef {
238 key: "desktop",
239 min_mode: Mode::Long,
240 },
241 FieldDef {
242 key: "wm",
243 min_mode: Mode::Long,
244 },
245 FieldDef {
247 key: "theme",
248 min_mode: Mode::Full,
249 },
250 FieldDef {
251 key: "icons",
252 min_mode: Mode::Full,
253 },
254 FieldDef {
255 key: "cursor",
256 min_mode: Mode::Full,
257 },
258 FieldDef {
259 key: "font",
260 min_mode: Mode::Long,
261 },
262 FieldDef {
263 key: "users",
264 min_mode: Mode::Long,
265 },
266 FieldDef {
267 key: "packages",
268 min_mode: Mode::Long,
269 },
270 FieldDef {
271 key: "weather",
272 min_mode: Mode::Full,
273 },
274];
275
276pub fn fields_for(mode: Mode) -> Vec<String> {
282 FIELDS
283 .iter()
284 .filter(|f| f.min_mode <= mode)
285 .map(|f| f.key.to_string())
286 .collect()
287}
288
289pub fn all_keys() -> Vec<&'static str> {
291 FIELDS.iter().map(|f| f.key).collect()
292}
293
294pub fn config_fields_block() -> String {
301 const PER_LINE: usize = 6;
302 let mut out = String::new();
303 out.push_str("# List of fields to display (leave empty or omit to show all)\n");
304 out.push_str(
305 "# Note: \"phys-mem\" requires running as root (sudo) on Linux to read DMI memory tables.\n",
306 );
307 out.push_str(
308 "# Note: \"weather\" requires network access and is shown in full mode only by default.\n",
309 );
310 out.push_str(
311 "# Note: \"domain-search\" queries resolvectl and is shown in full mode only by default.\n",
312 );
313 out.push_str("# fields = [\n");
314 for chunk in FIELDS.chunks(PER_LINE) {
315 let quoted: Vec<String> = chunk.iter().map(|f| format!("\"{}\"", f.key)).collect();
316 out.push_str("# ");
317 out.push_str("ed.join(", "));
318 out.push_str(",\n");
319 }
320 if let Some(pos) = out.rfind(",\n") {
322 out.replace_range(pos..pos + 2, "\n");
323 }
324 out.push_str("# ]");
325 out
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use std::collections::HashSet;
332
333 #[test]
334 fn test_no_duplicate_keys() {
335 let mut seen = HashSet::new();
336 for f in FIELDS {
337 assert!(seen.insert(f.key), "duplicate field key: {}", f.key);
338 }
339 }
340
341 #[test]
342 fn test_strata_strictly_nested() {
343 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
344 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
345 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
346 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
347
348 assert!(
349 short.is_subset(&standard),
350 "short must be a subset of standard"
351 );
352 assert!(
353 standard.is_subset(&long),
354 "standard must be a subset of long"
355 );
356 assert!(long.is_subset(&full), "long must be a subset of full");
357 }
358
359 #[test]
360 fn test_strata_counts() {
361 assert_eq!(fields_for(Mode::Short).len(), 8, "short field count");
364 assert_eq!(fields_for(Mode::Standard).len(), 19, "standard field count");
365 assert_eq!(fields_for(Mode::Long).len(), 46, "long field count");
366 assert_eq!(fields_for(Mode::Full).len(), 52, "full field count");
367 }
368
369 #[test]
370 fn test_short_set_exact() {
371 let short: HashSet<_> = fields_for(Mode::Short).into_iter().collect();
372 let expected: HashSet<String> = [
373 "os", "kernel", "host", "cpu", "gpu", "memory", "disk", "net",
374 ]
375 .iter()
376 .map(|s| s.to_string())
377 .collect();
378 assert_eq!(short, expected);
379 }
380
381 #[test]
382 fn test_mode_membership_boundaries() {
383 let standard: HashSet<_> = fields_for(Mode::Standard).into_iter().collect();
385 assert!(standard.contains("phys-mem"));
386 assert!(standard.contains("cpu-cache"));
387 assert!(!standard.contains("bios"), "bios is long+, not standard");
388
389 let long: HashSet<_> = fields_for(Mode::Long).into_iter().collect();
390 assert!(long.contains("bios"));
391 assert!(long.contains("terminal-size"));
392 assert!(long.contains("wm"));
393 assert!(!long.contains("weather"), "weather is full-only");
394 assert!(!long.contains("gamepad"), "gamepad is full-only");
395
396 let full: HashSet<_> = fields_for(Mode::Full).into_iter().collect();
397 assert!(full.contains("weather"));
398 assert!(full.contains("domain-search"));
399 }
400
401 #[test]
402 fn test_config_block_shape() {
403 let block = config_fields_block();
404 assert!(block.contains("# fields = ["));
405 assert!(block.trim_end().ends_with("# ]"));
406 for key in all_keys() {
408 assert!(
409 block.contains(&format!("\"{}\"", key)),
410 "config block missing key: {}",
411 key
412 );
413 }
414 for line in block.lines() {
416 assert!(
417 line.starts_with('#'),
418 "uncommented line in config block: {line:?}"
419 );
420 }
421 assert!(
423 !block.contains(",\n# ]"),
424 "trailing comma before closing bracket"
425 );
426 }
427}