diff --git a/Cargo.lock b/Cargo.lock index 319f1c8..95efa15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,7 +359,7 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zenity" -version = "1.3.1" +version = "1.3.2" dependencies = [ "crossterm", "rand", diff --git a/Cargo.toml b/Cargo.toml index a1397fb..37bdb0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" categories = ["command-line-utilities", "command-line-interface"] -description = "100+ Cool Animations and Support for Multiple Animations at Once (Yet Another Spinner Lib)" +description = "100+ spinner animations and Progress Bars and Support for Multiple Animations at Once" repository = "https://github.com/Arteiii/zenity" keywords = ["console", "animations", "cli", "spinner", "loading"] homepage = "https://arteiii.github.io" diff --git a/README.md b/README.md index d706e47..2fd3dae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # zenity (Yet Another Spinner Lib) -Elevate your Rust command-line interfaces with 100+ spinner animations and multiline support +Elevate your Rust command-line interfaces with 100+ spinner animations and Progress Bars + multiline support [![Publish to Crates](https://github.com/Arteiii/zenity/actions/workflows/publish_crate.yml/badge.svg)](https://github.com/Arteiii/zenity/actions/workflows/publish_crate.yml) [![Compile Rust](https://github.com/Arteiii/zenity/actions/workflows/release_examples.yml/badge.svg)](https://github.com/Arteiii/zenity/actions/workflows/release_examples.yml) @@ -13,6 +13,10 @@ Elevate your Rust command-line interfaces with 100+ spinner animations and multi ![](./images/rustrover64_tlGiHM9JP0.gif) +![progress bar](./images/rustrover64_WupAJU44Lu.gif) + +checkout the examples for this^^ + Do you often find yourself gazing into the void of your terminal, wondering if your computer has decided to take a coffee break without notifying you? diff --git a/examples/progress.rs b/examples/progress.rs new file mode 100644 index 0000000..8affb75 --- /dev/null +++ b/examples/progress.rs @@ -0,0 +1,52 @@ +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use rand::Rng; + +use zenity::animations::{ + frames::progress::ProgressBarFrames, + progress::{Bar, Progress}, +}; + +fn main() { + // I know that this is not the best solution I will rework it asap + // contributions welcome + + println!("test Header line"); + thread::sleep(Duration::from_secs(8)); + + let mut progress = Progress::default(); + + let progress1 = progress.add(Bar::default()); + + // TODO: create wrapper for this + let progress2 = progress.add(Bar { + frames: Arc::new(Mutex::new(ProgressBarFrames::rect())), + size: Arc::new(Mutex::new(70_usize)), + current: Arc::new(Mutex::new(0_usize)), + goal: Arc::new(Mutex::new(253_usize)), + }); + + let progress3 = progress.add(Bar { + frames: Arc::new(Mutex::new(ProgressBarFrames::hash())), + size: Arc::new(Mutex::new(7_usize)), + current: Arc::new(Mutex::new(0_usize)), + goal: Arc::new(Mutex::new(253_usize)), + }); + + progress.run_all(); + + let loading = 1_usize; + + for loading in loading..=253 { + progress.set(&progress1, &loading); + progress.set(&progress2, &loading); + progress.set(&progress3, &loading); + + let sleep_time = rand::thread_rng().gen_range(1..=70); + thread::sleep(Duration::from_millis(sleep_time)); + } + + thread::sleep(Duration::from_millis(1000)); +} diff --git a/images/rustrover64_WupAJU44Lu.gif b/images/rustrover64_WupAJU44Lu.gif new file mode 100644 index 0000000..c45dde9 Binary files /dev/null and b/images/rustrover64_WupAJU44Lu.gif differ diff --git a/src/animations/frames/mod.rs b/src/animations/frames/mod.rs index 6500f6a..5bc3b5b 100644 --- a/src/animations/frames/mod.rs +++ b/src/animations/frames/mod.rs @@ -1,3 +1,5 @@ //! stores predefined animations and the `Frames` struct +/// +pub mod progress; pub mod spinner; diff --git a/src/animations/frames/progress.rs b/src/animations/frames/progress.rs new file mode 100644 index 0000000..e192875 --- /dev/null +++ b/src/animations/frames/progress.rs @@ -0,0 +1,51 @@ +//! progressbar frames + +/// struct storing the data needed to render a ProgressBar +pub struct ProgressBarFrames { + /// begin string + pub begin: Vec<&'static str>, + + /// string to place on complete places + pub bar_complete_char: Vec<&'static str>, + + /// string to place on incomplete places + pub bar_incomplete_char: Vec<&'static str>, + + /// ending string + pub end: Vec<&'static str>, +} + +// TODO: add animations by adding +1 for each bar so you can have a wave animation and others + +impl ProgressBarFrames { + /// '=' as the complete char and '-' as the incomplete char + pub fn equal() -> Self { + Self { + begin: vec!["["], + bar_complete_char: vec!["="], + bar_incomplete_char: vec!["-"], + end: vec!["]"], + } + } + + /// '#' as the complete char and '.' as the incomplete char + pub fn hash() -> Self { + Self { + begin: vec!["["], + bar_complete_char: vec!["#"], + bar_incomplete_char: vec!["."], + end: vec!["]"], + } + } + /// '#' as the complete char and '.' as the incomplete char + pub fn rect() -> Self { + Self { + begin: vec![" "], + bar_complete_char: vec!["\u{25A0}"], + bar_incomplete_char: vec![" "], + end: vec![" "], + } + } + + // TODO: add more +} diff --git a/src/animations/frames/spinner.rs b/src/animations/frames/spinner.rs index ce129eb..fe95dec 100644 --- a/src/animations/frames/spinner.rs +++ b/src/animations/frames/spinner.rs @@ -1,6 +1,5 @@ //! Predefined Frames and other aniamtions - /// represents a collection of frames and their display speed, typically used for animations /// /// # Example diff --git a/src/animations/mod.rs b/src/animations/mod.rs index c6f8934..bd6b29d 100644 --- a/src/animations/mod.rs +++ b/src/animations/mod.rs @@ -3,4 +3,5 @@ pub(crate) mod animation; pub mod frames; +pub mod progress; pub mod spinner; diff --git a/src/animations/progress.rs b/src/animations/progress.rs new file mode 100644 index 0000000..19d3394 --- /dev/null +++ b/src/animations/progress.rs @@ -0,0 +1,161 @@ +//! mod for progress bars + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; + +use rand::Rng; + +use crate::animations::frames::progress::ProgressBarFrames; +use crate::terminal::{console_cursor, console_render}; + +/// bar struct encapsulating the loading bar data animation +pub struct Bar { + /// frames to use for animation + pub frames: Arc>, + + /// size of progress bar + pub size: Arc>, + + /// goal value + pub goal: Arc>, + + /// current value + pub current: Arc>, +} + +impl Default for Bar { + fn default() -> Self { + Bar { + frames: Arc::new(Mutex::new(ProgressBarFrames::equal())), + size: Arc::new(Mutex::new(31)), + goal: Arc::new(Mutex::new(253)), + current: Arc::new(Mutex::new(0)), + } + } +} + +/// struct holding multiple bars +pub struct Progress { + // TODO: instead of random ids go after creation and increment by one + // this would allow to render them line for line based on this and order them correctly + bar: Arc>>, + stop: Arc>, +} + +impl Default for Progress { + fn default() -> Self { + Self::new() + } +} + +impl Progress { + /// creates a new Progress instance + /// + /// ## Example + /// ``` + /// # use zenity::multi_spinner::MultiSpinner; + /// let _spinner = MultiSpinner::new(); + /// ``` + pub fn new() -> Self { + // console_cursor::reset_cursor(); + + console_cursor::save_hide_cursor(); + + Progress { + bar: Arc::new(Mutex::new(HashMap::new())), + stop: Arc::new(Mutex::new(false)), + } + } + + /// add a new progress bar + pub fn add(&self, bar: Bar) -> usize { + let mut rng = rand::thread_rng(); + let mut uid: usize; + + loop { + uid = rng.gen(); + if !self.bar.lock().unwrap().contains_key(&uid) { + break; + } + } + + self.bar.lock().unwrap().insert(uid, bar); + + uid + } + + /// set the current + /// + /// # Arguments + /// + /// * `uid` - the unique identifier of the progress bar + /// * `new_current` - the new value to set as the current progress + /// + /// **NOTE:** + /// - if the UID is invalid, this function does nothing + /// - this function locks the progress bar associated with the provided uid and updates its current value + pub fn set(&self, uid: &usize, new_current: &usize) { + if let Some(bar) = self.bar.lock().unwrap().get(uid) { + let mut current = bar.current.lock().unwrap(); + *current = *new_current; + } + } + + /// start each queued progressbar + pub fn run_all(&mut self) { + let bars = Arc::clone(&self.bar); + let stop = Arc::clone(&self.stop); + + thread::spawn(move || { + while !*stop.lock().unwrap() { + let mut rendered_frames = Vec::new(); + + for (_, spinner) in bars.lock().unwrap().iter() { + let frames = spinner.frames.lock().unwrap(); + let begin: &str = frames.begin[0]; + let end: &str = frames.end[0]; + let current_incomplete: &str = frames.bar_incomplete_char[0]; + let current_complete: &str = frames.bar_complete_char[0]; + + let size: usize = *spinner.size.lock().unwrap(); + let goal = *spinner.goal.lock().unwrap(); + let current: usize = *spinner.current.lock().unwrap(); + + // calculate percentage completion + let completion_percentage = (current as f64 / goal as f64) * 100.0; + + // calculate number of characters to represent the completion percentage + let complete_size = ((completion_percentage / 100.0) * size as f64) as usize; + let incomplete_size = size - complete_size; + + // Render the frame with the updated incomplete string and add it to the vector + let rendered_frame = format!( + "{begin}{}{}{end} {:.2}% | {}/{}", + current_complete.repeat(complete_size), + current_incomplete.repeat(incomplete_size), + completion_percentage, + current, + goal, + ); + rendered_frames.push(rendered_frame); + } + + // Join all the rendered frames from the vector + let combined_output = rendered_frames.join("\n"); + + // render the frame with the updated incomplete string + console_render::render_frame(&combined_output); + } + }); + } +} + +impl Drop for Progress { + /// stops the thread when the object is dropped + fn drop(&mut self) { + // cleanup methods + console_render::cleanup(); + console_cursor::reset_cursor(); + } +} diff --git a/src/animations/spinner.rs b/src/animations/spinner.rs index ffddd62..0ef7a3c 100644 --- a/src/animations/spinner.rs +++ b/src/animations/spinner.rs @@ -1,17 +1,15 @@ //! mod for multiline spinners use std::collections::HashMap; -use std::io::stdout; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use crate::helper::iterators; -use crossterm::style::Print; -use crossterm::{cursor, execute, terminal}; use rand::Rng; +use crate::helper::iterators; use crate::spinner::Frames; +use crate::terminal::{console_cursor, console_render}; /// spinner struct encapsulating the spinner animation pub struct Spinner { @@ -86,7 +84,6 @@ impl MultiSpinner { uid } - /// set text of a specific spinner /// /// if the uid is invalid this does nothing @@ -149,6 +146,8 @@ impl MultiSpinner { let stop = Arc::clone(&self.stop); thread::spawn(move || { + console_cursor::save_hide_cursor(); + let mut index = 1_usize; while !*stop.lock().unwrap() { @@ -176,19 +175,6 @@ impl MultiSpinner { }); } - /// helper function to output frame using crossterm - fn render_frame(frame: &str) { - execute!( - stdout(), - cursor::Hide, - cursor::MoveTo(0, 1), - cursor::SavePosition, - terminal::Clear(terminal::ClearType::FromCursorDown), - Print(frame), - ) - .unwrap(); - } - /// helper function to render frames to stdout fn render_frames(frames: &[Vec<&str>], index: usize, texts: &[String], should_stop: &[bool]) { let first_frame = iterators::balanced_iterator(index, frames); @@ -207,20 +193,15 @@ impl MultiSpinner { .collect::>() .join("\n"); - MultiSpinner::render_frame(&combined_string); + console_render::render_frame(&combined_string); } /// helper function to clean-up after animation stop fn cleanup(&mut self) { *self.stop.lock().unwrap() = true; - execute!( - stdout(), - cursor::MoveTo(0, 0), - terminal::Clear(terminal::ClearType::FromCursorDown), - cursor::Show, - ) - .unwrap(); + console_render::cleanup(); + console_cursor::reset_cursor(); } } diff --git a/src/lib.rs b/src/lib.rs index 33a779b..b8d454e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,8 @@ pub use crate::animations::frames::spinner; pub use crate::helper::colors::{combine_attributes, CliColorConfig}; pub mod animations; -mod helper; +pub(crate) mod helper; +pub(crate) mod terminal; /// `LoadingAnimation` is a struct that provides a straightforward interface for creating and managing customizable loading animations. /// diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..95e7520 --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,55 @@ +pub(crate) mod console_render { + use std::io::stdout; + + use crossterm::{cursor, execute, style, terminal}; + + // use crossterm::style::ContentStyle; + + pub fn render_frame(frame: &str) { + execute!(stdout(), cursor::RestorePosition, style::Print(frame),).unwrap(); + } + + // pub fn render_styled_frame(frame: &String, style: ContentStyle) { + // execute!( + // stdout(), + // cursor::RestorePosition, + // style::SetStyle(style), // set animation color + // style::Print(frame), + // style::ResetColor, // reset colors + // ) + // .unwrap(); + // } + + pub fn cleanup() { + execute!( + stdout(), + cursor::RestorePosition, + cursor::MoveToNextLine(2), + terminal::Clear(terminal::ClearType::FromCursorDown), + ) + .unwrap(); + } +} + +pub(crate) mod console_cursor { + use std::io::stdout; + + use crossterm::{cursor, execute, terminal}; + + /// sets the cursor to be hidden, moves it to the next line,saves its current position, + /// and clears the terminal screen from the cursor position down + pub fn save_hide_cursor() { + execute!( + stdout(), + cursor::Hide, + cursor::SavePosition, + terminal::Clear(terminal::ClearType::FromCursorDown), + ) + .unwrap(); + } + + /// resets the cursor to be shown and restores its saved position + pub fn reset_cursor() { + execute!(stdout(), cursor::RestorePosition, cursor::Show).unwrap(); + } +}