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