diff --git a/Cargo.lock b/Cargo.lock index e04cc55..7f3fbe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,7 @@ dependencies = [ "clap", "futures", "image", + "libc", "reqwest", "rmp-serde", "serde", diff --git a/Cargo.toml b/Cargo.toml index e460138..734ff92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ chrono = { version = "0.4.38", features = ["serde"] } clap = { version = "4.5.19", features = ["derive"] } futures = "0.3.30" image = { version = "0.25.2", default-features = false, features = ["png"] } +libc = "0.2.159" reqwest = { version = "0.12.8", features = ["blocking", "http2", "rustls-tls", "stream"], default-features = false } rmp-serde = "1.3.0" serde = { version = "1.0.210", features = ["derive"] } diff --git a/src/core/color.rs b/src/core/color.rs new file mode 100644 index 0000000..782f912 --- /dev/null +++ b/src/core/color.rs @@ -0,0 +1,177 @@ +use std::fmt::Display; + +#[derive(Debug, Clone, Copy)] +pub enum Color { + Reset, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, +} + +impl Color { + pub fn to_ansi_code(self, is_bg: bool) -> &'static str { + match self { + Color::Reset => "\x1B[0m", + Color::Black => { + if is_bg { + "\x1B[40m" + } else { + "\x1B[30m" + } + } + Color::Red => { + if is_bg { + "\x1B[41m" + } else { + "\x1B[31m" + } + } + Color::Green => { + if is_bg { + "\x1B[42m" + } else { + "\x1B[32m" + } + } + Color::Yellow => { + if is_bg { + "\x1B[43m" + } else { + "\x1B[33m" + } + } + Color::Blue => { + if is_bg { + "\x1B[44m" + } else { + "\x1B[34m" + } + } + Color::Magenta => { + if is_bg { + "\x1B[45m" + } else { + "\x1B[35m" + } + } + Color::Cyan => { + if is_bg { + "\x1B[46m" + } else { + "\x1B[36m" + } + } + Color::White => { + if is_bg { + "\x1B[47m" + } else { + "\x1B[37m" + } + } + Color::BrightBlack => { + if is_bg { + "\x1B[100m" + } else { + "\x1B[90m" + } + } + Color::BrightRed => { + if is_bg { + "\x1B[101m" + } else { + "\x1B[91m" + } + } + Color::BrightGreen => { + if is_bg { + "\x1B[102m" + } else { + "\x1B[92m" + } + } + Color::BrightYellow => { + if is_bg { + "\x1B[103m" + } else { + "\x1B[93m" + } + } + Color::BrightBlue => { + if is_bg { + "\x1B[104m" + } else { + "\x1B[94m" + } + } + Color::BrightMagenta => { + if is_bg { + "\x1B[105m" + } else { + "\x1B[95m" + } + } + Color::BrightCyan => { + if is_bg { + "\x1B[106m" + } else { + "\x1B[96m" + } + } + Color::BrightWhite => { + if is_bg { + "\x1B[107m" + } else { + "\x1B[97m" + } + } + } + } +} + +pub trait ColorExt { + fn color(self, color: Color) -> String; + fn bg_color(self, color: Color) -> String; + fn bold(self) -> String; +} + +impl ColorExt for T { + fn color(self, color: Color) -> String { + format!( + "{}{}{}", + color.to_ansi_code(false), + self, + Color::Reset.to_ansi_code(false) + ) + } + + fn bg_color(self, color: Color) -> String { + format!( + "{}{}{}", + color.to_ansi_code(true), + self, + Color::Reset.to_ansi_code(true) + ) + } + + fn bold(self) -> String { + format!( + "{}\x1B[1m{}{}", + Color::Reset.to_ansi_code(false), + self, + Color::Reset.to_ansi_code(false) + ) + } +} diff --git a/src/core/config.rs b/src/core/config.rs index ccb438e..640fae9 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -2,6 +2,8 @@ use std::{env, fs, path::PathBuf, sync::LazyLock}; use serde::{Deserialize, Serialize}; +use crate::core::color::{Color, ColorExt}; + use super::{constant::REGISTRY_PATH, util::home_config_path}; /// Application's configuration @@ -52,8 +54,9 @@ impl Config { Err(e) if e.kind() == std::io::ErrorKind::NotFound => { fs::create_dir_all(&pkg_config).unwrap(); eprintln!( - "Config not found. Generating default config at {}", - config_path.to_string_lossy() + "{} Generating default config at {}", + "Config not found".color(Color::Red), + config_path.to_string_lossy().color(Color::Green) ); Config::generate(config_path) } diff --git a/src/core/grid.rs b/src/core/grid.rs new file mode 100644 index 0000000..5c0f155 --- /dev/null +++ b/src/core/grid.rs @@ -0,0 +1,141 @@ +use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ}; +use std::mem; + +pub struct Grid { + cols: usize, + col_widths: Vec>, + col_ratios: Vec, + rows: Vec>, + separator: String, +} + +impl Grid { + pub fn builder(cols: usize) -> GridBuilder { + GridBuilder { + cols, + col_widths: vec![None; cols], + col_ratios: vec![1.0; cols], + separator: String::new(), + } + } + + fn get_terminal_width() -> usize { + let mut w: winsize = unsafe { mem::zeroed() }; + + if unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut w) } == 0 { + if w.ws_col > 0 { + w.ws_col as usize + } else { + 80 + } + } else { + 80 + } + } + + fn calculate_column_widths(&mut self) { + let mut total_fixed_width = 0; + let mut total_ratio = 0.0; + + for (i, &width) in self.col_widths.iter().enumerate() { + if let Some(w) = width { + total_fixed_width += w; + } else { + total_ratio += self.col_ratios[i]; + } + } + + let terminal_width = Grid::get_terminal_width(); + + if total_fixed_width >= terminal_width { + panic!("Total fixed column widths exceed the terminal width"); + } + + let remaining_width = terminal_width.saturating_sub(total_fixed_width); + + for (i, width) in self.col_widths.iter_mut().enumerate() { + if width.is_none() { + let ratio = self.col_ratios[i]; + *width = Some((remaining_width as f64 * (ratio / total_ratio)).round() as usize); + } + } + } + + pub fn row(&mut self, row: Vec) -> &mut Self { + if row.len() != self.cols { + panic!("Row length must match the number of columns."); + } + self.rows.push(row); + self + } + + fn truncate_row(row: &[String], widths: &[usize]) -> Vec { + row.iter() + .enumerate() + .map(|(i, col)| { + let max_width = widths[i]; + if col.len() > max_width { + format!("{}...", &col[..max_width.saturating_sub(3)]) + } else { + col.clone() + } + }) + .collect() + } + + pub fn print(mut self) { + self.calculate_column_widths(); + + let column_widths: Vec = self.col_widths.iter().map(|w| w.unwrap_or(0)).collect(); + + for row in &self.rows { + let truncated_row = Grid::truncate_row(row, &column_widths); + for (i, col) in truncated_row.iter().enumerate() { + let width = column_widths[i]; + print!("{:width$} ", col, width = width); + if i < self.cols - 1 { + print!("{}", self.separator) + } + } + println!(); + } + } +} + +pub struct GridBuilder { + cols: usize, + col_widths: Vec>, + col_ratios: Vec, + separator: String, +} + +impl GridBuilder { + pub fn set_width(mut self, col: usize, width: usize) -> Self { + if col < self.cols { + self.col_widths[col] = Some(width); + } + self + } + + pub fn set_ratio(mut self, col: usize, ratio: f64) -> Self { + if col < self.cols { + self.col_ratios[col] = ratio; + } + self + } + + pub fn set_separator(mut self, separator: String) -> Self { + self.separator = separator; + self + } + + pub fn build(self) -> Grid { + Grid { + cols: self.cols, + col_widths: self.col_widths, + col_ratios: self.col_ratios, + rows: Vec::new(), + separator: self.separator, + } + } +} diff --git a/src/core/log.rs b/src/core/log.rs new file mode 100644 index 0000000..20ca435 --- /dev/null +++ b/src/core/log.rs @@ -0,0 +1,27 @@ +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => { + println!("{} {}", "[WARN]".color(Color::BrightYellow).bold(), format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => { + println!("{} {}", "[INFO]".color(Color::BrightBlue).bold(), format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => { + eprintln!("{} {}", "[ERROR]".color(Color::BrightRed).bold(), format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! success { + ($($arg:tt)*) => { + println!("{} {}", "[SUCCESS]".color(Color::BrightGreen).bold(), format!($($arg)*)) + }; +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 485431b..c56ffa7 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,6 @@ +pub mod color; pub mod config; pub mod constant; +pub mod grid; +pub mod log; pub mod util; diff --git a/src/core/util.rs b/src/core/util.rs index 4219832..b006817 100644 --- a/src/core/util.rs +++ b/src/core/util.rs @@ -13,7 +13,10 @@ use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, }; -use super::constant::{BIN_PATH, INSTALL_TRACK_PATH, PACKAGES_PATH}; +use super::{ + color::{Color, ColorExt}, + constant::{BIN_PATH, INSTALL_TRACK_PATH, PACKAGES_PATH}, +}; pub fn home_path() -> String { env::var("HOME").unwrap_or_else(|_| { @@ -140,26 +143,32 @@ pub async fn validate_checksum(checksum: &str, file_path: &Path) -> Result<()> { pub async fn setup_required_paths() -> Result<()> { if !BIN_PATH.exists() { - fs::create_dir_all(&*BIN_PATH).await.context(format!( - "Failed to create bin directory {}", - BIN_PATH.to_string_lossy() - ))?; + fs::create_dir_all(&*BIN_PATH).await.with_context(|| { + format!( + "Failed to create bin directory {}", + BIN_PATH.to_string_lossy().color(Color::Blue) + ) + })?; } if !INSTALL_TRACK_PATH.exists() { fs::create_dir_all(&*INSTALL_TRACK_PATH) .await - .context(format!( - "Failed to create path: {}", - INSTALL_TRACK_PATH.to_string_lossy() - ))?; + .with_context(|| { + format!( + "Failed to create installs directory: {}", + INSTALL_TRACK_PATH.to_string_lossy().color(Color::Blue) + ) + })?; } if !PACKAGES_PATH.exists() { - fs::create_dir_all(&*PACKAGES_PATH).await.context(format!( - "Failed to create path: {}", - PACKAGES_PATH.to_string_lossy() - ))?; + fs::create_dir_all(&*PACKAGES_PATH).await.with_context(|| { + format!( + "Failed to create packages directory: {}", + PACKAGES_PATH.to_string_lossy().color(Color::Blue) + ) + })?; } Ok(()) @@ -172,9 +181,9 @@ pub async fn download(url: &str, what: &str) -> Result> { if !response.status().is_success() { return Err(anyhow::anyhow!( "Error fetching {} from {} [{}]", - what, - url, - response.status() + what.color(Color::Cyan), + url.color(Color::Blue), + response.status().color(Color::Red) )); } @@ -182,8 +191,8 @@ pub async fn download(url: &str, what: &str) -> Result> { println!( "Fetching {} from {} [{}]", - what, - url, + what.color(Color::Cyan), + url.color(Color::Blue), format_bytes(response.content_length().unwrap_or_default()) ); diff --git a/src/lib.rs b/src/lib.rs index d232b89..56a709d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use misc::download::download_and_save; use registry::PackageRegistry; use core::{ + color::{Color, ColorExt}, config, constant::BIN_PATH, util::{cleanup, setup_required_paths}, @@ -12,7 +13,7 @@ use core::{ use std::{env, path::Path}; mod cli; -mod core; +pub mod core; mod misc; mod registry; @@ -24,9 +25,10 @@ pub async fn init() -> Result<()> { let path_env = env::var("PATH")?; if !path_env.split(':').any(|p| Path::new(p) == *BIN_PATH) { - eprintln!( - "{} is not in PATH. Please add it to PATH to use installed binaries.", - &*BIN_PATH.to_string_lossy() + warn!( + "{} is not in {1}. Please add it to {1} to use installed binaries.", + &*BIN_PATH.to_string_lossy().color(Color::Blue), + "PATH".color(Color::BrightGreen).bold() ); } @@ -39,9 +41,7 @@ pub async fn init() -> Result<()> { portable_config, } => { if portable.is_some() && (portable_home.is_some() || portable_config.is_some()) { - eprintln!( - "Error: --portable cannot be used with --portable-home or --portable-config" - ); + error!("--portable cannot be used with --portable-home or --portable-config"); std::process::exit(1); } registry diff --git a/src/main.rs b/src/main.rs index 7a1e47e..c68b940 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ #![allow(clippy::needless_return)] -use soar::init; +use soar::{core::color::Color, core::color::ColorExt, error, init}; #[tokio::main] async fn main() { if let Err(e) = init().await { - eprintln!("{}", e); + error!("{}", e); } } diff --git a/src/misc/download.rs b/src/misc/download.rs index 7824850..d4f95f8 100644 --- a/src/misc/download.rs +++ b/src/misc/download.rs @@ -6,7 +6,13 @@ use futures::StreamExt; use reqwest::Url; use tokio::fs; -use crate::core::util::format_bytes; +use crate::{ + core::{ + color::{Color, ColorExt}, + util::format_bytes, + }, + error, success, +}; fn extract_filename(url: &str) -> String { Path::new(url) @@ -31,8 +37,8 @@ async fn download(url: &str) -> Result<()> { if !response.status().is_success() { return Err(anyhow::anyhow!( "Error fetching {} [{}]", - url, - response.status() + url.color(Color::Blue), + response.status().color(Color::Red) )); } @@ -41,8 +47,8 @@ async fn download(url: &str) -> Result<()> { println!( "Downloading file from {} [{}]", - url, - format_bytes(response.content_length().unwrap_or_default()) + url.color(Color::Blue), + format_bytes(response.content_length().unwrap_or_default()).color(Color::Yellow) ); let mut stream = response.bytes_stream(); @@ -58,7 +64,7 @@ async fn download(url: &str) -> Result<()> { fs::set_permissions(&filename, Permissions::from_mode(0o755)).await?; } - println!("Downloaded {}", filename); + success!("Downloaded {}", filename.color(Color::Blue)); Ok(()) } @@ -68,7 +74,7 @@ pub async fn download_and_save(links: &[String]) -> Result<()> { if let Ok(url) = Url::parse(link) { download(url.as_str()).await?; } else { - eprintln!("{} is not a valid URL", link); + error!("{} is not a valid URL", link.color(Color::Blue)); }; } diff --git a/src/registry/fetcher.rs b/src/registry/fetcher.rs index 93e36f9..f0e5fea 100644 --- a/src/registry/fetcher.rs +++ b/src/registry/fetcher.rs @@ -5,6 +5,7 @@ use serde::Deserialize; use tokio::fs; use crate::core::{ + color::{Color, ColorExt}, config::Repository, util::{download, get_platform}, }; @@ -76,9 +77,12 @@ impl RegistryFetcher { .context("Failed to create registry directory")?; } - fs::write(&path, &content) - .await - .with_context(|| format!("Failed to write registry for {}", repository.name))?; + fs::write(&path, &content).await.with_context(|| { + format!( + "Failed to write registry for {}", + repository.name.clone().color(Color::Yellow) + ) + })?; Ok(content) } diff --git a/src/registry/installed.rs b/src/registry/installed.rs index 16b9e77..b8eedf3 100644 --- a/src/registry/installed.rs +++ b/src/registry/installed.rs @@ -7,6 +7,7 @@ use tokio::fs; use crate::{ core::{ + color::{Color, ColorExt}, constant::{BIN_PATH, INSTALL_TRACK_PATH}, util::{format_bytes, parse_size}, }, @@ -141,9 +142,10 @@ impl InstalledPackages { let content = rmp_serde::to_vec(&self) .context("Failed to serialize installed packages to MessagePack")?; - fs::write(&path, content) - .await - .context(format!("Failed to write to {}", path.to_string_lossy()))?; + fs::write(&path, content).await.context(format!( + "Failed to write to {}", + path.to_string_lossy().color(Color::Red) + ))?; Ok(()) } @@ -179,13 +181,15 @@ impl InstalledPackages { resolved_packages.iter().for_each(|package| { println!( - "- [{}] {}:{}-{} ({}) ({})", - package.root_path, - package.name, - package.name, - package.version, - package.timestamp.format("%Y-%m-%d %H:%M:%S"), - format_bytes(package.size) + "- [{}] {1}:{1}-{2} ({3}) ({4})", + package.root_path.clone().color(Color::BrightGreen), + package.name.clone().color(Color::Blue), + package.version.clone().color(Color::Green), + package + .timestamp + .format("%Y-%m-%d %H:%M:%S") + .color(Color::Yellow), + format_bytes(package.size).color(Color::Magenta) ); match package.root_path { @@ -200,22 +204,27 @@ impl InstalledPackages { println!( "{:<4} base: {} ({})", "", - total_base.0, + total_base.0.color(Color::BrightGreen), format_bytes(total_base.1) ); println!( "{:<4} bin: {} ({})", "", - total_bin.0, + total_bin.0.color(Color::BrightBlue), format_bytes(total_bin.1) ); println!( "{:<4} pkg: {} ({})", "", - total_pkg.0, + total_pkg.0.color(Color::BrightRed), format_bytes(total_pkg.1) ); - println!("{:<2} Total: {} ({})", "", total.0, format_bytes(total.1)); + println!( + "{:<2} Total: {} ({})", + "", + total.0.color(Color::BrightYellow), + format_bytes(total.1) + ); Ok(()) } @@ -244,7 +253,7 @@ impl InstalledPackages { if xattr::get_deref(symlink_path, "user.managed_by")?.as_deref() != Some(b"soar") { return Err(anyhow::anyhow!( "{} is not managed by soar", - symlink_path.to_string_lossy() + symlink_path.to_string_lossy().color(Color::Blue) )); } fs::remove_file(symlink_path).await?; @@ -254,8 +263,8 @@ impl InstalledPackages { .await .context(format!( "Failed to link {} to {}", - install_path.to_string_lossy(), - symlink_path.to_string_lossy() + install_path.to_string_lossy().color(Color::Blue), + symlink_path.to_string_lossy().color(Color::Blue) ))?; } else { return Err(anyhow::anyhow!("NOT_INSTALLED")); diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 94a415f..b094fdb 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -10,7 +10,13 @@ use serde::Deserialize; use storage::{PackageStorage, RepositoryPackages}; use tokio::sync::Mutex; -use crate::core::config::CONFIG; +use crate::{ + core::{ + color::{Color, ColorExt}, + config::CONFIG, + }, + error, info, success, +}; mod fetcher; pub mod installed; @@ -57,7 +63,7 @@ impl PackageRegistry { let packages = match RepositoryPackages::deserialize(&mut de) { Ok(packages) => packages, Err(_) => { - eprintln!("Registry is invalid. Refetching..."); + error!("Registry is invalid. Refetching..."); let content = fetcher.execute(repo).await?; let mut de = rmp_serde::Deserializer::new(&content[..]); RepositoryPackages::deserialize(&mut de)? @@ -124,8 +130,8 @@ impl PackageRegistry { println!( "[{}] [{}] {}: {}", installed, - pkg.root_path, - pkg.package.full_name('/'), + pkg.root_path.clone().color(Color::BrightGreen), + pkg.package.full_name('/').color(Color::Blue), pkg.package.description, ); }); @@ -145,37 +151,43 @@ impl PackageRegistry { for pkg in result { let installed_pkg = installed_guard.find_package(&pkg); let print_data = |key: &str, value: &dyn Display| { - println!("{:<16}: {}", key, value); + println!("{}: {}", key, value); }; - let data: Vec<(&str, &dyn Display)> = vec![ - ("Root Path", &pkg.root_path), - ("Name", &pkg.package.name), - ("Binary", &pkg.package.bin_name), - ("Description", &pkg.package.description), - ("Version", &pkg.package.version), - ("Download URL", &pkg.package.download_url), - ("Size", &pkg.package.size), - ("Checksum", &pkg.package.bsum), - ("Build Date", &pkg.package.build_date), - ("Build Log", &pkg.package.build_log), - ("Build Script", &pkg.package.build_script), - ("Category", &pkg.package.category), - ("Extra Bins", &pkg.package.extra_bins), + let root_path = pkg.root_path.clone().to_string(); + let data: Vec<(String, &str)> = vec![ + ("Root Path".color(Color::Blue), &root_path), + ("Name".color(Color::Green), &pkg.package.name), + ("Binary".color(Color::Red), &pkg.package.bin_name), + ("Description".color(Color::Yellow), &pkg.package.description), + ("Version".color(Color::Magenta), &pkg.package.version), + ( + "Download URL".color(Color::Green), + &pkg.package.download_url, + ), + ("Size".color(Color::Blue), &pkg.package.size), + ("Checksum".color(Color::Yellow), &pkg.package.bsum), + ("Build Date".color(Color::Magenta), &pkg.package.build_date), + ("Build Log".color(Color::Red), &pkg.package.build_log), + ("Build Script".color(Color::Blue), &pkg.package.build_script), + ("Category".color(Color::Yellow), &pkg.package.category), + ("Extra Bins".color(Color::Green), &pkg.package.extra_bins), ]; data.iter().for_each(|(k, v)| { - print_data(k, v); + if !v.is_empty() && v != &"null" { + print_data(k.as_str(), v); + } }); if let Some(installed) = installed_pkg { print_data( - "Install Path", + &"Install Path".color(Color::Magenta), &pkg.package .get_install_path(&installed.checksum) .to_string_lossy(), ); print_data( - "Install Date", + &"Install Date".color(Color::Red), &installed.timestamp.format("%Y-%m-%d %H:%M:%S"), ); } @@ -204,7 +216,10 @@ impl PackageRegistry { "base" => Ok(Some(RootPath::Base)), "bin" => Ok(Some(RootPath::Bin)), "pkg" => Ok(Some(RootPath::Pkg)), - _ => Err(anyhow::anyhow!("Invalid root path: {}", rp)), + _ => Err(anyhow::anyhow!( + "Invalid root path: {}", + rp.color(Color::BrightGreen) + )), }, None => Ok(None), }?; @@ -223,14 +238,13 @@ impl PackageRegistry { "-" }; println!( - "[{}] [{}] {}{}:{}-{} ({})", - install_prefix, - resolved_package.root_path, - variant_prefix, - package.name, - package.name, - package.version, - package.size + "[{0}] [{1}] {2}{3}:{3}-{4} ({5})", + install_prefix.color(Color::Red), + resolved_package.root_path.color(Color::BrightGreen), + variant_prefix.color(Color::Blue), + package.name.color(Color::Blue), + package.version.color(Color::Green), + package.size.color(Color::Magenta) ); } Ok(()) @@ -251,12 +265,15 @@ impl PackageRegistry { drop(installed_guard); match result { Ok(_) => { - println!("{} linked to binary path", package_name); + success!( + "{} is linked to binary path", + package_name.color(Color::Blue) + ); Ok(()) } Err(e) => { if e.to_string() == "NOT_INSTALLED" { - println!("Package is not yet installed."); + error!("Package is not yet installed."); let package_name = resolved_package.package.full_name('/'); self.storage .install_packages( @@ -280,16 +297,16 @@ impl PackageRegistry { } pub fn select_package_variant(packages: &[ResolvedPackage]) -> Result<&ResolvedPackage> { - println!( + info!( "Multiple packages available for {}", - packages[0].package.name + packages[0].package.name.clone().color(Color::Blue) ); for (i, package) in packages.iter().enumerate() { println!( " [{}] [{}] {}: {}", i + 1, - package.root_path, - package.package.full_name('/'), + package.root_path.clone().color(Color::BrightGreen), + package.package.full_name('/').color(Color::Blue), package.package.description ); } @@ -303,7 +320,7 @@ pub fn select_package_variant(packages: &[ResolvedPackage]) -> Result<&ResolvedP match input.trim().parse::() { Ok(n) if n > 0 && n <= packages.len() => break n - 1, - _ => println!("Invalid selection, please try again."), + _ => error!("Invalid selection, please try again."), } }; println!(); diff --git a/src/registry/package/appimage.rs b/src/registry/package/appimage.rs index 51f421f..4ca9dea 100644 --- a/src/registry/package/appimage.rs +++ b/src/registry/package/appimage.rs @@ -10,9 +10,13 @@ use backhand::{kind::Kind, FilesystemReader, InnerNode, Node, SquashfsFileReader use image::{imageops::FilterType, DynamicImage, GenericImageView}; use tokio::{fs, try_join}; -use crate::core::{ - constant::{BIN_PATH, PACKAGES_PATH}, - util::{download, home_data_path}, +use crate::{ + core::{ + color::{Color, ColorExt}, + constant::{BIN_PATH, PACKAGES_PATH}, + util::{download, home_data_path}, + }, + error, info, }; use super::Package; @@ -64,7 +68,7 @@ fn normalize_image(image: DynamicImage) -> DynamicImage { let (new_width, new_height) = find_nearest_supported_dimension(width, height); if (width, height) != (new_width, new_height) { - println!( + info!( "Resizing image from {}x{} to {}x{}", width, height, new_width, new_height ); @@ -89,7 +93,10 @@ fn is_appimage(file: &mut BufReader) -> bool { async fn create_symlink(from: &Path, to: &Path) -> Result<()> { if to.exists() { if to.read_link().is_ok() && !to.read_link()?.starts_with(&*PACKAGES_PATH) { - eprintln!("{} is not managed by soar", to.to_string_lossy()); + error!( + "{} is not managed by soar", + to.to_string_lossy().color(Color::Blue) + ); return Ok(()); } fs::remove_file(to).await?; @@ -102,7 +109,10 @@ async fn create_symlink(from: &Path, to: &Path) -> Result<()> { async fn remove_link(path: &Path) -> Result<()> { if path.exists() { if path.read_link().is_ok() && !path.read_link()?.starts_with(&*PACKAGES_PATH) { - eprintln!("{} is not managed by soar", path.to_string_lossy()); + error!( + "{} is not managed by soar", + path.to_string_lossy().color(Color::Blue) + ); return Ok(()); } fs::remove_file(path).await?; @@ -167,7 +177,7 @@ pub async fn extract_appimage(package: &Package, file_path: &Path) -> Result<()> .await?; } } - Err(e) => eprintln!("Failed to extract {}: {}", node_path, e), + Err(e) => error!("Failed to extract {}: {}", node_path.color(Color::Blue), e), } } } @@ -238,7 +248,7 @@ async fn process_icon(output_path: &Path, name: &str, data_path: &Path) -> Resul if let Some(parent) = final_path.parent() { fs::create_dir_all(parent).await.context(anyhow::anyhow!( "Failed to create icon directory at {}", - parent.to_string_lossy() + parent.to_string_lossy().color(Color::Blue) ))?; } create_symlink(output_path, &final_path).await?; @@ -281,7 +291,7 @@ async fn process_desktop( if let Some(parent) = final_path.parent() { fs::create_dir_all(parent).await.context(anyhow::anyhow!( "Failed to create desktop files directory at {}", - parent.to_string_lossy() + parent.to_string_lossy().color(Color::Blue) ))?; } @@ -334,31 +344,30 @@ pub async fn setup_portable_dir( let pkg_home = package_path.with_extension("home"); let (portable_home, portable_config) = if let Some(portable) = portable { - ( - Some(portable.join(bin_name).with_extension("home")), - Some(portable.join(bin_name).with_extension("config")), - ) + (Some(portable.clone()), Some(portable.clone())) } else { (portable_home, portable_config) }; if let Some(portable_home) = portable_home { + let portable_home = portable_home.join(bin_name).with_extension("home"); fs::create_dir_all(&portable_home) .await .context(anyhow::anyhow!( "Failed to create or access directory at {}", - &portable_home.to_string_lossy() + &portable_home.to_string_lossy().color(Color::Blue) ))?; create_symlink(&portable_home, &pkg_home).await?; } else { fs::create_dir(&pkg_home).await?; } if let Some(portable_config) = portable_config { + let portable_config = portable_config.join(bin_name).with_extension("config"); fs::create_dir_all(&portable_config) .await .context(anyhow::anyhow!( "Failed to create or access directory at {}", - &portable_config.to_string_lossy() + &portable_config.to_string_lossy().color(Color::Blue) ))?; create_symlink(&portable_config, &pkg_config).await?; } else { diff --git a/src/registry/package/install.rs b/src/registry/package/install.rs index 3646f07..d9c05d9 100644 --- a/src/registry/package/install.rs +++ b/src/registry/package/install.rs @@ -6,9 +6,11 @@ use tokio::{fs, io::AsyncWriteExt, sync::Mutex}; use crate::{ core::{ + color::{Color, ColorExt}, constant::{BIN_PATH, PACKAGES_PATH}, util::{calculate_checksum, format_bytes, validate_checksum}, }, + error, registry::{ installed::InstalledPackages, package::{ @@ -16,6 +18,7 @@ use crate::{ RootPath, }, }, + warn, }; use super::ResolvedPackage; @@ -56,22 +59,27 @@ impl Installer { .await .is_installed(&self.resolved_package); - let prefix = format!("[{}/{}] {}", idx + 1, total, package.full_name('/')); + let prefix = format!( + "[{}/{}] {}", + (idx + 1).color(Color::Green), + total.color(Color::Cyan), + package.full_name('/').color(Color::BrightBlue) + ); if !force && is_installed { - println!("{}: Package is already installed", prefix); + error!("{}: Package is already installed", prefix); return Err(anyhow::anyhow!("")); } if is_installed && !is_update { - println!("{}: Reinstalling package", prefix); + warn!("{}: Reinstalling package", prefix); } if let Some(parent) = self.temp_path.parent() { fs::create_dir_all(parent).await.context(format!( "{}: Failed to create temp directory {}", prefix, - self.temp_path.to_string_lossy() + self.temp_path.to_string_lossy().color(Color::Blue) ))?; } @@ -97,14 +105,14 @@ impl Installer { println!( "{}: Downloading package [{}]", prefix, - format_bytes(total_size) + format_bytes(total_size).color(Color::Yellow) ); if !response.status().is_success() { return Err(anyhow::anyhow!( - "{} Download failed with status code {:?}", + "{} Download failed {:?}", prefix, - response.status(), + response.status().color(Color::Red), )); } @@ -126,9 +134,9 @@ impl Installer { } if package.bsum == "null" { - eprintln!( + error!( "Missing checksum for {}. Installing anyway.", - package.full_name('/') + package.full_name('/').color(Color::BrightBlue) ); } else { let result = validate_checksum(&package.bsum, &self.temp_path).await; @@ -155,7 +163,7 @@ impl Installer { fs::create_dir_all(parent).await.context(format!( "{}: Failed to create install directory {}", prefix, - self.install_path.to_string_lossy() + self.install_path.to_string_lossy().color(Color::Blue) ))?; } @@ -183,9 +191,13 @@ impl Installer { println!("{}: Installed package.", prefix); if !package.note.is_empty() { println!( - "{}: [Note] {}", + "{}: [{}] {}", prefix, - package.note.replace("
", "\n ") + "Note".color(Color::Magenta), + package + .note + .replace("
", "\n ") + .color(Color::BrightYellow) ); } @@ -216,13 +228,13 @@ impl Installer { if let Some(path_owner) = installed_guard.reverse_package_search(link.strip_prefix(&*PACKAGES_PATH)?) { - println!( - "Warning: The package {} owns the binary {}", + warn!( + "The package {} owns the binary {}", path_owner.name, &package.bin_name ); print!( "Do you want to switch to {} (y/N)? ", - package.full_name('/') + package.full_name('/').color(Color::Blue) ); std::io::stdout().flush()?; diff --git a/src/registry/package/mod.rs b/src/registry/package/mod.rs index f61e6dc..3a77577 100644 --- a/src/registry/package/mod.rs +++ b/src/registry/package/mod.rs @@ -12,7 +12,13 @@ use remove::Remover; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; -use crate::core::constant::PACKAGES_PATH; +use crate::{ + core::{ + color::{Color, ColorExt}, + constant::PACKAGES_PATH, + }, + error, +}; use super::installed::InstalledPackages; @@ -126,7 +132,7 @@ pub fn parse_package_query(query: &str) -> PackageQuery { "bin" => Some(RootPath::Bin), "pkg" => Some(RootPath::Pkg), _ => { - eprintln!("Invalid root path provided for {}", query); + error!("Invalid root path provided for {}", query.color(Color::Red)); std::process::exit(-1); } }, diff --git a/src/registry/package/remove.rs b/src/registry/package/remove.rs index 997daad..7bc3ed4 100644 --- a/src/registry/package/remove.rs +++ b/src/registry/package/remove.rs @@ -4,8 +4,12 @@ use anyhow::{Context, Result}; use tokio::fs; use crate::{ - core::constant::BIN_PATH, + core::{ + color::{Color, ColorExt}, + constant::BIN_PATH, + }, registry::{installed::InstalledPackages, package::appimage::remove_applinks}, + success, }; use super::ResolvedPackage; @@ -27,8 +31,8 @@ impl Remover { let Some(installed) = installed else { return Err(anyhow::anyhow!( "Package {}-{} is not installed.", - package.full_name('/'), - package.version + package.full_name('/').color(Color::Blue), + package.version.clone().color(Color::Green) )); }; @@ -41,7 +45,10 @@ impl Remover { .unregister_package(&self.resolved_package) .await?; - println!("Package {} removed successfully.", package.full_name('/')); + success!( + "Package {} removed successfully.", + package.full_name('/').color(Color::Blue) + ); Ok(()) } @@ -63,7 +70,7 @@ impl Remover { if install_dir.exists() { fs::remove_dir_all(&install_dir).await.context(format!( "Failed to remove package file: {}", - install_dir.to_string_lossy() + install_dir.to_string_lossy().color(Color::Blue) ))?; } diff --git a/src/registry/package/run.rs b/src/registry/package/run.rs index 620c1c8..a855f4e 100644 --- a/src/registry/package/run.rs +++ b/src/registry/package/run.rs @@ -4,7 +4,13 @@ use anyhow::{Context, Result}; use futures::StreamExt; use tokio::{fs, io::AsyncWriteExt}; -use crate::core::util::{format_bytes, validate_checksum}; +use crate::{ + core::{ + color::{Color, ColorExt}, + util::{format_bytes, validate_checksum}, + }, + error, info, warn, +}; use super::ResolvedPackage; @@ -34,10 +40,13 @@ impl Runner { if xattr::get(&self.install_path, "user.managed_by")?.as_deref() != Some(b"soar") { return Err(anyhow::anyhow!( "Path {} is not managed by soar. Exiting.", - self.install_path.to_string_lossy() + self.install_path.to_string_lossy().color(Color::Blue) )); } else { - println!("Found existing cache for {}", package_name); + info!( + "Found existing cache for {}", + package_name.color(Color::Blue) + ); return self.run().await; } } @@ -62,15 +71,15 @@ impl Runner { .unwrap_or(0); println!( "{}: Downloading package [{}]", - package_name, - format_bytes(total_size) + package_name.color(Color::Blue), + format_bytes(total_size).color(Color::Yellow) ); if !response.status().is_success() { return Err(anyhow::anyhow!( - "{}: Download failed with status code {:?}", - package_name, - response.status() + "{}: Download failed {:?}", + package_name.color(Color::Blue), + response.status().color(Color::Red) )); } @@ -83,27 +92,33 @@ impl Runner { .await .context(format!( "{}: Failed to open temp file for writing", - package_name + package_name.color(Color::Blue) ))?; let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { - let chunk = chunk.context(format!("{}: Failed to read chunk", package_name))?; + let chunk = chunk.context(format!( + "{}: Failed to read chunk", + package_name.color(Color::Blue) + ))?; file.write_all(&chunk).await?; } file.flush().await?; } if package.bsum == "null" { - eprintln!( + warn!( "Missing checksum for {}. Installing anyway.", - package.full_name('/') + package.full_name('/').color(Color::Blue) ); } else { let result = validate_checksum(&package.bsum, &self.temp_path).await; if result.is_err() { - eprintln!("\n{}: Checksum verification failed.", package_name); + error!( + "\n{}: Checksum verification failed.", + package_name.color(Color::Blue) + ); } } diff --git a/src/registry/package/update.rs b/src/registry/package/update.rs index e403628..b9eca66 100644 --- a/src/registry/package/update.rs +++ b/src/registry/package/update.rs @@ -1,7 +1,12 @@ use anyhow::Result; use tokio::sync::MutexGuard; -use crate::registry::{installed::InstalledPackages, PackageRegistry}; +use crate::{ + core::color::{Color, ColorExt}, + error, + registry::{installed::InstalledPackages, PackageRegistry}, + success, +}; use super::{parse_package_query, PackageQuery, ResolvedPackage}; @@ -58,15 +63,15 @@ impl Updater { packages_to_update.push(package); } } else { - println!( + error!( "Package {} is not installed.", - package.package.full_name('/') + package.package.full_name('/').color(Color::Blue) ); } } if packages_to_update.is_empty() { - eprintln!("No updates available"); + error!("No updates available"); } else { let mut update_count = 0; for (idx, package) in packages_to_update.iter().enumerate() { @@ -84,7 +89,10 @@ impl Updater { .await?; update_count += 1; } - println!("{} packages updated.", update_count); + success!( + "{} packages updated.", + update_count.color(Color::BrightMagenta) + ); } Ok(()) diff --git a/src/registry/storage.rs b/src/registry/storage.rs index ff5b257..c985862 100644 --- a/src/registry/storage.rs +++ b/src/registry/storage.rs @@ -18,13 +18,16 @@ use tokio::{ use crate::{ core::{ + color::{Color, ColorExt}, config::CONFIG, util::{build_path, format_bytes, get_platform, home_cache_path}, }, + error, registry::{ installed::InstalledPackages, package::{parse_package_query, ResolvedPackage}, }, + success, warn, }; use super::{ @@ -122,7 +125,7 @@ impl PackageStorage { ) .await { - eprintln!("{}", e); + error!("{}", e); } else { ic.fetch_add(1, Ordering::Relaxed); }; @@ -150,16 +153,16 @@ impl PackageStorage { ) .await { - eprintln!("{}", e); + error!("{}", e); } else { installed_count.fetch_add(1, Ordering::Relaxed); }; } } - println!( + success!( "Installed {}/{} packages", - installed_count.load(Ordering::Relaxed), - resolved_packages.len() + installed_count.load(Ordering::Relaxed).color(Color::Blue), + resolved_packages.len().color(Color::BrightBlue) ); Ok(()) } @@ -342,16 +345,16 @@ impl PackageStorage { if !response.status().is_success() { return Err(anyhow::anyhow!( "Error fetching log from {} [{}]", - url, - response.status() + url.color(Color::Blue), + response.status().color(Color::Red) )); } let content_length = response.content_length().unwrap_or_default(); if content_length > 1_048_576 { - print!( + warn!( "The log file is too large ({}). Do you really want to download and view it (y/N)? ", - format_bytes(content_length) + format_bytes(content_length).color(Color::Magenta) ); std::io::stdout().flush()?; @@ -366,8 +369,8 @@ impl PackageStorage { println!( "Fetching log from {} [{}]", - url, - format_bytes(response.content_length().unwrap_or_default()) + url.color(Color::Blue), + format_bytes(response.content_length().unwrap_or_default()).color(Color::Magenta) ); let mut stream = response.bytes_stream();