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