├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── index.gif ├── examples ├── simple.rs ├── stop_and_persist.rs └── stream.rs └── src ├── lib.rs ├── spinners.rs ├── streams.rs └── utils.rs /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ad4mx_github@proton.me. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Lint 20 | run: cargo clippy 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .vscode/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spinoff" 3 | version = "0.8.0" 4 | edition = "2021" 5 | authors = ["ad4m"] 6 | description = "Simple to use Rust library for displaying spinners in the terminal" 7 | license = "MIT" 8 | homepage = "https://github.com/ad4mx/spinoff" 9 | repository = "https://github.com/ad4mx/spinoff" 10 | readme = "README.md" 11 | keywords = ["spinner", "spin", "loader", "cli", "terminal"] 12 | categories = ["command-line-interface"] 13 | include = ["src/**/*", "README.md"] 14 | 15 | [dependencies] 16 | once_cell = "1.13.0" 17 | colored = "3.0.0" 18 | paste = "1.0.11" 19 | 20 | [features] 21 | default = ["all"] 22 | 23 | all = [ 24 | "dots", "dots2", "dots3", "dots4", "dots5", "dots6", "dots7", "dots8", "dots9", 25 | "dots10", "dots11", "dots12", "dots8bit", "line", "line2", "pipe", "simple_dots", 26 | "simple_dots_scrolling", "star", "star2", "flip", "hamburger", "grow_verticle", 27 | "grow_horizontal", "balloon", "balloon2", "noise", "bounce", "box_bounce", 28 | "box_bounce2", "triangle", "arc", "circle", "square_corners", "circle_quarters", 29 | "circle_halves", "squish", "toggle", "toggle2", "toggle3", "toggle4", "toggle5", 30 | "toggle6", "toggle7", "toggle8", "toggle9", "toggle10", "toggle11", "toggle12", 31 | "toggle13", "arrow", "arrow2", "arrow3", "bouncing_bar", "bouncing_ball", 32 | "smiley", "monkey", "hearts", "clock", "earth", "moon", "runner", "pong", "shark", 33 | "material", "weather", "christmas", "grenade", "point", "layer", "beta_wave", 34 | "finger_dance", "fist_bump", "dqpb", "soccer_header", "mindblown", "speaker", 35 | "orange_pulse", "blue_pulse", "orange_blue_pulse", "time_travel", "aesthetic", 36 | "binary", "cute" 37 | ] 38 | 39 | dots = [] 40 | dots2 = [] 41 | dots3 = [] 42 | dots4 = [] 43 | dots5 = [] 44 | dots6 = [] 45 | dots7 = [] 46 | dots8 = [] 47 | dots9 = [] 48 | dots10 = [] 49 | dots11 = [] 50 | dots12 = [] 51 | dots8bit = [] 52 | 53 | line = [] 54 | line2 = [] 55 | pipe = [] 56 | 57 | simple_dots = [] 58 | simple_dots_scrolling = [] 59 | 60 | star = [] 61 | star2 = [] 62 | flip = [] 63 | hamburger = [] 64 | grow_horizontal = [] 65 | grow_verticle = [] 66 | balloon = [] 67 | balloon2 = [] 68 | noise = [] 69 | bounce = [] 70 | box_bounce = [] 71 | box_bounce2 = [] 72 | triangle = [] 73 | arc = [] 74 | circle = [] 75 | square_corners = [] 76 | circle_quarters = [] 77 | circle_halves = [] 78 | squish = [] 79 | 80 | toggle = [] 81 | toggle2 = [] 82 | toggle3 = [] 83 | toggle4 = [] 84 | toggle5 = [] 85 | toggle6 = [] 86 | toggle7 = [] 87 | toggle8 = [] 88 | toggle9 = [] 89 | toggle10 = [] 90 | toggle11 = [] 91 | toggle12 = [] 92 | toggle13 = [] 93 | 94 | arrow = [] 95 | arrow2 = [] 96 | arrow3 = [] 97 | 98 | bouncing_bar = [] 99 | bouncing_ball = [] 100 | smiley = [] 101 | monkey = [] 102 | hearts = [] 103 | clock = [] 104 | earth = [] 105 | moon = [] 106 | runner = [] 107 | material = [] 108 | pong = [] 109 | shark = [] 110 | weather = [] 111 | christmas = [] 112 | grenade = [] 113 | point = [] 114 | layer = [] 115 | beta_wave = [] 116 | finger_dance = [] 117 | fist_bump = [] 118 | dqpb = [] 119 | soccer_header = [] 120 | mindblown = [] 121 | speaker = [] 122 | orange_pulse = [] 123 | blue_pulse = [] 124 | orange_blue_pulse = [] 125 | time_travel = [] 126 | aesthetic = [] 127 | binary = [] 128 | cute = [] 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ad4m 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spinoff 2 | > an easy to use, robust library for displaying spinners in the terminal 3 | 4 | [![Version](https://img.shields.io/crates/v/spinoff.svg)](https://crates.io/crates/spinoff) [![Downloads](https://img.shields.io/crates/d/spinoff)](https://crates.io/crates/spinoff) [![Docs](https://img.shields.io/docsrs/spinoff)](https://docs.rs/spinoff/latest/spinoff) [![License](https://img.shields.io/crates/l/spinoff)](https://crates.io/crates/spinoff) ![Actions](https://img.shields.io/github/actions/workflow/status/ad4mx/spinoff/rust.yml?branch=main) 5 | 6 | 7 | ![](assets/index.gif) 8 | ## 🔨 Install 9 | Add as a dependency to your `Cargo.toml`: 10 | 11 | ```toml 12 | [dependencies] 13 | spinoff = "0.8.0" 14 | ``` 15 | 16 | ## ⚡ Usage 17 | 18 | ```rust 19 | use spinoff::{Spinner, spinners, Color}; 20 | use std::thread::sleep; 21 | use std::time::Duration; 22 | 23 | let mut spinner = Spinner::new(spinners::Dots, "Loading...", Color::Blue); 24 | sleep(Duration::from_secs(3)); 25 | spinner.success("Done!"); 26 | ``` 27 | 28 | ### Update a spinner 29 | 30 | ```rust 31 | use spinoff::{Spinner, spinners, Color}; 32 | use std::thread::sleep; 33 | use std::time::Duration; 34 | 35 | let mut spinner = Spinner::new(spinners::Aesthetic, "Loading...", Color::Red); 36 | sleep(Duration::from_secs(3)); 37 | spinner.update(spinners::Dots2, "Retrying...", None); 38 | sleep(Duration::from_secs(3)); 39 | spinner.stop() 40 | ``` 41 | 42 | ### Specify an output stream 43 | 44 | ```rust 45 | use spinoff::{Spinner, spinners, Color, Streams}; 46 | use std::thread::sleep; 47 | use std::time::Duration; 48 | 49 | let mut spinner = Spinner::new_with_stream(spinners::Line, "Loading...", Color::Yellow, Streams::Stderr); 50 | sleep(Duration::from_secs(3)); 51 | spinner.stop_and_persist("📜", "Task done."); 52 | ``` 53 | 54 | ## 💫 Spinners 55 | 56 | `spinoff` includes over 80+ spinner variants out of the box. 57 | All spinner variants are treated as features that can be enabled or disabled. By default, all of them are enabled for ease of use. 58 | To disable/enable variants, you will have to edit your `cargo.toml` file: 59 | 60 | ```toml 61 | [dependencies] 62 | spinoff = { version = "0.8.0", features = ["dots", "arc", "line"] } 63 | ``` 64 | 65 | Any suggestions for new spinner variants are welcome. 66 | 67 | ### Creating your own spinner 68 | You can create your own spinner using the `spinner!` macro: 69 | 70 | ```rust 71 | use spinoff::*; 72 | use std::thread::sleep; 73 | use std::time::Duration; 74 | 75 | let frames = spinner!([">", ">>", ">>>"], 100); 76 | let mut sp = Spinner::new(frames, "Hello World!", None); 77 | sleep(Duration::from_millis(800)); 78 | sp.stop(); 79 | ``` 80 | 81 | ### Multiline messages 82 | `spinoff` doesn't support spinners with multiline text out of the box. If you want to use it in your project, please look at [#27](https://github.com/ad4mx/spinoff/issues/27). 83 | 84 | ## ❗Note for Windows Users 85 | For colors to work properly, you need to add a few extra lines to your code: 86 | ```rust 87 | use colored::control 88 | control::set_virtual_terminal(true).unwrap(); 89 | ``` 90 | 91 | ## 📖 Documentation 92 | 93 | * All relevant documentation can be found on the [Docs.rs page](https://docs.rs/spinoff/latest/spinoff/). 94 | * If you want to see all the available `spinner` options, refer to [the `spinner` module](https://docs.rs/spinoff/0.7.0/spinoff/spinners/index.html). 95 | 96 | ## ⚙ Examples 97 | 98 | ```bash 99 | cargo run --example simple 100 | ``` 101 | ```bash 102 | cargo run --example stream 103 | ``` 104 | ```bash 105 | cargo run --example stop_and_persist 106 | ``` 107 | Other examples can be found in the [documentation](https://docs.rs/spinoff/latest/spinoff/). 108 | ## 🚧 Contributing 109 | 110 | Any contributions to this crate are highly appreciated. If you have any ideas/suggestions/bug fixes, please open an [issue](https://github.com/ad4mx/spinoff/issues) or a [pull request](https://github.com/ad4mx/spinoff/pulls). 111 | If you like the project, [star this project on GitHub.](https://github.com/ad4mx/spinoff) 112 | 113 | ## 📑 License 114 | 115 | This crate is licensed under the [MIT license](LICENSE). 116 | -------------------------------------------------------------------------------- /assets/index.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad4mx/spinoff/4b5787b9aa1201045ce2a52e69911ef5875c7b13/assets/index.gif -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | use spinoff::{spinners, Color, Spinner}; 3 | use std::{thread::sleep, time::Duration}; 4 | 5 | #[cfg(feature = "dots")] 6 | fn main() { 7 | let mut sp = Spinner::new(spinners::Dots, "Loading...", Color::Blue); 8 | sleep(Duration::from_millis(8000)); 9 | sp.success("Done!"); 10 | } 11 | 12 | #[cfg(not(feature = "dots"))] 13 | fn main() { 14 | println!("This example requires the 'dots' feature to be enabled."); 15 | } 16 | -------------------------------------------------------------------------------- /examples/stop_and_persist.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | use spinoff::{spinners, Color, Spinner}; 3 | use std::{thread::sleep, time::Duration}; 4 | 5 | #[cfg(feature = "arc")] 6 | fn main() { 7 | let mut sp = Spinner::new(spinners::Arc, "Loading...", Color::Blue); 8 | sleep(Duration::from_secs(5)); 9 | sp.stop_and_persist("🍕", "Pizza!"); 10 | } 11 | 12 | #[cfg(not(feature = "arc"))] 13 | fn main() { 14 | println!("This example requires the 'arc' feature to be enabled."); 15 | } 16 | -------------------------------------------------------------------------------- /examples/stream.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | use spinoff::{spinners, Spinner, Streams}; 3 | use std::{thread::sleep, time::Duration}; 4 | 5 | #[cfg(feature = "aesthetic")] 6 | fn main() { 7 | let mut sp = Spinner::new_with_stream(spinners::Aesthetic, "Loading in stderr...", None, Streams::Stderr); 8 | sleep(Duration::from_millis(8000)); 9 | sp.success("Done!"); 10 | } 11 | 12 | #[cfg(not(feature = "aesthetic"))] 13 | fn main() { 14 | println!("This example requires the 'aesthetic' feature to be enabled."); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | ## spinoff 3 | 4 | `spinoff` is a simple library for displaying spinners in the terminal. 5 | 6 | ### Usage 7 | 8 | ``` 9 | # use spinoff::*; 10 | # use std::thread::sleep; 11 | # use std::time::Duration; 12 | # 13 | let mut sp = Spinner::new(spinners::Dots, "Loading...", None); 14 | sleep(Duration::from_millis(800)); 15 | sp.success("Success!"); 16 | ``` 17 | 18 | ### Spinners 19 | 20 | This crate provides 80+ spinners out of the box, which you can find in the 21 | [`spinners`] module. 22 | 23 | Each spinner provided in this crate is broken up into its own feature. For 24 | example, if you want to use the `dots9` spinner, you need to enable the `dots9` 25 | feature in your `Cargo.toml` (the `dots` feature is enabled by default). 26 | 27 | If you want to use a custom spinner, you can use the [`spinner!`] macro. 28 | 29 | ``` 30 | # use spinoff::*; 31 | # use std::thread::sleep; 32 | # use std::time::Duration; 33 | # 34 | let frames = spinner!([">", ">>", ">>>"], 100); 35 | let mut sp = Spinner::new(frames, "Loading...", None); 36 | sleep(Duration::from_millis(800)); 37 | sp.success("Success!"); 38 | ``` 39 | 40 | ### Colors 41 | 42 | You can also color your spinners without any hassle. Simply pass a color to the `color` option. 43 | There are 9 colors available: blue, green, red, yellow, cyan, white, magenta, black and a custom variant. 44 | Don't want any of that? Simply pass `None` to the `color` option. 45 | 46 | ### Note 47 | 48 | Currently, the library is designed in a way that doesn't support using multiple spinners at a time. However, that may change in the future. 49 | 50 | */ 51 | #![allow(clippy::nursery)] 52 | #![warn(clippy::pedantic)] 53 | use colored::Colorize; 54 | use std::borrow::Cow; 55 | use std::io::Write; 56 | use std::sync::{atomic::AtomicBool, Arc}; 57 | use std::thread::sleep; 58 | use std::thread::{self, JoinHandle}; 59 | use std::time::Duration; 60 | 61 | pub mod spinners; 62 | mod streams; 63 | mod utils; 64 | 65 | use spinners::SpinnerFrames; 66 | pub use streams::Streams; 67 | pub use utils::Color; 68 | use utils::{colorize, delete_last_line}; 69 | 70 | /// Terminal spinner. 71 | pub struct Spinner { 72 | thread_handle: Option>, 73 | /// This struct has an `Arc` field, which is later used in the `stop` type methods to stop the thread printing the spinner. 74 | still_spinning: Arc, 75 | spinner_frames: SpinnerFrames, 76 | msg: Cow<'static, str>, 77 | stream: Streams, 78 | color: Option, 79 | } 80 | 81 | /** 82 | Create a new `SpinnerFrames` struct 83 | 84 | # Arguments 85 | 86 | * `frames` - An array of frames you want to use 87 | * `interval` - The time (in milliseconds) that will pass between frames 88 | 89 | # Example 90 | 91 | ``` 92 | # use spinoff::*; 93 | # use std::thread::sleep; 94 | # use std::time::Duration; 95 | # 96 | let frames = spinner!([">", ">>", ">>>"], 100); 97 | let mut sp = Spinner::new(frames, "Hello World!", None); 98 | sleep(Duration::from_millis(800)); 99 | sp.stop(); 100 | ``` 101 | */ 102 | #[macro_export] 103 | macro_rules! spinner { 104 | ( [ $( $frame:expr ),* ], $interval:expr ) => { 105 | spinners::SpinnerFrames { 106 | interval: $interval, 107 | frames: vec![$($frame),*] 108 | } 109 | }; 110 | } 111 | 112 | impl Spinner { 113 | /** 114 | Create a new spinner. 115 | 116 | # Arguments 117 | 118 | * `spinner_type` - The spinner to use. 119 | * `msg` - The message to display. 120 | * `color` - The color of the spinner. 121 | 122 | # Example 123 | 124 | ``` 125 | # use spinoff::*; 126 | # use std::thread::sleep; 127 | # use std::time::Duration; 128 | # 129 | let mut sp = Spinner::new(spinners::Dots, "Hello World!", Color::Blue); 130 | sleep(Duration::from_millis(800)); 131 | sp.stop(); 132 | ``` 133 | 134 | # Notes 135 | 136 | * The spinner immediately starts spinning upon creation. 137 | * This function outputs to the `stdout` stream. If you want to use a different stream, use the [`Spinner::new_with_stream`] function. 138 | */ 139 | pub fn new(spinner_type: S, msg: T, color: U) -> Self 140 | where 141 | S: Into, 142 | T: Into>, 143 | U: Into>, 144 | { 145 | Self::new_with_stream(spinner_type, msg, color, Streams::default()) 146 | } 147 | /** 148 | Create a new spinner outputting to a specific stream. 149 | 150 | # Arguments 151 | 152 | * `spinner_type` - The spinner to use. 153 | * `msg` - The message to display. 154 | * `color` - The color of the spinner. 155 | * `stream` - The stream to output to. 156 | 157 | # Example 158 | 159 | ``` 160 | # use spinoff::*; 161 | # use std::thread::sleep; 162 | # use std::time::Duration; 163 | # 164 | let mut sp = Spinner::new_with_stream(spinners::Dots, "I'm outputting to stderr!", Color::Yellow, Streams::Stderr); 165 | sleep(Duration::from_millis(800)); 166 | sp.clear(); 167 | ``` 168 | 169 | # Notes 170 | 171 | * The spinner immediately starts spinning upon creation. 172 | 173 | */ 174 | pub fn new_with_stream(spinner_type: S, msg: T, color: U, stream: Streams) -> Self 175 | where 176 | S: Into, 177 | T: Into>, 178 | U: Into>, 179 | { 180 | let still_spinning = Arc::new(AtomicBool::new(true)); 181 | // Gain ownership of the message and color for the thread to use 182 | let spinner_frames = spinner_type.into(); 183 | let msg = msg.into(); 184 | let color = color.into(); 185 | // We use atomic bools to make the thread stop itself when the `spinner.stop()` method is called. 186 | let handle = thread::spawn({ 187 | // Clone the atomic bool so that we can use it in the thread and return the original one later. 188 | let still_spinning = Arc::clone(&still_spinning); 189 | let spinner_frames = spinner_frames.clone(); 190 | let msg = msg.clone(); 191 | move || { 192 | // Iterate over all the frames of the spinner while the atomic bool is true. 193 | let frames = spinner_frames 194 | .frames 195 | .iter() 196 | .cycle() 197 | .take_while(|_| still_spinning.load(std::sync::atomic::Ordering::Relaxed)); 198 | // Dynamically delete the last line of the terminal depending on the length of the message + spinner. 199 | let mut last_length = 0; 200 | for frame in frames { 201 | let frame_str = format!("{} {}", colorize(color, frame), msg); 202 | // Get us back to the start of the line. 203 | delete_last_line(last_length, stream); 204 | last_length = frame_str.bytes().len(); 205 | write!(stream, "{frame_str}"); 206 | stream 207 | .get_stream() 208 | .flush() 209 | .expect("error: failed to flush stream"); 210 | 211 | thread::sleep(std::time::Duration::from_millis( 212 | u64::from(spinner_frames.interval) 213 | )); 214 | } 215 | delete_last_line(last_length, stream); 216 | } 217 | }); 218 | 219 | // Return a Spinner struct 220 | Self { 221 | thread_handle: Some(handle), 222 | still_spinning, 223 | spinner_frames, 224 | msg, 225 | stream, 226 | color, 227 | } 228 | } 229 | /** 230 | Stop the spinner. 231 | 232 | # Example 233 | 234 | ``` 235 | # use spinoff::{spinners, Spinner}; 236 | # use std::thread::sleep; 237 | # use std::time::Duration; 238 | # 239 | let mut sp = Spinner::new(spinners::Dots9, "Spinning...", None); 240 | sleep(Duration::from_millis(800)); 241 | sp.stop(); 242 | # 243 | ``` 244 | 245 | # Notes 246 | 247 | * The spinner will be dropped after this method is called, the message will remain though. 248 | 249 | */ 250 | pub fn stop(&mut self) { 251 | self.stop_spinner_thread(); 252 | // print message 253 | writeln!(self.stream, "{}", self.msg); 254 | } 255 | 256 | /** 257 | Stops the spinner and prints a message on a new line. 258 | 259 | # Example 260 | 261 | ``` 262 | # use spinoff::{spinners, Spinner}; 263 | # use std::thread::sleep; 264 | # use std::time::Duration; 265 | # 266 | let mut sp = Spinner::new(spinners::Dots2, "Hello", None); 267 | sleep(Duration::from_millis(800)); 268 | sp.stop_with_message("Bye"); 269 | # 270 | ``` 271 | 272 | */ 273 | pub fn stop_with_message(&mut self, msg: &str) { 274 | self.stop_spinner_thread(); 275 | // put the message over the spinner 276 | writeln!(self.stream, "{msg}"); 277 | } 278 | 279 | /** 280 | Deletes the spinner and message and prints a new line with a symbol and message. 281 | 282 | # Example 283 | 284 | ``` 285 | # use spinoff::{spinners, Spinner}; 286 | # use std::thread::sleep; 287 | # use std::time::Duration; 288 | # 289 | let mut sp = Spinner::new(spinners::Mindblown, "Guess what's coming...", None); 290 | sleep(Duration::from_millis(800)); 291 | sp.stop_and_persist("🍕", "Pizza!"); 292 | # 293 | ``` 294 | 295 | */ 296 | pub fn stop_and_persist(&mut self, symbol: &str, msg: &str) { 297 | self.stop_spinner_thread(); 298 | writeln!(self.stream, "{symbol} {msg}"); 299 | } 300 | 301 | /** 302 | Deletes the last line of the terminal and prints a success symbol with a message. 303 | 304 | # Example 305 | 306 | ``` 307 | # use spinoff::{spinners, Spinner}; 308 | # use std::thread::sleep; 309 | # use std::time::Duration; 310 | # 311 | let mut sp = Spinner::new(spinners::Aesthetic, "Trying to load information...", None); 312 | sleep(Duration::from_millis(800)); 313 | sp.success("Success!"); 314 | # 315 | ``` 316 | 317 | */ 318 | pub fn success(&mut self, msg: &str) { 319 | self.stop_spinner_thread(); 320 | writeln!(self.stream, "{} {}", colorize(Some(Color::Green), "✓").bold(), msg); 321 | } 322 | 323 | /** 324 | Deletes the last line of the terminal and prints a failure symbol with a message to stderr. 325 | 326 | # Example 327 | 328 | ``` 329 | # use spinoff::{spinners, Spinner, Color}; 330 | # use std::thread::sleep; 331 | # use std::time::Duration; 332 | # 333 | let mut sp = Spinner::new(spinners::BouncingBar, "Executing code...", Color::Green); 334 | sleep(Duration::from_millis(800)); 335 | sp.fail("Code failed to compile!"); 336 | # 337 | ``` 338 | 339 | */ 340 | pub fn fail(&mut self, msg: &str) { 341 | self.stop_spinner_thread(); 342 | writeln!(self.stream, "{} {}", colorize(Some(Color::Red), "✗").bold(), msg); 343 | } 344 | 345 | /** 346 | Deletes the last line of the terminal and prints a warning symbol with a message. 347 | 348 | # Example 349 | 350 | ``` 351 | # use spinoff::{spinners, Spinner}; 352 | # use std::thread::sleep; 353 | # use std::time::Duration; 354 | # 355 | let mut sp = Spinner::new(spinners::Material, "Measuring network speed...", None); 356 | sleep(Duration::from_millis(800)); 357 | sp.warn("You might want to check your internet connection..."); 358 | # 359 | ``` 360 | 361 | */ 362 | pub fn warn(&mut self, msg: &str) { 363 | self.stop_spinner_thread(); 364 | writeln!(self.stream, "{} {}", colorize(Some(Color::Yellow), "⚠").bold(), msg); 365 | } 366 | /** 367 | Deletes the last line of the terminal and prints an info symbol with a message. 368 | 369 | # Example 370 | 371 | ``` 372 | # use spinoff::{spinners, Spinner}; 373 | # use std::thread::sleep; 374 | # use std::time::Duration; 375 | # 376 | let mut sp = Spinner::new(spinners::Dots9, "Loading info message...", None); 377 | sleep(Duration::from_millis(800)); 378 | sp.info("This is an info message!"); 379 | # 380 | ``` 381 | 382 | */ 383 | pub fn info(&mut self, msg: &str) { 384 | self.stop_spinner_thread(); 385 | writeln!(self.stream, "{} {}", colorize(Some(Color::Blue), "ℹ").bold(), msg); 386 | } 387 | 388 | /** 389 | Updates the spinner. 390 | 391 | # Example 392 | 393 | ``` 394 | # use spinoff::*; 395 | # use std::thread::sleep; 396 | # use std::time::Duration; 397 | # 398 | let mut sp = Spinner::new(spinners::Dots, "Hello", None); 399 | 400 | sleep(Duration::from_millis(800)); 401 | sp.update(spinners::Dots2, "World", None); 402 | sleep(Duration::from_millis(800)); 403 | 404 | sp.stop(); 405 | # 406 | ``` 407 | 408 | */ 409 | pub fn update(&mut self, spinner: S, msg: T, color: U) 410 | where 411 | S: Into, 412 | T: Into>, 413 | U: Into>, 414 | { 415 | self.stop_spinner_thread(); 416 | let _replaced = std::mem::replace( 417 | self, 418 | Self::new_with_stream(spinner, msg, color, self.stream), 419 | ); 420 | } 421 | 422 | /** 423 | Update the spinner text. 424 | 425 | # Example 426 | 427 | ``` 428 | # use spinoff::*; 429 | # use std::thread::sleep; 430 | # use std::time::Duration; 431 | # 432 | let mut sp = Spinner::new(spinners::Arc, "Loading...", Color::Magenta); 433 | sleep(Duration::from_millis(800)); 434 | sp.update_text("Not quite finished..."); 435 | sleep(Duration::from_millis(800)); 436 | sp.update_text("Almost done..."); 437 | sleep(Duration::from_millis(800)); 438 | sp.success("Done!"); 439 | # 440 | ``` 441 | 442 | */ 443 | pub fn update_text(&mut self, msg: T) 444 | where 445 | T: Into>, 446 | { 447 | self.stop_spinner_thread(); 448 | let _replaced = std::mem::replace( 449 | self, 450 | Self::new_with_stream(self.spinner_frames.clone(), msg, self.color, self.stream), 451 | ); 452 | } 453 | /** 454 | Updates the spinner text after a certain amount of time has passed since the initial `::new` call. 455 | 456 | # Example 457 | 458 | ``` 459 | # use spinoff::*; 460 | # use std::thread::sleep; 461 | # use std::time::Duration; 462 | # 463 | let mut sp = Spinner::new(spinners::Arc, "Loading...", Color::Blue); 464 | sp.update_after_time("Not Done Yet...", Duration::from_secs(2)); 465 | sleep(Duration::from_millis(800)); 466 | sp.success("Done!"); 467 | # 468 | ``` 469 | 470 | # Notes 471 | 472 | * This could be used to assure the user that the program is still running. 473 | 474 | */ 475 | pub fn update_after_time(&mut self, updated_msg: T, duration: Duration) 476 | where 477 | T: Into> 478 | { 479 | sleep(duration); 480 | self.stop_spinner_thread(); 481 | let _replaced = std::mem::replace( 482 | self, 483 | Self::new_with_stream(self.spinner_frames.clone(), updated_msg, self.color, self.stream), 484 | ); 485 | } 486 | /** 487 | Deletes the last line of the terminal. 488 | 489 | # Example 490 | 491 | ``` 492 | # use spinoff::{spinners, Spinner}; 493 | # use std::thread::sleep; 494 | # use std::time::Duration; 495 | # 496 | let mut sp = Spinner::new(spinners::Grenade, "Clearing...", None); 497 | sleep(Duration::from_millis(800)); 498 | sp.clear(); 499 | # 500 | ``` 501 | 502 | */ 503 | pub fn clear(&mut self) { 504 | self.stop_spinner_thread(); 505 | } 506 | 507 | /// Stop the spinner thread and wait for it. 508 | fn stop_spinner_thread(&mut self) { 509 | // Set flag to signal thread to stop 510 | self.still_spinning 511 | .store(false, std::sync::atomic::Ordering::Relaxed); 512 | 513 | // Wait for the thread to actually stop 514 | // Also deletes the last line of the terminal after stopped 515 | self.thread_handle 516 | .take() 517 | .expect("Stopping the spinner thread should only happen once.") 518 | .join() 519 | .expect("Thread to join."); 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/spinners.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use once_cell::sync::Lazy; 3 | use paste::paste; 4 | 5 | /// A Struct that contains the data for a spinner. 6 | /// Frames is a Vec of &str, each &str is a frame of the spinner. 7 | /// Interval is the number of milliseconds to wait before moving to the next frame. 8 | #[derive(Debug, Clone)] 9 | pub struct SpinnerFrames { 10 | pub frames: Vec<&'static str>, 11 | pub interval: u16, 12 | } 13 | 14 | macro_rules! spinner_frames { 15 | ( $name:expr, [ $( $frame:expr ),* ], $interval:expr ) => { 16 | paste! { 17 | #[cfg(feature = $name)] 18 | pub struct [< $name:camel >]; 19 | 20 | #[cfg(feature = $name)] 21 | impl From<[< $name:camel >]> for SpinnerFrames { 22 | fn from(_: [< $name:camel >]) -> SpinnerFrames { 23 | #[cfg(feature = $name)] 24 | [< $name:upper >].clone() 25 | } 26 | } 27 | 28 | #[cfg(feature = $name)] 29 | static [< $name:upper >]: Lazy 30 | = Lazy::new(|| SpinnerFrames { 31 | interval: $interval, 32 | frames: vec![$($frame),*] 33 | }); 34 | } 35 | }; 36 | } 37 | 38 | spinner_frames!( 39 | "dots", 40 | ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], 41 | 80 42 | ); 43 | 44 | spinner_frames!("dots2", ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], 80); 45 | 46 | spinner_frames!( 47 | "dots3", 48 | ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"], 49 | 80 50 | ); 51 | 52 | spinner_frames!( 53 | "dots4", 54 | [ 55 | "⠄", "⠆", "⠇", "⠋", "⠙", "⠸", "⠰", "⠠", "⠰", "⠸", "⠙", "⠋", "⠇", "⠆" 56 | ], 57 | 80 58 | ); 59 | 60 | spinner_frames!( 61 | "dots5", 62 | [ 63 | "⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋" 64 | ], 65 | 80 66 | ); 67 | 68 | spinner_frames!( 69 | "dots6", 70 | [ 71 | "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", 72 | "⠂", "⠒", "⠚", "⠙", "⠉", "⠁" 73 | ], 74 | 80 75 | ); 76 | 77 | spinner_frames!( 78 | "dots7", 79 | [ 80 | "⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", 81 | "⠐", "⠒", "⠓", "⠋", "⠉", "⠈" 82 | ], 83 | 80 84 | ); 85 | spinner_frames!( 86 | "dots8", 87 | [ 88 | "⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", 89 | "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈" 90 | ], 91 | 80 92 | ); 93 | spinner_frames!("dots9", ["⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"], 80); 94 | spinner_frames!("dots10", ["⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"], 80); 95 | spinner_frames!("dots11", ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"], 80); 96 | spinner_frames!( 97 | "dots12", 98 | [ 99 | "⢀⠀", "⡀⠀", "⠄⠀", "⢂⠀", "⡂⠀", "⠅⠀", "⢃⠀", "⡃⠀", "⠍⠀", "⢋⠀", "⡋⠀", "⠍⠁", "⢋⠁", "⡋⠁", "⠍⠉", 100 | "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⢈⠩", "⡀⢙", "⠄⡙", "⢂⠩", "⡂⢘", "⠅⡘", "⢃⠨", "⡃⢐", 101 | "⠍⡐", "⢋⠠", "⡋⢀", "⠍⡁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⠈⠩", 102 | "⠀⢙", "⠀⡙", "⠀⠩", "⠀⢘", "⠀⡘", "⠀⠨", "⠀⢐", "⠀⡐", "⠀⠠", "⠀⢀", "⠀⡀" 103 | ], 104 | 80 105 | ); 106 | spinner_frames!( 107 | "dots8bit", 108 | [ 109 | "⠀", "⠁", "⠂", "⠃", "⠄", "⠅", "⠆", "⠇", "⡀", "⡁", "⡂", "⡃", "⡄", "⡅", "⡆", "⡇", "⠈", "⠉", 110 | "⠊", "⠋", "⠌", "⠍", "⠎", "⠏", "⡈", "⡉", "⡊", "⡋", "⡌", "⡍", "⡎", "⡏", "⠐", "⠑", "⠒", "⠓", 111 | "⠔", "⠕", "⠖", "⠗", "⡐", "⡑", "⡒", "⡓", "⡔", "⡕", "⡖", "⡗", "⠘", "⠙", "⠚", "⠛", "⠜", "⠝", 112 | "⠞", "⠟", "⡘", "⡙", "⡚", "⡛", "⡜", "⡝", "⡞", "⡟", "⠠", "⠡", "⠢", "⠣", "⠤", "⠥", "⠦", "⠧", 113 | "⡠", "⡡", "⡢", "⡣", "⡤", "⡥", "⡦", "⡧", "⠨", "⠩", "⠪", "⠫", "⠬", "⠭", "⠮", "⠯", "⡨", "⡩", 114 | "⡪", "⡫", "⡬", "⡭", "⡮", "⡯", "⠰", "⠱", "⠲", "⠳", "⠴", "⠵", "⠶", "⠷", "⡰", "⡱", "⡲", "⡳", 115 | "⡴", "⡵", "⡶", "⡷", "⠸", "⠹", "⠺", "⠻", "⠼", "⠽", "⠾", "⠿", "⡸", "⡹", "⡺", "⡻", "⡼", "⡽", 116 | "⡾", "⡿", "⢀", "⢁", "⢂", "⢃", "⢄", "⢅", "⢆", "⢇", "⣀", "⣁", "⣂", "⣃", "⣄", "⣅", "⣆", "⣇", 117 | "⢈", "⢉", "⢊", "⢋", "⢌", "⢍", "⢎", "⢏", "⣈", "⣉", "⣊", "⣋", "⣌", "⣍", "⣎", "⣏", "⢐", "⢑", 118 | "⢒", "⢓", "⢔", "⢕", "⢖", "⢗", "⣐", "⣑", "⣒", "⣓", "⣔", "⣕", "⣖", "⣗", "⢘", "⢙", "⢚", "⢛", 119 | "⢜", "⢝", "⢞", "⢟", "⣘", "⣙", "⣚", "⣛", "⣜", "⣝", "⣞", "⣟", "⢠", "⢡", "⢢", "⢣", "⢤", "⢥", 120 | "⢦", "⢧", "⣠", "⣡", "⣢", "⣣", "⣤", "⣥", "⣦", "⣧", "⢨", "⢩", "⢪", "⢫", "⢬", "⢭", "⢮", "⢯", 121 | "⣨", "⣩", "⣪", "⣫", "⣬", "⣭", "⣮", "⣯", "⢰", "⢱", "⢲", "⢳", "⢴", "⢵", "⢶", "⢷", "⣰", "⣱", 122 | "⣲", "⣳", "⣴", "⣵", "⣶", "⣷", "⢸", "⢹", "⢺", "⢻", "⢼", "⢽", "⢾", "⢿", "⣸", "⣹", "⣺", "⣻", 123 | "⣼", "⣽", "⣾", "⣿" 124 | ], 125 | 80 126 | ); 127 | 128 | spinner_frames!("line", ["-", "\\", "|", "/"], 130); 129 | 130 | spinner_frames!("line2", ["⠂", "-", "–", "—", "–", "-"], 100); 131 | 132 | spinner_frames!("pipe", ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"], 100); 133 | 134 | spinner_frames!("simple_dots", [". ", ".. ", "...", " "], 400); 135 | 136 | spinner_frames!( 137 | "simple_dots_scrolling", 138 | [". ", ".. ", "...", " ..", " .", " "], 139 | 200 140 | ); 141 | 142 | spinner_frames!("star", ["✶", "✸", "✹", "✺", "✹", "✷"], 70); 143 | 144 | spinner_frames!("star2", ["+", "x", "*"], 80); 145 | 146 | spinner_frames!( 147 | "flip", 148 | ["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"], 149 | 70 150 | ); 151 | 152 | spinner_frames!("hamburger", ["☱", "☲", "☴"], 100); 153 | 154 | spinner_frames!( 155 | "grow_vertical", 156 | ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"], 157 | 120 158 | ); 159 | 160 | spinner_frames!( 161 | "grow_horizontal", 162 | ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "▊", "▋", "▌", "▍", "▎"], 163 | 120 164 | ); 165 | 166 | spinner_frames!("balloon", [" ", ".", "o", "O", "@", "*", " "], 140); 167 | 168 | spinner_frames!("balloon2", [".", "o", "O", "°", "O", "o", "."], 120); 169 | 170 | spinner_frames!("noise", ["▓", "▒", "░"], 100); 171 | 172 | spinner_frames!("bounce", ["⠁", "⠂", "⠄", "⠂"], 120); 173 | 174 | spinner_frames!("box_bounce", ["▖", "▘", "▝", "▗"], 120); 175 | 176 | spinner_frames!("box_bounce2", ["▌", "▀", "▐", "▄"], 100); 177 | 178 | spinner_frames!("triangle", ["◢", "◣", "◤", "◥"], 50); 179 | 180 | spinner_frames!("arc", ["◜", "◠", "◝", "◞", "◡", "◟"], 100); 181 | 182 | spinner_frames!("circle", ["◡", "⊙", "◠"], 120); 183 | 184 | spinner_frames!("square_corners", ["◰", "◳", "◲", "◱"], 180); 185 | 186 | spinner_frames!("circle_quarters", ["◴", "◷", "◶", "◵"], 120); 187 | 188 | spinner_frames!("circle_halves", ["◐", "◓", "◑", "◒"], 50); 189 | 190 | spinner_frames!("squish", ["╫", "╪"], 100); 191 | 192 | spinner_frames!("toggle", ["⊶", "⊷"], 250); 193 | 194 | spinner_frames!("toggle2", ["▫", "▪"], 80); 195 | 196 | spinner_frames!("toggle3", ["□", "■"], 120); 197 | 198 | spinner_frames!("toggle4", ["■", "□", "▪", "▫"], 100); 199 | 200 | spinner_frames!("toggle5", ["▮", "▯"], 100); 201 | 202 | spinner_frames!("toggle6", ["ဝ", "၀"], 300); 203 | 204 | spinner_frames!("toggle7", ["⦾", "⦿"], 80); 205 | 206 | spinner_frames!("toggle8", ["◍", "◌"], 100); 207 | 208 | spinner_frames!("toggle9", ["◉", "◎"], 100); 209 | 210 | spinner_frames!("toggle10", ["㊂", "㊀", "㊁"], 100); 211 | 212 | spinner_frames!("toggle11", ["⧇", "⧆"], 50); 213 | 214 | spinner_frames!("toggle12", ["☗", "☖"], 120); 215 | 216 | spinner_frames!("toggle13", ["=", "*", "-"], 80); 217 | 218 | spinner_frames!("arrow", ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"], 100); 219 | 220 | spinner_frames!( 221 | "arrow2", 222 | ["⬆️ ", "↗️ ", "➡️ ", "↘️ ", "⬇️ ", "↙️ ", "⬅️ ", "↖️ "], 223 | 80 224 | ); 225 | 226 | spinner_frames!( 227 | "arrow3", 228 | ["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"], 229 | 120 230 | ); 231 | 232 | spinner_frames!( 233 | "bouncing_bar", 234 | [ 235 | "[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", 236 | "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]" 237 | ], 238 | 80 239 | ); 240 | 241 | spinner_frames!( 242 | "bouncing_ball", 243 | [ 244 | "( ● )", 245 | "( ● )", 246 | "( ● )", 247 | "( ● )", 248 | "( ●)", 249 | "( ● )", 250 | "( ● )", 251 | "( ● )", 252 | "( ● )", 253 | "(● )" 254 | ], 255 | 80 256 | ); 257 | 258 | spinner_frames!("smiley", ["😄 ", "😝 "], 200); 259 | 260 | spinner_frames!("monkey", ["🙈 ", "🙈 ", "🙉 ", "🙊 "], 300); 261 | 262 | spinner_frames!("hearts", ["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "], 100); 263 | 264 | spinner_frames!( 265 | "clock", 266 | ["🕛 ", "🕐 ", "🕑 ", "🕒 ", "🕓 ", "🕔 ", "🕕 "], 267 | 100 268 | ); 269 | 270 | spinner_frames!( 271 | "material", 272 | [ 273 | "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 274 | "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 275 | "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 276 | "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 277 | "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 278 | "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 279 | "███████▁▁▁▁▁▁▁▁▁▁▁▁▁", 280 | "████████▁▁▁▁▁▁▁▁▁▁▁▁", 281 | "█████████▁▁▁▁▁▁▁▁▁▁▁", 282 | "█████████▁▁▁▁▁▁▁▁▁▁▁", 283 | "██████████▁▁▁▁▁▁▁▁▁▁", 284 | "███████████▁▁▁▁▁▁▁▁▁", 285 | "█████████████▁▁▁▁▁▁▁", 286 | "██████████████▁▁▁▁▁▁", 287 | "██████████████▁▁▁▁▁▁", 288 | "▁██████████████▁▁▁▁▁", 289 | "▁██████████████▁▁▁▁▁", 290 | "▁██████████████▁▁▁▁▁", 291 | "▁▁██████████████▁▁▁▁", 292 | "▁▁▁██████████████▁▁▁", 293 | "▁▁▁▁█████████████▁▁▁", 294 | "▁▁▁▁██████████████▁▁", 295 | "▁▁▁▁██████████████▁▁", 296 | "▁▁▁▁▁██████████████▁", 297 | "▁▁▁▁▁██████████████▁", 298 | "▁▁▁▁▁██████████████▁", 299 | "▁▁▁▁▁▁██████████████", 300 | "▁▁▁▁▁▁██████████████", 301 | "▁▁▁▁▁▁▁█████████████", 302 | "▁▁▁▁▁▁▁█████████████", 303 | "▁▁▁▁▁▁▁▁████████████", 304 | "▁▁▁▁▁▁▁▁████████████", 305 | "▁▁▁▁▁▁▁▁▁███████████", 306 | "▁▁▁▁▁▁▁▁▁███████████", 307 | "▁▁▁▁▁▁▁▁▁▁██████████", 308 | "▁▁▁▁▁▁▁▁▁▁██████████", 309 | "▁▁▁▁▁▁▁▁▁▁▁▁████████", 310 | "▁▁▁▁▁▁▁▁▁▁▁▁▁███████", 311 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████", 312 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████", 313 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████", 314 | "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████", 315 | "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███", 316 | "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███", 317 | "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███", 318 | "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██", 319 | "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█", 320 | "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█", 321 | "██████▁▁▁▁▁▁▁▁▁▁▁▁▁█", 322 | "████████▁▁▁▁▁▁▁▁▁▁▁▁", 323 | "█████████▁▁▁▁▁▁▁▁▁▁▁", 324 | "█████████▁▁▁▁▁▁▁▁▁▁▁", 325 | "█████████▁▁▁▁▁▁▁▁▁▁▁", 326 | "█████████▁▁▁▁▁▁▁▁▁▁▁", 327 | "███████████▁▁▁▁▁▁▁▁▁", 328 | "████████████▁▁▁▁▁▁▁▁", 329 | "████████████▁▁▁▁▁▁▁▁", 330 | "██████████████▁▁▁▁▁▁", 331 | "██████████████▁▁▁▁▁▁", 332 | "▁██████████████▁▁▁▁▁", 333 | "▁██████████████▁▁▁▁▁", 334 | "▁▁▁█████████████▁▁▁▁", 335 | "▁▁▁▁▁████████████▁▁▁", 336 | "▁▁▁▁▁████████████▁▁▁", 337 | "▁▁▁▁▁▁███████████▁▁▁", 338 | "▁▁▁▁▁▁▁▁█████████▁▁▁", 339 | "▁▁▁▁▁▁▁▁█████████▁▁▁", 340 | "▁▁▁▁▁▁▁▁▁█████████▁▁", 341 | "▁▁▁▁▁▁▁▁▁█████████▁▁", 342 | "▁▁▁▁▁▁▁▁▁▁█████████▁", 343 | "▁▁▁▁▁▁▁▁▁▁▁████████▁", 344 | "▁▁▁▁▁▁▁▁▁▁▁████████▁", 345 | "▁▁▁▁▁▁▁▁▁▁▁▁███████▁", 346 | "▁▁▁▁▁▁▁▁▁▁▁▁███████▁", 347 | "▁▁▁▁▁▁▁▁▁▁▁▁▁███████", 348 | "▁▁▁▁▁▁▁▁▁▁▁▁▁███████", 349 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████", 350 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████", 351 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████", 352 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████", 353 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███", 354 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███", 355 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██", 356 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██", 357 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██", 358 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█", 359 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█", 360 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█", 361 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 362 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 363 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 364 | "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁" 365 | ], 366 | 17 367 | ); 368 | 369 | spinner_frames!("earth", ["🌍 ", "🌎 ", "🌏 "], 180); 370 | 371 | spinner_frames!( 372 | "moon", 373 | ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "], 374 | 80 375 | ); 376 | 377 | spinner_frames!("runner", ["🚶 ", "🏃 "], 140); 378 | 379 | spinner_frames!( 380 | "pong", 381 | [ 382 | "▐⠂ ▌", 383 | "▐⠈ ▌", 384 | "▐ ⠂ ▌", 385 | "▐ ⠠ ▌", 386 | "▐ ⡀ ▌", 387 | "▐ ⠠ ▌", 388 | "▐ ⠂ ▌", 389 | "▐ ⠈ ▌", 390 | "▐ ⠂ ▌", 391 | "▐ ⠠ ▌", 392 | "▐ ⡀ ▌", 393 | "▐ ⠠ ▌", 394 | "▐ ⠂ ▌", 395 | "▐ ⠈ ▌", 396 | "▐ ⠂▌", 397 | "▐ ⠠▌", 398 | "▐ ⡀▌", 399 | "▐ ⠠ ▌", 400 | "▐ ⠂ ▌", 401 | "▐ ⠈ ▌", 402 | "▐ ⠂ ▌", 403 | "▐ ⠠ ▌", 404 | "▐ ⡀ ▌", 405 | "▐ ⠠ ▌", 406 | "▐ ⠂ ▌", 407 | "▐ ⠈ ▌", 408 | "▐ ⠂ ▌", 409 | "▐ ⠠ ▌", 410 | "▐ ⡀ ▌", 411 | "▐⠠ ▌" 412 | ], 413 | 80 414 | ); 415 | 416 | spinner_frames!( 417 | "shark", 418 | [ 419 | "▐|\\____________▌", 420 | "▐_|\\___________▌", 421 | "▐__|\\__________▌", 422 | "▐___|\\_________▌", 423 | "▐____|\\________▌", 424 | "▐_____|\\_______▌", 425 | "▐______|\\______▌", 426 | "▐_______|\\_____▌", 427 | "▐________|\\____▌", 428 | "▐_________|\\___▌", 429 | "▐__________|\\__▌", 430 | "▐___________|\\_▌", 431 | "▐____________|\\▌", 432 | "▐____________/|▌", 433 | "▐___________/|_▌", 434 | "▐__________/|__▌", 435 | "▐_________/|___▌", 436 | "▐________/|____▌", 437 | "▐_______/|_____▌", 438 | "▐______/|______▌", 439 | "▐_____/|_______▌", 440 | "▐____/|________▌", 441 | "▐___/|_________▌", 442 | "▐__/|__________▌", 443 | "▐_/|___________▌", 444 | "▐/|____________▌" 445 | ], 446 | 120 447 | ); 448 | 449 | spinner_frames!("dqpb", ["d", "q", "p", "b"], 100); 450 | 451 | spinner_frames!( 452 | "weather", 453 | [ 454 | "☀️ ", "☀️ ", "☀️ ", "🌤 ", "⛅️ ", "🌥 ", "☁️ ", "🌧 ", "🌨 ", "🌧 ", "🌨 ", "🌧 ", "🌨 ", "⛈ ", "🌨 ", 455 | "🌧 ", "🌨 ", "☁️ ", "🌥 ", "⛅️ ", "🌤 ", "☀️ ", "☀️ " 456 | ], 457 | 100 458 | ); 459 | 460 | spinner_frames!("christmas", ["🌲", "🎄"], 400); 461 | 462 | spinner_frames!( 463 | "grenade", 464 | [ 465 | "، ", "′ ", " ´ ", " ‾ ", " ⸌", " ⸊", " |", " ⁎", " ⁕", " ෴ ", " ⁓", " ", " ", 466 | " " 467 | ], 468 | 80 469 | ); 470 | 471 | spinner_frames!("point", ["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"], 125); 472 | 473 | spinner_frames!("layer", ["-", "=", "≡"], 150); 474 | 475 | spinner_frames!( 476 | "beta_wave", 477 | [ 478 | "ρββββββ", 479 | "βρβββββ", 480 | "ββρββββ", 481 | "βββρβββ", 482 | "ββββρββ", 483 | "βββββρβ", 484 | "ββββββρ" 485 | ], 486 | 80 487 | ); 488 | 489 | spinner_frames!( 490 | "finger_dance", 491 | ["🤘 ", "🤟 ", "🖖 ", "✋ ", "🤚 ", "👆 "], 492 | 160 493 | ); 494 | 495 | spinner_frames!( 496 | "fist_bump", 497 | [ 498 | "🤜    🤛 ", 499 | "🤜    🤛 ", 500 | "🤜    🤛 ", 501 | " 🤜  🤛  ", 502 | "  🤜🤛   ", 503 | " 🤜✨🤛   ", 504 | "🤜 ✨ 🤛  " 505 | ], 506 | 80 507 | ); 508 | 509 | spinner_frames!( 510 | "soccer_header", 511 | [ 512 | " 🧑⚽️ 🧑 ", 513 | "🧑 ⚽️ 🧑 ", 514 | "🧑 ⚽️ 🧑 ", 515 | "🧑 ⚽️ 🧑 ", 516 | "🧑 ⚽️ 🧑 ", 517 | "🧑 ⚽️ 🧑 ", 518 | "🧑 ⚽️🧑 ", 519 | "🧑 ⚽️ 🧑 ", 520 | "🧑 ⚽️ 🧑 ", 521 | "🧑 ⚽️ 🧑 ", 522 | "🧑 ⚽️ 🧑 ", 523 | "🧑 ⚽️ 🧑 " 524 | ], 525 | 80 526 | ); 527 | 528 | spinner_frames!( 529 | "mindblown", 530 | [ 531 | "😐 ", "😐 ", "😮 ", "😮 ", "😦 ", "😦 ", "😧 ", "😧 ", "🤯 ", "💥 ", "✨ ", "  ", "  ", 532 | "  " 533 | ], 534 | 160 535 | ); 536 | 537 | spinner_frames!("speaker", ["🔈 ", "🔉 ", "🔊 ", "🔉 "], 160); 538 | 539 | spinner_frames!("orange_pulse", ["🔸 ", "🔶 ", "🟠 ", "🟠 ", "🔶 "], 100); 540 | 541 | spinner_frames!("blue_pulse", ["🔹 ", "🔷 ", "🔵 ", "🔵 ", "🔷 "], 100); 542 | 543 | spinner_frames!( 544 | "orange_blue_pulse", 545 | ["🔸 ", "🔶 ", "🟠 ", "🟠 ", "🔶 ", "🔹 ", "🔷 ", "🔵 ", "🔵 ", "🔷 "], 546 | 100 547 | ); 548 | 549 | spinner_frames!( 550 | "time_travel", 551 | [ 552 | "🕛 ", "🕚 ", "🕙 ", "🕘 ", "🕗 ", "🕖 ", "🕕 ", "🕔 ", "🕓 ", "🕒 ", "🕑 ", "🕐 " 553 | ], 554 | 100 555 | ); 556 | 557 | spinner_frames!( 558 | "aesthetic", 559 | [ 560 | "▰▱▱▱▱▱▱", 561 | "▰▰▱▱▱▱▱", 562 | "▰▰▰▱▱▱▱", 563 | "▰▰▰▰▱▱▱", 564 | "▰▰▰▰▰▱▱", 565 | "▰▰▰▰▰▰▱", 566 | "▰▰▰▰▰▰▰", 567 | "▰▱▱▱▱▱▱" 568 | ], 569 | 80 570 | ); 571 | 572 | spinner_frames!( 573 | "binary", 574 | ["010010", "001100", "100101", "111010", "111101", "010111"], 575 | 50 576 | ); 577 | 578 | spinner_frames!( 579 | "cute", 580 | [ 581 | "( ´・ω・)", 582 | "( ´・ω)", 583 | "(  ´・)", 584 | "(   ´)", 585 | "( )", 586 | "(`  )", 587 | "(・` )", 588 | "(ω・` )", 589 | "(・ω・` )", 590 | "(´・ω・`)" 591 | ], 592 | 100 593 | ); 594 | -------------------------------------------------------------------------------- /src/streams.rs: -------------------------------------------------------------------------------- 1 | use std::io::{stderr, stdout, Write}; 2 | /// Simplified type for a stream. 3 | /// By default, `spinoff` uses `Streams::Stdout`. 4 | #[derive(Default, Copy, Clone, Debug)] 5 | pub enum Streams { 6 | #[default] 7 | Stdout, 8 | Stderr, 9 | } 10 | 11 | impl Streams { 12 | // Returns the stream to use. 13 | #[must_use = "Stream must be retrieved"] 14 | pub fn get_stream(self) -> Box { 15 | match self { 16 | Self::Stdout => Box::new(stdout()), 17 | Self::Stderr => Box::new(stderr()), 18 | } 19 | } 20 | // Clever implementation that allows us to automatically get the stream when `write!` is called. 21 | pub fn write_fmt(self, fmt: T) 22 | where 23 | T: std::fmt::Display, 24 | { 25 | write!(self.get_stream(), "{fmt}").expect("error: failed to write to stream"); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::Streams; 2 | use colored::{ColoredString, Colorize}; 3 | 4 | /// Color for spinner. Supports the 8 basic colors and a custom color variant. 5 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 6 | #[non_exhaustive] 7 | pub enum Color { 8 | Blue, 9 | Green, 10 | Red, 11 | Yellow, 12 | Cyan, 13 | White, 14 | Black, 15 | Magenta, 16 | TrueColor { r: u8, g: u8, b: u8 }, 17 | } 18 | 19 | pub fn colorize(color: Option, frame: &str) -> ColoredString { 20 | match color { 21 | Some(Color::Blue) => frame.blue(), 22 | Some(Color::Green) => frame.green(), 23 | Some(Color::Red) => frame.red(), 24 | Some(Color::Yellow) => frame.yellow(), 25 | Some(Color::Cyan) => frame.cyan(), 26 | Some(Color::White) => frame.white(), 27 | Some(Color::Black) => frame.black(), 28 | Some(Color::Magenta) => frame.magenta(), 29 | Some(Color::TrueColor { r, g, b }) => frame.truecolor(r, g, b), 30 | None => frame.normal() 31 | } 32 | } 33 | 34 | /// Internal function for deleting the last line in a terminal. 35 | /// This is used to clear the spinner. 36 | pub fn delete_last_line(clear_length: usize, stream: Streams) { 37 | write!(stream, "\r"); 38 | for _ in 0..clear_length { 39 | write!(stream, " "); 40 | } 41 | write!(stream, "\r"); 42 | } 43 | 44 | 45 | --------------------------------------------------------------------------------