Skip to main content

retch_cli/
display.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright (C) 2026 l1a
3
4//! Formatting and display logic for terminal output.
5//!
6//! Handles text rendering, layout, and image/ASCII logo rendering.
7
8use crate::cli::Cli;
9use crate::config::Config;
10use crate::fetch::SystemInfo;
11use crate::logo;
12use crate::theme::Theme;
13use owo_colors::OwoColorize;
14
15impl SystemInfo {
16    /// Renders the collected system information to the terminal.
17    ///
18    /// This method handles theme selection, logo rendering (including fallbacks
19    /// between graphics, Chafa, and ASCII), and field filtering based on
20    /// CLI flags and configuration.
21    pub fn display(&self, cli: &Cli, _config: &Config) -> anyhow::Result<()> {
22        println!();
23        let theme_name = _config.theme.as_deref().or(cli.theme.as_deref());
24        let mut theme = match theme_name {
25            Some(name) => Theme::from_name(name),
26            None => Theme::detect_system_theme(), // Default to system preference
27        };
28
29        // Apply custom theme overrides from config if present
30        if let Some(custom) = &_config.custom_theme {
31            theme = Theme::with_custom_overrides(theme, custom);
32        }
33
34        let show_logo = _config.show_logo.unwrap_or(true) && !cli.no_logo;
35        if show_logo {
36            let distro_hint = _config.logo.clone().or_else(logo::detect_distro);
37            #[cfg(feature = "graphics")]
38            let mut printed_logo = false;
39            #[cfg(not(feature = "graphics"))]
40            let printed_logo = false;
41
42            if !cli.ascii_only {
43                // 1. Try user-provided custom logo first (config override)
44                let user_logo = if let Some(config_dir) = dirs::config_dir() {
45                    let p = config_dir.join("retch").join("logo.png");
46                    if p.exists() {
47                        Some(p)
48                    } else {
49                        None
50                    }
51                } else {
52                    None
53                };
54
55                // 2. Kitty mode
56                #[cfg(feature = "graphics")]
57                if !printed_logo && logo::supports_kitty() {
58                    if let Some(path) = &user_logo {
59                        logo::print_graphical_logo_from_path(path);
60                        printed_logo = true;
61                    } else if let Some(distro) = &distro_hint {
62                        if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
63                            logo::print_graphical_logo(bytes);
64                            printed_logo = true;
65                        }
66                    }
67                }
68
69                // 2.3 iTerm2 mode
70                #[cfg(feature = "graphics")]
71                if !printed_logo && logo::supports_iterm2() {
72                    if let Some(path) = &user_logo {
73                        logo::print_iterm2_logo_from_path(path);
74                        printed_logo = true;
75                    } else if let Some(distro) = &distro_hint {
76                        if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
77                            logo::print_iterm2_logo(bytes);
78                            printed_logo = true;
79                        }
80                    }
81                }
82
83                // 2.5 Sixel mode
84                #[cfg(feature = "graphics")]
85                if !printed_logo && logo::supports_sixel() {
86                    if let Some(path) = &user_logo {
87                        logo::print_sixel_logo_from_path(path);
88                        printed_logo = true;
89                    } else if let Some(distro) = &distro_hint {
90                        if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
91                            logo::print_sixel_logo(bytes);
92                            printed_logo = true;
93                        }
94                    }
95                }
96
97                // 3. Chafa fallback (high-quality symbols)
98                if !printed_logo && logo::chafa_available() {
99                    if let Some(path) = &user_logo {
100                        if logo::print_with_chafa(path) {
101                            printed_logo = true;
102                        }
103                    } else if distro_hint.is_some() {
104                        // For chafa we need a file, so we skip embedded for now
105                        // (user can provide logo.png for chafa path)
106                    }
107                }
108            }
109
110            // 3. Final fallback: Real Fastfetch ASCII logo
111            if !printed_logo {
112                // Use the unified priority logic (graphic -> chafa -> ASCII)
113                logo::print_distro_logo_with_ascii(distro_hint.as_deref(), cli.ascii_only);
114            }
115            println!(); // spacing after logo
116        }
117
118        // Determine which fields to show
119        let allowed_fields: Option<Vec<String>> = if cli.long {
120            None // show everything
121        } else if cli.short {
122            Some(vec![
123                "os".to_string(),
124                "kernel".to_string(),
125                "host".to_string(),
126                "cpu".to_string(),
127                "gpu".to_string(),
128                "memory".to_string(),
129                "disk".to_string(),
130                "net".to_string(),
131            ])
132        } else if let Some(fields) = &_config.fields {
133            Some(fields.iter().map(|s| s.to_lowercase()).collect())
134        } else {
135            // Default set
136            Some(vec![
137                "os".to_string(),
138                "kernel".to_string(),
139                "host".to_string(),
140                "cpu".to_string(),
141                "motherboard".to_string(),
142                "bios".to_string(),
143                "gpu".to_string(),
144                "display".to_string(),
145                "audio".to_string(),
146                "memory".to_string(),
147                "swap".to_string(),
148                "load".to_string(),
149                "disk".to_string(),
150                "net".to_string(),
151                "uptime".to_string(),
152            ])
153        };
154
155        let should_show = |label: &str| -> bool {
156            match &allowed_fields {
157                Some(fields) => {
158                    let norm_label = label.to_lowercase().replace(['-', '_'], " ");
159                    let norm_label_no_spaces = norm_label.replace(' ', "");
160                    fields.iter().any(|f| {
161                        let norm_f = f.to_lowercase().replace(['-', '_'], " ");
162                        norm_f == norm_label || norm_f.replace(' ', "") == norm_label_no_spaces
163                    })
164                }
165                None => true,
166            }
167        };
168
169        // Helper for right-aligned labels
170        let label_width = 10;
171        let print_line = |label: &str, value: &str| {
172            if should_show(label) {
173                println!(
174                    "{:>width$}{} {}",
175                    theme.color_label(label),
176                    theme.color_separator(":"),
177                    theme.color_value(value),
178                    width = label_width
179                );
180            }
181        };
182
183        print_line("OS", &self.os);
184        if let Some(kernel) = &self.kernel {
185            print_line("Kernel", kernel);
186        }
187        if let Some(host) = &self.hostname {
188            print_line("Host", host);
189        }
190        if let Some(user) = &self.current_user {
191            print_line("User", user);
192        }
193        print_line("Arch", &self.arch);
194        print_line("CPU", &format!("{} ({} cores)", self.cpu, self.cpu_cores));
195        if let Some(freq) = &self.cpu_freq {
196            print_line("CPU Freq", freq);
197        }
198        if let Some(motherboard) = &self.motherboard {
199            print_line("Motherboard", motherboard);
200        }
201        if let Some(bios) = &self.bios {
202            print_line("BIOS", bios);
203        }
204        if should_show("GPU") {
205            for gpu in &self.gpu {
206                print_line("GPU", gpu);
207            }
208        }
209        if should_show("Display") {
210            for display in &self.displays {
211                print_line("Display", display);
212            }
213        }
214        if let Some(audio) = &self.audio {
215            print_line("Audio", audio);
216        }
217        print_line("Memory", &self.memory);
218        print_line("Swap", &self.swap);
219        print_line("Procs", &self.processes.to_string());
220        if let Some(load) = &self.load_avg {
221            print_line("Load", load);
222        }
223
224        if should_show("Disk") {
225            for disk in &self.disks {
226                print_line("Disk", disk);
227            }
228        }
229
230        if should_show("Temp") {
231            for temp in &self.temps {
232                print_line("Temp", temp);
233            }
234        }
235
236        if should_show("Net") {
237            if cli.long {
238                for net in &self.networks {
239                    if let Some(ref active) = self.active_interface {
240                        if net.contains(active) {
241                            print_line("Net", &net.bright_blue().to_string());
242                        }
243                    }
244                }
245                for net in &self.networks {
246                    if let Some(ref active) = self.active_interface {
247                        if net.contains(active) {
248                            continue;
249                        }
250                    }
251                    print_line("Net", net);
252                }
253            } else {
254                let mut printed = false;
255                if let Some(ref active) = self.active_interface {
256                    for net in &self.networks {
257                        if net.contains(active) {
258                            print_line("Net", net);
259                            printed = true;
260                            break;
261                        }
262                    }
263                }
264                if !printed {
265                    for net in &self.networks {
266                        if net.contains("[Up]") {
267                            print_line("Net", net);
268                            break;
269                        }
270                    }
271                }
272            }
273        }
274
275        if let Some(ip) = &self.public_ip {
276            print_line("Public IP", ip);
277        }
278
279        if let Some(wifi) = &self.wifi {
280            print_line("Wi-Fi", wifi);
281        }
282
283        if let Some(bt) = &self.bluetooth {
284            print_line("Bluetooth", bt);
285        }
286
287        // Uptime: human duration first, then ISO boot time with timezone
288        let uptime_str = format_uptime(&self.uptime);
289        let boot_display = format!("{} since {}", uptime_str, self.boot_time);
290        print_line("Uptime", &boot_display);
291
292        if let Some(bat) = &self.battery {
293            print_line("Battery", bat);
294        }
295
296        if let Some(shell) = &self.shell {
297            print_line("Shell", shell);
298        }
299        if let Some(term) = &self.terminal {
300            print_line("Terminal", term);
301        }
302        if let Some(de) = &self.desktop {
303            print_line("Desktop", de);
304        }
305        if let Some(ui_theme) = &self.ui_theme {
306            print_line("Theme", ui_theme);
307        }
308        if let Some(icons) = &self.icons {
309            print_line("Icons", icons);
310        }
311        if let Some(cursor) = &self.cursor {
312            print_line("Cursor", cursor);
313        }
314        if let Some(font) = &self.font {
315            print_line("Font", font);
316        }
317        print_line("Users", &self.users.to_string());
318        if let Some(pkgs) = self.packages {
319            if pkgs > 0 {
320                print_line("Packages", &pkgs.to_string());
321            }
322        }
323
324        Ok(())
325    }
326}
327
328/// Formats a raw uptime string (in seconds) into a human-readable duration.
329///
330/// Example: "45224s" -> "12h 33m 44s"
331fn format_uptime(uptime: &str) -> String {
332    // Parse the uptime string (e.g. "45224s")
333    let seconds: u64 = uptime.trim_end_matches('s').parse().unwrap_or(0);
334
335    let years = seconds / (365 * 24 * 3600);
336    let days = (seconds % (365 * 24 * 3600)) / (24 * 3600);
337    let hours = (seconds % (24 * 3600)) / 3600;
338    let minutes = (seconds % 3600) / 60;
339    let secs = seconds % 60;
340
341    let mut parts = Vec::new();
342    if years > 0 {
343        parts.push(format!("{}y", years));
344    }
345    if days > 0 {
346        parts.push(format!("{}d", days));
347    }
348    if hours > 0 {
349        parts.push(format!("{}h", hours));
350    }
351    if minutes > 0 {
352        parts.push(format!("{}m", minutes));
353    }
354    if secs > 0 || parts.is_empty() {
355        parts.push(format!("{}s", secs));
356    }
357
358    parts.join(" ")
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_format_uptime() {
367        assert_eq!(format_uptime("60s"), "1m");
368        assert_eq!(format_uptime("3600s"), "1h");
369        assert_eq!(format_uptime("3661s"), "1h 1m 1s");
370        assert_eq!(format_uptime("86400s"), "1d");
371        assert_eq!(format_uptime("90061s"), "1d 1h 1m 1s");
372        assert_eq!(format_uptime("31536000s"), "1y");
373        assert_eq!(format_uptime("31626061s"), "1y 1d 1h 1m 1s");
374        assert_eq!(format_uptime("0s"), "0s");
375    }
376}