diff --git a/.github/workflows/publish_crate.yml b/.github/workflows/publish_crate.yml index d170132..c317738 100644 --- a/.github/workflows/publish_crate.yml +++ b/.github/workflows/publish_crate.yml @@ -1,4 +1,4 @@ -name: Update Cargo.toml Version and Publish to Crates +name: Publish to Crates on: push: diff --git a/.github/workflows/release_examples.yml b/.github/workflows/release_examples.yml index 7209347..bff8178 100644 --- a/.github/workflows/release_examples.yml +++ b/.github/workflows/release_examples.yml @@ -1,4 +1,4 @@ -name: Compile Rust Examples and Upload to Release +name: Compile Rust on: push: diff --git a/Cargo.lock b/Cargo.lock index 03df015..fd4b31c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "libc" version = "0.2.153" @@ -108,6 +119,61 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.9.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31e63ea85be51c423e52ba8f2e68a3efd53eed30203ee029dd09947333693e" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78674ef918c19451dbd250f8201f8619b494f64c9aa6f3adb28fd8a0f1f6da46" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc89dffba8377c5ec847d12bb41492bda235dba31a25e8b695cd0fe6589eb8c9" +dependencies = [ + "getrandom", + "zerocopy", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -159,12 +225,35 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "unicode-icons" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89c13b079e3553b48fb0ac71561aafb8a8778a69a6aa042c8d545f47dc3b870d" +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utils_arteii_rs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9c5823f79e81febf86830168f563c62cf006f6d34e71ada7586c5dc9b99f20" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -261,8 +350,30 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zenity" -version = "1.0.4" +version = "1.0.6" dependencies = [ "crossterm", + "rand", "unicode-icons", + "utils_arteii_rs", +] + +[[package]] +name = "zerocopy" +version = "0.8.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db678a6ee512bd06adf35c35be471cae2f9c82a5aed2b5d15e03628c98bddd57" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201585ea96d37ee69f2ac769925ca57160cef31acb137c16f38b02b76f4c1e62" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index e88f614..9098ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" categories = ["command-line-utilities", "command-line-interface"] -description = "easy to use cli animation lib (Yet Another Spinner Lib ) 81+ predefined" +description = "Elevate your Rust command-line interfaces with 81+ spinner animations and multiline support (Yet Another Spinner Lib)" repository = "https://github.com/Arteiii/zenity" keywords = ["console", "animations", "cli", "spinner", "loading"] homepage = "https://arteiii.github.io" @@ -16,6 +16,8 @@ homepage = "https://arteiii.github.io" [dependencies] crossterm = "0.27.0" +utils_arteii_rs = "0.1.0" +rand = "0.9.0-alpha.1" [dev-dependencies] unicode-icons = "1.0.1" diff --git a/README.md b/README.md index 30d6a0f..1a2d545 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ # zenity (Yet Another Spinner Lib) +Elevate your Rust command-line interfaces with 81+ spinner animations and 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 and Upload to Release](https://github.com/Arteiii/zenity/actions/workflows/release_examples.yml/badge.svg)](https://github.com/Arteiii/zenity/actions/workflows/release_examples.yml) -[![CodeFactor](https://www.codefactor.io/repository/github/arteiii/zenity/badge)](https://www.codefactor.io/repository/github/arteiii/zenity) - - ![Crates.io Version](https://img.shields.io/crates/v/zenity) ![Crates.io License](https://img.shields.io/crates/l/zenity) ![docs.rs](https://img.shields.io/docsrs/zenity) -![preview gif](./images/WindowsTerminal_ZlTLEYK249.gif) +[![CodeFactor](https://www.codefactor.io/repository/github/arteiii/zenity/badge)](https://www.codefactor.io/repository/github/arteiii/zenity) + +![multiline preview](./images/rustrover64_4bzlv2mWxK.gif) + +![](./images/rustrover64_tlGiHM9JP0.gif) -easy to use cli animation lib based on crossterm 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/multi_spinner.rs b/examples/multi_spinner.rs new file mode 100644 index 0000000..5e0d494 --- /dev/null +++ b/examples/multi_spinner.rs @@ -0,0 +1,53 @@ +use std::thread::sleep; +use std::time::Duration; + +use unicode_icons::symbols::{check_mark_button,cross_mark}; + +use zenity::multi_spinner::MultiSpinner; +use zenity::spinner::PreDefined; + +fn main() { + let check_mark_text = check_mark_button(); + let cross_mark_text = cross_mark(); + + + let mut spinner = MultiSpinner::new(); + + // main thread operations + let spinner1 = spinner.add(PreDefined::dot_spinner11(false)); + let spinner2 = spinner.add(PreDefined::binary(false)); + let spinner3 = spinner.add(PreDefined::dot_spinner9(false)); + let spinner4 = spinner.add(PreDefined::dot_spinner8(false)); + + // access the spinner through the Arc> reference + spinner.run_all(); + + sleep(Duration::from_secs(2)); + + + sleep(Duration::from_secs(8)); + spinner.set_text(&spinner2, "spinner2".to_string()); + // stop spinner1 + spinner.set_text(&spinner1, "spinner1".to_string()); + + sleep(Duration::from_secs(2)); + + spinner.stop(&spinner2); + spinner.set_text(&spinner2, format!("{} Successfully", &check_mark_text)); + + + sleep(Duration::from_secs(2)); + spinner.set_text(&spinner1, "spinner1 stopped".to_string()); + + spinner.stop(&spinner1); + + sleep(Duration::from_secs(9)); + spinner.stop(&spinner3); + spinner.stop(&spinner4); + + spinner.set_text(&spinner3, format!("{} Failed!", &cross_mark_text)); + spinner.set_text(&spinner4, format!("{} Failed!", &cross_mark_text)); + + + sleep(Duration::from_secs(3000000)); +} diff --git a/images/WindowsTerminal_ZlTLEYK249.gif b/images/WindowsTerminal_ZlTLEYK249.gif deleted file mode 100644 index 732b534..0000000 Binary files a/images/WindowsTerminal_ZlTLEYK249.gif and /dev/null differ diff --git a/images/rustrover64_4bzlv2mWxK.gif b/images/rustrover64_4bzlv2mWxK.gif new file mode 100644 index 0000000..73982a6 Binary files /dev/null and b/images/rustrover64_4bzlv2mWxK.gif differ diff --git a/images/rustrover64_tlGiHM9JP0.gif b/images/rustrover64_tlGiHM9JP0.gif new file mode 100644 index 0000000..e3ed016 Binary files /dev/null and b/images/rustrover64_tlGiHM9JP0.gif differ diff --git a/src/animations/mod.rs b/src/animations/mod.rs index a06d963..0e69f08 100644 --- a/src/animations/mod.rs +++ b/src/animations/mod.rs @@ -1,2 +1,4 @@ pub(crate) mod animation; pub mod frames; + +pub mod spinner; \ No newline at end of file diff --git a/src/animations/spinner.rs b/src/animations/spinner.rs index 9866c5d..de24d02 100644 --- a/src/animations/spinner.rs +++ b/src/animations/spinner.rs @@ -1,28 +1,144 @@ use std::collections::HashMap; -use std::io::{stdout, Stdout, Write}; +use std::io::{stdout}; use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; -use crossterm::{cursor, queue, terminal}; -use crossterm::{cursor, queue, terminal}; -use crossterm::style::{ContentStyle, Print, ResetColor, SetStyle}; +use crossterm::{cursor, execute, terminal}; +use crossterm::style::Print; +use rand::Rng; use utils_arteii_rs::vector_operations::iterators; use crate::spinner::Frames; +/// spinner struct encapsulating the spinner animation +pub struct Spinner { + frames: Arc>, + text: Arc>, + should_stop: Arc>, +} + /// struct holding multiple spinners -pub struct MultiSpinner {} +pub struct MultiSpinner { + spinner: Arc>>, + // index: usize, +} impl MultiSpinner { - fn render_frame(mut stdout: &Stdout, frame: &str) { - queue!(stdout, Print(frame),).unwrap(); - stdout.flush().unwrap(); + pub fn new() -> Self { + MultiSpinner { + spinner: Arc::new(Mutex::new(HashMap::new())), + // index: 1_usize, + } + } + + /// create a new spinner + /// + /// # Returns + /// unique identifier + pub fn add(&self, frames: Frames) -> usize { + let mut rng = rand::thread_rng(); + let mut uid: usize; + + let new_spinner = Spinner { + frames: Arc::new(Mutex::new(frames)), + text: Arc::new(Mutex::new("".to_string())), + should_stop: Arc::new(Mutex::new(false)), + }; + + loop { + uid = rng.gen(); + if !self.spinner.lock().unwrap().contains_key(&uid) { + break; + } + } + + self.spinner.lock().unwrap().insert(uid, new_spinner); + uid } - fn prepare_frame(){ - iterators::balanced_iterator(); - let frame = ""; + pub fn set_text(&self, uid: &usize, new_text: String) { + if let Some(spinner) = self.spinner.lock().unwrap().get(uid) { + let mut text = spinner.text.lock().unwrap(); + *text = new_text; + } + } - render_frame(frame); + /// stops a spinner if the uid is invalid this does nothing + pub fn stop(&self, uid: &usize) { + if let Some(spinner) = self.spinner.lock().unwrap().get(&uid) { + *spinner.should_stop.lock().unwrap() = true; + } } + + pub fn run_all(&mut self) { + let spinners = Arc::clone(&self.spinner); + + thread::spawn(move || { + let mut index = 1_usize; + + loop { + let mut all_frames = Vec::new(); + let mut all_texts = Vec::new(); + let mut all_should_stop = Vec::new(); + + // collect frames and texts from all spinners + for (_, spinner) in spinners.lock().unwrap().iter() { + let frames = spinner.frames.lock().unwrap().frames.clone(); + let text = spinner.text.lock().unwrap().clone(); + let should_stop = spinner.should_stop.lock().unwrap().clone(); + all_should_stop.push(should_stop); + all_frames.push(frames); + all_texts.push(text); + } + + // render frames with corresponding texts + MultiSpinner::render_frames(&all_frames, index, &all_texts, &all_should_stop); + + index += 1; + + thread::sleep(Duration::from_millis(100)); + } + }); + } + + /// output frame using crossterm + fn render_frame(frame: &str) { + execute!( + stdout(), + cursor::SavePosition, + cursor::MoveTo(0, 1), + terminal::Clear(terminal::ClearType::FromCursorDown), + cursor::Hide, + Print(frame), + cursor::RestorePosition + ) + .unwrap(); + } + + /// 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); + + let combined_string: String = first_frame + .iter() + .zip(texts.iter()) + .zip(should_stop.iter()) + .filter_map(|((frame, text), should_stop)| { + if *should_stop { + Some(format!("{}",text)) + } else { + if let Some(frame) = frame { + Some(format!("{} {}", frame, text)) + } else { + None + } + } + }) + .collect::>() + .join("\n"); + + MultiSpinner::render_frame(&combined_string); + } } diff --git a/src/lib.rs b/src/lib.rs index b55d71b..877109e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,9 @@ pub use crossterm::style; pub mod animations; mod helper; +pub use animations::spinner as multi_spinner; + + // define helper functions for the cross-term colors pub use crate::helper::colors::combine_attributes;