diff --git a/Cargo.toml b/Cargo.toml index 34c9a9b..fc33a19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,12 @@ test = true [features] -default = ["spinner", "progressbar", "menu"] +default = ["spinner", "progressbar", "menu", "log"] spinner = [] progressbar = [] -menu = [] +menu = ["spinner"] +log = [] [dependencies] @@ -38,6 +39,9 @@ crossterm = "0.27.0" supports-color = "3.0.0" lazy_static = "1.4.0" regex = "1.10.4" +log = { version = "0.4.21", features = ["std"] } +chrono = "0.4.38" + [dev-dependencies] rand = "0.8.5" diff --git a/README.md b/README.md index 857a389..b9fcc4f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Zenity svg logo

Yet Another Spinner Lib

-

Elevate your Rust command-line interfaces with 100+ spinner animations and Progress Bars + multiline support

+

Upgrade your Rust CLIs with 100+ spinner animations, progress bars, and multiline support, plus user input validation, logging, and automatic requirement checks

Publish to Crates diff --git a/examples/all.rs b/examples/all.rs index 3b5523e..50731ef 100644 --- a/examples/all.rs +++ b/examples/all.rs @@ -8,7 +8,8 @@ static TOTAL_ANIMATIONS: AtomicUsize = AtomicUsize::new(0); macro_rules! test_predefined_animation { ($animation:expr, $text:expr) => {{ - let custom = MultiSpinner::new($animation); + let custom = MultiSpinner::new(); + custom.add($animation); custom.run_all(); custom.set_text(&custom.get_last(), $text.to_string()); sleep(Duration::from_secs(5)); diff --git a/examples/custom_frames.rs b/examples/custom_frames.rs index 96d4842..d1c52b3 100644 --- a/examples/custom_frames.rs +++ b/examples/custom_frames.rs @@ -15,7 +15,8 @@ fn main() { }; // create a MultiSpinner instance using the new custom animation - let spinner = MultiSpinner::new(custom_frames); + let spinner = MultiSpinner::new(); + spinner.add(custom_frames); spinner.run_all(); // wait for 5 seconds to showcase the loading animation with the custom animation diff --git a/examples/log.rs b/examples/log.rs new file mode 100644 index 0000000..2b16ce7 --- /dev/null +++ b/examples/log.rs @@ -0,0 +1,42 @@ +use log::Level; + +use zenity::log::Logger; + +mod foo { + mod bar { + pub fn run() { + log::error!("[bar] error"); + log::warn!("[bar] warn"); + log::info!("[bar] info"); + log::debug!("[bar] debug"); + log::trace!("[bar] trace"); + } + } + + pub fn run() { + log::error!("[foo] error"); + log::warn!("[foo] warn"); + log::info!("[foo] info"); + log::debug!("[foo] debug"); + log::trace!("[foo] trace"); + bar::run(); + } +} + +fn main() { + // Set the custom logger as the global logger + Logger::new() + .with_env("TEST_LEVEL") + .with_arg() + .set_log_level(Level::Trace) + .init() + .unwrap(); + + // Now you can use log::debug! to output messages + log::error!("[root] This is a error message"); + log::warn!("[root] This is a warn message"); + log::info!("[root] This is a info message"); + log::debug!("[root] This is a debug message"); + log::trace!("[root] This is a trace message"); + foo::run(); +} diff --git a/examples/requirements.rs b/examples/requirements.rs new file mode 100644 index 0000000..d5f31e6 --- /dev/null +++ b/examples/requirements.rs @@ -0,0 +1,8 @@ +use zenity::menu::requirements; + +fn main() { + match requirements::verify_requirements(vec!["uidmap", "bridge-utils"]) { + Ok(_) => println!("All required packages are installed."), + Err(err) => eprintln!("Error verifying requirements: {}", err), + } +} diff --git a/src/color.rs b/src/color.rs index e5727c9..f499ada 100644 --- a/src/color.rs +++ b/src/color.rs @@ -135,20 +135,24 @@ impl CliColorConfig { } } + /// parse args to check for --color=always|auto|never + /// parse args to check for --color=always|auto|never fn parse_arguments(args: &[String]) -> ColorOption { - if args.len() > 1 { - let arg = &args[1]; - return match arg.as_str() { - "--color=always" => ColorOption::Always, - "--color=never" => ColorOption::Never, - _ => { - // removed error message print - ColorOption::Auto - } - }; + for arg in args.iter() { + if arg.starts_with("--color=") { + return match arg.split('=').nth(1) { + Some("always") => ColorOption::Always, + Some("auto") => ColorOption::Auto, + Some("never") => ColorOption::Never, + _ => { + ColorOption::Auto // Default to Auto in case of invalid option + } + }; + } } + // If no color option is found, default to Auto ColorOption::Auto } diff --git a/src/lib.rs b/src/lib.rs index c5159ae..c9443f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,6 +92,9 @@ pub mod progress; #[cfg(feature = "spinner")] pub mod spinner; +#[cfg(feature = "log")] +pub mod log; + // Crate pub(crate) mod iterators; pub(crate) mod terminal; diff --git a/src/log/mod.rs b/src/log/mod.rs new file mode 100644 index 0000000..d721f03 --- /dev/null +++ b/src/log/mod.rs @@ -0,0 +1,277 @@ +//! The `log` module provides a customizable logger implementation. +//! +//! This logger allows customizing log output format and log levels, and it supports colorized output if enabled. +//! +//! # Examples +//! +//! ``` +//! use zenity::log::Logger; +//! use log::Level; +//! +//! // Initialize logger with default settings +//! let logger = Logger::new().init().unwrap(); +//! ``` +//! ``` +//! # use zenity::log::Logger; +//! # use log::Level; +//! # +//! // Set log level based on environment variable +//! let logger = Logger::new().with_env("LOG_LEVEL").init().unwrap(); +//! ``` +//! ``` +//! # use zenity::log::Logger; +//! # use log::Level; +//! # +//! // Set log level based on command-line argument +//! let logger = Logger::new().with_arg().init().unwrap(); +//! ``` +//! ``` +//! # use zenity::log::Logger; +//! # use log::Level; +//! # +//! // Set log level directly +//! let logger = Logger::new().set_log_level(Level::Debug).init().unwrap(); +//! ``` + +use std::env; +use std::io::{stdout, Write}; + +use chrono::Local; +use crossterm::queue; +use crossterm::style::{Attribute, ContentStyle}; +use log::{Level, Metadata, Record, SetLoggerError}; + +use crate::style::combine_attributes; +use crate::{color::ENABLE_COLOR, style, style::Color}; + +macro_rules! enable_color { + ($stdout:expr, $foreground_color:expr) => { + if *ENABLE_COLOR { + queue!( + $stdout, + style::SetStyle(ContentStyle { + foreground_color: Some($foreground_color), + background_color: None, + underline_color: None, + attributes: combine_attributes(&[&Attribute::Italic]), + }), + ) + .unwrap(); + } + }; +} + +// Define a custom logger +struct MyLogger; + +impl log::Log for MyLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + // This logger will be enabled for all levels based on the configured log level + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + // Customize the log output format here + let current_time = Local::now().format("%Y-%m-%d %H:%M:%S"); + let level_str = format!("{}", record.level()); + let padded_level_str = format!("{:^5}", level_str); // Center-align the level string within a width of 5 + let mut stdout = stdout(); + + enable_color!(stdout, Color::DarkGrey); + + queue!( + stdout, + style::Print(format!("{} ", current_time)), + style::ResetColor, // reset colors + ) + .unwrap(); + + if *ENABLE_COLOR { + let level_color = match record.level() { + Level::Error => ContentStyle { + foreground_color: Some(Color::Red), + background_color: None, + underline_color: None, + attributes: combine_attributes(&[&Attribute::Bold]), + }, + Level::Warn => ContentStyle { + foreground_color: Some(Color::Yellow), + background_color: None, + underline_color: None, + attributes: combine_attributes(&[&Attribute::Bold]), + }, + Level::Info => ContentStyle { + foreground_color: Some(Color::Green), + background_color: None, + underline_color: None, + attributes: combine_attributes(&[&Attribute::Bold]), + }, + Level::Debug => ContentStyle { + foreground_color: Some(Color::Blue), + background_color: None, + underline_color: None, + attributes: combine_attributes(&[&Attribute::Bold]), + }, + Level::Trace => ContentStyle { + foreground_color: Some(Color::Magenta), + background_color: None, + underline_color: None, + attributes: combine_attributes(&[&Attribute::Bold]), + }, + }; + + queue!(stdout, style::SetStyle(level_color),).unwrap(); + } + + queue!(stdout, style::Print(&padded_level_str), style::ResetColor,).unwrap(); + + enable_color!(stdout, Color::DarkGrey); + + queue!( + stdout, + style::Print(format!( + " ({}:{:?}):", + record.target(), + record.line().unwrap() + )), + style::ResetColor, + ) + .unwrap(); + + queue!( + stdout, + style::Print(format!(" {}", record.args())), + style::ResetColor, + ) + .unwrap(); + + queue!(stdout, style::Print("\n")).unwrap(); + + stdout.flush().unwrap(); + } + + fn flush(&self) {} +} + +/// Implement a builder pattern for initializing the logger +pub struct Logger { + log_level: Level, +} + +impl Default for Logger { + fn default() -> Self { + Self::new() + } +} + +impl Logger { + /// Constructs a new `Logger` with default log level `Info`. + /// + /// # Examples + /// + /// ``` + /// use zenity::log::Logger; + /// + /// let logger = Logger::new(); + /// ``` + pub fn new() -> Logger { + Logger { + log_level: Level::Info, + } + } + + /// Initializes the logger with the custom formatter and log level based on environment variable. + /// + /// # Examples + /// + /// ``` + /// use zenity::log::Logger; + /// + /// let logger = Logger::new().with_env("LOG_LEVEL").init().unwrap(); + /// ``` + /// + /// # Notes + /// + /// This method sets the logger and the log level based on the environment variable or defaults to `Info`. + pub fn init(self) -> Result<(), SetLoggerError> { + log::set_logger(&MyLogger)?; + log::set_max_level(self.log_level.to_level_filter()); + Ok(()) + } + + /// Sets log level based on the environment variable. + /// + /// # Examples + /// + /// ``` + /// use zenity::log::Logger; + /// + /// let logger = Logger::new().with_env("LOG_LEVEL"); + /// ``` + /// + /// # Notes + /// + /// This method checks the specified environment variable for the log level and updates if it's lower than the current level. + pub fn with_env(mut self, env_var: &str) -> Logger { + if let Ok(log_level) = env::var(env_var) { + if let Ok(level) = log_level.parse::() { + if level < self.log_level { + self.log_level = level; + } + } + } + self + } + + /// Sets log level based on the command-line argument if provided. + /// + /// # Examples + /// + /// ``` + /// use zenity::log::Logger; + /// + /// let logger = Logger::new().with_arg(); + /// ``` + /// + /// # Notes + /// + /// This method checks command-line arguments for the `--log-level=` option and updates the log level if provided and higher than the current level. + pub fn with_arg(mut self) -> Logger { + // Get command-line arguments + let args: Vec = env::args().collect(); + + // Check if the "--log-level=" option is provided in the arguments + if let Some(arg) = args.iter().find(|&arg| arg.starts_with("--log-level=")) { + // Extract the log level from the argument + if let Some(level_str) = arg.split('=').nth(1) { + if let Ok(level) = level_str.parse::() { + // Prefer the higher log level between the argument and the one from the environment variable + if level < self.log_level { + self.log_level = level; + } + } + } + } + self + } + + /// Sets log level. This overwrites the current value. + /// + /// # Examples + /// + /// ``` + /// use zenity::log::Logger; + /// use log::Level; + /// + /// let logger = Logger::new().set_log_level(Level::Debug); + /// ``` + /// + /// # Notes + /// + /// This method directly sets the log level, overwriting the current value. + pub fn set_log_level(mut self, level: Level) -> Logger { + self.log_level = level; + + self + } +} diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 5aa92b6..66561d6 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -3,6 +3,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; pub mod input; +pub mod requirements; pub(crate) fn handle_key_input(buffer: &mut String, force: &mut bool) -> bool { _handle_key_input(buffer, crossterm::event::read().unwrap(), force) diff --git a/src/menu/requirements/mod.rs b/src/menu/requirements/mod.rs new file mode 100644 index 0000000..b9b79d4 --- /dev/null +++ b/src/menu/requirements/mod.rs @@ -0,0 +1,99 @@ +//! The `requirements` module provides functions to verify that required packages are installed. +//! +//! # Examples +//! +//! ``` +//! use zenity::menu::requirements::verify_requirements; +//! +//! match verify_requirements(vec!["uidmap", "bridge-utils"]) { +//! Ok(_) => println!("All required packages are installed."), +//! Err(err) => eprintln!("Error verifying requirements: {}", err), +//! } +//! ``` + +use std::time::Duration; +use std::{io, thread}; + +use crossterm::style::Color; + +use crate::spinner; +use crate::spinner::Frames; +use crate::style::StyledString; + +mod pckgm; + +// TODO: add windows support + +struct LoadingReq { + spinner_id: usize, + name: String, +} + +/// Verifies that required packages are installed. +/// +/// This function checks if certain packages are installed on the system. +/// +/// # Examples +/// +/// ``` +/// use zenity::menu::requirements::verify_requirements; +/// +/// match verify_requirements(vec!["uidmap", "bridge-utils"]) { +/// Ok(_) => println!("All required packages are installed."), +/// Err(err) => eprintln!("Error verifying requirements: {}", err), +/// } +/// ``` +pub fn verify_requirements(packages_to_check: Vec<&str>) -> Result<(), io::Error> { + let mut reqs = Vec::new(); + let spinner = spinner::MultiSpinner::new(); + + for package in packages_to_check { + let spinner_id = spinner.add(Frames::dot_spinner1()); + spinner.set_styled_text( + &spinner_id, + StyledString::simple(package, Some(Color::Yellow), None, None), + ); + + reqs.push(LoadingReq { + spinner_id, + name: package.to_string(), + }); + } + + spinner.run_all(); + + // Check if each package is installed + for package in reqs { + spinner.set_styled_text( + &package.spinner_id, + StyledString::simple( + &format!("Checking if {} is installed...", &package.name), + Some(Color::Yellow), + None, + None, + ), + ); + + let (is_installed, msg) = match pckgm::is_package_installed(&package.name) { + Ok(_) => (true, format!("{} is installed\n", &package.name)), + Err(err) => (false, format!("{}", err)), + }; + + if is_installed { + spinner.set_styled_text( + &package.spinner_id, + StyledString::simple(&msg, Some(Color::Green), None, None), + ) + } else { + spinner.set_styled_text( + &package.spinner_id, + StyledString::simple(&msg, Some(Color::Red), None, None), + ) + } + spinner.stop(&package.spinner_id); + + thread::sleep(Duration::from_secs(3)); + } + + Ok(()) +} diff --git a/src/menu/requirements/pckgm.rs b/src/menu/requirements/pckgm.rs new file mode 100644 index 0000000..a9718f3 --- /dev/null +++ b/src/menu/requirements/pckgm.rs @@ -0,0 +1,83 @@ +use std::io; +use std::process::Command; + +use lazy_static::lazy_static; + +use crate::menu::requirements::pckgm; + +/// determine which package manager is available on the system +/// +/// NOTES: +/// - IDK if there is a better way to do this or maybe already a lib for this +fn detect_package_manager() -> Option<&'static str> { + if which("apt-get").is_some() { + Some("apt") + } else if which("yum").is_some() { + Some("yum") + } else if which("dnf").is_some() { + Some("dnf") + } else if which("pacman").is_some() { + Some("pacman") + } else if which("brew").is_some() { + Some("brew") + } else if which("zypper").is_some() { + Some("zypper") + } else if which("apk").is_some() { + Some("apk") + } else { + None + } +} + +/// Check if a command is available in the system's PATH +fn which(command: &str) -> Option { + match Command::new("which").arg(command).output() { + Ok(output) if output.status.success() => { + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } + _ => None, + } +} + +lazy_static! { + pub static ref PACKAGE_MANAGER: Option<&'static str> = detect_package_manager(); +} + +/// Check if a package is installed using the detected package manager +pub(crate) fn is_package_installed(package: &str) -> Result<(), io::Error> { + let package_manager = match *pckgm::PACKAGE_MANAGER { + Some(pm) => pm, + None => { + return Err(io::Error::new( + io::ErrorKind::Other, + "No package manager detected", + )) + } + }; + + let output = match package_manager { + "apt" => Command::new("dpkg-query").args(["-W", package]).output(), + "yum" => Command::new("rpm").args(["-q", package]).output(), + "dnf" => Command::new("dnf") + .args(["list", "installed", package]) + .output(), + "pacman" => Command::new("pacman").args(["-Q", package]).output(), + "brew" => Command::new("brew").args(["list", package]).output(), + "zypper" => Command::new("zypper").args(["se", "-i", package]).output(), + "apk" => Command::new("apk").args(["info", package]).output(), + _ => { + return Err(io::Error::new( + io::ErrorKind::Other, + "Unsupported package manager", + )) + } + }; + + match output { + Ok(output) if output.status.success() => Ok(()), + _ => Err(io::Error::new( + io::ErrorKind::Other, + format!("Package '{}' is not installed", package), + )), + } +} diff --git a/src/spinner/frames.rs b/src/spinner/frames.rs index a49fa26..bb4ee13 100644 --- a/src/spinner/frames.rs +++ b/src/spinner/frames.rs @@ -52,7 +52,8 @@ impl Default for Frames { /// ## Example /// ``` /// use zenity::spinner::{Frames, MultiSpinner}; - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::new(); + /// spinner.add(Frames::default()); /// ``` fn default() -> Self { Self::dots_simple_big3() @@ -67,7 +68,9 @@ impl AsRef for Frames { /// ``` /// use zenity::spinner::{Frames, MultiSpinner}; -/// let spinner = MultiSpinner::new(Frames::default()); +/// +/// let spinner = MultiSpinner::new(); +/// spinner.add(Frames::default()); /// ``` impl Frames { /// generates frames for spinner animation based on the provided pattern, inversion flag, and speed diff --git a/src/spinner/mod.rs b/src/spinner/mod.rs index ac52319..b3cb596 100644 --- a/src/spinner/mod.rs +++ b/src/spinner/mod.rs @@ -42,7 +42,7 @@ pub mod frames; /// use std::time::Duration; /// use zenity::spinner::{Frames, MultiSpinner}; /// -/// let spinner = MultiSpinner::new(Frames::dot_spinner11()); +/// let spinner = MultiSpinner::default(); /// spinner.run_all(); /// /// sleep(Duration::from_secs(4)); @@ -69,7 +69,8 @@ impl Default for MultiSpinner { /// let spinner = MultiSpinner::default(); /// ``` fn default() -> Self { - let spinner = Self::new(Frames::default()); + let spinner = Self::new(); + spinner.add(Frames::default()); spinner.run_all(); spinner @@ -80,8 +81,8 @@ impl Default for MultiSpinner { /// use std::time::Duration; /// use zenity::spinner::{Frames, MultiSpinner}; /// -/// let spinner = MultiSpinner::new(Frames::dot_spinner11()); -/// let spinner1 = spinner.get_last(); // get last created uid +/// let spinner = MultiSpinner::default(); +/// let spinner1 = spinner.get_last(); /// let spinner2 = spinner.add(Frames::default()); // this already returns the uid /// /// spinner.run_all(); @@ -100,19 +101,15 @@ impl MultiSpinner { /// /// ## Example /// ``` - /// use zenity::spinner::{MultiSpinner, Frames}; - /// let spinner = MultiSpinner::new(Frames::default()); + /// use zenity::spinner::MultiSpinner; + /// let spinner = MultiSpinner::new(); /// ``` - pub fn new(frames: Frames) -> Self { - let spinner = MultiSpinner { + pub fn new() -> Self { + MultiSpinner { spinner: Arc::new(Mutex::new(HashMap::new())), stop: Arc::new(Mutex::new(false)), show_line_number: Arc::new(Mutex::new(false)), - }; - - spinner.add(frames); - - spinner + } } /// create a new spinner @@ -126,7 +123,7 @@ impl MultiSpinner { /// use zenity::spinner::MultiSpinner; /// use zenity::spinner::Frames; /// - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::new(); /// /// spinner.add(Frames::aesthetic_load()); /// ``` @@ -150,7 +147,8 @@ impl MultiSpinner { /// use zenity::spinner::MultiSpinner; /// use zenity::spinner::Frames; /// - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::new(); + /// spinner.add(Frames::default()); /// /// // the return values is an id you will need to edit the spinner later on /// let spinner1_uid = spinner.get_last(); @@ -158,7 +156,12 @@ impl MultiSpinner { pub fn get_last(&self) -> usize { let spinner_map = self.spinner.lock().unwrap(); - // get the maximum key value (uid) from the spinner map + // Check if the spinner is empty + if spinner_map.is_empty() { + return 0; + } + + // Get the maximum key value (uid) from the spinner map spinner_map.keys().copied().max().unwrap() } @@ -172,7 +175,7 @@ impl MultiSpinner { /// use zenity::spinner::Frames; /// use zenity::style::StyledString; /// - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::default(); /// /// spinner.set_text(&spinner.get_last(),"example".to_string()); /// ``` @@ -193,7 +196,7 @@ impl MultiSpinner { /// use zenity::spinner::Frames; /// use zenity::style::StyledString; /// - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::default(); /// /// spinner.set_styled_text(&spinner.get_last(), /// StyledString::simple("test string", Some(Color::Red), Some(Color::Black), None)); @@ -212,7 +215,7 @@ impl MultiSpinner { /// use zenity::spinner::MultiSpinner; /// use zenity::spinner::Frames; /// - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::default(); /// /// spinner.stop(&spinner.get_last()); /// ``` @@ -232,7 +235,7 @@ impl MultiSpinner { /// use zenity::spinner::MultiSpinner; /// use zenity::spinner::Frames; /// - /// let spinner = MultiSpinner::new(Frames::default()); + /// let spinner = MultiSpinner::default(); /// /// spinner.show_line_number(); /// ``` @@ -248,7 +251,7 @@ impl MultiSpinner { /// use zenity::spinner::Frames; /// /// // make spinner mutable - /// let mut spinner = MultiSpinner::new(Frames::dots_simple_big1()); + /// let mut spinner = MultiSpinner::default(); /// /// // queue spinners for execution /// let spinner_num1 = spinner.get_last();