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
43struct LayoutPlan {
46 side_by_side: bool,
47 text_column_width: usize,
48}
49
50fn plan_layout(
66 info_widths: &[usize],
67 logo_height: usize,
68 logo_width: usize,
69 term_width: usize,
70 show_logo: bool,
71) -> LayoutPlan {
72 let beside_count = info_widths.len().min(logo_height);
73 let max_beside_width = info_widths[..beside_count]
74 .iter()
75 .copied()
76 .max()
77 .unwrap_or(0);
78 let text_column_width = std::cmp::max(max_beside_width + 4, 45);
79 let side_by_side =
80 show_logo && term_width >= 95 && term_width >= text_column_width + logo_width;
81 LayoutPlan {
82 side_by_side,
83 text_column_width,
84 }
85}
86
87fn split_wifi_line(wifi: &str) -> (&str, Option<&str>) {
96 match wifi.split_once(" - ") {
97 Some((hardware, connection)) => (hardware, Some(connection)),
98 None => (wifi, None),
99 }
100}
101
102fn render_graphical_side_by_side(
114 text_column_width: usize,
115 info_lines: &[String],
116 logo_rows: usize,
117 draw: impl FnOnce(),
118) {
119 use std::io::Write;
120 print!("\x1b[{}C\x1b7", text_column_width);
122 draw(); print!("\x1b8\r");
124 for line in info_lines {
125 println!("{}", line);
126 }
127 for _ in info_lines.len()..logo_rows {
130 println!();
131 }
132 let _ = std::io::stdout().flush();
133}
134
135pub fn display(info: &SystemInfo, cli: &Cli, config: &Config) -> anyhow::Result<()> {
141 let _config = config;
142 let theme_name = _config.theme.as_deref().or(cli.theme.as_deref());
143 let mut theme = match theme_name {
144 Some(name) => Theme::from_name(name),
145 None => Theme::detect_system_theme(), };
147
148 if let Some(custom) = &_config.custom_theme {
150 theme = Theme::with_custom_overrides(theme, custom);
151 }
152
153 let term_size = terminal_size::terminal_size();
155 let term_width = if let Some((terminal_size::Width(w), _)) = term_size {
156 w as usize
157 } else {
158 80
159 };
160 let stdout_is_tty = std::io::IsTerminal::is_terminal(&std::io::stdout());
163
164 let show_logo = should_show_logo(
165 _config.show_logo,
166 cli.no_logo,
167 cli.ascii_logo,
168 stdout_is_tty,
169 );
170
171 let allowed_fields: Option<Vec<String>> = if cli.full {
176 Some(fields::fields_for(Mode::Full))
177 } else if cli.long {
178 Some(fields::fields_for(Mode::Long))
179 } else if cli.short {
180 Some(fields::fields_for(Mode::Short))
181 } else if let Some(fields) = &_config.fields {
182 Some(fields.iter().map(|s| s.to_lowercase()).collect())
183 } else {
184 Some(fields::fields_for(Mode::Standard))
185 };
186
187 let should_show = |label: &str| -> bool {
188 match &allowed_fields {
189 Some(fields) => {
190 let norm_label = label.to_lowercase().replace(['-', '_'], " ");
191 let norm_label_no_spaces = norm_label.replace(' ', "");
192 fields.iter().any(|f| {
193 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
194 norm_f == norm_label
195 || norm_f.replace(' ', "") == norm_label_no_spaces
196 || (norm_label == "dns server" && norm_f == "dns")
198 || (norm_label == "memory usage" && norm_f == "memory")
200 || (norm_label == "wi fi link" && norm_f == "wifi")
202 })
203 }
204 None => true,
205 }
206 };
207
208 let label_width = 10;
210 let mut info_lines = Vec::new();
211 let mut print_line = |label: &str, value: &str| {
212 if should_show(label) {
213 info_lines.push(format!(
214 "{:>width$}{} {}",
215 theme.color_label(label),
216 theme.color_separator(":"),
217 theme.color_value(value),
218 width = label_width
219 ));
220 }
221 };
222
223 print_line("OS", &info.os);
225 if let Some(kernel) = &info.kernel {
226 print_line("Kernel", kernel);
227 }
228 if let Some(host) = &info.hostname {
229 print_line("Host", host);
230 }
231 if let Some(domain) = &info.domain {
232 print_line("Domain", domain);
233 }
234 if should_show("domain-search") {
235 for entry in &info.domain_search {
236 print_line("Domain Search", entry);
237 }
238 }
239 if let Some(chassis) = &info.chassis {
240 print_line("Chassis", chassis);
241 }
242 if let Some(init) = &info.init_system {
243 print_line("Init", init);
244 }
245 if let Some(locale) = &info.locale {
246 print_line("Locale", locale);
247 }
248 print_line("Arch", &info.arch);
249 if info.users > 0 {
253 print_line("Users", &info.users.to_string());
254 }
255 if let Some(pkgs) = info.packages {
256 if pkgs > 0 {
257 print_line("Packages", &pkgs.to_string());
258 }
259 }
260 if let Some(user) = &info.current_user {
261 print_line("User", user);
262 }
263 let uptime_str = format_uptime(&info.uptime);
265 let boot_display = format!("{} since {}", uptime_str, info.boot_time);
266 print_line("Uptime", &boot_display);
267
268 print_line("CPU", &format!("{} ({})", info.cpu, info.cpu_core_info));
270 if let Some(freq) = &info.cpu_freq {
271 print_line("CPU Freq", freq);
272 }
273 if let Some(cache) = &info.cpu_cache {
274 print_line("CPU Cache", cache);
275 }
276 if let Some(usage) = &info.cpu_usage {
277 print_line("CPU Usage", usage);
278 }
279 if let Some(motherboard) = &info.motherboard {
280 print_line("Motherboard", motherboard);
281 }
282 if let Some(bios) = &info.bios {
283 print_line("BIOS", bios);
284 }
285 if let Some(bootmgr) = &info.bootmgr {
286 print_line("Bootmgr", bootmgr);
287 }
288 if should_show("GPU") {
289 for gpu in &info.gpu {
290 print_line("GPU", gpu);
291 }
292 }
293 if should_show("Display") {
294 for display in &info.displays {
295 print_line("Display", display);
296 }
297 }
298 if let Some(brightness) = &info.brightness {
299 print_line("Brightness", brightness);
300 }
301 if let Some(audio) = &info.audio {
302 print_line("Audio", audio);
303 }
304 if should_show("Camera") {
305 for cam in &info.camera {
306 print_line("Camera", cam);
307 }
308 }
309 if should_show("Gamepad") {
310 for gp in &info.gamepad {
311 print_line("Gamepad", gp);
312 }
313 }
314 if let Some(wifi) = &info.wifi {
315 let (hardware, connection) = split_wifi_line(wifi);
318 print_line("Wi-Fi", hardware);
319 if let Some(conn) = connection {
320 print_line("Wi-Fi Link", conn);
321 }
322 }
323 if let Some(bt) = &info.bluetooth {
324 print_line("Bluetooth", bt);
325 }
326 if let Some(bat) = &info.battery {
327 print_line("Battery", bat);
328 }
329 if let Some(power) = &info.power_adapter {
330 print_line("Power Adapter", power);
331 }
332 print_line("Memory Usage", &info.memory);
333 if let Some(phys_mem) = &info.physical_memory {
334 print_line("Phys Mem", phys_mem);
335 }
336 print_line("Swap", &info.swap);
337 print_line("Procs", &info.processes.to_string());
338 if let Some(load) = &info.load_avg {
339 print_line("Load", load);
340 }
341 if should_show("Disk") {
342 for disk in &info.disks {
343 print_line("Disk", disk);
344 }
345 }
346 if should_show("Phys Disk") {
347 for disk in &info.physical_disks {
348 print_line("Phys Disk", disk);
349 }
350 }
351 if should_show("Btrfs") {
352 for vol in &info.btrfs {
353 print_line("Btrfs", vol);
354 }
355 }
356 if should_show("Zpool") {
357 for pool in &info.zpool {
358 print_line("Zpool", pool);
359 }
360 }
361 if should_show("Temp") {
362 if cli.full {
363 for temp in &info.temps {
364 print_line("Temp", temp);
365 }
366 } else {
367 for temp in consolidate_temps(&info.temps) {
368 print_line("Temp", &temp);
369 }
370 }
371 }
372
373 if should_show("Net") {
375 if cli.long || cli.full {
376 for net in &info.networks {
377 if let Some(ref active) = info.active_interface {
378 if net.contains(active) {
379 print_line("Net", &colorize_nested(net, ACTIVE_IFACE_PREFIX));
383 }
384 }
385 }
386 for net in &info.networks {
387 if let Some(ref active) = info.active_interface {
388 if net.contains(active) {
389 continue;
390 }
391 }
392 print_line("Net", net);
393 }
394 } else {
395 let mut printed = false;
396 if let Some(ref active) = info.active_interface {
397 for net in &info.networks {
398 if net.contains(active) {
399 print_line("Net", net);
400 printed = true;
401 break;
402 }
403 }
404 }
405 if !printed {
406 for net in &info.networks {
407 if net.contains("[Up]") {
408 print_line("Net", net);
409 break;
410 }
411 }
412 }
413 }
414 }
415 if let Some(ip) = &info.public_ip {
416 print_line("Public IP", ip);
417 }
418 if !info.dns.is_empty() {
419 print_line("DNS Server", &info.dns.join(", "));
420 }
421
422 if let Some(shell) = &info.shell {
424 print_line("Shell", shell);
425 }
426 if let Some(editor) = &info.editor {
427 print_line("Editor", editor);
428 }
429 if let Some(term) = &info.terminal {
430 print_line("Terminal", term);
431 }
432 if let Some(ts) = &info.terminal_size {
433 print_line("Terminal Size", ts);
434 }
435 if let Some(de) = &info.desktop {
436 print_line("Desktop", de);
437 }
438 if let Some(wm) = &info.wm {
439 let duplicate = info
440 .desktop
441 .as_deref()
442 .map(|de| de.to_lowercase() == wm.to_lowercase())
443 .unwrap_or(false);
444 if !duplicate {
445 print_line("WM", wm);
446 }
447 }
448 if let Some(lm) = &info.login_manager {
449 print_line("Login Manager", lm);
450 }
451 if let Some(ui_theme) = &info.ui_theme {
452 print_line("Theme", ui_theme);
453 }
454 if let Some(icons) = &info.icons {
455 print_line("Icons", icons);
456 }
457 if let Some(cursor) = &info.cursor {
458 print_line("Cursor", cursor);
459 }
460 if let Some(font) = &info.font {
461 print_line("Font", font);
462 }
463 if let Some(term_font) = &info.terminal_font {
464 print_line("Terminal Font", term_font);
465 }
466 if let Some(weather) = &info.weather {
467 print_line("Weather", weather);
468 }
469
470 enum ActiveLogo {
472 Lines(Vec<String>),
473 Kitty(Vec<u8>, usize), Iterm2(Vec<u8>, usize),
475 Sixel(Vec<u8>, usize),
476 None,
477 }
478
479 let mut active_logo = ActiveLogo::None;
480
481 if show_logo {
482 let distro_hint = _config.logo.clone().or_else(logo::detect_distro);
483 let user_logo = if let Some(config_dir) = dirs::config_dir() {
484 let p = config_dir.join("retch").join("logo.png");
485 if p.exists() {
486 Some(p)
487 } else {
488 None
489 }
490 } else {
491 None
492 };
493
494 if cli.ascii_logo {
495 active_logo = ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
496 } else if _config.chafa.unwrap_or(false) || cli.chafa_logo {
497 let mut resolved = false;
498 if logo::chafa_available() {
499 if let Some(path) = &user_logo {
500 if let Some(lines) = logo::get_chafa_logo_lines(path) {
501 active_logo = ActiveLogo::Lines(lines);
502 resolved = true;
503 }
504 } else if let Some(distro) = &distro_hint {
505 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
506 let temp_path = std::env::temp_dir()
507 .join(format!("retch_logo_{}.png", std::process::id()));
508 if std::fs::write(&temp_path, bytes).is_ok() {
509 if let Some(lines) = logo::get_chafa_logo_lines(&temp_path) {
510 active_logo = ActiveLogo::Lines(lines);
511 resolved = true;
512 }
513 let _ = std::fs::remove_file(&temp_path);
514 }
515 }
516 }
517 }
518 if !resolved {
519 active_logo =
520 ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
521 }
522 } else {
523 let mut resolved = false;
524
525 #[cfg(feature = "graphics")]
527 if !resolved && logo::supports_kitty() {
528 if let Some(path) = &user_logo {
529 if let Ok(bytes) = std::fs::read(path) {
530 let h = graphical_logo_height_lines(&bytes);
531 active_logo = ActiveLogo::Kitty(bytes, h);
532 resolved = true;
533 }
534 } else if let Some(distro) = &distro_hint {
535 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
536 let h = graphical_logo_height_lines(bytes);
537 active_logo = ActiveLogo::Kitty(bytes.to_vec(), h);
538 resolved = true;
539 }
540 }
541 }
542
543 #[cfg(feature = "graphics")]
545 if !resolved && logo::supports_iterm2() {
546 if let Some(path) = &user_logo {
547 if let Ok(bytes) = std::fs::read(path) {
548 let h = graphical_logo_height_lines(&bytes);
549 active_logo = ActiveLogo::Iterm2(bytes, h);
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 h = graphical_logo_height_lines(bytes);
555 active_logo = ActiveLogo::Iterm2(bytes.to_vec(), h);
556 resolved = true;
557 }
558 }
559 }
560
561 #[cfg(feature = "graphics")]
563 if !resolved && logo::supports_sixel() {
564 if let Some(path) = &user_logo {
565 if let Ok(bytes) = std::fs::read(path) {
566 let h = graphical_logo_height_lines(&bytes);
567 active_logo = ActiveLogo::Sixel(bytes, h);
568 resolved = true;
569 }
570 } else if let Some(distro) = &distro_hint {
571 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
572 let h = graphical_logo_height_lines(bytes);
573 active_logo = ActiveLogo::Sixel(bytes.to_vec(), h);
574 resolved = true;
575 }
576 }
577 }
578
579 if !resolved && logo::chafa_available() {
581 if let Some(path) = &user_logo {
582 if let Some(lines) = logo::get_chafa_logo_lines(path) {
583 active_logo = ActiveLogo::Lines(lines);
584 resolved = true;
585 }
586 } else if let Some(distro) = &distro_hint {
587 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
588 let temp_path = std::env::temp_dir()
590 .join(format!("retch_logo_{}.png", std::process::id()));
591 if std::fs::write(&temp_path, bytes).is_ok() {
592 if let Some(lines) = logo::get_chafa_logo_lines(&temp_path) {
593 active_logo = ActiveLogo::Lines(lines);
594 resolved = true;
595 }
596 let _ = std::fs::remove_file(&temp_path);
597 }
598 }
599 }
600 }
601
602 if !resolved {
604 active_logo =
605 ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
606 }
607 }
608 }
609
610 let visible_len = |s: &str| -> usize {
612 let mut count = 0;
613 let mut in_esc = false;
614 for c in s.chars() {
615 if c == '\x1b' {
616 in_esc = true;
617 } else if in_esc {
618 if c.is_ascii_alphabetic() {
619 in_esc = false;
620 }
621 } else {
622 count += 1;
623 }
624 }
625 count
626 };
627
628 let info_widths: Vec<usize> = info_lines.iter().map(|line| visible_len(line)).collect();
629
630 let (logo_height, max_logo_width) = match &active_logo {
634 ActiveLogo::Lines(logo_lines) => (
635 logo_lines.len(),
636 logo_lines
637 .iter()
638 .map(|line| visible_len(line))
639 .max()
640 .unwrap_or(0),
641 ),
642 ActiveLogo::Kitty(_, h) | ActiveLogo::Iterm2(_, h) | ActiveLogo::Sixel(_, h) => (*h, 40),
643 ActiveLogo::None => (0, 0),
644 };
645
646 let LayoutPlan {
649 side_by_side,
650 text_column_width,
651 } = plan_layout(
652 &info_widths,
653 logo_height,
654 max_logo_width,
655 term_width,
656 show_logo,
657 );
658
659 println!(); if side_by_side {
662 match active_logo {
663 ActiveLogo::Lines(logo_lines) => {
664 let max_lines = std::cmp::max(info_lines.len(), logo_lines.len());
665 for i in 0..max_lines {
666 let info_line = info_lines.get(i).cloned().unwrap_or_default();
667 let logo_line = logo_lines.get(i).cloned().unwrap_or_default();
668 let vis_len = visible_len(&info_line);
669 let padding = if vis_len < text_column_width {
670 " ".repeat(text_column_width - vis_len)
671 } else {
672 String::new()
673 };
674 println!("{}{}{}", info_line, padding, logo_line);
675 }
676 }
677 ActiveLogo::Kitty(bytes, logo_rows) => {
678 render_graphical_side_by_side(text_column_width, &info_lines, logo_rows, || {
679 logo::print_graphical_logo(&bytes)
680 });
681 }
682 ActiveLogo::Iterm2(bytes, logo_rows) => {
683 render_graphical_side_by_side(text_column_width, &info_lines, logo_rows, || {
684 logo::print_iterm2_logo(&bytes)
685 });
686 }
687 ActiveLogo::Sixel(bytes, logo_rows) => {
688 render_graphical_side_by_side(text_column_width, &info_lines, logo_rows, || {
689 logo::print_sixel_logo(&bytes)
690 });
691 }
692 ActiveLogo::None => {
693 for line in &info_lines {
694 println!("{}", line);
695 }
696 }
697 }
698 } else {
699 match active_logo {
701 ActiveLogo::Lines(logo_lines) => {
702 for line in logo_lines {
703 println!("{}", line);
704 }
705 println!();
706 }
707 ActiveLogo::Kitty(bytes, _) => {
708 logo::print_graphical_logo(&bytes);
709 println!();
710 }
711 ActiveLogo::Iterm2(bytes, _) => {
712 logo::print_iterm2_logo(&bytes);
713 println!();
714 }
715 ActiveLogo::Sixel(bytes, _) => {
716 logo::print_sixel_logo(&bytes);
717 println!();
718 }
719 ActiveLogo::None => {}
720 }
721 for line in &info_lines {
722 println!("{}", line);
723 }
724 }
725
726 Ok(())
727}
728
729fn consolidate_temps(temps: &[String]) -> Vec<String> {
735 fn categorize(label: &str) -> &'static str {
736 let l = label.to_lowercase();
737 if l.contains("cpu")
738 || l.contains("core")
739 || l.contains("k10temp")
740 || l.contains("k8temp")
741 || l.contains("coretemp")
742 || l.contains("tctl")
743 || l.contains("tdie")
744 || l.contains("tccd")
745 || l.contains("package")
746 {
747 "CPU"
748 } else if l.contains("gpu")
749 || l.contains("nouveau")
750 || l.contains("radeon")
751 || l.contains("amdgpu")
752 {
753 "GPU"
754 } else if l.contains("nvme") || l.contains("nand") {
755 "NVMe"
756 } else if l.contains("ath")
757 || l.contains("wifi")
758 || l.contains("wireless")
759 || l.contains("wlan")
760 || l.contains("iwl")
761 {
762 "WiFi"
763 } else if l.contains("bat") {
764 "Battery"
765 } else {
766 "System"
767 }
768 }
769
770 let mut max: std::collections::HashMap<&str, f32> = std::collections::HashMap::new();
771 for s in temps {
772 if let Some((label_part, val_part)) = s.rsplit_once(':') {
774 let val_str = val_part.trim().trim_end_matches("°C");
775 if let Ok(val) = val_str.parse::<f32>() {
776 let cat = categorize(label_part.trim());
777 let entry = max.entry(cat).or_insert(f32::NEG_INFINITY);
778 if val > *entry {
779 *entry = val;
780 }
781 }
782 }
783 }
784
785 const ORDER: &[&str] = &["CPU", "GPU", "NVMe", "WiFi", "Battery", "System"];
786 ORDER
787 .iter()
788 .filter_map(|cat| max.get(cat).map(|v| format!("{}: {:.0}°C", cat, v)))
789 .collect()
790}
791
792fn format_uptime(uptime: &str) -> String {
796 let seconds: u64 = uptime.trim_end_matches('s').parse().unwrap_or(0);
798
799 let years = seconds / (365 * 24 * 3600);
800 let days = (seconds % (365 * 24 * 3600)) / (24 * 3600);
801 let hours = (seconds % (24 * 3600)) / 3600;
802 let minutes = (seconds % 3600) / 60;
803 let secs = seconds % 60;
804
805 let mut parts = Vec::new();
806 if years > 0 {
807 parts.push(format!("{}y", years));
808 }
809 if days > 0 {
810 parts.push(format!("{}d", days));
811 }
812 if hours > 0 {
813 parts.push(format!("{}h", hours));
814 }
815 if minutes > 0 {
816 parts.push(format!("{}m", minutes));
817 }
818 if secs > 0 || parts.is_empty() {
819 parts.push(format!("{}s", secs));
820 }
821
822 parts.join(" ")
823}
824
825#[cfg(feature = "graphics")]
830fn graphical_logo_height_lines(bytes: &[u8]) -> usize {
831 let img_h = image::load_from_memory(bytes)
832 .map(|img| img.height() as usize)
833 .unwrap_or(384);
834 let cell_h = terminal_cell_height_px();
835 img_h.div_ceil(cell_h)
836}
837
838fn terminal_cell_height_px() -> usize {
840 #[cfg(unix)]
841 {
842 use std::mem::MaybeUninit;
843 let mut ws: libc::winsize = unsafe { MaybeUninit::zeroed().assume_init() };
844 let ret = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
845 if ret == 0 && ws.ws_row > 0 && ws.ws_ypixel > 0 {
846 return ws.ws_ypixel as usize / ws.ws_row as usize;
847 }
848 }
849 20
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 #[test]
859 fn test_show_logo_auto_requires_tty() {
860 assert!(should_show_logo(None, false, false, true));
862 assert!(!should_show_logo(None, false, false, false));
863 }
864
865 #[test]
866 fn test_show_logo_ascii_forces_without_tty() {
867 assert!(should_show_logo(None, false, true, false));
869 assert!(should_show_logo(None, false, true, true));
870 }
871
872 #[test]
873 fn test_show_logo_no_logo_always_wins() {
874 assert!(!should_show_logo(None, true, true, true));
876 assert!(!should_show_logo(None, true, false, true));
877 }
878
879 #[test]
880 fn test_show_logo_config_disable() {
881 assert!(!should_show_logo(Some(false), false, false, true));
883 assert!(should_show_logo(Some(false), false, true, false));
885 }
886
887 fn realistic_full_widths() -> Vec<usize> {
892 let mut w = vec![40; 20]; w[13] = 54; w.extend([158, 91, 79, 60, 45, 62]); w
896 }
897
898 #[test]
899 fn test_layout_long_line_below_logo_stays_side_by_side() {
900 let p = plan_layout(&realistic_full_widths(), 20, 40, 120, true);
902 assert!(p.side_by_side);
903 assert_eq!(p.text_column_width, 58); }
906
907 #[test]
908 fn test_layout_old_behavior_would_have_stacked() {
909 let widths = realistic_full_widths();
912 let old_text_col = std::cmp::max(widths.iter().copied().max().unwrap() + 4, 45);
913 assert!(120 < old_text_col + 40); assert!(plan_layout(&widths, 20, 40, 120, true).side_by_side); }
916
917 #[test]
918 fn test_layout_long_line_within_logo_forces_stack() {
919 let mut w = vec![40; 20];
921 w[5] = 158;
922 assert!(!plan_layout(&w, 20, 40, 120, true).side_by_side);
923 }
924
925 #[test]
926 fn test_layout_narrow_terminal_stacks() {
927 assert!(!plan_layout(&[40; 30], 20, 40, 94, true).side_by_side); assert!(!plan_layout(&[40; 30], 20, 40, 80, true).side_by_side);
929 }
930
931 #[test]
932 fn test_layout_show_logo_false_stacks() {
933 assert!(!plan_layout(&[40; 30], 20, 40, 200, false).side_by_side);
934 }
935
936 #[test]
937 fn test_layout_column_floor_and_graphical_width() {
938 let p = plan_layout(&[10; 25], 20, 40, 100, true);
940 assert!(p.side_by_side);
941 assert_eq!(p.text_column_width, 45); }
943
944 #[test]
945 fn test_layout_logo_taller_than_text() {
946 let p = plan_layout(&[50, 30, 54], 20, 40, 120, true);
948 assert!(p.side_by_side);
949 assert_eq!(p.text_column_width, 58); }
951
952 #[test]
955 fn test_split_wifi_hardware_and_connection() {
956 let s = "MEDIATEK Corp. MT7925 802.11be [Filogic 360] [wlp194s0] - myssid (5.0 GHz ch36 [↓866 ↑866])";
958 let (hw, conn) = split_wifi_line(s);
959 assert_eq!(
960 hw,
961 "MEDIATEK Corp. MT7925 802.11be [Filogic 360] [wlp194s0]"
962 );
963 assert_eq!(conn, Some("myssid (5.0 GHz ch36 [↓866 ↑866])"));
964 }
965
966 #[test]
967 fn test_split_wifi_splits_on_first_separator() {
968 let (hw, conn) = split_wifi_line("Card X [wlan0] - Guest - 5G (5 GHz)");
971 assert_eq!(hw, "Card X [wlan0]");
972 assert_eq!(conn, Some("Guest - 5G (5 GHz)"));
973 }
974
975 #[test]
976 fn test_split_wifi_connection_only_fallback() {
977 let (hw, conn) = split_wifi_line("myssid (300 Mbps)");
979 assert_eq!(hw, "myssid (300 Mbps)");
980 assert_eq!(conn, None);
981 }
982
983 #[test]
984 fn test_consolidate_temps_basic() {
985 let raw = vec![
986 "k10temp Tctl: 83°C".to_string(),
987 "amdgpu edge: 65°C".to_string(),
988 "nvme Composite: 62°C".to_string(),
989 "ath11k_hwmon temp1: 58°C".to_string(),
990 "acpitz temp1: 77°C".to_string(),
991 ];
992 let result = consolidate_temps(&raw);
993 assert_eq!(
994 result,
995 vec![
996 "CPU: 83°C",
997 "GPU: 65°C",
998 "NVMe: 62°C",
999 "WiFi: 58°C",
1000 "System: 77°C"
1001 ]
1002 );
1003 }
1004
1005 #[test]
1006 fn test_consolidate_temps_highest_wins() {
1007 let raw = vec![
1008 "thinkpad CPU: 83°C".to_string(),
1009 "k10temp Tctl: 79°C".to_string(),
1010 "nvme Composite: 62°C".to_string(),
1011 "nvme Sensor 1: 59°C".to_string(),
1012 "nvme Sensor 2: 56°C".to_string(),
1013 ];
1014 let result = consolidate_temps(&raw);
1015 assert!(result.contains(&"CPU: 83°C".to_string()));
1016 assert!(result.contains(&"NVMe: 62°C".to_string()));
1017 assert!(!result
1018 .iter()
1019 .any(|s| s.contains("79") || s.contains("59") || s.contains("56")));
1020 }
1021
1022 #[test]
1023 fn test_consolidate_temps_order() {
1024 let raw = vec![
1025 "acpitz: 60°C".to_string(),
1026 "nvme: 55°C".to_string(),
1027 "amdgpu edge: 65°C".to_string(),
1028 "k10temp Tctl: 80°C".to_string(),
1029 ];
1030 let result = consolidate_temps(&raw);
1031 let cpu_pos = result.iter().position(|s| s.starts_with("CPU"));
1032 let gpu_pos = result.iter().position(|s| s.starts_with("GPU"));
1033 let nvme_pos = result.iter().position(|s| s.starts_with("NVMe"));
1034 let sys_pos = result.iter().position(|s| s.starts_with("System"));
1035 assert!(cpu_pos < gpu_pos);
1036 assert!(gpu_pos < nvme_pos);
1037 assert!(nvme_pos < sys_pos);
1038 }
1039
1040 #[test]
1041 fn test_consolidate_temps_empty() {
1042 assert!(consolidate_temps(&[]).is_empty());
1043 }
1044
1045 #[test]
1046 fn test_format_uptime() {
1047 assert_eq!(format_uptime("60s"), "1m");
1048 assert_eq!(format_uptime("3600s"), "1h");
1049 assert_eq!(format_uptime("3661s"), "1h 1m 1s");
1050 assert_eq!(format_uptime("86400s"), "1d");
1051 assert_eq!(format_uptime("90061s"), "1d 1h 1m 1s");
1052 assert_eq!(format_uptime("31536000s"), "1y");
1053 assert_eq!(format_uptime("31626061s"), "1y 1d 1h 1m 1s");
1054 assert_eq!(format_uptime("0s"), "0s");
1055 }
1056}