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