1use crate::cli::Cli;
9use crate::config::Config;
10use crate::fetch::SystemInfo;
11use crate::fields::{self, Mode};
12use crate::logo;
13use crate::theme::{colorize_nested, Theme, ACTIVE_IFACE_PREFIX};
14
15fn should_show_logo(
29 config_show_logo: Option<bool>,
30 no_logo: bool,
31 ascii_logo: bool,
32 stdout_is_tty: bool,
33) -> bool {
34 if no_logo {
35 return false; }
37 if ascii_logo {
38 return true; }
40 config_show_logo.unwrap_or(true) && stdout_is_tty }
42
43pub fn display(info: &SystemInfo, cli: &Cli, config: &Config) -> anyhow::Result<()> {
49 let _config = config;
50 let theme_name = _config.theme.as_deref().or(cli.theme.as_deref());
51 let mut theme = match theme_name {
52 Some(name) => Theme::from_name(name),
53 None => Theme::detect_system_theme(), };
55
56 if let Some(custom) = &_config.custom_theme {
58 theme = Theme::with_custom_overrides(theme, custom);
59 }
60
61 let term_size = terminal_size::terminal_size();
63 let term_width = if let Some((terminal_size::Width(w), _)) = term_size {
64 w as usize
65 } else {
66 80
67 };
68 let stdout_is_tty = std::io::IsTerminal::is_terminal(&std::io::stdout());
71
72 let show_logo = should_show_logo(
73 _config.show_logo,
74 cli.no_logo,
75 cli.ascii_logo,
76 stdout_is_tty,
77 );
78
79 let allowed_fields: Option<Vec<String>> = if cli.full {
84 Some(fields::fields_for(Mode::Full))
85 } else if cli.long {
86 Some(fields::fields_for(Mode::Long))
87 } else if cli.short {
88 Some(fields::fields_for(Mode::Short))
89 } else if let Some(fields) = &_config.fields {
90 Some(fields.iter().map(|s| s.to_lowercase()).collect())
91 } else {
92 Some(fields::fields_for(Mode::Standard))
93 };
94
95 let should_show = |label: &str| -> bool {
96 match &allowed_fields {
97 Some(fields) => {
98 let norm_label = label.to_lowercase().replace(['-', '_'], " ");
99 let norm_label_no_spaces = norm_label.replace(' ', "");
100 fields.iter().any(|f| {
101 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
102 norm_f == norm_label
103 || norm_f.replace(' ', "") == norm_label_no_spaces
104 || (norm_label == "dns server" && norm_f == "dns")
106 || (norm_label == "memory usage" && norm_f == "memory")
108 })
109 }
110 None => true,
111 }
112 };
113
114 let label_width = 10;
116 let mut info_lines = Vec::new();
117 let mut print_line = |label: &str, value: &str| {
118 if should_show(label) {
119 info_lines.push(format!(
120 "{:>width$}{} {}",
121 theme.color_label(label),
122 theme.color_separator(":"),
123 theme.color_value(value),
124 width = label_width
125 ));
126 }
127 };
128
129 print_line("OS", &info.os);
131 if let Some(kernel) = &info.kernel {
132 print_line("Kernel", kernel);
133 }
134 if let Some(host) = &info.hostname {
135 print_line("Host", host);
136 }
137 if let Some(domain) = &info.domain {
138 print_line("Domain", domain);
139 }
140 if should_show("domain-search") {
141 for entry in &info.domain_search {
142 print_line("Domain Search", entry);
143 }
144 }
145 if let Some(chassis) = &info.chassis {
146 print_line("Chassis", chassis);
147 }
148 if let Some(init) = &info.init_system {
149 print_line("Init", init);
150 }
151 if let Some(locale) = &info.locale {
152 print_line("Locale", locale);
153 }
154 print_line("Arch", &info.arch);
155 if info.users > 0 {
159 print_line("Users", &info.users.to_string());
160 }
161 if let Some(pkgs) = info.packages {
162 if pkgs > 0 {
163 print_line("Packages", &pkgs.to_string());
164 }
165 }
166 if let Some(user) = &info.current_user {
167 print_line("User", user);
168 }
169 let uptime_str = format_uptime(&info.uptime);
171 let boot_display = format!("{} since {}", uptime_str, info.boot_time);
172 print_line("Uptime", &boot_display);
173
174 print_line("CPU", &format!("{} ({})", info.cpu, info.cpu_core_info));
176 if let Some(freq) = &info.cpu_freq {
177 print_line("CPU Freq", freq);
178 }
179 if let Some(cache) = &info.cpu_cache {
180 print_line("CPU Cache", cache);
181 }
182 if let Some(usage) = &info.cpu_usage {
183 print_line("CPU Usage", usage);
184 }
185 if let Some(motherboard) = &info.motherboard {
186 print_line("Motherboard", motherboard);
187 }
188 if let Some(bios) = &info.bios {
189 print_line("BIOS", bios);
190 }
191 if let Some(bootmgr) = &info.bootmgr {
192 print_line("Bootmgr", bootmgr);
193 }
194 if should_show("GPU") {
195 for gpu in &info.gpu {
196 print_line("GPU", gpu);
197 }
198 }
199 if should_show("Display") {
200 for display in &info.displays {
201 print_line("Display", display);
202 }
203 }
204 if let Some(brightness) = &info.brightness {
205 print_line("Brightness", brightness);
206 }
207 if let Some(audio) = &info.audio {
208 print_line("Audio", audio);
209 }
210 if should_show("Camera") {
211 for cam in &info.camera {
212 print_line("Camera", cam);
213 }
214 }
215 if should_show("Gamepad") {
216 for gp in &info.gamepad {
217 print_line("Gamepad", gp);
218 }
219 }
220 if let Some(wifi) = &info.wifi {
221 print_line("Wi-Fi", wifi);
222 }
223 if let Some(bt) = &info.bluetooth {
224 print_line("Bluetooth", bt);
225 }
226 if let Some(bat) = &info.battery {
227 print_line("Battery", bat);
228 }
229 if let Some(power) = &info.power_adapter {
230 print_line("Power Adapter", power);
231 }
232 print_line("Memory Usage", &info.memory);
233 if let Some(phys_mem) = &info.physical_memory {
234 print_line("Phys Mem", phys_mem);
235 }
236 print_line("Swap", &info.swap);
237 print_line("Procs", &info.processes.to_string());
238 if let Some(load) = &info.load_avg {
239 print_line("Load", load);
240 }
241 if should_show("Disk") {
242 for disk in &info.disks {
243 print_line("Disk", disk);
244 }
245 }
246 if should_show("Phys Disk") {
247 for disk in &info.physical_disks {
248 print_line("Phys Disk", disk);
249 }
250 }
251 if should_show("Btrfs") {
252 for vol in &info.btrfs {
253 print_line("Btrfs", vol);
254 }
255 }
256 if should_show("Zpool") {
257 for pool in &info.zpool {
258 print_line("Zpool", pool);
259 }
260 }
261 if should_show("Temp") {
262 if cli.full {
263 for temp in &info.temps {
264 print_line("Temp", temp);
265 }
266 } else {
267 for temp in consolidate_temps(&info.temps) {
268 print_line("Temp", &temp);
269 }
270 }
271 }
272
273 if should_show("Net") {
275 if cli.long || cli.full {
276 for net in &info.networks {
277 if let Some(ref active) = info.active_interface {
278 if net.contains(active) {
279 print_line("Net", &colorize_nested(net, ACTIVE_IFACE_PREFIX));
283 }
284 }
285 }
286 for net in &info.networks {
287 if let Some(ref active) = info.active_interface {
288 if net.contains(active) {
289 continue;
290 }
291 }
292 print_line("Net", net);
293 }
294 } else {
295 let mut printed = false;
296 if let Some(ref active) = info.active_interface {
297 for net in &info.networks {
298 if net.contains(active) {
299 print_line("Net", net);
300 printed = true;
301 break;
302 }
303 }
304 }
305 if !printed {
306 for net in &info.networks {
307 if net.contains("[Up]") {
308 print_line("Net", net);
309 break;
310 }
311 }
312 }
313 }
314 }
315 if let Some(ip) = &info.public_ip {
316 print_line("Public IP", ip);
317 }
318 if !info.dns.is_empty() {
319 print_line("DNS Server", &info.dns.join(", "));
320 }
321
322 if let Some(shell) = &info.shell {
324 print_line("Shell", shell);
325 }
326 if let Some(editor) = &info.editor {
327 print_line("Editor", editor);
328 }
329 if let Some(term) = &info.terminal {
330 print_line("Terminal", term);
331 }
332 if let Some(ts) = &info.terminal_size {
333 print_line("Terminal Size", ts);
334 }
335 if let Some(de) = &info.desktop {
336 print_line("Desktop", de);
337 }
338 if let Some(wm) = &info.wm {
339 let duplicate = info
340 .desktop
341 .as_deref()
342 .map(|de| de.to_lowercase() == wm.to_lowercase())
343 .unwrap_or(false);
344 if !duplicate {
345 print_line("WM", wm);
346 }
347 }
348 if let Some(lm) = &info.login_manager {
349 print_line("Login Manager", lm);
350 }
351 if let Some(ui_theme) = &info.ui_theme {
352 print_line("Theme", ui_theme);
353 }
354 if let Some(icons) = &info.icons {
355 print_line("Icons", icons);
356 }
357 if let Some(cursor) = &info.cursor {
358 print_line("Cursor", cursor);
359 }
360 if let Some(font) = &info.font {
361 print_line("Font", font);
362 }
363 if let Some(term_font) = &info.terminal_font {
364 print_line("Terminal Font", term_font);
365 }
366 if let Some(weather) = &info.weather {
367 print_line("Weather", weather);
368 }
369
370 enum ActiveLogo {
372 Lines(Vec<String>),
373 Kitty(Vec<u8>, usize), Iterm2(Vec<u8>, usize),
375 Sixel(Vec<u8>, usize),
376 None,
377 }
378
379 let mut active_logo = ActiveLogo::None;
380
381 if show_logo {
382 let distro_hint = _config.logo.clone().or_else(logo::detect_distro);
383 let user_logo = if let Some(config_dir) = dirs::config_dir() {
384 let p = config_dir.join("retch").join("logo.png");
385 if p.exists() {
386 Some(p)
387 } else {
388 None
389 }
390 } else {
391 None
392 };
393
394 if cli.ascii_logo {
395 active_logo = ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
396 } else if _config.chafa.unwrap_or(false) || cli.chafa_logo {
397 let mut resolved = false;
398 if logo::chafa_available() {
399 if let Some(path) = &user_logo {
400 if let Some(lines) = logo::get_chafa_logo_lines(path) {
401 active_logo = ActiveLogo::Lines(lines);
402 resolved = true;
403 }
404 } else if let Some(distro) = &distro_hint {
405 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
406 let temp_path = std::env::temp_dir()
407 .join(format!("retch_logo_{}.png", std::process::id()));
408 if std::fs::write(&temp_path, bytes).is_ok() {
409 if let Some(lines) = logo::get_chafa_logo_lines(&temp_path) {
410 active_logo = ActiveLogo::Lines(lines);
411 resolved = true;
412 }
413 let _ = std::fs::remove_file(&temp_path);
414 }
415 }
416 }
417 }
418 if !resolved {
419 active_logo =
420 ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
421 }
422 } else {
423 let mut resolved = false;
424
425 #[cfg(feature = "graphics")]
427 if !resolved && logo::supports_kitty() {
428 if let Some(path) = &user_logo {
429 if let Ok(bytes) = std::fs::read(path) {
430 let h = graphical_logo_height_lines(&bytes);
431 active_logo = ActiveLogo::Kitty(bytes, h);
432 resolved = true;
433 }
434 } else if let Some(distro) = &distro_hint {
435 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
436 let h = graphical_logo_height_lines(bytes);
437 active_logo = ActiveLogo::Kitty(bytes.to_vec(), h);
438 resolved = true;
439 }
440 }
441 }
442
443 #[cfg(feature = "graphics")]
445 if !resolved && logo::supports_iterm2() {
446 if let Some(path) = &user_logo {
447 if let Ok(bytes) = std::fs::read(path) {
448 let h = graphical_logo_height_lines(&bytes);
449 active_logo = ActiveLogo::Iterm2(bytes, h);
450 resolved = true;
451 }
452 } else if let Some(distro) = &distro_hint {
453 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
454 let h = graphical_logo_height_lines(bytes);
455 active_logo = ActiveLogo::Iterm2(bytes.to_vec(), h);
456 resolved = true;
457 }
458 }
459 }
460
461 #[cfg(feature = "graphics")]
463 if !resolved && logo::supports_sixel() {
464 if let Some(path) = &user_logo {
465 if let Ok(bytes) = std::fs::read(path) {
466 let h = graphical_logo_height_lines(&bytes);
467 active_logo = ActiveLogo::Sixel(bytes, h);
468 resolved = true;
469 }
470 } else if let Some(distro) = &distro_hint {
471 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
472 let h = graphical_logo_height_lines(bytes);
473 active_logo = ActiveLogo::Sixel(bytes.to_vec(), h);
474 resolved = true;
475 }
476 }
477 }
478
479 if !resolved && logo::chafa_available() {
481 if let Some(path) = &user_logo {
482 if let Some(lines) = logo::get_chafa_logo_lines(path) {
483 active_logo = ActiveLogo::Lines(lines);
484 resolved = true;
485 }
486 } else if let Some(distro) = &distro_hint {
487 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
488 let temp_path = std::env::temp_dir()
490 .join(format!("retch_logo_{}.png", std::process::id()));
491 if std::fs::write(&temp_path, bytes).is_ok() {
492 if let Some(lines) = logo::get_chafa_logo_lines(&temp_path) {
493 active_logo = ActiveLogo::Lines(lines);
494 resolved = true;
495 }
496 let _ = std::fs::remove_file(&temp_path);
497 }
498 }
499 }
500 }
501
502 if !resolved {
504 active_logo =
505 ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
506 }
507 }
508 }
509
510 let visible_len = |s: &str| -> usize {
512 let mut count = 0;
513 let mut in_esc = false;
514 for c in s.chars() {
515 if c == '\x1b' {
516 in_esc = true;
517 } else if in_esc {
518 if c.is_ascii_alphabetic() {
519 in_esc = false;
520 }
521 } else {
522 count += 1;
523 }
524 }
525 count
526 };
527
528 let max_text_width = info_lines
529 .iter()
530 .map(|line| visible_len(line))
531 .max()
532 .unwrap_or(0);
533 let text_column_width = std::cmp::max(max_text_width + 4, 45);
534
535 let max_logo_width = match &active_logo {
536 ActiveLogo::Lines(logo_lines) => logo_lines
537 .iter()
538 .map(|line| visible_len(line))
539 .max()
540 .unwrap_or(0),
541 ActiveLogo::Kitty(_, _) | ActiveLogo::Iterm2(_, _) | ActiveLogo::Sixel(_, _) => 40,
542 ActiveLogo::None => 0,
543 };
544
545 let side_by_side =
546 show_logo && term_width >= 95 && term_width >= (text_column_width + max_logo_width);
547
548 println!(); if side_by_side {
551 match active_logo {
552 ActiveLogo::Lines(logo_lines) => {
553 let max_lines = std::cmp::max(info_lines.len(), logo_lines.len());
554 for i in 0..max_lines {
555 let info_line = info_lines.get(i).cloned().unwrap_or_default();
556 let logo_line = logo_lines.get(i).cloned().unwrap_or_default();
557 let vis_len = visible_len(&info_line);
558 let padding = if vis_len < text_column_width {
559 " ".repeat(text_column_width - vis_len)
560 } else {
561 String::new()
562 };
563 println!("{}{}{}", info_line, padding, logo_line);
564 }
565 }
566 ActiveLogo::Kitty(bytes, height_lines) => {
567 for line in &info_lines {
568 println!("{}", line);
569 }
570 let num_lines = info_lines.len();
571 print!("\x1b7"); if num_lines > 0 {
573 print!("\x1b[{}A", num_lines); }
575 print!("\x1b[{}C", text_column_width); logo::print_graphical_logo(&bytes);
577 print!("\x1b8"); let overflow = height_lines.saturating_sub(num_lines);
580 if overflow > 0 {
581 print!("\x1b[{}B", overflow);
582 }
583 }
584 ActiveLogo::Iterm2(bytes, height_lines) => {
585 for line in &info_lines {
586 println!("{}", line);
587 }
588 let num_lines = info_lines.len();
589 print!("\x1b7"); if num_lines > 0 {
591 print!("\x1b[{}A", num_lines); }
593 print!("\x1b[{}C", text_column_width); logo::print_iterm2_logo(&bytes);
595 print!("\x1b8"); let overflow = height_lines.saturating_sub(num_lines);
597 if overflow > 0 {
598 print!("\x1b[{}B", overflow);
599 }
600 }
601 ActiveLogo::Sixel(bytes, height_lines) => {
602 for line in &info_lines {
603 println!("{}", line);
604 }
605 let num_lines = info_lines.len();
606 print!("\x1b7"); if num_lines > 0 {
608 print!("\x1b[{}A", num_lines); }
610 print!("\x1b[{}C", text_column_width); logo::print_sixel_logo(&bytes);
612 print!("\x1b8"); let overflow = height_lines.saturating_sub(num_lines);
614 if overflow > 0 {
615 print!("\x1b[{}B", overflow);
616 }
617 }
618 ActiveLogo::None => {
619 for line in &info_lines {
620 println!("{}", line);
621 }
622 }
623 }
624 } else {
625 match active_logo {
627 ActiveLogo::Lines(logo_lines) => {
628 for line in logo_lines {
629 println!("{}", line);
630 }
631 println!();
632 }
633 ActiveLogo::Kitty(bytes, _) => {
634 logo::print_graphical_logo(&bytes);
635 println!();
636 }
637 ActiveLogo::Iterm2(bytes, _) => {
638 logo::print_iterm2_logo(&bytes);
639 println!();
640 }
641 ActiveLogo::Sixel(bytes, _) => {
642 logo::print_sixel_logo(&bytes);
643 println!();
644 }
645 ActiveLogo::None => {}
646 }
647 for line in &info_lines {
648 println!("{}", line);
649 }
650 }
651
652 Ok(())
653}
654
655fn consolidate_temps(temps: &[String]) -> Vec<String> {
661 fn categorize(label: &str) -> &'static str {
662 let l = label.to_lowercase();
663 if l.contains("cpu")
664 || l.contains("core")
665 || l.contains("k10temp")
666 || l.contains("k8temp")
667 || l.contains("coretemp")
668 || l.contains("tctl")
669 || l.contains("tdie")
670 || l.contains("tccd")
671 || l.contains("package")
672 {
673 "CPU"
674 } else if l.contains("gpu")
675 || l.contains("nouveau")
676 || l.contains("radeon")
677 || l.contains("amdgpu")
678 {
679 "GPU"
680 } else if l.contains("nvme") || l.contains("nand") {
681 "NVMe"
682 } else if l.contains("ath")
683 || l.contains("wifi")
684 || l.contains("wireless")
685 || l.contains("wlan")
686 || l.contains("iwl")
687 {
688 "WiFi"
689 } else if l.contains("bat") {
690 "Battery"
691 } else {
692 "System"
693 }
694 }
695
696 let mut max: std::collections::HashMap<&str, f32> = std::collections::HashMap::new();
697 for s in temps {
698 if let Some((label_part, val_part)) = s.rsplit_once(':') {
700 let val_str = val_part.trim().trim_end_matches("°C");
701 if let Ok(val) = val_str.parse::<f32>() {
702 let cat = categorize(label_part.trim());
703 let entry = max.entry(cat).or_insert(f32::NEG_INFINITY);
704 if val > *entry {
705 *entry = val;
706 }
707 }
708 }
709 }
710
711 const ORDER: &[&str] = &["CPU", "GPU", "NVMe", "WiFi", "Battery", "System"];
712 ORDER
713 .iter()
714 .filter_map(|cat| max.get(cat).map(|v| format!("{}: {:.0}°C", cat, v)))
715 .collect()
716}
717
718fn format_uptime(uptime: &str) -> String {
722 let seconds: u64 = uptime.trim_end_matches('s').parse().unwrap_or(0);
724
725 let years = seconds / (365 * 24 * 3600);
726 let days = (seconds % (365 * 24 * 3600)) / (24 * 3600);
727 let hours = (seconds % (24 * 3600)) / 3600;
728 let minutes = (seconds % 3600) / 60;
729 let secs = seconds % 60;
730
731 let mut parts = Vec::new();
732 if years > 0 {
733 parts.push(format!("{}y", years));
734 }
735 if days > 0 {
736 parts.push(format!("{}d", days));
737 }
738 if hours > 0 {
739 parts.push(format!("{}h", hours));
740 }
741 if minutes > 0 {
742 parts.push(format!("{}m", minutes));
743 }
744 if secs > 0 || parts.is_empty() {
745 parts.push(format!("{}s", secs));
746 }
747
748 parts.join(" ")
749}
750
751#[cfg(feature = "graphics")]
756fn graphical_logo_height_lines(bytes: &[u8]) -> usize {
757 let img_h = image::load_from_memory(bytes)
758 .map(|img| img.height() as usize)
759 .unwrap_or(384);
760 let cell_h = terminal_cell_height_px();
761 img_h.div_ceil(cell_h)
762}
763
764fn terminal_cell_height_px() -> usize {
766 #[cfg(unix)]
767 {
768 use std::mem::MaybeUninit;
769 let mut ws: libc::winsize = unsafe { MaybeUninit::zeroed().assume_init() };
770 let ret = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
771 if ret == 0 && ws.ws_row > 0 && ws.ws_ypixel > 0 {
772 return ws.ws_ypixel as usize / ws.ws_row as usize;
773 }
774 }
775 20
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781
782 #[test]
785 fn test_show_logo_auto_requires_tty() {
786 assert!(should_show_logo(None, false, false, true));
788 assert!(!should_show_logo(None, false, false, false));
789 }
790
791 #[test]
792 fn test_show_logo_ascii_forces_without_tty() {
793 assert!(should_show_logo(None, false, true, false));
795 assert!(should_show_logo(None, false, true, true));
796 }
797
798 #[test]
799 fn test_show_logo_no_logo_always_wins() {
800 assert!(!should_show_logo(None, true, true, true));
802 assert!(!should_show_logo(None, true, false, true));
803 }
804
805 #[test]
806 fn test_show_logo_config_disable() {
807 assert!(!should_show_logo(Some(false), false, false, true));
809 assert!(should_show_logo(Some(false), false, true, false));
811 }
812
813 #[test]
814 fn test_consolidate_temps_basic() {
815 let raw = vec![
816 "k10temp Tctl: 83°C".to_string(),
817 "amdgpu edge: 65°C".to_string(),
818 "nvme Composite: 62°C".to_string(),
819 "ath11k_hwmon temp1: 58°C".to_string(),
820 "acpitz temp1: 77°C".to_string(),
821 ];
822 let result = consolidate_temps(&raw);
823 assert_eq!(
824 result,
825 vec![
826 "CPU: 83°C",
827 "GPU: 65°C",
828 "NVMe: 62°C",
829 "WiFi: 58°C",
830 "System: 77°C"
831 ]
832 );
833 }
834
835 #[test]
836 fn test_consolidate_temps_highest_wins() {
837 let raw = vec![
838 "thinkpad CPU: 83°C".to_string(),
839 "k10temp Tctl: 79°C".to_string(),
840 "nvme Composite: 62°C".to_string(),
841 "nvme Sensor 1: 59°C".to_string(),
842 "nvme Sensor 2: 56°C".to_string(),
843 ];
844 let result = consolidate_temps(&raw);
845 assert!(result.contains(&"CPU: 83°C".to_string()));
846 assert!(result.contains(&"NVMe: 62°C".to_string()));
847 assert!(!result
848 .iter()
849 .any(|s| s.contains("79") || s.contains("59") || s.contains("56")));
850 }
851
852 #[test]
853 fn test_consolidate_temps_order() {
854 let raw = vec![
855 "acpitz: 60°C".to_string(),
856 "nvme: 55°C".to_string(),
857 "amdgpu edge: 65°C".to_string(),
858 "k10temp Tctl: 80°C".to_string(),
859 ];
860 let result = consolidate_temps(&raw);
861 let cpu_pos = result.iter().position(|s| s.starts_with("CPU"));
862 let gpu_pos = result.iter().position(|s| s.starts_with("GPU"));
863 let nvme_pos = result.iter().position(|s| s.starts_with("NVMe"));
864 let sys_pos = result.iter().position(|s| s.starts_with("System"));
865 assert!(cpu_pos < gpu_pos);
866 assert!(gpu_pos < nvme_pos);
867 assert!(nvme_pos < sys_pos);
868 }
869
870 #[test]
871 fn test_consolidate_temps_empty() {
872 assert!(consolidate_temps(&[]).is_empty());
873 }
874
875 #[test]
876 fn test_format_uptime() {
877 assert_eq!(format_uptime("60s"), "1m");
878 assert_eq!(format_uptime("3600s"), "1h");
879 assert_eq!(format_uptime("3661s"), "1h 1m 1s");
880 assert_eq!(format_uptime("86400s"), "1d");
881 assert_eq!(format_uptime("90061s"), "1d 1h 1m 1s");
882 assert_eq!(format_uptime("31536000s"), "1y");
883 assert_eq!(format_uptime("31626061s"), "1y 1d 1h 1m 1s");
884 assert_eq!(format_uptime("0s"), "0s");
885 }
886}