1#![allow(clippy::collapsible_if)] #![allow(clippy::branches_sharing_code)] use std::borrow::Cow;
20
21#[must_use]
34pub fn format_bytes_si(bytes: u64) -> String {
35 const UNITS: &[&str] = &["B", "K", "M", "G", "T", "P"];
36 const BASE: f64 = 1000.0;
37
38 if bytes == 0 {
39 return "0B".to_string();
40 }
41
42 let bytes_f = bytes as f64;
43 let exp = (bytes_f.log10() / BASE.log10()).floor() as usize;
44 let exp = exp.min(UNITS.len() - 1);
45 let value = bytes_f / BASE.powi(exp as i32);
46
47 if exp == 0 {
48 format!("{bytes}B")
49 } else if value >= 100.0 {
50 format!("{:.0}{}", value, UNITS[exp])
51 } else if value >= 10.0 {
52 format!("{:.1}{}", value, UNITS[exp])
53 } else {
54 format!("{:.2}{}", value, UNITS[exp])
55 }
56}
57
58#[must_use]
60pub fn format_bytes_iec(bytes: u64) -> String {
61 const UNITS: &[&str] = &["B", "Ki", "Mi", "Gi", "Ti", "Pi"];
62 const BASE: f64 = 1024.0;
63
64 if bytes == 0 {
65 return "0B".to_string();
66 }
67
68 let bytes_f = bytes as f64;
69 let exp = (bytes_f.log2() / 10.0).floor() as usize;
70 let exp = exp.min(UNITS.len() - 1);
71 let value = bytes_f / BASE.powi(exp as i32);
72
73 if exp == 0 {
74 format!("{bytes}B")
75 } else if value >= 100.0 {
76 format!("{:.0}{}", value, UNITS[exp])
77 } else if value >= 10.0 {
78 format!("{:.1}{}", value, UNITS[exp])
79 } else {
80 format!("{:.2}{}", value, UNITS[exp])
81 }
82}
83
84#[must_use]
86pub fn format_rate(bytes_per_sec: u64) -> String {
87 format!("{}/s", format_bytes_si(bytes_per_sec))
88}
89
90#[must_use]
100pub fn format_percent(value: f32) -> String {
101 if value >= 100.0 {
102 format!("{value:.0}%")
103 } else if value >= 10.0 {
104 format!("{value:.1}%")
105 } else if value >= 0.1 {
106 format!("{value:.1}%")
107 } else if value > 0.0 {
108 format!("{value:.2}%")
109 } else {
110 "0%".to_string()
111 }
112}
113
114#[must_use]
116pub fn format_percent_clamped(value: f32) -> String {
117 format_percent(value.clamp(0.0, 100.0))
118}
119
120#[must_use]
122pub fn format_percent_fixed(value: f32, width: usize) -> String {
123 let s = format_percent(value);
124 if s.len() >= width {
125 s
126 } else {
127 format!("{s:>width$}")
128 }
129}
130
131#[must_use]
142pub fn format_duration(secs: u64) -> String {
143 if secs < 60 {
144 format!("{secs}s")
145 } else if secs < 3600 {
146 let m = secs / 60;
147 let s = secs % 60;
148 if s == 0 {
149 format!("{m}m")
150 } else {
151 format!("{m}:{s:02}")
152 }
153 } else if secs < 86400 {
154 let h = secs / 3600;
155 let m = (secs % 3600) / 60;
156 let s = secs % 60;
157 if s == 0 && m == 0 {
158 format!("{h}h")
159 } else if s == 0 {
160 format!("{h}:{m:02}")
161 } else {
162 format!("{h}:{m:02}:{s:02}")
163 }
164 } else {
165 let d = secs / 86400;
166 let h = (secs % 86400) / 3600;
167 if h == 0 {
168 format!("{d}d")
169 } else {
170 format!("{d}d {h}h")
171 }
172 }
173}
174
175#[must_use]
177pub fn format_duration_compact(secs: u64) -> String {
178 if secs < 60 {
179 format!("{secs}s")
180 } else if secs < 3600 {
181 format!("{}m", secs / 60)
182 } else if secs < 86400 {
183 format!("{}h", secs / 3600)
184 } else {
185 format!("{}d", secs / 86400)
186 }
187}
188
189#[must_use]
195pub fn format_freq_mhz(mhz: u64) -> String {
196 if mhz >= 1000 {
197 let ghz = mhz as f64 / 1000.0;
198 if ghz >= 10.0 {
199 format!("{ghz:.1}G")
200 } else {
201 format!("{ghz:.2}G")
202 }
203 } else {
204 format!("{mhz}M")
205 }
206}
207
208#[must_use]
214pub fn format_temp_c(celsius: f32) -> String {
215 if celsius >= 100.0 {
216 format!("{celsius:.0}°C")
217 } else {
218 format!("{celsius:.1}°C")
219 }
220}
221
222#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
228pub enum TruncateStrategy {
229 #[default]
231 End,
232 Start,
234 Middle,
236 Path,
238 Command,
240}
241
242#[must_use]
252pub fn truncate(s: &str, width: usize, strategy: TruncateStrategy) -> Cow<'_, str> {
253 let char_count = s.chars().count();
254
255 if char_count <= width {
256 return Cow::Borrowed(s);
257 }
258
259 if width <= 3 {
260 return Cow::Owned("…".repeat(width.min(1)));
261 }
262
263 match strategy {
264 TruncateStrategy::End => {
265 let take = width - 1; let truncated: String = s.chars().take(take).collect();
267 Cow::Owned(format!("{truncated}…"))
268 }
269 TruncateStrategy::Start => {
270 let skip = char_count - width + 1;
271 let truncated: String = s.chars().skip(skip).collect();
272 Cow::Owned(format!("…{truncated}"))
273 }
274 TruncateStrategy::Middle => {
275 let half = (width - 1) / 2;
276 let start: String = s.chars().take(half).collect();
277 let end: String = s.chars().skip(char_count - half).collect();
278 Cow::Owned(format!("{start}…{end}"))
279 }
280 TruncateStrategy::Path => truncate_path(s, width),
281 TruncateStrategy::Command => truncate_command(s, width),
282 }
283}
284
285fn truncate_path(path: &str, width: usize) -> Cow<'_, str> {
287 if path.chars().count() <= width {
288 return Cow::Borrowed(path);
289 }
290
291 let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
292
293 if parts.is_empty() {
294 return Cow::Borrowed(path);
295 }
296
297 if parts.len() == 1 {
298 return truncate(path, width, TruncateStrategy::End);
299 }
300
301 let first = parts.first().unwrap_or(&"");
303 let last = parts.last().unwrap_or(&"");
304
305 let prefix = if path.starts_with('/') { "/" } else { "" };
306 let result = format!("{prefix}{first}/…/{last}");
307
308 if result.chars().count() > width {
310 let truncated: String = result.chars().take(width - 1).collect();
311 Cow::Owned(format!("{truncated}…"))
312 } else {
313 Cow::Owned(result)
314 }
315}
316
317#[inline]
321fn extract_key_args<'a>(args: &[&'a str]) -> Vec<&'a str> {
322 let mut key_args: Vec<&str> = Vec::new();
323 let mut i = 0;
324 while i < args.len() {
325 let arg = args[i];
326 let is_key = arg.contains('=')
327 || arg.chars().any(|c| c.is_ascii_digit())
328 || (arg.starts_with('-')
329 && i + 1 < args.len()
330 && args[i + 1].chars().any(|c| c.is_ascii_digit()));
331
332 if is_key {
333 if arg.starts_with('-') && !arg.contains('=') && i + 1 < args.len() {
334 key_args.push(arg);
335 key_args.push(args[i + 1]);
336 i += 2;
337 } else {
338 key_args.push(arg);
339 i += 1;
340 }
341 } else {
342 i += 1;
343 }
344 }
345 key_args
346}
347
348#[inline]
350fn build_suffix_from_key_args(key_args: &[&str], max_width: usize) -> String {
351 let mut suffix = String::new();
352 for &arg in key_args.iter().rev() {
353 let arg_len = arg.chars().count();
354 let new_len = if suffix.is_empty() {
355 arg_len
356 } else {
357 suffix.chars().count() + 1 + arg_len
358 };
359 if new_len <= max_width {
360 if suffix.is_empty() {
361 suffix = arg.to_string();
362 } else {
363 suffix = format!("{arg} {suffix}");
364 }
365 } else {
366 break;
367 }
368 }
369 suffix
370}
371
372#[inline]
374fn simple_truncate(s: &str, width: usize) -> String {
375 let truncated: String = s.chars().take(width - 1).collect();
376 format!("{truncated}…")
377}
378
379fn build_command_with_args(
381 basename: &str,
382 first_arg: &str,
383 key_args: &[&str],
384 width: usize,
385) -> String {
386 let ellipsis = " … ";
387 let base_len = basename.chars().count();
388
389 let mut result = basename.to_string();
390 let mut current_len = base_len;
391
392 if !first_arg.is_empty() && current_len + 1 + first_arg.chars().count() + 4 < width {
394 result.push(' ');
395 result.push_str(first_arg);
396 current_len = result.chars().count();
397 }
398
399 let space_for_keys = width.saturating_sub(current_len + ellipsis.chars().count());
401 if !key_args.is_empty() && space_for_keys > 5 {
402 let suffix = build_suffix_from_key_args(key_args, space_for_keys);
403 if !suffix.is_empty() {
404 result.push_str(ellipsis);
405 result.push_str(&suffix);
406 }
407 }
408
409 if result.chars().count() > width {
411 simple_truncate(&result, width)
412 } else {
413 result
414 }
415}
416
417fn truncate_command(cmd: &str, width: usize) -> Cow<'_, str> {
430 if cmd.chars().count() <= width {
431 return Cow::Borrowed(cmd);
432 }
433
434 if width <= 3 {
435 return Cow::Owned("…".repeat(width.min(1)));
436 }
437
438 if width < 12 {
439 return Cow::Owned(simple_truncate(cmd, width));
440 }
441
442 let parts: Vec<&str> = cmd.split_whitespace().collect();
444 if parts.is_empty() {
445 return Cow::Borrowed(cmd);
446 }
447
448 let basename = parts[0].rsplit('/').next().unwrap_or(parts[0]);
450
451 if parts.len() == 1 {
453 return if basename.len() <= width {
454 Cow::Owned(basename.to_string())
455 } else {
456 Cow::Owned(simple_truncate(basename, width))
457 };
458 }
459
460 let args = &parts[1..];
462 let key_args = extract_key_args(args);
463 let first_arg = args.first().copied().unwrap_or("");
464 Cow::Owned(build_command_with_args(
465 basename, first_arg, &key_args, width,
466 ))
467}
468
469#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
475pub enum ColumnAlign {
476 #[default]
477 Left,
478 Right,
479 Center,
480}
481
482#[must_use]
492pub fn format_column(
493 value: &str,
494 width: usize,
495 align: ColumnAlign,
496 truncate_strategy: TruncateStrategy,
497) -> String {
498 let truncated = truncate(value, width, truncate_strategy);
499 let len = truncated.chars().count();
500
501 if len >= width {
502 truncated.chars().take(width).collect()
504 } else {
505 let padding = width - len;
506 match align {
507 ColumnAlign::Left => format!("{truncated}{}", " ".repeat(padding)),
508 ColumnAlign::Right => format!("{}{truncated}", " ".repeat(padding)),
509 ColumnAlign::Center => {
510 let left = padding / 2;
511 let right = padding - left;
512 format!("{}{truncated}{}", " ".repeat(left), " ".repeat(right))
513 }
514 }
515 }
516}
517
518#[must_use]
520pub fn format_number_column(value: f64, width: usize, decimals: usize) -> String {
521 let formatted = if decimals == 0 {
522 format!("{value:.0}")
523 } else {
524 format!("{value:.decimals$}")
525 };
526 format_column(&formatted, width, ColumnAlign::Right, TruncateStrategy::End)
527}
528
529#[must_use]
531pub fn format_percent_column(value: f32, width: usize) -> String {
532 let formatted = format_percent(value);
533 format_column(&formatted, width, ColumnAlign::Right, TruncateStrategy::End)
534}
535
536#[must_use]
538pub fn format_bytes_column(bytes: u64, width: usize) -> String {
539 let formatted = format_bytes_si(bytes);
540 format_column(&formatted, width, ColumnAlign::Right, TruncateStrategy::End)
541}
542
543#[derive(Debug, Clone)]
549pub struct SearchResult<T> {
550 pub item: T,
552 pub score: f32,
554 pub matches: Vec<usize>,
556}
557
558#[derive(Debug, Clone)]
565pub struct FuzzyIndex<T: Clone> {
566 items: Vec<T>,
568 keys: Vec<String>,
570 trigrams: std::collections::HashMap<[u8; 3], Vec<usize>>,
572 char_index: std::collections::HashMap<char, Vec<usize>>,
574}
575
576impl<T: Clone> FuzzyIndex<T> {
577 pub fn new<F>(items: Vec<T>, key_fn: F) -> Self
583 where
584 F: Fn(&T) -> String,
585 {
586 let keys: Vec<String> = items
587 .iter()
588 .map(|item| key_fn(item).to_lowercase())
589 .collect();
590
591 let mut trigrams: std::collections::HashMap<[u8; 3], Vec<usize>> =
592 std::collections::HashMap::new();
593 let mut char_index: std::collections::HashMap<char, Vec<usize>> =
594 std::collections::HashMap::new();
595
596 for (idx, key) in keys.iter().enumerate() {
597 for ch in key.chars() {
599 char_index.entry(ch).or_default().push(idx);
600 }
601
602 let bytes = key.as_bytes();
604 if bytes.len() >= 3 {
605 for window in bytes.windows(3) {
606 let trigram: [u8; 3] = [window[0], window[1], window[2]];
607 trigrams.entry(trigram).or_default().push(idx);
608 }
609 }
610 }
611
612 for indices in trigrams.values_mut() {
614 indices.sort_unstable();
615 indices.dedup();
616 }
617 for indices in char_index.values_mut() {
618 indices.sort_unstable();
619 indices.dedup();
620 }
621
622 Self {
623 items,
624 keys,
625 trigrams,
626 char_index,
627 }
628 }
629
630 #[must_use]
639 pub fn search(&self, query: &str, limit: usize) -> Vec<SearchResult<T>> {
640 if query.is_empty() {
641 return Vec::new();
642 }
643
644 let query_lower = query.to_lowercase();
645 let query_chars: Vec<char> = query_lower.chars().collect();
646
647 let candidates = self.get_candidates(&query_lower);
649
650 let mut results: Vec<SearchResult<T>> = candidates
652 .into_iter()
653 .filter_map(|idx| {
654 let key = &self.keys[idx];
655 let (score, matches) = self.score_match(key, &query_chars);
656 if score > 0.0 {
657 Some(SearchResult {
658 item: self.items[idx].clone(),
659 score,
660 matches,
661 })
662 } else {
663 None
664 }
665 })
666 .collect();
667
668 results.sort_by(|a, b| {
670 b.score
671 .partial_cmp(&a.score)
672 .unwrap_or(std::cmp::Ordering::Equal)
673 });
674 results.truncate(limit);
675
676 results
677 }
678
679 fn get_candidates(&self, query: &str) -> Vec<usize> {
681 let bytes = query.as_bytes();
682
683 if bytes.len() >= 3 {
685 let trigram: [u8; 3] = [bytes[0], bytes[1], bytes[2]];
686 if let Some(indices) = self.trigrams.get(&trigram) {
687 return indices.clone();
688 }
689 }
690
691 if let Some(first_char) = query.chars().next() {
693 if let Some(indices) = self.char_index.get(&first_char) {
694 return indices.clone();
695 }
696 }
697
698 (0..self.items.len()).collect()
700 }
701
702 fn score_match(&self, key: &str, query_chars: &[char]) -> (f32, Vec<usize>) {
704 if query_chars.is_empty() {
705 return (0.0, Vec::new());
706 }
707
708 let key_chars: Vec<char> = key.chars().collect();
709 let mut matches = Vec::new();
710 let mut query_idx = 0;
711
712 for (key_idx, &key_char) in key_chars.iter().enumerate() {
714 if query_idx < query_chars.len() && key_char == query_chars[query_idx] {
715 matches.push(key_idx);
716 query_idx += 1;
717 }
718 }
719
720 if query_idx != query_chars.len() {
722 return (0.0, Vec::new());
723 }
724
725 let mut score = 1.0;
731
732 let mut consecutive = 0;
734 for i in 1..matches.len() {
735 if matches[i] == matches[i - 1] + 1 {
736 consecutive += 1;
737 }
738 }
739 score += consecutive as f32 * 0.1;
740
741 if !matches.is_empty() {
743 score += (1.0 - matches[0] as f32 / key_chars.len() as f32) * 0.3;
744 }
745
746 if key.starts_with(&query_chars.iter().collect::<String>()) {
748 score += 0.5;
749 }
750
751 if key_chars.len() == query_chars.len() {
753 score += 1.0;
754 }
755
756 score *= query_chars.len() as f32 / key_chars.len() as f32;
758
759 (score.min(1.0), matches)
760 }
761}
762
763#[cfg(test)]
768mod tests {
769 use super::*;
770
771 #[test]
776 fn test_format_bytes_si() {
777 assert_eq!(format_bytes_si(0), "0B");
778 assert_eq!(format_bytes_si(100), "100B");
779 assert_eq!(format_bytes_si(1000), "1.00K");
780 assert_eq!(format_bytes_si(1500), "1.50K");
781 assert_eq!(format_bytes_si(1_000_000), "1.00M");
782 assert_eq!(format_bytes_si(1_500_000_000), "1.50G");
783 assert_eq!(format_bytes_si(1_000_000_000_000), "1.00T");
784 }
785
786 #[test]
787 fn test_format_bytes_si_large() {
788 assert_eq!(format_bytes_si(150_000_000_000), "150G");
790 }
791
792 #[test]
793 fn test_format_bytes_iec() {
794 assert_eq!(format_bytes_iec(0), "0B");
795 assert_eq!(format_bytes_iec(100), "100B");
796 assert_eq!(format_bytes_iec(1024), "1.00Ki");
797 assert_eq!(format_bytes_iec(1024 * 1024), "1.00Mi");
798 assert_eq!(format_bytes_iec(1024 * 1024 * 1024), "1.00Gi");
799 }
800
801 #[test]
802 fn test_format_rate() {
803 assert_eq!(format_rate(1000), "1.00K/s");
804 assert_eq!(format_rate(1_000_000), "1.00M/s");
805 }
806
807 #[test]
812 fn test_format_percent() {
813 assert_eq!(format_percent(0.0), "0%");
814 assert_eq!(format_percent(5.0), "5.0%");
815 assert_eq!(format_percent(45.3), "45.3%");
816 assert_eq!(format_percent(100.0), "100%");
817 assert_eq!(format_percent(153.2), "153%");
818 }
819
820 #[test]
821 fn test_format_percent_small() {
822 assert_eq!(format_percent(0.05), "0.05%");
823 }
824
825 #[test]
826 fn test_format_percent_clamped() {
827 assert_eq!(format_percent_clamped(150.0), "100%");
828 assert_eq!(format_percent_clamped(-10.0), "0%");
829 }
830
831 #[test]
832 fn test_format_percent_fixed() {
833 let result = format_percent_fixed(50.0, 8);
834 assert_eq!(result.chars().count(), 8);
835 }
836
837 #[test]
838 fn test_format_percent_fixed_no_padding() {
839 let result = format_percent_fixed(100.0, 3);
840 assert_eq!(result, "100%");
841 }
842
843 #[test]
848 fn test_format_duration() {
849 assert_eq!(format_duration(45), "45s");
850 assert_eq!(format_duration(90), "1:30");
851 assert_eq!(format_duration(3661), "1:01:01");
852 assert_eq!(format_duration(86400), "1d");
853 assert_eq!(format_duration(90000), "1d 1h");
854 }
855
856 #[test]
857 fn test_format_duration_exact_minutes() {
858 assert_eq!(format_duration(60), "1m");
859 assert_eq!(format_duration(120), "2m");
860 }
861
862 #[test]
863 fn test_format_duration_exact_hours() {
864 assert_eq!(format_duration(3600), "1h");
865 assert_eq!(format_duration(7200), "2h");
866 }
867
868 #[test]
869 fn test_format_duration_hours_minutes() {
870 assert_eq!(format_duration(3660), "1:01");
871 }
872
873 #[test]
874 fn test_format_duration_compact() {
875 assert_eq!(format_duration_compact(30), "30s");
876 assert_eq!(format_duration_compact(120), "2m");
877 assert_eq!(format_duration_compact(7200), "2h");
878 assert_eq!(format_duration_compact(172800), "2d");
879 }
880
881 #[test]
886 fn test_format_freq_mhz() {
887 assert_eq!(format_freq_mhz(500), "500M");
888 assert_eq!(format_freq_mhz(1000), "1.00G");
889 assert_eq!(format_freq_mhz(3500), "3.50G");
890 assert_eq!(format_freq_mhz(10500), "10.5G");
891 }
892
893 #[test]
898 fn test_format_temp_c() {
899 assert_eq!(format_temp_c(45.5), "45.5°C");
900 assert_eq!(format_temp_c(105.0), "105°C");
901 }
902
903 #[test]
908 fn test_truncate_strategy_default() {
909 assert_eq!(TruncateStrategy::default(), TruncateStrategy::End);
910 }
911
912 #[test]
913 fn test_truncate_end() {
914 assert_eq!(truncate("hello", 10, TruncateStrategy::End), "hello");
915 assert_eq!(
916 truncate("hello world", 8, TruncateStrategy::End),
917 "hello w…"
918 );
919 assert_eq!(truncate("hi", 1, TruncateStrategy::End), "…");
920 }
921
922 #[test]
923 fn test_truncate_start() {
924 assert_eq!(
927 truncate("hello world", 8, TruncateStrategy::Start),
928 "…o world"
929 );
930 }
931
932 #[test]
933 fn test_truncate_middle() {
934 assert_eq!(
935 truncate("hello_world_here", 10, TruncateStrategy::Middle),
936 "hell…here"
937 );
938 }
939
940 #[test]
941 fn test_truncate_very_short() {
942 assert_eq!(truncate("hello", 2, TruncateStrategy::End), "…");
943 assert_eq!(truncate("hello", 3, TruncateStrategy::End), "…");
944 }
945
946 #[test]
947 fn test_truncate_path() {
948 assert_eq!(
949 truncate("/home/user/documents/file.txt", 20, TruncateStrategy::Path),
950 "/home/…/file.txt"
951 );
952 }
953
954 #[test]
955 fn test_truncate_path_single_part() {
956 let result = truncate("filename", 5, TruncateStrategy::Path);
957 assert!(result.chars().count() <= 5);
958 }
959
960 #[test]
961 fn test_truncate_path_empty() {
962 let result = truncate("", 10, TruncateStrategy::Path);
963 assert_eq!(result, "");
964 }
965
966 #[test]
967 fn test_truncate_path_no_slash() {
968 let result = truncate("verylongfilename.txt", 10, TruncateStrategy::Path);
969 assert!(result.chars().count() <= 10);
970 }
971
972 #[test]
973 fn test_truncate_command() {
974 let cmd = "/usr/bin/python script.py";
975 let result = truncate(cmd, 20, TruncateStrategy::Command);
976 assert!(result.starts_with("python"));
977
978 let long_cmd = "/usr/lib/firefox/firefox -contentproc -parentBuildID 20240101 -childID 5 -isForBrowser";
979 let result = truncate(long_cmd, 50, TruncateStrategy::Command);
980 assert!(result.starts_with("firefox"));
981 assert!(result.contains("…"));
982 assert!(result.contains('5'));
983
984 assert_eq!(
985 truncate("command arg1 arg2", 8, TruncateStrategy::Command),
986 "command…"
987 );
988
989 let very_long = "/usr/lib/firefox/firefox -contentproc -childID 5 -isForBrowser -prefsLen 31398 -prefMapSize 244787";
990 let result = truncate(very_long, 40, TruncateStrategy::Command);
991 assert!(
992 result.chars().count() <= 40,
993 "Result '{}' exceeds 40 chars",
994 result
995 );
996
997 let with_eq = "python script.py --port=8080";
998 let result = truncate(with_eq, 30, TruncateStrategy::Command);
999 assert!(
1000 result.contains("8080") || result == "python script.py --port=8080",
1001 "Result '{}' should contain 8080 or fit entirely",
1002 result
1003 );
1004 }
1005
1006 #[test]
1007 fn test_truncate_command_short_width() {
1008 let result = truncate("/usr/bin/python", 5, TruncateStrategy::Command);
1009 assert!(result.chars().count() <= 5);
1010 }
1011
1012 #[test]
1013 fn test_truncate_command_single_word() {
1014 let result = truncate("/usr/bin/python", 15, TruncateStrategy::Command);
1016 assert_eq!(result, "/usr/bin/python");
1017
1018 let result = truncate("/usr/bin/python", 10, TruncateStrategy::Command);
1020 assert!(result.chars().count() <= 10);
1021 }
1022
1023 #[test]
1024 fn test_truncate_command_basename_only_too_long() {
1025 let result = truncate(
1026 "/usr/bin/verylongexecutablename",
1027 10,
1028 TruncateStrategy::Command,
1029 );
1030 assert!(result.chars().count() <= 10);
1031 }
1032
1033 #[test]
1038 fn test_column_align_default() {
1039 assert_eq!(ColumnAlign::default(), ColumnAlign::Left);
1040 }
1041
1042 #[test]
1043 fn test_format_column_never_bleeds() {
1044 let result = format_column(
1045 "very_long_text_that_should_be_truncated",
1046 10,
1047 ColumnAlign::Left,
1048 TruncateStrategy::End,
1049 );
1050 assert_eq!(result.chars().count(), 10);
1051
1052 let result = format_column("short", 10, ColumnAlign::Right, TruncateStrategy::End);
1053 assert_eq!(result.chars().count(), 10);
1054 assert!(result.starts_with(" "));
1055 }
1056
1057 #[test]
1058 fn test_format_column_center() {
1059 let result = format_column("hi", 10, ColumnAlign::Center, TruncateStrategy::End);
1060 assert_eq!(result.chars().count(), 10);
1061 }
1063
1064 #[test]
1065 fn test_format_number_column() {
1066 let result = format_number_column(1.23456, 8, 2);
1067 assert_eq!(result.chars().count(), 8);
1068 }
1069
1070 #[test]
1071 fn test_format_number_column_no_decimals() {
1072 let result = format_number_column(42.0, 6, 0);
1073 assert_eq!(result.chars().count(), 6);
1074 }
1075
1076 #[test]
1077 fn test_format_percent_column() {
1078 let result = format_percent_column(50.0, 8);
1079 assert_eq!(result.chars().count(), 8);
1080 }
1081
1082 #[test]
1083 fn test_format_bytes_column() {
1084 let result = format_bytes_column(1_000_000, 8);
1085 assert_eq!(result.chars().count(), 8);
1086 }
1087
1088 #[test]
1093 fn test_fuzzy_search() {
1094 let items = vec![
1095 "firefox".to_string(),
1096 "thunderbird".to_string(),
1097 "chrome".to_string(),
1098 "chromium".to_string(),
1099 "firefox-developer".to_string(),
1100 ];
1101
1102 let index = FuzzyIndex::new(items, |s| s.clone());
1103
1104 let results = index.search("fire", 5);
1105 assert!(!results.is_empty());
1106 assert!(results[0].item.contains("fire"));
1107
1108 let results = index.search("chro", 5);
1109 assert!(results.len() >= 2);
1110 }
1111
1112 #[test]
1113 fn test_fuzzy_search_empty_query() {
1114 let items = vec!["test".to_string()];
1115 let index = FuzzyIndex::new(items, |s| s.clone());
1116 let results = index.search("", 5);
1117 assert!(results.is_empty());
1118 }
1119
1120 #[test]
1121 fn test_fuzzy_search_no_match() {
1122 let items = vec!["apple".to_string(), "banana".to_string()];
1123 let index = FuzzyIndex::new(items, |s| s.clone());
1124 let results = index.search("xyz", 5);
1125 assert!(results.is_empty());
1126 }
1127
1128 #[test]
1129 fn test_fuzzy_search_exact_match() {
1130 let items = vec!["test".to_string(), "testing".to_string()];
1131 let index = FuzzyIndex::new(items, |s| s.clone());
1132 let results = index.search("test", 5);
1133 assert!(!results.is_empty());
1134 assert_eq!(results[0].item, "test");
1136 }
1137
1138 #[test]
1139 fn test_fuzzy_search_single_char() {
1140 let items = vec![
1141 "apple".to_string(),
1142 "banana".to_string(),
1143 "apricot".to_string(),
1144 ];
1145 let index = FuzzyIndex::new(items, |s| s.clone());
1146 let results = index.search("a", 5);
1147 assert!(results.len() >= 2); }
1149}
1150
1151#[derive(Debug, Clone, PartialEq, Eq)]
1159pub enum DisplayAction {
1160 Show,
1162 Hide,
1164 ShowPlaceholder(String),
1166 Compact,
1168 Expand,
1170}
1171
1172impl Default for DisplayAction {
1173 fn default() -> Self {
1174 Self::Show
1175 }
1176}
1177
1178#[derive(Debug, Clone, Default)]
1180pub struct SystemCapabilities {
1181 pub has_nvidia: bool,
1183 pub has_amd: bool,
1185 pub has_apple_silicon: bool,
1187 pub has_psi: bool,
1189 pub has_sensors: bool,
1191 pub has_battery: bool,
1193 pub in_container: bool,
1195}
1196
1197impl SystemCapabilities {
1198 #[cfg(target_os = "linux")]
1200 pub fn detect() -> Self {
1201 use std::path::Path;
1202
1203 Self {
1204 has_nvidia: Path::new("/dev/nvidia0").exists()
1205 || Path::new("/proc/driver/nvidia").exists(),
1206 has_amd: Path::new("/sys/class/drm/card0/device/vendor").exists(),
1207 has_apple_silicon: false,
1208 has_psi: Path::new("/proc/pressure/cpu").exists(),
1209 has_sensors: Path::new("/sys/class/hwmon/hwmon0").exists(),
1210 has_battery: Path::new("/sys/class/power_supply/BAT0").exists()
1211 || Path::new("/sys/class/power_supply/BAT1").exists(),
1212 in_container: Path::new("/.dockerenv").exists() || std::env::var("container").is_ok(),
1213 }
1214 }
1215
1216 #[cfg(not(target_os = "linux"))]
1217 pub fn detect() -> Self {
1218 Self::default()
1219 }
1220}
1221
1222#[derive(Debug, Clone, Copy, Default)]
1224pub struct TerminalSize {
1225 pub width: u16,
1226 pub height: u16,
1227}
1228
1229#[derive(Debug, Clone, Default)]
1233pub struct DataAvailability {
1234 pub psi_available: bool,
1236 pub sensors_available: bool,
1238 pub sensor_count: usize,
1240 pub gpu_available: bool,
1242 pub battery_available: bool,
1244 pub treemap_ready: bool,
1246 pub connections_available: bool,
1248 pub connection_count: usize,
1250}
1251
1252#[derive(Debug)]
1256pub struct DisplayContext<'a> {
1257 pub system: &'a SystemCapabilities,
1259 pub terminal: TerminalSize,
1261 pub data: DataAvailability,
1263}
1264
1265pub trait DisplayRules {
1270 fn evaluate(&self, ctx: &DisplayContext<'_>) -> DisplayAction;
1275
1276 fn panel_id(&self) -> &'static str;
1278}
1279
1280#[derive(Debug)]
1284pub struct DefaultDisplayRules {
1285 panel_id: &'static str,
1286}
1287
1288impl DefaultDisplayRules {
1289 pub fn new(panel_id: &'static str) -> Self {
1290 Self { panel_id }
1291 }
1292}
1293
1294impl DisplayRules for DefaultDisplayRules {
1295 fn evaluate(&self, _ctx: &DisplayContext<'_>) -> DisplayAction {
1296 DisplayAction::Show
1297 }
1298
1299 fn panel_id(&self) -> &'static str {
1300 self.panel_id
1301 }
1302}
1303
1304#[derive(Debug)]
1308pub struct PsiDisplayRules;
1309
1310impl DisplayRules for PsiDisplayRules {
1311 fn evaluate(&self, ctx: &DisplayContext<'_>) -> DisplayAction {
1312 if !ctx.system.has_psi || !ctx.data.psi_available {
1313 DisplayAction::Hide
1314 } else {
1315 DisplayAction::Show
1316 }
1317 }
1318
1319 fn panel_id(&self) -> &'static str {
1320 "psi"
1321 }
1322}
1323
1324#[derive(Debug)]
1328pub struct SensorsDisplayRules;
1329
1330impl DisplayRules for SensorsDisplayRules {
1331 fn evaluate(&self, ctx: &DisplayContext<'_>) -> DisplayAction {
1332 if !ctx.system.has_sensors || !ctx.data.sensors_available {
1333 DisplayAction::Hide
1334 } else if ctx.data.sensor_count < 3 {
1335 DisplayAction::Compact
1336 } else {
1337 DisplayAction::Show
1338 }
1339 }
1340
1341 fn panel_id(&self) -> &'static str {
1342 "sensors"
1343 }
1344}
1345
1346#[derive(Debug)]
1350pub struct GpuDisplayRules;
1351
1352impl DisplayRules for GpuDisplayRules {
1353 fn evaluate(&self, ctx: &DisplayContext<'_>) -> DisplayAction {
1354 if ctx.data.gpu_available {
1355 DisplayAction::Show
1356 } else {
1357 DisplayAction::Hide
1358 }
1359 }
1360
1361 fn panel_id(&self) -> &'static str {
1362 "gpu"
1363 }
1364}
1365
1366#[derive(Debug)]
1370pub struct BatteryDisplayRules;
1371
1372impl DisplayRules for BatteryDisplayRules {
1373 fn evaluate(&self, ctx: &DisplayContext<'_>) -> DisplayAction {
1374 if !ctx.system.has_battery || !ctx.data.battery_available {
1375 DisplayAction::Hide
1376 } else {
1377 DisplayAction::Show
1378 }
1379 }
1380
1381 fn panel_id(&self) -> &'static str {
1382 "battery"
1383 }
1384}
1385
1386#[derive(Debug)]
1390pub struct FilesDisplayRules;
1391
1392impl DisplayRules for FilesDisplayRules {
1393 fn evaluate(&self, ctx: &DisplayContext<'_>) -> DisplayAction {
1394 if ctx.data.treemap_ready {
1395 DisplayAction::Show
1396 } else {
1397 DisplayAction::ShowPlaceholder("Scanning filesystem...".to_string())
1398 }
1399 }
1400
1401 fn panel_id(&self) -> &'static str {
1402 "files"
1403 }
1404}
1405
1406#[cfg(test)]
1407mod display_rules_tests {
1408 use super::*;
1409
1410 #[test]
1411 fn test_psi_hides_when_unavailable() {
1412 let rules = PsiDisplayRules;
1413 let system = SystemCapabilities {
1414 has_psi: false,
1415 ..Default::default()
1416 };
1417 let ctx = DisplayContext {
1418 system: &system,
1419 terminal: TerminalSize::default(),
1420 data: DataAvailability::default(),
1421 };
1422
1423 assert_eq!(rules.evaluate(&ctx), DisplayAction::Hide);
1424 }
1425
1426 #[test]
1427 fn test_psi_shows_when_available() {
1428 let rules = PsiDisplayRules;
1429 let system = SystemCapabilities {
1430 has_psi: true,
1431 ..Default::default()
1432 };
1433 let ctx = DisplayContext {
1434 system: &system,
1435 terminal: TerminalSize::default(),
1436 data: DataAvailability {
1437 psi_available: true,
1438 ..Default::default()
1439 },
1440 };
1441
1442 assert_eq!(rules.evaluate(&ctx), DisplayAction::Show);
1443 }
1444
1445 #[test]
1446 fn test_sensors_compact_with_few() {
1447 let rules = SensorsDisplayRules;
1448 let system = SystemCapabilities {
1449 has_sensors: true,
1450 ..Default::default()
1451 };
1452 let ctx = DisplayContext {
1453 system: &system,
1454 terminal: TerminalSize::default(),
1455 data: DataAvailability {
1456 sensors_available: true,
1457 sensor_count: 2,
1458 ..Default::default()
1459 },
1460 };
1461
1462 assert_eq!(rules.evaluate(&ctx), DisplayAction::Compact);
1463 }
1464
1465 #[test]
1466 fn test_battery_hides_on_desktop() {
1467 let rules = BatteryDisplayRules;
1468 let system = SystemCapabilities {
1469 has_battery: false,
1470 ..Default::default()
1471 };
1472 let ctx = DisplayContext {
1473 system: &system,
1474 terminal: TerminalSize::default(),
1475 data: DataAvailability::default(),
1476 };
1477
1478 assert_eq!(rules.evaluate(&ctx), DisplayAction::Hide);
1479 }
1480
1481 #[test]
1482 fn test_files_placeholder_while_scanning() {
1483 let rules = FilesDisplayRules;
1484 let system = SystemCapabilities::default();
1485 let ctx = DisplayContext {
1486 system: &system,
1487 terminal: TerminalSize::default(),
1488 data: DataAvailability {
1489 treemap_ready: false,
1490 ..Default::default()
1491 },
1492 };
1493
1494 match rules.evaluate(&ctx) {
1495 DisplayAction::ShowPlaceholder(msg) => {
1496 assert!(msg.contains("Scanning"));
1497 }
1498 _ => panic!("Expected ShowPlaceholder"),
1499 }
1500 }
1501
1502 #[test]
1505 fn test_format_bytes_si_zero() {
1506 assert_eq!(format_bytes_si(0), "0B");
1507 }
1508
1509 #[test]
1510 fn test_format_bytes_si_small() {
1511 assert_eq!(format_bytes_si(500), "500B");
1512 assert_eq!(format_bytes_si(999), "999B");
1513 }
1514
1515 #[test]
1516 fn test_format_bytes_si_kilobytes() {
1517 assert_eq!(format_bytes_si(1000), "1.00K");
1518 assert_eq!(format_bytes_si(1500), "1.50K");
1519 assert_eq!(format_bytes_si(15000), "15.0K");
1520 }
1521
1522 #[test]
1523 fn test_format_bytes_si_megabytes() {
1524 assert_eq!(format_bytes_si(1_000_000), "1.00M");
1525 assert_eq!(format_bytes_si(1_500_000), "1.50M");
1526 assert_eq!(format_bytes_si(100_000_000), "100M");
1527 }
1528
1529 #[test]
1530 fn test_format_bytes_si_gigabytes() {
1531 assert_eq!(format_bytes_si(1_000_000_000), "1.00G");
1532 assert_eq!(format_bytes_si(10_000_000_000), "10.0G");
1533 }
1534
1535 #[test]
1536 fn test_format_bytes_si_terabytes() {
1537 assert_eq!(format_bytes_si(1_000_000_000_000), "1.00T");
1538 }
1539
1540 #[test]
1541 fn test_format_bytes_iec_zero() {
1542 assert_eq!(format_bytes_iec(0), "0B");
1543 }
1544
1545 #[test]
1546 fn test_format_bytes_iec_small() {
1547 assert_eq!(format_bytes_iec(500), "500B");
1548 assert_eq!(format_bytes_iec(1023), "1023B");
1549 }
1550
1551 #[test]
1552 fn test_format_bytes_iec_kibibytes() {
1553 assert_eq!(format_bytes_iec(1024), "1.00Ki");
1554 assert_eq!(format_bytes_iec(1536), "1.50Ki");
1555 }
1556
1557 #[test]
1558 fn test_format_bytes_iec_mebibytes() {
1559 assert_eq!(format_bytes_iec(1024 * 1024), "1.00Mi");
1560 assert_eq!(format_bytes_iec(1024 * 1024 * 100), "100Mi");
1561 }
1562
1563 #[test]
1564 fn test_format_bytes_iec_gibibytes() {
1565 assert_eq!(format_bytes_iec(1024 * 1024 * 1024), "1.00Gi");
1566 }
1567
1568 #[test]
1569 fn test_format_rate() {
1570 assert_eq!(format_rate(0), "0B/s");
1571 assert_eq!(format_rate(1500), "1.50K/s");
1572 }
1573
1574 #[test]
1577 fn test_format_percent_small() {
1578 let result = format_percent(5.25);
1579 assert!(result.contains("5."));
1580 }
1581
1582 #[test]
1583 fn test_format_percent_medium() {
1584 let result = format_percent(45.3);
1585 assert!(result.contains("45"));
1586 }
1587
1588 #[test]
1589 fn test_format_percent_full() {
1590 let result = format_percent(100.0);
1591 assert!(result.contains("100"));
1592 }
1593
1594 #[test]
1595 fn test_format_percent_over() {
1596 let result = format_percent(153.5);
1597 assert!(result.contains("153") || result.contains("154"));
1598 }
1599
1600 #[test]
1603 fn test_truncate_middle_short() {
1604 let result = truncate("hello", 10, TruncateStrategy::Middle);
1605 assert_eq!(result, "hello");
1606 }
1607
1608 #[test]
1609 fn test_truncate_middle_exact() {
1610 let result = truncate("hello", 5, TruncateStrategy::Middle);
1611 assert_eq!(result, "hello");
1612 }
1613
1614 #[test]
1615 fn test_truncate_middle_needs_truncation() {
1616 let result = truncate("hello world", 8, TruncateStrategy::Middle);
1617 assert!(result.chars().count() <= 8);
1618 assert!(result.contains("…"));
1619 }
1620
1621 #[test]
1622 fn test_truncate_end_short() {
1623 let result = truncate("hello", 10, TruncateStrategy::End);
1624 assert_eq!(result, "hello");
1625 }
1626
1627 #[test]
1628 fn test_truncate_end_needs_truncation() {
1629 let result = truncate("hello world", 8, TruncateStrategy::End);
1630 assert!(result.chars().count() <= 8);
1631 }
1632
1633 #[test]
1634 fn test_truncate_start_short() {
1635 let result = truncate("hello", 10, TruncateStrategy::Start);
1636 assert_eq!(result, "hello");
1637 }
1638
1639 #[test]
1640 fn test_truncate_start_needs_truncation() {
1641 let result = truncate("/very/long/path/to/file.txt", 15, TruncateStrategy::Start);
1642 assert!(result.chars().count() <= 15);
1643 }
1644
1645 #[test]
1646 fn test_truncate_path_strategy() {
1647 let result = truncate(
1648 "/home/user/very/long/path/file.txt",
1649 20,
1650 TruncateStrategy::Path,
1651 );
1652 assert!(result.chars().count() <= 20);
1653 }
1654
1655 #[test]
1656 fn test_truncate_command_strategy() {
1657 let result = truncate(
1658 "command --with-very-long-argument",
1659 15,
1660 TruncateStrategy::Command,
1661 );
1662 assert!(result.chars().count() <= 15);
1663 }
1664
1665 #[test]
1666 fn test_truncate_very_small_width() {
1667 let result = truncate("hello world", 2, TruncateStrategy::End);
1668 assert!(result.chars().count() <= 2);
1669 }
1670
1671 #[test]
1672 fn test_truncate_strategy_default() {
1673 let strategy = TruncateStrategy::default();
1674 assert_eq!(strategy, TruncateStrategy::End);
1675 }
1676
1677 #[test]
1680 fn test_display_action_eq() {
1681 assert_eq!(DisplayAction::Show, DisplayAction::Show);
1682 assert_eq!(DisplayAction::Hide, DisplayAction::Hide);
1683 assert_eq!(DisplayAction::Compact, DisplayAction::Compact);
1684 assert_ne!(DisplayAction::Show, DisplayAction::Hide);
1685 }
1686
1687 #[test]
1688 fn test_display_action_placeholder_eq() {
1689 assert_eq!(
1690 DisplayAction::ShowPlaceholder("Loading".to_string()),
1691 DisplayAction::ShowPlaceholder("Loading".to_string())
1692 );
1693 assert_ne!(
1694 DisplayAction::ShowPlaceholder("Loading".to_string()),
1695 DisplayAction::ShowPlaceholder("Other".to_string())
1696 );
1697 }
1698
1699 #[test]
1700 fn test_display_action_debug() {
1701 let action = DisplayAction::Show;
1702 let debug = format!("{:?}", action);
1703 assert!(debug.contains("Show"));
1704 }
1705
1706 #[test]
1707 fn test_display_action_clone() {
1708 let action = DisplayAction::ShowPlaceholder("test".to_string());
1709 let cloned = action.clone();
1710 assert_eq!(action, cloned);
1711 }
1712
1713 #[test]
1716 fn test_system_capabilities_default() {
1717 let caps = SystemCapabilities::default();
1718 assert!(!caps.has_battery);
1719 assert!(!caps.has_nvidia);
1720 assert!(!caps.has_psi);
1721 }
1722
1723 #[test]
1724 fn test_system_capabilities_clone() {
1725 let caps = SystemCapabilities {
1726 has_battery: true,
1727 has_nvidia: true,
1728 has_sensors: true,
1729 has_psi: true,
1730 ..Default::default()
1731 };
1732 let cloned = caps;
1733 assert!(cloned.has_battery);
1734 assert!(cloned.has_nvidia);
1735 }
1736
1737 #[test]
1740 fn test_terminal_size_default() {
1741 let size = TerminalSize::default();
1742 assert_eq!(size.width, 0);
1743 assert_eq!(size.height, 0);
1744 }
1745
1746 #[test]
1747 fn test_terminal_size_clone() {
1748 let size = TerminalSize {
1749 width: 120,
1750 height: 40,
1751 };
1752 let cloned = size;
1753 assert_eq!(cloned.width, 120);
1754 assert_eq!(cloned.height, 40);
1755 }
1756
1757 #[test]
1760 fn test_data_availability_default() {
1761 let data = DataAvailability::default();
1762 assert!(!data.psi_available);
1763 assert!(!data.gpu_available);
1764 assert!(!data.battery_available);
1765 }
1766
1767 #[test]
1768 fn test_data_availability_clone() {
1769 let data = DataAvailability {
1770 psi_available: true,
1771 gpu_available: true,
1772 battery_available: true,
1773 sensors_available: true,
1774 treemap_ready: true,
1775 sensor_count: 10,
1776 ..Default::default()
1777 };
1778 let cloned = data;
1779 assert_eq!(cloned.sensor_count, 10);
1780 }
1781
1782 #[test]
1785 fn test_psi_panel_id() {
1786 let rules = PsiDisplayRules;
1787 assert_eq!(rules.panel_id(), "psi");
1788 }
1789
1790 #[test]
1791 fn test_sensors_panel_id() {
1792 let rules = SensorsDisplayRules;
1793 assert_eq!(rules.panel_id(), "sensors");
1794 }
1795
1796 #[test]
1797 fn test_gpu_panel_id() {
1798 let rules = GpuDisplayRules;
1799 assert_eq!(rules.panel_id(), "gpu");
1800 }
1801
1802 #[test]
1803 fn test_battery_panel_id() {
1804 let rules = BatteryDisplayRules;
1805 assert_eq!(rules.panel_id(), "battery");
1806 }
1807
1808 #[test]
1809 fn test_files_panel_id() {
1810 let rules = FilesDisplayRules;
1811 assert_eq!(rules.panel_id(), "files");
1812 }
1813
1814 #[test]
1815 fn test_gpu_shows_when_available() {
1816 let rules = GpuDisplayRules;
1817 let system = SystemCapabilities::default();
1818 let ctx = DisplayContext {
1819 system: &system,
1820 terminal: TerminalSize::default(),
1821 data: DataAvailability {
1822 gpu_available: true,
1823 ..Default::default()
1824 },
1825 };
1826 assert_eq!(rules.evaluate(&ctx), DisplayAction::Show);
1827 }
1828
1829 #[test]
1830 fn test_gpu_hides_when_unavailable() {
1831 let rules = GpuDisplayRules;
1832 let system = SystemCapabilities::default();
1833 let ctx = DisplayContext {
1834 system: &system,
1835 terminal: TerminalSize::default(),
1836 data: DataAvailability {
1837 gpu_available: false,
1838 ..Default::default()
1839 },
1840 };
1841 assert_eq!(rules.evaluate(&ctx), DisplayAction::Hide);
1842 }
1843
1844 #[test]
1845 fn test_sensors_hides_when_no_sensors() {
1846 let rules = SensorsDisplayRules;
1847 let system = SystemCapabilities {
1848 has_sensors: false,
1849 ..Default::default()
1850 };
1851 let ctx = DisplayContext {
1852 system: &system,
1853 terminal: TerminalSize::default(),
1854 data: DataAvailability::default(),
1855 };
1856 assert_eq!(rules.evaluate(&ctx), DisplayAction::Hide);
1857 }
1858
1859 #[test]
1860 fn test_sensors_shows_with_many() {
1861 let rules = SensorsDisplayRules;
1862 let system = SystemCapabilities {
1863 has_sensors: true,
1864 ..Default::default()
1865 };
1866 let ctx = DisplayContext {
1867 system: &system,
1868 terminal: TerminalSize::default(),
1869 data: DataAvailability {
1870 sensors_available: true,
1871 sensor_count: 10,
1872 ..Default::default()
1873 },
1874 };
1875 assert_eq!(rules.evaluate(&ctx), DisplayAction::Show);
1876 }
1877
1878 #[test]
1879 fn test_battery_shows_when_available() {
1880 let rules = BatteryDisplayRules;
1881 let system = SystemCapabilities {
1882 has_battery: true,
1883 ..Default::default()
1884 };
1885 let ctx = DisplayContext {
1886 system: &system,
1887 terminal: TerminalSize::default(),
1888 data: DataAvailability {
1889 battery_available: true,
1890 ..Default::default()
1891 },
1892 };
1893 assert_eq!(rules.evaluate(&ctx), DisplayAction::Show);
1894 }
1895
1896 #[test]
1897 fn test_files_shows_when_ready() {
1898 let rules = FilesDisplayRules;
1899 let system = SystemCapabilities::default();
1900 let ctx = DisplayContext {
1901 system: &system,
1902 terminal: TerminalSize::default(),
1903 data: DataAvailability {
1904 treemap_ready: true,
1905 ..Default::default()
1906 },
1907 };
1908 assert_eq!(rules.evaluate(&ctx), DisplayAction::Show);
1909 }
1910
1911 #[test]
1914 fn test_format_duration_seconds() {
1915 assert_eq!(format_duration(30), "30s");
1916 assert_eq!(format_duration(59), "59s");
1917 }
1918
1919 #[test]
1920 fn test_format_duration_minutes() {
1921 assert_eq!(format_duration(60), "1m");
1922 assert_eq!(format_duration(90), "1:30");
1923 assert_eq!(format_duration(3599), "59:59");
1924 }
1925
1926 #[test]
1927 fn test_format_duration_hours() {
1928 assert_eq!(format_duration(3600), "1h");
1929 assert_eq!(format_duration(7200), "2h");
1930 assert_eq!(format_duration(5400), "1:30");
1931 }
1932
1933 #[test]
1934 fn test_format_duration_days() {
1935 assert_eq!(format_duration(86400), "1d");
1936 assert_eq!(format_duration(90000), "1d 1h");
1937 }
1938
1939 #[test]
1940 fn test_format_duration_compact() {
1941 assert_eq!(format_duration_compact(30), "30s");
1942 assert_eq!(format_duration_compact(90), "1m");
1943 assert_eq!(format_duration_compact(7200), "2h");
1944 assert_eq!(format_duration_compact(90000), "1d");
1945 }
1946
1947 #[test]
1948 fn test_format_freq_mhz() {
1949 assert_eq!(format_freq_mhz(800), "800M");
1950 assert_eq!(format_freq_mhz(1000), "1.00G");
1951 assert_eq!(format_freq_mhz(3600), "3.60G");
1952 assert_eq!(format_freq_mhz(10000), "10.0G");
1953 }
1954
1955 #[test]
1956 fn test_format_temp_c() {
1957 assert_eq!(format_temp_c(45.5), "45.5°C");
1958 assert_eq!(format_temp_c(100.0), "100°C");
1959 }
1960
1961 #[test]
1962 fn test_format_percent_clamped() {
1963 assert!(format_percent_clamped(150.0).contains("100"));
1964 assert!(format_percent_clamped(-10.0).contains('0'));
1965 }
1966
1967 #[test]
1968 fn test_format_percent_fixed() {
1969 let result = format_percent_fixed(5.0, 8);
1970 assert_eq!(result.len(), 8);
1971 }
1972
1973 #[test]
1974 fn test_truncate_strategy_debug() {
1975 let strategy = TruncateStrategy::End;
1976 let debug = format!("{:?}", strategy);
1977 assert!(debug.contains("End"));
1978 }
1979
1980 #[test]
1981 fn test_truncate_strategy_clone() {
1982 let strategy = TruncateStrategy::Middle;
1983 let cloned = strategy;
1984 assert_eq!(strategy, cloned);
1985 }
1986}