1use crate::cli::Cli;
9use crate::config::Config;
10use crate::fetch::SystemInfo;
11use crate::logo;
12use crate::theme::Theme;
13use owo_colors::OwoColorize;
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_width = if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
35 w as usize
36 } else {
37 80
38 };
39
40 let show_logo = _config.show_logo.unwrap_or(true) && !cli.no_logo;
41
42 let allowed_fields: Option<Vec<String>> = if cli.long {
44 None } else if cli.short {
46 Some(vec![
47 "os".to_string(),
48 "kernel".to_string(),
49 "host".to_string(),
50 "cpu".to_string(),
51 "gpu".to_string(),
52 "memory".to_string(),
53 "disk".to_string(),
54 "net".to_string(),
55 ])
56 } else if let Some(fields) = &_config.fields {
57 Some(fields.iter().map(|s| s.to_lowercase()).collect())
58 } else {
59 Some(vec![
61 "os".to_string(),
62 "kernel".to_string(),
63 "host".to_string(),
64 "cpu".to_string(),
65 "motherboard".to_string(),
66 "bios".to_string(),
67 "gpu".to_string(),
68 "display".to_string(),
69 "audio".to_string(),
70 "camera".to_string(),
71 "gamepad".to_string(),
72 "memory".to_string(),
73 "swap".to_string(),
74 "load".to_string(),
75 "disk".to_string(),
76 "net".to_string(),
77 "uptime".to_string(),
78 ])
79 };
80
81 let should_show = |label: &str| -> bool {
82 match &allowed_fields {
83 Some(fields) => {
84 let norm_label = label.to_lowercase().replace(['-', '_'], " ");
85 let norm_label_no_spaces = norm_label.replace(' ', "");
86 fields.iter().any(|f| {
87 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
88 norm_f == norm_label || norm_f.replace(' ', "") == norm_label_no_spaces
89 })
90 }
91 None => true,
92 }
93 };
94
95 let label_width = 10;
97 let mut info_lines = Vec::new();
98 let mut print_line = |label: &str, value: &str| {
99 if should_show(label) {
100 info_lines.push(format!(
101 "{:>width$}{} {}",
102 theme.color_label(label),
103 theme.color_separator(":"),
104 theme.color_value(value),
105 width = label_width
106 ));
107 }
108 };
109
110 print_line("OS", &info.os);
111 if let Some(kernel) = &info.kernel {
112 print_line("Kernel", kernel);
113 }
114 if let Some(host) = &info.hostname {
115 print_line("Host", host);
116 }
117 if let Some(user) = &info.current_user {
118 print_line("User", user);
119 }
120 print_line("Arch", &info.arch);
121 print_line("CPU", &format!("{} ({} cores)", info.cpu, info.cpu_cores));
122 if let Some(freq) = &info.cpu_freq {
123 print_line("CPU Freq", freq);
124 }
125 if let Some(motherboard) = &info.motherboard {
126 print_line("Motherboard", motherboard);
127 }
128 if let Some(bios) = &info.bios {
129 print_line("BIOS", bios);
130 }
131 if should_show("GPU") {
132 for gpu in &info.gpu {
133 print_line("GPU", gpu);
134 }
135 }
136 if should_show("Display") {
137 for display in &info.displays {
138 print_line("Display", display);
139 }
140 }
141 if let Some(audio) = &info.audio {
142 print_line("Audio", audio);
143 }
144 if should_show("Camera") {
145 for cam in &info.camera {
146 print_line("Camera", cam);
147 }
148 }
149 if should_show("Gamepad") {
150 for gp in &info.gamepad {
151 print_line("Gamepad", gp);
152 }
153 }
154 print_line("Memory", &info.memory);
155 print_line("Swap", &info.swap);
156 print_line("Procs", &info.processes.to_string());
157 if let Some(load) = &info.load_avg {
158 print_line("Load", load);
159 }
160
161 if should_show("Disk") {
162 for disk in &info.disks {
163 print_line("Disk", disk);
164 }
165 }
166
167 if should_show("Temp") {
168 for temp in &info.temps {
169 print_line("Temp", temp);
170 }
171 }
172
173 if should_show("Net") {
174 if cli.long {
175 for net in &info.networks {
176 if let Some(ref active) = info.active_interface {
177 if net.contains(active) {
178 print_line("Net", &net.bright_blue().to_string());
179 }
180 }
181 }
182 for net in &info.networks {
183 if let Some(ref active) = info.active_interface {
184 if net.contains(active) {
185 continue;
186 }
187 }
188 print_line("Net", net);
189 }
190 } else {
191 let mut printed = false;
192 if let Some(ref active) = info.active_interface {
193 for net in &info.networks {
194 if net.contains(active) {
195 print_line("Net", net);
196 printed = true;
197 break;
198 }
199 }
200 }
201 if !printed {
202 for net in &info.networks {
203 if net.contains("[Up]") {
204 print_line("Net", net);
205 break;
206 }
207 }
208 }
209 }
210 }
211
212 if let Some(ip) = &info.public_ip {
213 print_line("Public IP", ip);
214 }
215
216 if let Some(wifi) = &info.wifi {
217 print_line("Wi-Fi", wifi);
218 }
219
220 if let Some(bt) = &info.bluetooth {
221 print_line("Bluetooth", bt);
222 }
223
224 let uptime_str = format_uptime(&info.uptime);
226 let boot_display = format!("{} since {}", uptime_str, info.boot_time);
227 print_line("Uptime", &boot_display);
228
229 if let Some(bat) = &info.battery {
230 print_line("Battery", bat);
231 }
232
233 if let Some(shell) = &info.shell {
234 print_line("Shell", shell);
235 }
236 if let Some(term) = &info.terminal {
237 print_line("Terminal", term);
238 }
239 if let Some(de) = &info.desktop {
240 print_line("Desktop", de);
241 }
242 if let Some(ui_theme) = &info.ui_theme {
243 print_line("Theme", ui_theme);
244 }
245 if let Some(icons) = &info.icons {
246 print_line("Icons", icons);
247 }
248 if let Some(cursor) = &info.cursor {
249 print_line("Cursor", cursor);
250 }
251 if let Some(font) = &info.font {
252 print_line("Font", font);
253 }
254 if let Some(term_font) = &info.terminal_font {
255 print_line("Terminal Font", term_font);
256 }
257 print_line("Users", &info.users.to_string());
258 if let Some(pkgs) = info.packages {
259 if pkgs > 0 {
260 print_line("Packages", &pkgs.to_string());
261 }
262 }
263
264 enum ActiveLogo {
266 Lines(Vec<String>),
267 Kitty(Vec<u8>),
268 Iterm2(Vec<u8>),
269 Sixel(Vec<u8>),
270 None,
271 }
272
273 let mut active_logo = ActiveLogo::None;
274
275 if show_logo {
276 let distro_hint = _config.logo.clone().or_else(logo::detect_distro);
277 let user_logo = if let Some(config_dir) = dirs::config_dir() {
278 let p = config_dir.join("retch").join("logo.png");
279 if p.exists() {
280 Some(p)
281 } else {
282 None
283 }
284 } else {
285 None
286 };
287
288 if cli.ascii_logo {
289 active_logo = ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
290 } else if _config.chafa.unwrap_or(false) || cli.chafa_logo {
291 let mut resolved = false;
292 if logo::chafa_available() {
293 if let Some(path) = &user_logo {
294 if let Some(lines) = logo::get_chafa_logo_lines(path) {
295 active_logo = ActiveLogo::Lines(lines);
296 resolved = true;
297 }
298 } else if let Some(distro) = &distro_hint {
299 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
300 let temp_path = std::env::temp_dir()
301 .join(format!("retch_logo_{}.png", std::process::id()));
302 if std::fs::write(&temp_path, bytes).is_ok() {
303 if let Some(lines) = logo::get_chafa_logo_lines(&temp_path) {
304 active_logo = ActiveLogo::Lines(lines);
305 resolved = true;
306 }
307 let _ = std::fs::remove_file(&temp_path);
308 }
309 }
310 }
311 }
312 if !resolved {
313 active_logo =
314 ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
315 }
316 } else {
317 let mut resolved = false;
318
319 #[cfg(feature = "graphics")]
321 if !resolved && logo::supports_kitty() {
322 if let Some(path) = &user_logo {
323 if let Ok(bytes) = std::fs::read(path) {
324 active_logo = ActiveLogo::Kitty(bytes);
325 resolved = true;
326 }
327 } else if let Some(distro) = &distro_hint {
328 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
329 active_logo = ActiveLogo::Kitty(bytes.to_vec());
330 resolved = true;
331 }
332 }
333 }
334
335 #[cfg(feature = "graphics")]
337 if !resolved && logo::supports_iterm2() {
338 if let Some(path) = &user_logo {
339 if let Ok(bytes) = std::fs::read(path) {
340 active_logo = ActiveLogo::Iterm2(bytes);
341 resolved = true;
342 }
343 } else if let Some(distro) = &distro_hint {
344 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
345 active_logo = ActiveLogo::Iterm2(bytes.to_vec());
346 resolved = true;
347 }
348 }
349 }
350
351 #[cfg(feature = "graphics")]
353 if !resolved && logo::supports_sixel() {
354 if let Some(path) = &user_logo {
355 if let Ok(bytes) = std::fs::read(path) {
356 active_logo = ActiveLogo::Sixel(bytes);
357 resolved = true;
358 }
359 } else if let Some(distro) = &distro_hint {
360 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
361 active_logo = ActiveLogo::Sixel(bytes.to_vec());
362 resolved = true;
363 }
364 }
365 }
366
367 if !resolved && logo::chafa_available() {
369 if let Some(path) = &user_logo {
370 if let Some(lines) = logo::get_chafa_logo_lines(path) {
371 active_logo = ActiveLogo::Lines(lines);
372 resolved = true;
373 }
374 } else if let Some(distro) = &distro_hint {
375 if let Some(bytes) = logo::get_embedded_logo(Some(distro)) {
376 let temp_path = std::env::temp_dir()
378 .join(format!("retch_logo_{}.png", std::process::id()));
379 if std::fs::write(&temp_path, bytes).is_ok() {
380 if let Some(lines) = logo::get_chafa_logo_lines(&temp_path) {
381 active_logo = ActiveLogo::Lines(lines);
382 resolved = true;
383 }
384 let _ = std::fs::remove_file(&temp_path);
385 }
386 }
387 }
388 }
389
390 if !resolved {
392 active_logo =
393 ActiveLogo::Lines(logo::get_distro_logo_lines(distro_hint.as_deref()));
394 }
395 }
396 }
397
398 let visible_len = |s: &str| -> usize {
400 let mut count = 0;
401 let mut in_esc = false;
402 for c in s.chars() {
403 if c == '\x1b' {
404 in_esc = true;
405 } else if in_esc {
406 if c.is_ascii_alphabetic() {
407 in_esc = false;
408 }
409 } else {
410 count += 1;
411 }
412 }
413 count
414 };
415
416 let max_text_width = info_lines
417 .iter()
418 .map(|line| visible_len(line))
419 .max()
420 .unwrap_or(0);
421 let text_column_width = std::cmp::max(max_text_width + 4, 45);
422
423 let max_logo_width = match &active_logo {
424 ActiveLogo::Lines(logo_lines) => logo_lines
425 .iter()
426 .map(|line| visible_len(line))
427 .max()
428 .unwrap_or(0),
429 ActiveLogo::Kitty(_) | ActiveLogo::Iterm2(_) | ActiveLogo::Sixel(_) => 40,
430 ActiveLogo::None => 0,
431 };
432
433 let side_by_side =
434 show_logo && term_width >= 95 && term_width >= (text_column_width + max_logo_width);
435
436 println!(); if side_by_side {
439 match active_logo {
440 ActiveLogo::Lines(logo_lines) => {
441 let max_lines = std::cmp::max(info_lines.len(), logo_lines.len());
442 for i in 0..max_lines {
443 let info_line = info_lines.get(i).cloned().unwrap_or_default();
444 let logo_line = logo_lines.get(i).cloned().unwrap_or_default();
445 let vis_len = visible_len(&info_line);
446 let padding = if vis_len < text_column_width {
447 " ".repeat(text_column_width - vis_len)
448 } else {
449 String::new()
450 };
451 println!("{}{}{}", info_line, padding, logo_line);
452 }
453 }
454 ActiveLogo::Kitty(bytes) => {
455 for line in &info_lines {
457 println!("{}", line);
458 }
459 let num_lines = info_lines.len();
461 print!("\x1b7"); if num_lines > 0 {
463 print!("\x1b[{}A", num_lines); }
465 print!("\x1b[{}C", text_column_width); logo::print_graphical_logo(&bytes);
467 print!("\x1b8"); }
469 ActiveLogo::Iterm2(bytes) => {
470 for line in &info_lines {
472 println!("{}", line);
473 }
474 let num_lines = info_lines.len();
476 print!("\x1b7"); if num_lines > 0 {
478 print!("\x1b[{}A", num_lines); }
480 print!("\x1b[{}C", text_column_width); logo::print_iterm2_logo(&bytes);
482 print!("\x1b8"); }
484 ActiveLogo::Sixel(bytes) => {
485 for line in &info_lines {
487 println!("{}", line);
488 }
489 let num_lines = info_lines.len();
491 print!("\x1b7"); if num_lines > 0 {
493 print!("\x1b[{}A", num_lines); }
495 print!("\x1b[{}C", text_column_width); logo::print_sixel_logo(&bytes);
497 print!("\x1b8"); }
499 ActiveLogo::None => {
500 for line in &info_lines {
501 println!("{}", line);
502 }
503 }
504 }
505 } else {
506 match active_logo {
508 ActiveLogo::Lines(logo_lines) => {
509 for line in logo_lines {
510 println!("{}", line);
511 }
512 println!();
513 }
514 ActiveLogo::Kitty(bytes) => {
515 logo::print_graphical_logo(&bytes);
516 println!();
517 }
518 ActiveLogo::Iterm2(bytes) => {
519 logo::print_iterm2_logo(&bytes);
520 println!();
521 }
522 ActiveLogo::Sixel(bytes) => {
523 logo::print_sixel_logo(&bytes);
524 println!();
525 }
526 ActiveLogo::None => {}
527 }
528 for line in &info_lines {
529 println!("{}", line);
530 }
531 }
532
533 Ok(())
534}
535
536fn format_uptime(uptime: &str) -> String {
540 let seconds: u64 = uptime.trim_end_matches('s').parse().unwrap_or(0);
542
543 let years = seconds / (365 * 24 * 3600);
544 let days = (seconds % (365 * 24 * 3600)) / (24 * 3600);
545 let hours = (seconds % (24 * 3600)) / 3600;
546 let minutes = (seconds % 3600) / 60;
547 let secs = seconds % 60;
548
549 let mut parts = Vec::new();
550 if years > 0 {
551 parts.push(format!("{}y", years));
552 }
553 if days > 0 {
554 parts.push(format!("{}d", days));
555 }
556 if hours > 0 {
557 parts.push(format!("{}h", hours));
558 }
559 if minutes > 0 {
560 parts.push(format!("{}m", minutes));
561 }
562 if secs > 0 || parts.is_empty() {
563 parts.push(format!("{}s", secs));
564 }
565
566 parts.join(" ")
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn test_format_uptime() {
575 assert_eq!(format_uptime("60s"), "1m");
576 assert_eq!(format_uptime("3600s"), "1h");
577 assert_eq!(format_uptime("3661s"), "1h 1m 1s");
578 assert_eq!(format_uptime("86400s"), "1d");
579 assert_eq!(format_uptime("90061s"), "1d 1h 1m 1s");
580 assert_eq!(format_uptime("31536000s"), "1y");
581 assert_eq!(format_uptime("31626061s"), "1y 1d 1h 1m 1s");
582 assert_eq!(format_uptime("0s"), "0s");
583 }
584}