Skip to main content

retch_cli/
display.rs

1use crate::cli::Cli;
2use crate::config::Config;
3use crate::fetch::SystemInfo;
4use crate::logo;
5use crate::theme::Theme;
6
7impl SystemInfo {
8    /// Renders the collected system information to the terminal.
9    ///
10    /// This method handles theme selection, logo rendering (including fallbacks
11    /// between graphics, Chafa, and ASCII), and field filtering based on
12    /// CLI flags and configuration.
13    pub fn display(&self, cli: &Cli, _config: &Config) -> anyhow::Result<()> {
14        println!();
15        let theme_name = _config.theme.as_deref().or(cli.theme.as_deref());
16        let mut theme = match theme_name {
17            Some(name) => Theme::from_name(name),
18            None => Theme::detect_system_theme(), // Default to system preference
19        };
20
21        // Apply custom theme overrides from config if present
22        if let Some(custom) = &_config.custom_theme {
23            theme = Theme::with_custom_overrides(theme, custom);
24        }
25
26        let show_logo = _config.show_logo.unwrap_or(true) && !cli.no_logo;
27        if show_logo {
28            #[cfg(feature = "graphics")]
29            let mut printed_logo = false;
30            #[cfg(not(feature = "graphics"))]
31            let printed_logo = false;
32
33            if !cli.ascii_only {
34                // 1. Try user-provided custom logo first (config override)
35                let user_logo = if let Some(config_dir) = dirs::config_dir() {
36                    let p = config_dir.join("retch").join("logo.png");
37                    if p.exists() {
38                        Some(p)
39                    } else {
40                        None
41                    }
42                } else {
43                    None
44                };
45
46                // 2. Kitty mode
47                #[cfg(feature = "graphics")]
48                if !printed_logo && logo::supports_kitty() {
49                    if let Some(path) = &user_logo {
50                        logo::print_graphical_logo_from_path(path);
51                        printed_logo = true;
52                    } else if let Some(distro) = logo::detect_distro() {
53                        if let Some(bytes) = logo::get_embedded_logo(Some(&distro)) {
54                            logo::print_graphical_logo(bytes);
55                            printed_logo = true;
56                        }
57                    }
58                }
59
60                // 2.3 iTerm2 mode
61                #[cfg(feature = "graphics")]
62                if !printed_logo && logo::supports_iterm2() {
63                    if let Some(path) = &user_logo {
64                        logo::print_iterm2_logo_from_path(path);
65                        printed_logo = true;
66                    } else if let Some(distro) = logo::detect_distro() {
67                        if let Some(bytes) = logo::get_embedded_logo(Some(&distro)) {
68                            logo::print_iterm2_logo(bytes);
69                            printed_logo = true;
70                        }
71                    }
72                }
73
74                // 2.5 Sixel mode
75                #[cfg(feature = "graphics")]
76                if !printed_logo && logo::supports_sixel() {
77                    if let Some(path) = &user_logo {
78                        logo::print_sixel_logo_from_path(path);
79                        printed_logo = true;
80                    } else if let Some(distro) = logo::detect_distro() {
81                        if let Some(bytes) = logo::get_embedded_logo(Some(&distro)) {
82                            logo::print_sixel_logo(bytes);
83                            printed_logo = true;
84                        }
85                    }
86                }
87
88                // 3. Chafa fallback (high-quality symbols)
89                if !printed_logo && logo::chafa_available() {
90                    if let Some(path) = &user_logo {
91                        if logo::print_with_chafa(path) {
92                            printed_logo = true;
93                        }
94                    } else if logo::detect_distro().is_some() {
95                        // For chafa we need a file, so we skip embedded for now
96                        // (user can provide logo.png for chafa path)
97                    }
98                }
99            }
100
101            // 3. Final fallback: Real Fastfetch ASCII logo
102            if !printed_logo {
103                let distro_hint = logo::detect_distro();
104                // Use the unified priority logic (graphic -> chafa -> ASCII)
105                logo::print_distro_logo_with_ascii(distro_hint.as_deref(), cli.ascii_only);
106            }
107            println!(); // spacing after logo
108        }
109
110        // Determine which fields to show
111        let allowed_fields: Option<Vec<String>> = if cli.long {
112            None // show everything
113        } else if cli.short {
114            Some(vec![
115                "os".to_string(),
116                "kernel".to_string(),
117                "host".to_string(),
118                "cpu".to_string(),
119                "gpu".to_string(),
120                "memory".to_string(),
121                "disk".to_string(),
122            ])
123        } else if let Some(fields) = &_config.fields {
124            Some(fields.iter().map(|s| s.to_lowercase()).collect())
125        } else {
126            // Default set
127            Some(vec![
128                "os".to_string(),
129                "kernel".to_string(),
130                "host".to_string(),
131                "cpu".to_string(),
132                "gpu".to_string(),
133                "memory".to_string(),
134                "swap".to_string(),
135                "load".to_string(),
136                "disk".to_string(),
137                "net".to_string(),
138                "uptime".to_string(),
139            ])
140        };
141
142        let should_show = |label: &str| -> bool {
143            match &allowed_fields {
144                Some(fields) => fields.contains(&label.to_lowercase()),
145                None => true,
146            }
147        };
148
149        // Helper for right-aligned labels
150        let label_width = 10;
151        let print_line = |label: &str, value: &str| {
152            if should_show(label) {
153                println!(
154                    "{:>width$}{} {}",
155                    theme.color_label(label),
156                    theme.color_separator(":"),
157                    theme.color_value(value),
158                    width = label_width
159                );
160            }
161        };
162
163        print_line("OS", &self.os);
164        if let Some(kernel) = &self.kernel {
165            print_line("Kernel", kernel);
166        }
167        if let Some(host) = &self.hostname {
168            print_line("Host", host);
169        }
170        if let Some(user) = &self.current_user {
171            print_line("User", user);
172        }
173        print_line("Arch", &self.arch);
174        print_line("CPU", &format!("{} ({} cores)", self.cpu, self.cpu_cores));
175        if let Some(freq) = &self.cpu_freq {
176            print_line("CPU Freq", freq);
177        }
178        if should_show("GPU") {
179            for gpu in &self.gpu {
180                print_line("GPU", gpu);
181            }
182        }
183        print_line("Memory", &self.memory);
184        print_line("Swap", &self.swap);
185        print_line("Procs", &self.processes.to_string());
186        if let Some(load) = &self.load_avg {
187            print_line("Load", load);
188        }
189
190        if should_show("Disk") {
191            for disk in &self.disks {
192                print_line("Disk", disk);
193            }
194        }
195
196        if should_show("Temp") {
197            for temp in &self.temps {
198                print_line("Temp", temp);
199            }
200        }
201
202        if should_show("Net") {
203            for net in &self.networks {
204                print_line("Net", net);
205            }
206        }
207
208        // Uptime: human duration first, then ISO boot time with timezone
209        let uptime_str = format_uptime(&self.uptime);
210        let boot_display = format!("{} since {}", uptime_str, self.boot_time);
211        print_line("Uptime", &boot_display);
212
213        if let Some(bat) = &self.battery {
214            print_line("Battery", bat);
215        }
216
217        if let Some(shell) = &self.shell {
218            print_line("Shell", shell);
219        }
220        if let Some(term) = &self.terminal {
221            print_line("Terminal", term);
222        }
223        if let Some(de) = &self.desktop {
224            print_line("Desktop", de);
225        }
226        print_line("Users", &self.users.to_string());
227        if let Some(pkgs) = self.packages {
228            if pkgs > 0 {
229                print_line("Packages", &pkgs.to_string());
230            }
231        }
232
233        Ok(())
234    }
235}
236
237/// Formats a raw uptime string (in seconds) into a human-readable duration.
238///
239/// Example: "45224s" -> "12h 33m 44s"
240fn format_uptime(uptime: &str) -> String {
241    // Parse the uptime string (e.g. "45224s")
242    let seconds: u64 = uptime.trim_end_matches('s').parse().unwrap_or(0);
243
244    let years = seconds / (365 * 24 * 3600);
245    let days = (seconds % (365 * 24 * 3600)) / (24 * 3600);
246    let hours = (seconds % (24 * 3600)) / 3600;
247    let minutes = (seconds % 3600) / 60;
248    let secs = seconds % 60;
249
250    let mut parts = Vec::new();
251    if years > 0 {
252        parts.push(format!("{}y", years));
253    }
254    if days > 0 {
255        parts.push(format!("{}d", days));
256    }
257    if hours > 0 {
258        parts.push(format!("{}h", hours));
259    }
260    if minutes > 0 {
261        parts.push(format!("{}m", minutes));
262    }
263    if secs > 0 || parts.is_empty() {
264        parts.push(format!("{}s", secs));
265    }
266
267    parts.join(" ")
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_format_uptime() {
276        assert_eq!(format_uptime("60s"), "1m");
277        assert_eq!(format_uptime("3600s"), "1h");
278        assert_eq!(format_uptime("3661s"), "1h 1m 1s");
279        assert_eq!(format_uptime("86400s"), "1d");
280        assert_eq!(format_uptime("90061s"), "1d 1h 1m 1s");
281        assert_eq!(format_uptime("31536000s"), "1y");
282        assert_eq!(format_uptime("31626061s"), "1y 1d 1h 1m 1s");
283        assert_eq!(format_uptime("0s"), "0s");
284    }
285}