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