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