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