//! FetchX - A fast, feature-rich system information tool written in Rust. //! //! An exact clone of neofetch with extra features: emoji support, image display, //! Nerd Font icons, and blazing fast performance. mod ascii; mod colors; mod config; mod display; mod image_backend; mod image_cache; mod system; mod utils; use clap::Parser; use config::Config; use display::Display; use system::SystemInfo; /// FetchX - Fast system information tool with emoji & image support #[derive(Parser, Debug)] #[command(name = "fetchx")] #[command(author = "FetchX Contributors")] #[command(version = "0.2.0")] #[command(about = "A fast, modern system information tool — neofetch clone in Rust", long_about = None)] struct Args { /// Disable colors in output #[arg(long)] no_color: bool, /// Enable/disable bold text #[arg(long)] bold: Option, /// Image backend: ascii, kitty, sixel, chafa, off #[arg(short, long, value_name = "BACKEND")] backend: Option, /// Image source: auto, ascii, wallpaper, or /path/to/image #[arg(short, long, value_name = "SOURCE")] source: Option, /// Which distro's ASCII art to display #[arg(long, value_name = "DISTRO")] ascii_distro: Option, /// Use emoji mode instead of ASCII art #[arg(long)] emoji: bool, /// Use Nerd Font icons for info labels #[arg(long)] nerd_font: bool, /// Path to custom image to display #[arg(short, long, value_name = "PATH")] custom_image: Option, /// Only show the logo/ASCII art, no system info #[arg(short, long)] logo_only: bool, /// Gap between logo and info text #[arg(long, value_name = "NUM")] gap: Option, /// Separator between label and value #[arg(long, value_name = "SEP")] separator: Option, /// Disable all formatting (for piping) #[arg(long)] stdout: bool, /// Show the config file path and exit #[arg(long)] show_config: bool, /// Print the default config to stdout and exit #[arg(long)] print_config: bool, /// Create the default config file if it doesn't exist and exit #[arg(long)] create_config: bool, /// Print output as JSON #[arg(long)] json: bool, /// Color block range start (0-15) #[arg(long)] block_range_start: Option, /// Color block range end (0-15) #[arg(long)] block_range_end: Option, /// Width of color blocks #[arg(long)] block_width: Option, /// Interactive image selector using fzf #[arg(long)] select_image: bool, /// Run as daemon (updates status file every 10 seconds) #[arg(long)] daemon: bool, /// Show tray status (compact output for system tray/widget) #[arg(long)] tray_status: bool, /// Initialize config with API image fetching (one-command setup) #[arg(long)] init_api_image: bool, /// Internal: background download helper (not for user use) #[arg(long, hide = true)] bg_download: bool, } fn main() { let args = Args::parse(); // Handle special flags if args.show_config { let path = Config::config_path(); println!("Config file: {}", path.display()); return; } if args.print_config { println!("{}", Config::example_config()); return; } if args.create_config { match Config::create_default_config() { Ok(path) => { println!("Config file created at: {}", path.display()); } Err(e) => { eprintln!("Error creating config: {}", e); std::process::exit(1); } } return; } if args.bg_download { // Internal: called as a detached child process to download image if let Err(e) = image_cache::download_image_from_api() { eprintln!("fetchx bg-download error: {}", e); } return; } if args.init_api_image { init_api_image_config(); return; } if args.select_image { select_image_interactive(); return; } if args.daemon { run_daemon(); return; } if args.tray_status { show_tray_status(); return; } // Load config, then override with CLI args let mut config = Config::load_or_default(); apply_args_to_config(&mut config, &args); // Auto-detect terminal capabilities if backend is "auto" if config.image_backend == "auto" { let capabilities = image_cache::detect_terminal_capabilities(); config.image_backend = capabilities.backend.clone(); } // Only use API image fetching when user explicitly configured it: // image_backend must be a graphics backend (not "ascii" / "off") // AND image_source must be "auto" (API mode) // AND no custom_image already set by user let uses_graphics_backend = matches!( config.image_backend.as_str(), "kitty" | "sixel" | "chafa" | "w3m" | "iterm2" ); let uses_api_source = config.image_source == "auto"; if uses_graphics_backend && uses_api_source && config.custom_image.is_none() { // Use cached image from previous run (instant, no delay) if let Some(cached_img) = image_cache::get_available_image() { config.custom_image = Some(cached_img.to_string_lossy().to_string()); } // Download a new image in background for next run (non-blocking, zero delay) image_cache::download_image_background(); } // Gather system info let sys_info = SystemInfo::collect(); if args.json { print_json(&sys_info); return; } // Render let display = Display::new(&config); display.render(&sys_info); } fn apply_args_to_config(config: &mut Config, args: &Args) { // Boolean flags: only set if explicitly passed if args.no_color || args.stdout { config.no_color = true; } if args.stdout { config.stdout = true; config.bold = false; } if let Some(bold) = args.bold { config.bold = bold; } if let Some(ref backend) = args.backend { config.image_backend = backend.clone(); } if let Some(ref source) = args.source { config.image_source = source.clone(); } if let Some(ref distro) = args.ascii_distro { config.ascii_distro = distro.clone(); } if args.emoji { config.emoji_mode = true; } if args.nerd_font { config.nerd_font = true; } if args.custom_image.is_some() { config.custom_image = args.custom_image.clone(); } if args.logo_only { config.logo_only = true; } if let Some(gap) = args.gap { config.gap = gap; } if let Some(ref sep) = args.separator { config.separator = sep.clone(); } if let Some(start) = args.block_range_start { config.block_range.0 = start; } if let Some(end) = args.block_range_end { config.block_range.1 = end; } if let Some(width) = args.block_width { config.block_width = width; } } fn print_json(info: &SystemInfo) { let gpu_str = info.gpu.join(", "); println!("{{"); let fields: Vec<(&str, &str)> = vec![ ("os", &info.os), ("host", &info.host), ("kernel", &info.kernel), ("uptime", &info.uptime), ("packages", &info.packages), ("shell", &info.shell), ("resolution", &info.resolution), ("de", &info.de), ("wm", &info.wm), ("wm_theme", &info.wm_theme), ("theme", &info.theme), ("icons", &info.icons), ("terminal", &info.terminal), ("terminal_font", &info.term_font), ("cpu", &info.cpu), ("gpu", &gpu_str), ("memory", &info.memory), ("disk", &info.disk), ("battery", &info.battery), ("local_ip", &info.local_ip), ("public_ip", &info.public_ip), ("locale", &info.locale), ("song", &info.song), ("users", &info.users), ]; for (i, (key, val)) in fields.iter().enumerate() { let comma = if i < fields.len() - 1 { "," } else { "" }; let escaped = val.replace('\\', "\\\\").replace('"', "\\\""); println!(" \"{}\": \"{}\"{}", key, escaped, comma); } println!("}}"); } /// Interactive image selector using fzf fn select_image_interactive() { use std::fs; use std::process::{Command, Stdio}; let mut image_paths = Vec::new(); // Scan common image directories let search_dirs = vec![ dirs::download_dir(), dirs::picture_dir(), dirs::home_dir().map(|h| h.join("Desktop")), ]; for dir_opt in search_dirs { if let Some(dir) = dir_opt { if let Ok(entries) = fs::read_dir(&dir) { for entry in entries.flatten() { let path = entry.path(); if let Some(ext) = path.extension().and_then(|e| e.to_str()) { if matches!(