├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── deny.toml ├── examples ├── cargo.rs ├── cargowrap.rs ├── download-continued.rs ├── download-speed.rs ├── download.rs ├── fastbar.rs ├── finebars.rs ├── iterator.rs ├── log.rs ├── long-spinner.rs ├── morebars.rs ├── multi-tree-ext.rs ├── multi-tree.rs ├── multi.rs ├── single.rs ├── tokio.rs └── yarnish.rs ├── screenshots ├── download.gif ├── multi-progress.gif ├── single.gif └── yarn.gif ├── src ├── draw_target.rs ├── format.rs ├── in_memory.rs ├── iter.rs ├── lib.rs ├── multi.rs ├── progress_bar.rs ├── rayon.rs ├── state.rs ├── style.rs └── term_like.rs └── tests ├── multi-autodrop.rs └── render.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | schedule: 8 | - cron: "43 6 * * 5" 9 | 10 | env: 11 | CLICOLOR_FORCE: 1 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | rust: [stable] 19 | features: [--all-features] 20 | target: [""] 21 | include: 22 | - os: ubuntu-latest 23 | rust: stable 24 | features: "" 25 | - os: ubuntu-latest 26 | rust: beta 27 | features: --all-features 28 | - os: ubuntu-latest 29 | rust: stable 30 | features: --all-features 31 | target: --target armv5te-unknown-linux-gnueabi 32 | use-cross: true 33 | 34 | runs-on: ${{ matrix.os }} 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dtolnay/rust-toolchain@master 39 | with: 40 | toolchain: ${{ matrix.rust }} 41 | - run: cargo test ${{ matrix.features }} 42 | 43 | cross: 44 | runs-on: ubuntu-latest 45 | 46 | strategy: 47 | matrix: 48 | target: [armv5te-unknown-linux-gnueabi] 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: dtolnay/rust-toolchain@stable 53 | with: 54 | target: ${{ matrix.target }} 55 | - uses: taiki-e/install-action@cross 56 | - run: cross build --target ${{ matrix.target }} --all-features 57 | 58 | msrv: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: dtolnay/rust-toolchain@master 63 | with: 64 | toolchain: "1.70" 65 | - run: cargo check --lib --all-features 66 | lint: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v4 70 | - uses: dtolnay/rust-toolchain@master 71 | with: 72 | toolchain: stable 73 | components: rustfmt, clippy 74 | - run: cargo fmt --all -- --check 75 | - run: cargo clippy --all-targets --all-features -- -D warnings 76 | - name: doc 77 | run: cargo doc --no-deps --document-private-items 78 | env: 79 | RUSTDOCFLAGS: -Dwarnings 80 | 81 | audit: 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v4 85 | - uses: EmbarkStudios/cargo-deny-action@v2 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "indicatif" 3 | version = "0.17.11" 4 | edition = "2021" 5 | rust-version = "1.70" 6 | description = "A progress bar and cli reporting library for Rust" 7 | keywords = ["cli", "progress", "pb", "colors", "progressbar"] 8 | categories = ["command-line-interface"] 9 | license = "MIT" 10 | repository = "https://github.com/console-rs/indicatif" 11 | documentation = "https://docs.rs/indicatif" 12 | readme = "README.md" 13 | exclude = ["screenshots/*"] 14 | 15 | [dependencies] 16 | console = { version = "0.15", default-features = false, features = ["ansi-parsing"] } 17 | futures-core = { version = "0.3", default-features = false, optional = true } 18 | number_prefix = "0.4" 19 | portable-atomic = "1.0.0" 20 | rayon = { version = "1.1", optional = true } 21 | tokio = { version = "1", optional = true, features = ["io-util"] } 22 | unicode-segmentation = { version = "1", optional = true } 23 | unicode-width = { version = "0.2", optional = true } 24 | vt100 = { version = "0.15.1", optional = true } 25 | 26 | [dev-dependencies] 27 | clap = { version = "4", features = ["color", "derive"] } 28 | once_cell = "1" 29 | rand = "0.9" 30 | tokio = { version = "1", features = ["fs", "time", "rt"] } 31 | futures = "0.3" # so the doctest for wrap_stream is nice 32 | pretty_assertions = "1.4.0" 33 | 34 | [target.'cfg(target_arch = "wasm32")'.dependencies] 35 | web-time = "1.1.0" 36 | 37 | [features] 38 | default = ["unicode-width", "console/unicode-width"] 39 | improved_unicode = ["unicode-segmentation", "unicode-width", "console/unicode-width"] 40 | in_memory = ["vt100"] 41 | futures = ["dep:futures-core"] 42 | 43 | [package.metadata.docs.rs] 44 | all-features = true 45 | rustdoc-args = ["--cfg", "docsrs"] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Armin Ronacher 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # indicatif 2 | 3 | [![Documentation](https://docs.rs/indicatif/badge.svg)](https://docs.rs/indicatif/) 4 | [![Crates.io](https://img.shields.io/crates/v/indicatif.svg)](https://crates.io/crates/indicatif) 5 | [![Build status](https://github.com/console-rs/indicatif/workflows/CI/badge.svg)](https://github.com/console-rs/indicatif/actions/workflows/rust.yml) 6 | [![Chat](https://img.shields.io/discord/976380008299917365?logo=discord)](https://discord.gg/YHmNA3De4W) 7 | 8 | A Rust library for indicating progress in command line applications to users. 9 | 10 | This currently primarily provides progress bars and spinners as well as basic 11 | color support, but there are bigger plans for the future of this! 12 | 13 | ## Examples 14 | 15 | [examples/yarnish.rs](examples/yarnish.rs) 16 | 17 | 18 | [examples/download.rs](examples/download.rs) 19 | 20 | 21 | [examples/multi.rs](examples/multi.rs) 22 | 23 | 24 | [examples/single.rs](examples/single.rs) 25 | 26 | 27 | ## Integrations 28 | 29 | You can use [indicatif-log-bridge](https://crates.io/crates/indicatif-log-bridge) to integrate with the 30 | [log crate](https://crates.io/crates/log) and avoid having both fight for your terminal. 31 | 32 | You can use [tracing-indicatif](https://crates.io/crates/tracing-indicatif) to integrate with the 33 | [tracing crate](https://crates.io/crates/tracing) with automatic progress bar management 34 | for active tracing spans, as well as ensure that tracing 35 | log events do not interfere with active progress bars. 36 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | allow = ["Apache-2.0", "MIT", "Unicode-3.0"] 3 | -------------------------------------------------------------------------------- /examples/cargo.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{mpsc, Arc, Mutex}; 2 | use std::thread; 3 | use std::time::{Duration, Instant}; 4 | 5 | use console::{Style, Term}; 6 | use indicatif::{HumanDuration, ProgressBar, ProgressStyle}; 7 | use rand::Rng; 8 | 9 | static CRATES: &[(&str, &str)] = &[ 10 | ("console", "v0.14.1"), 11 | ("lazy_static", "v1.4.0"), 12 | ("libc", "v0.2.93"), 13 | ("regex", "v1.4.6"), 14 | ("regex-syntax", "v0.6.23"), 15 | ("terminal_size", "v0.1.16"), 16 | ("libc", "v0.2.93"), 17 | ("unicode-width", "v0.1.8"), 18 | ("lazy_static", "v1.4.0"), 19 | ("number_prefix", "v0.4.0"), 20 | ("regex", "v1.4.6"), 21 | ("rand", "v0.8.3"), 22 | ("getrandom", "v0.2.2"), 23 | ("cfg-if", "v1.0.0"), 24 | ("libc", "v0.2.93"), 25 | ("rand_chacha", "v0.3.0"), 26 | ("ppv-lite86", "v0.2.10"), 27 | ("rand_core", "v0.6.2"), 28 | ("getrandom", "v0.2.2"), 29 | ("rand_core", "v0.6.2"), 30 | ("tokio", "v1.5.0"), 31 | ("bytes", "v1.0.1"), 32 | ("pin-project-lite", "v0.2.6"), 33 | ("slab", "v0.4.3"), 34 | ("indicatif", "v0.15.0"), 35 | ]; 36 | 37 | fn main() { 38 | // number of cpus 39 | const NUM_CPUS: usize = 4; 40 | let start = Instant::now(); 41 | 42 | // mimic cargo progress bar although it behaves a bit different 43 | let pb = ProgressBar::new(CRATES.len() as u64); 44 | pb.set_style( 45 | ProgressStyle::with_template( 46 | // note that bar size is fixed unlike cargo which is dynamic 47 | // and also the truncation in cargo uses trailers (`...`) 48 | if Term::stdout().size().1 > 80 { 49 | "{prefix:>12.cyan.bold} [{bar:57}] {pos}/{len} {wide_msg}" 50 | } else { 51 | "{prefix:>12.cyan.bold} [{bar:57}] {pos}/{len}" 52 | }, 53 | ) 54 | .unwrap() 55 | .progress_chars("=> "), 56 | ); 57 | pb.set_prefix("Building"); 58 | 59 | // process in another thread 60 | // crates to be iterated but not exactly a tree 61 | let crates = Arc::new(Mutex::new(CRATES.iter())); 62 | let (tx, rx) = mpsc::channel(); 63 | for n in 0..NUM_CPUS { 64 | let tx = tx.clone(); 65 | let crates = crates.clone(); 66 | thread::spawn(move || { 67 | let mut rng = rand::rng(); 68 | loop { 69 | let krate = crates.lock().unwrap().next(); 70 | // notify main thread if n thread is processing a crate 71 | tx.send((n, krate)).unwrap(); 72 | if let Some(krate) = krate { 73 | thread::sleep(Duration::from_millis( 74 | // last compile and linking is always slow, let's mimic that 75 | if CRATES.last() == Some(krate) { 76 | rng.random_range(1_000..2_000) 77 | } else { 78 | rng.random_range(250..1_000) 79 | }, 80 | )); 81 | } else { 82 | break; 83 | } 84 | } 85 | }); 86 | } 87 | // drop tx to stop waiting 88 | drop(tx); 89 | 90 | let green_bold = Style::new().green().bold(); 91 | 92 | // do progress drawing in main thread 93 | let mut processing = [None; NUM_CPUS]; 94 | while let Ok((n, krate)) = rx.recv() { 95 | processing[n] = krate; 96 | let crates: Vec<&str> = processing 97 | .iter() 98 | .filter_map(|t| t.copied().map(|(name, _)| name)) 99 | .collect(); 100 | pb.set_message(crates.join(", ")); 101 | if let Some((name, version)) = krate { 102 | // crate is being built 103 | let line = format!( 104 | "{:>12} {} {}", 105 | green_bold.apply_to("Compiling"), 106 | name, 107 | version 108 | ); 109 | pb.println(line); 110 | 111 | pb.inc(1); 112 | } 113 | } 114 | pb.finish_and_clear(); 115 | 116 | // compilation is finished 117 | println!( 118 | "{:>12} dev [unoptimized + debuginfo] target(s) in {}", 119 | green_bold.apply_to("Finished"), 120 | HumanDuration(start.elapsed()) 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /examples/cargowrap.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader}; 2 | use std::process; 3 | use std::time::{Duration, Instant}; 4 | 5 | use indicatif::{HumanDuration, ProgressBar, ProgressStyle}; 6 | 7 | pub fn main() { 8 | let started = Instant::now(); 9 | 10 | println!("Compiling package in release mode..."); 11 | 12 | let pb = ProgressBar::new_spinner(); 13 | pb.enable_steady_tick(Duration::from_millis(200)); 14 | pb.set_style( 15 | ProgressStyle::with_template("{spinner:.dim.bold} cargo: {wide_msg}") 16 | .unwrap() 17 | .tick_chars("/|\\- "), 18 | ); 19 | 20 | let mut p = process::Command::new("cargo") 21 | .arg("build") 22 | .arg("--release") 23 | .stderr(process::Stdio::piped()) 24 | .spawn() 25 | .unwrap(); 26 | 27 | for line in BufReader::new(p.stderr.take().unwrap()).lines() { 28 | let line = line.unwrap(); 29 | let stripped_line = line.trim(); 30 | if !stripped_line.is_empty() { 31 | pb.set_message(stripped_line.to_owned()); 32 | } 33 | pb.tick(); 34 | } 35 | 36 | p.wait().unwrap(); 37 | 38 | pb.finish_and_clear(); 39 | 40 | println!("Done in {}", HumanDuration(started.elapsed())); 41 | } 42 | -------------------------------------------------------------------------------- /examples/download-continued.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::thread; 3 | use std::time::Duration; 4 | 5 | use indicatif::{ProgressBar, ProgressStyle}; 6 | 7 | fn main() { 8 | let mut downloaded = 69369369; 9 | let total_size = 231231231; 10 | 11 | let pb = ProgressBar::new(total_size); 12 | pb.set_style( 13 | ProgressStyle::with_template( 14 | "{spinner:.green} [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})", 15 | ) 16 | .unwrap() 17 | .progress_chars("#>-"), 18 | ); 19 | pb.set_position(downloaded); 20 | pb.reset_eta(); 21 | 22 | while downloaded < total_size { 23 | downloaded = min(downloaded + 123211, total_size); 24 | pb.set_position(downloaded); 25 | thread::sleep(Duration::from_millis(12)); 26 | } 27 | 28 | pb.finish_with_message("downloaded"); 29 | } 30 | -------------------------------------------------------------------------------- /examples/download-speed.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::thread; 3 | use std::time::Duration; 4 | 5 | use indicatif::{ProgressBar, ProgressStyle}; 6 | 7 | fn main() { 8 | let mut downloaded = 0; 9 | let total_size = 231231231; 10 | 11 | let pb = ProgressBar::new(total_size); 12 | pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") 13 | .unwrap() 14 | .progress_chars("#>-")); 15 | 16 | while downloaded < total_size { 17 | let new = min(downloaded + 223211, total_size); 18 | downloaded = new; 19 | pb.set_position(new); 20 | thread::sleep(Duration::from_millis(12)); 21 | } 22 | 23 | pb.finish_with_message("downloaded"); 24 | } 25 | -------------------------------------------------------------------------------- /examples/download.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | use std::{cmp::min, fmt::Write}; 4 | 5 | use indicatif::{ProgressBar, ProgressState, ProgressStyle}; 6 | 7 | fn main() { 8 | let mut downloaded = 0; 9 | let total_size = 231231231; 10 | 11 | let pb = ProgressBar::new(total_size); 12 | pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") 13 | .unwrap() 14 | .with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) 15 | .progress_chars("#>-")); 16 | 17 | while downloaded < total_size { 18 | let new = min(downloaded + 223211, total_size); 19 | downloaded = new; 20 | pb.set_position(new); 21 | thread::sleep(Duration::from_millis(12)); 22 | } 23 | 24 | pb.finish_with_message("downloaded"); 25 | } 26 | -------------------------------------------------------------------------------- /examples/fastbar.rs: -------------------------------------------------------------------------------- 1 | use indicatif::ProgressBar; 2 | 3 | fn many_units_of_easy_work(n: u64, label: &str) { 4 | let pb = ProgressBar::new(n); 5 | 6 | let mut sum = 0; 7 | for i in 0..n { 8 | // Any quick computation, followed by an update to the progress bar. 9 | sum += 2 * i + 3; 10 | pb.inc(1); 11 | } 12 | pb.finish(); 13 | 14 | println!("[{}] Sum ({}) calculated in {:?}", label, sum, pb.elapsed()); 15 | } 16 | 17 | fn main() { 18 | const N: u64 = 1 << 20; 19 | 20 | // Perform a long sequence of many simple computations monitored by a 21 | // default progress bar. 22 | many_units_of_easy_work(N, "Default progress bar "); 23 | } 24 | -------------------------------------------------------------------------------- /examples/finebars.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 5 | use rand::Rng; 6 | 7 | fn main() { 8 | let styles = [ 9 | ("Rough bar:", "█ ", "red"), 10 | ("Fine bar: ", "█▉▊▋▌▍▎▏ ", "yellow"), 11 | ("Vertical: ", "█▇▆▅▄▃▂▁ ", "green"), 12 | ("Fade in: ", "█▓▒░ ", "blue"), 13 | ("Blocky: ", "█▛▌▖ ", "magenta"), 14 | ]; 15 | 16 | let m = MultiProgress::new(); 17 | 18 | let handles: Vec<_> = styles 19 | .iter() 20 | .map(|s| { 21 | let pb = m.add(ProgressBar::new(512)); 22 | pb.set_style( 23 | ProgressStyle::with_template(&format!("{{prefix:.bold}}▕{{bar:.{}}}▏{{msg}}", s.2)) 24 | .unwrap() 25 | .progress_chars(s.1), 26 | ); 27 | pb.set_prefix(s.0); 28 | let wait = Duration::from_millis(rand::rng().random_range(10..30)); 29 | thread::spawn(move || { 30 | for i in 0..512 { 31 | thread::sleep(wait); 32 | pb.inc(1); 33 | pb.set_message(format!("{:3}%", 100 * i / 512)); 34 | } 35 | pb.finish_with_message("100%"); 36 | }) 37 | }) 38 | .collect(); 39 | 40 | for h in handles { 41 | let _ = h.join(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/iterator.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::{ProgressBar, ProgressIterator, ProgressStyle}; 5 | 6 | fn main() { 7 | // Default styling, attempt to use Iterator::size_hint to count input size 8 | for _ in (0..1000).progress() { 9 | // ... 10 | thread::sleep(Duration::from_millis(5)); 11 | } 12 | 13 | // Provide explicit number of elements in iterator 14 | for _ in (0..1000).progress_count(1000) { 15 | // ... 16 | thread::sleep(Duration::from_millis(5)); 17 | } 18 | 19 | // Provide a custom bar style 20 | let pb = ProgressBar::new(1000); 21 | pb.set_style( 22 | ProgressStyle::with_template( 23 | "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] ({pos}/{len}, ETA {eta})", 24 | ) 25 | .unwrap(), 26 | ); 27 | for _ in (0..1000).progress_with(pb) { 28 | // ... 29 | thread::sleep(Duration::from_millis(5)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/log.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::ProgressBar; 5 | 6 | fn main() { 7 | let pb = ProgressBar::new(100); 8 | for i in 0..100 { 9 | thread::sleep(Duration::from_millis(25)); 10 | pb.println(format!("[+] finished #{i}")); 11 | pb.inc(1); 12 | } 13 | pb.finish_with_message("done"); 14 | } 15 | -------------------------------------------------------------------------------- /examples/long-spinner.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::{ProgressBar, ProgressStyle}; 5 | 6 | fn main() { 7 | let pb = ProgressBar::new_spinner(); 8 | pb.enable_steady_tick(Duration::from_millis(120)); 9 | pb.set_style( 10 | ProgressStyle::with_template("{spinner:.blue} {msg}") 11 | .unwrap() 12 | // For more spinners check out the cli-spinners project: 13 | // https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json 14 | .tick_strings(&[ 15 | "▹▹▹▹▹", 16 | "▸▹▹▹▹", 17 | "▹▸▹▹▹", 18 | "▹▹▸▹▹", 19 | "▹▹▹▸▹", 20 | "▹▹▹▹▸", 21 | "▪▪▪▪▪", 22 | ]), 23 | ); 24 | pb.set_message("Calculating..."); 25 | thread::sleep(Duration::from_secs(5)); 26 | pb.finish_with_message("Done"); 27 | } 28 | -------------------------------------------------------------------------------- /examples/morebars.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::thread; 3 | use std::time::Duration; 4 | 5 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 6 | 7 | fn main() { 8 | let m = Arc::new(MultiProgress::new()); 9 | let sty = ProgressStyle::with_template("{bar:40.green/yellow} {pos:>7}/{len:7}").unwrap(); 10 | 11 | let pb = m.add(ProgressBar::new(5)); 12 | pb.set_style(sty.clone()); 13 | 14 | // make sure we show up at all. otherwise no rendering 15 | // event. 16 | pb.tick(); 17 | for _ in 0..5 { 18 | let pb2 = m.add(ProgressBar::new(128)); 19 | pb2.set_style(sty.clone()); 20 | for _ in 0..128 { 21 | thread::sleep(Duration::from_millis(5)); 22 | pb2.inc(1); 23 | } 24 | pb2.finish(); 25 | pb.inc(1); 26 | } 27 | pb.finish_with_message("done"); 28 | } 29 | -------------------------------------------------------------------------------- /examples/multi-tree-ext.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::fmt::Debug; 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | use std::sync::Arc; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | use console::style; 9 | use indicatif::{MultiProgress, MultiProgressAlignment, ProgressBar, ProgressStyle}; 10 | use once_cell::sync::Lazy; 11 | use rand::rngs::ThreadRng; 12 | use rand::{Rng, RngCore}; 13 | 14 | #[derive(Debug, Clone)] 15 | enum Action { 16 | ModifyTree(usize), 17 | IncProgressBar(usize), 18 | Stop, 19 | } 20 | 21 | #[derive(Clone, Debug)] 22 | enum Elem { 23 | AddItem(Item), 24 | RemoveItem(Index), 25 | } 26 | 27 | #[derive(Clone, Debug)] 28 | struct Item { 29 | key: String, 30 | index: usize, 31 | indent: usize, 32 | progress_bar: ProgressBar, 33 | } 34 | 35 | #[derive(Clone, Debug)] 36 | struct Index(usize); 37 | 38 | const PB_LEN: u64 = 32; 39 | static ELEM_IDX: AtomicUsize = AtomicUsize::new(0); 40 | 41 | static ELEMENTS: Lazy<[Elem; 27]> = Lazy::new(|| { 42 | [ 43 | Elem::AddItem(Item { 44 | indent: 9, 45 | index: 0, 46 | progress_bar: ProgressBar::new(PB_LEN), 47 | key: "dog".to_string(), 48 | }), 49 | Elem::AddItem(Item { 50 | indent: 0, 51 | index: 0, 52 | progress_bar: ProgressBar::new(PB_LEN), 53 | key: "temp_1".to_string(), 54 | }), 55 | Elem::AddItem(Item { 56 | indent: 8, 57 | index: 1, 58 | progress_bar: ProgressBar::new(PB_LEN), 59 | key: "lazy".to_string(), 60 | }), 61 | Elem::AddItem(Item { 62 | indent: 0, 63 | index: 1, 64 | progress_bar: ProgressBar::new(PB_LEN), 65 | key: "temp_2".to_string(), 66 | }), 67 | Elem::AddItem(Item { 68 | indent: 1, 69 | index: 0, 70 | progress_bar: ProgressBar::new(PB_LEN), 71 | key: "the".to_string(), 72 | }), 73 | Elem::AddItem(Item { 74 | indent: 0, 75 | index: 0, 76 | progress_bar: ProgressBar::new(PB_LEN), 77 | key: "temp_3".to_string(), 78 | }), 79 | Elem::AddItem(Item { 80 | indent: 7, 81 | index: 3, 82 | progress_bar: ProgressBar::new(PB_LEN), 83 | key: "a".to_string(), 84 | }), 85 | Elem::AddItem(Item { 86 | indent: 0, 87 | index: 3, 88 | progress_bar: ProgressBar::new(PB_LEN), 89 | key: "temp_4".to_string(), 90 | }), 91 | Elem::AddItem(Item { 92 | indent: 6, 93 | index: 2, 94 | progress_bar: ProgressBar::new(PB_LEN), 95 | key: "over".to_string(), 96 | }), 97 | Elem::RemoveItem(Index(6)), 98 | Elem::RemoveItem(Index(4)), 99 | Elem::RemoveItem(Index(3)), 100 | Elem::RemoveItem(Index(0)), 101 | Elem::AddItem(Item { 102 | indent: 0, 103 | index: 2, 104 | progress_bar: ProgressBar::new(PB_LEN), 105 | key: "temp_5".to_string(), 106 | }), 107 | Elem::AddItem(Item { 108 | indent: 4, 109 | index: 1, 110 | progress_bar: ProgressBar::new(PB_LEN), 111 | key: "fox".to_string(), 112 | }), 113 | Elem::AddItem(Item { 114 | indent: 0, 115 | index: 1, 116 | progress_bar: ProgressBar::new(PB_LEN), 117 | key: "temp_6".to_string(), 118 | }), 119 | Elem::AddItem(Item { 120 | indent: 2, 121 | index: 1, 122 | progress_bar: ProgressBar::new(PB_LEN), 123 | key: "quick".to_string(), 124 | }), 125 | Elem::AddItem(Item { 126 | indent: 0, 127 | index: 1, 128 | progress_bar: ProgressBar::new(PB_LEN), 129 | key: "temp_7".to_string(), 130 | }), 131 | Elem::AddItem(Item { 132 | indent: 5, 133 | index: 5, 134 | progress_bar: ProgressBar::new(PB_LEN), 135 | key: "jumps".to_string(), 136 | }), 137 | Elem::AddItem(Item { 138 | indent: 0, 139 | index: 5, 140 | progress_bar: ProgressBar::new(PB_LEN), 141 | key: "temp_8".to_string(), 142 | }), 143 | Elem::AddItem(Item { 144 | indent: 3, 145 | index: 4, 146 | progress_bar: ProgressBar::new(PB_LEN), 147 | key: "brown".to_string(), 148 | }), 149 | Elem::AddItem(Item { 150 | indent: 0, 151 | index: 3, 152 | progress_bar: ProgressBar::new(PB_LEN), 153 | key: "temp_9".to_string(), 154 | }), 155 | Elem::RemoveItem(Index(10)), 156 | Elem::RemoveItem(Index(7)), 157 | Elem::RemoveItem(Index(4)), 158 | Elem::RemoveItem(Index(3)), 159 | Elem::RemoveItem(Index(1)), 160 | ] 161 | }); 162 | 163 | #[derive(Debug, Parser)] 164 | pub struct Config { 165 | #[clap(long)] 166 | bottom_alignment: bool, 167 | } 168 | 169 | /// The example demonstrates the usage of `MultiProgress` and further extends `multi-tree` example. 170 | /// Now the example has 3 different actions implemented, and the item tree can be modified 171 | /// by inserting or removing progress bars. The progress bars to be removed eventually 172 | /// have messages with pattern `"temp_*"`. 173 | /// 174 | /// Also the command option `--bottom-alignment` is used to control the vertical alignment of the 175 | /// `MultiProgress`. To enable this run it with 176 | /// ```ignore 177 | /// cargo run --example multi-tree-ext -- --bottom-alignment 178 | /// ``` 179 | pub fn main() { 180 | let conf: Config = Config::parse(); 181 | let mp = Arc::new(MultiProgress::new()); 182 | let alignment = if conf.bottom_alignment { 183 | MultiProgressAlignment::Bottom 184 | } else { 185 | MultiProgressAlignment::Top 186 | }; 187 | mp.set_alignment(alignment); 188 | let sty_main = ProgressStyle::with_template("{bar:40.green/yellow} {pos:>4}/{len:4}").unwrap(); 189 | let sty_aux = 190 | ProgressStyle::with_template("[{pos:>2}/{len:2}] {prefix}{spinner:.green} {msg}").unwrap(); 191 | let sty_fin = ProgressStyle::with_template("[{pos:>2}/{len:2}] {prefix}{msg}").unwrap(); 192 | 193 | let pb_main = mp.add(ProgressBar::new( 194 | ELEMENTS 195 | .iter() 196 | .map(|e| match e { 197 | Elem::AddItem(item) => item.progress_bar.length().unwrap(), 198 | Elem::RemoveItem(_) => 1, 199 | }) 200 | .sum(), 201 | )); 202 | 203 | pb_main.set_style(sty_main); 204 | for e in ELEMENTS.iter() { 205 | match e { 206 | Elem::AddItem(item) => item.progress_bar.set_style(sty_aux.clone()), 207 | Elem::RemoveItem(_) => {} 208 | } 209 | } 210 | 211 | let mut items: Vec<&Item> = Vec::with_capacity(ELEMENTS.len()); 212 | 213 | let mp2 = Arc::clone(&mp); 214 | let mut rng = ThreadRng::default(); 215 | pb_main.tick(); 216 | loop { 217 | match get_action(&mut rng, &items) { 218 | Action::Stop => { 219 | // all elements were exhausted 220 | pb_main.finish(); 221 | return; 222 | } 223 | Action::ModifyTree(elem_idx) => match &ELEMENTS[elem_idx] { 224 | Elem::AddItem(item) => { 225 | let pb = mp2.insert(item.index, item.progress_bar.clone()); 226 | pb.set_prefix(" ".repeat(item.indent)); 227 | pb.set_message(&item.key); 228 | items.insert(item.index, item); 229 | } 230 | Elem::RemoveItem(Index(index)) => { 231 | let item = items.remove(*index); 232 | let pb = &item.progress_bar; 233 | mp2.remove(pb); 234 | pb_main.inc(pb.length().unwrap() - pb.position()); 235 | } 236 | }, 237 | Action::IncProgressBar(item_idx) => { 238 | let item = &items[item_idx]; 239 | item.progress_bar.inc(1); 240 | let pos = item.progress_bar.position(); 241 | if pos >= item.progress_bar.length().unwrap() { 242 | item.progress_bar.set_style(sty_fin.clone()); 243 | item.progress_bar.finish_with_message(format!( 244 | "{} {}", 245 | style("✔").green(), 246 | item.key 247 | )); 248 | } 249 | pb_main.inc(1); 250 | } 251 | } 252 | thread::sleep(Duration::from_millis(20)); 253 | } 254 | } 255 | 256 | /// The function guarantees to return the action, that is valid for the current tree. 257 | fn get_action(rng: &mut dyn RngCore, items: &[&Item]) -> Action { 258 | let elem_idx = ELEM_IDX.load(Ordering::SeqCst); 259 | // the indices of those items, that not completed yet 260 | let uncompleted = items 261 | .iter() 262 | .enumerate() 263 | .filter(|(_, item)| { 264 | let pos = item.progress_bar.position(); 265 | pos < item.progress_bar.length().unwrap() 266 | }) 267 | .map(|(idx, _)| idx) 268 | .collect::>(); 269 | let k = rng.random_range(0..16); 270 | if (k > 0 || k == 0 && elem_idx == ELEMENTS.len()) && !uncompleted.is_empty() { 271 | let idx = rng.random_range(0..uncompleted.len() as u64) as usize; 272 | Action::IncProgressBar(uncompleted[idx]) 273 | } else if elem_idx < ELEMENTS.len() { 274 | ELEM_IDX.fetch_add(1, Ordering::SeqCst); 275 | Action::ModifyTree(elem_idx) 276 | } else { 277 | // nothing to do more 278 | Action::Stop 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /examples/multi-tree.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::sync::{Arc, Mutex}; 3 | use std::thread; 4 | use std::time::Duration; 5 | 6 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 7 | use once_cell::sync::Lazy; 8 | use rand::rngs::ThreadRng; 9 | use rand::{Rng, RngCore}; 10 | 11 | #[derive(Debug, Clone)] 12 | enum Action { 13 | AddProgressBar(usize), 14 | IncProgressBar(usize), 15 | } 16 | 17 | #[derive(Clone, Debug)] 18 | struct Elem { 19 | key: String, 20 | index: usize, 21 | indent: usize, 22 | progress_bar: ProgressBar, 23 | } 24 | 25 | static ELEMENTS: Lazy<[Elem; 9]> = Lazy::new(|| { 26 | [ 27 | Elem { 28 | indent: 1, 29 | index: 0, 30 | progress_bar: ProgressBar::new(32), 31 | key: "jumps".to_string(), 32 | }, 33 | Elem { 34 | indent: 2, 35 | index: 1, 36 | progress_bar: ProgressBar::new(32), 37 | key: "lazy".to_string(), 38 | }, 39 | Elem { 40 | indent: 0, 41 | index: 0, 42 | progress_bar: ProgressBar::new(32), 43 | key: "the".to_string(), 44 | }, 45 | Elem { 46 | indent: 3, 47 | index: 3, 48 | progress_bar: ProgressBar::new(32), 49 | key: "dog".to_string(), 50 | }, 51 | Elem { 52 | indent: 2, 53 | index: 2, 54 | progress_bar: ProgressBar::new(32), 55 | key: "over".to_string(), 56 | }, 57 | Elem { 58 | indent: 2, 59 | index: 1, 60 | progress_bar: ProgressBar::new(32), 61 | key: "brown".to_string(), 62 | }, 63 | Elem { 64 | indent: 1, 65 | index: 1, 66 | progress_bar: ProgressBar::new(32), 67 | key: "quick".to_string(), 68 | }, 69 | Elem { 70 | indent: 3, 71 | index: 5, 72 | progress_bar: ProgressBar::new(32), 73 | key: "a".to_string(), 74 | }, 75 | Elem { 76 | indent: 3, 77 | index: 3, 78 | progress_bar: ProgressBar::new(32), 79 | key: "fox".to_string(), 80 | }, 81 | ] 82 | }); 83 | 84 | /// The example implements the tree-like collection of progress bars, where elements are 85 | /// added on the fly and progress bars get incremented until all elements is added and 86 | /// all progress bars finished. 87 | /// On each iteration `get_action` function returns some action, and when the tree gets 88 | /// complete, the function returns `None`, which finishes the loop. 89 | fn main() { 90 | let mp = Arc::new(MultiProgress::new()); 91 | let sty_main = ProgressStyle::with_template("{bar:40.green/yellow} {pos:>4}/{len:4}").unwrap(); 92 | let sty_aux = ProgressStyle::with_template("{spinner:.green} {msg} {pos:>4}/{len:4}").unwrap(); 93 | 94 | let pb_main = mp.add(ProgressBar::new( 95 | ELEMENTS 96 | .iter() 97 | .map(|e| e.progress_bar.length().unwrap()) 98 | .sum(), 99 | )); 100 | pb_main.set_style(sty_main); 101 | for elem in ELEMENTS.iter() { 102 | elem.progress_bar.set_style(sty_aux.clone()); 103 | } 104 | 105 | let tree: Arc>> = Arc::new(Mutex::new(Vec::with_capacity(ELEMENTS.len()))); 106 | let tree2 = Arc::clone(&tree); 107 | 108 | let mp2 = Arc::clone(&mp); 109 | let _ = thread::spawn(move || { 110 | let mut rng = ThreadRng::default(); 111 | pb_main.tick(); 112 | loop { 113 | thread::sleep(Duration::from_millis(15)); 114 | match get_action(&mut rng, &tree) { 115 | None => { 116 | // all elements were exhausted 117 | pb_main.finish(); 118 | return; 119 | } 120 | Some(Action::AddProgressBar(el_idx)) => { 121 | let elem = &ELEMENTS[el_idx]; 122 | let pb = mp2.insert(elem.index + 1, elem.progress_bar.clone()); 123 | pb.set_message(format!("{} {}", " ".repeat(elem.indent), elem.key)); 124 | tree.lock().unwrap().insert(elem.index, elem); 125 | } 126 | Some(Action::IncProgressBar(el_idx)) => { 127 | let elem = &tree.lock().unwrap()[el_idx]; 128 | elem.progress_bar.inc(1); 129 | let pos = elem.progress_bar.position(); 130 | if pos >= elem.progress_bar.length().unwrap() { 131 | elem.progress_bar.finish_with_message(format!( 132 | "{}{} {}", 133 | " ".repeat(elem.indent), 134 | "✔", 135 | elem.key 136 | )); 137 | } 138 | pb_main.inc(1); 139 | } 140 | } 141 | } 142 | }) 143 | .join(); 144 | 145 | println!("==============================="); 146 | println!("the tree should be the same as:"); 147 | for elem in tree2.lock().unwrap().iter() { 148 | println!("{} {}", " ".repeat(elem.indent), elem.key); 149 | } 150 | } 151 | 152 | /// The function guarantees to return the action, that is valid for the current tree. 153 | fn get_action(rng: &mut dyn RngCore, tree: &Mutex>) -> Option { 154 | let elem_len = ELEMENTS.len() as u64; 155 | let list_len = tree.lock().unwrap().len() as u64; 156 | let sum_free = tree 157 | .lock() 158 | .unwrap() 159 | .iter() 160 | .map(|e| { 161 | let pos = e.progress_bar.position(); 162 | let len = e.progress_bar.length().unwrap(); 163 | len - pos 164 | }) 165 | .sum::(); 166 | 167 | if sum_free == 0 && list_len == elem_len { 168 | // nothing to do more 169 | None 170 | } else if sum_free == 0 && list_len < elem_len { 171 | // there is no place to make an increment 172 | Some(Action::AddProgressBar(tree.lock().unwrap().len())) 173 | } else { 174 | loop { 175 | let list = tree.lock().unwrap(); 176 | let k = rng.random_range(0..17); 177 | if k == 0 && list_len < elem_len { 178 | return Some(Action::AddProgressBar(list.len())); 179 | } else { 180 | let l = (k % list_len) as usize; 181 | let pos = list[l].progress_bar.position(); 182 | let len = list[l].progress_bar.length(); 183 | if pos < len.unwrap() { 184 | return Some(Action::IncProgressBar(l)); 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/multi.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 5 | 6 | use rand::Rng; 7 | 8 | fn main() { 9 | let m = MultiProgress::new(); 10 | let sty = ProgressStyle::with_template( 11 | "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", 12 | ) 13 | .unwrap() 14 | .progress_chars("##-"); 15 | 16 | let n = 200; 17 | let pb = m.add(ProgressBar::new(n)); 18 | pb.set_style(sty.clone()); 19 | pb.set_message("todo"); 20 | let pb2 = m.add(ProgressBar::new(n)); 21 | pb2.set_style(sty.clone()); 22 | pb2.set_message("finished"); 23 | 24 | let pb3 = m.insert_after(&pb2, ProgressBar::new(1024)); 25 | pb3.set_style(sty); 26 | 27 | m.println("starting!").unwrap(); 28 | 29 | let mut threads = vec![]; 30 | 31 | let m_clone = m.clone(); 32 | let h3 = thread::spawn(move || { 33 | for i in 0..1024 { 34 | thread::sleep(Duration::from_millis(2)); 35 | pb3.set_message(format!("item #{}", i + 1)); 36 | pb3.inc(1); 37 | } 38 | m_clone.println("pb3 is done!").unwrap(); 39 | pb3.finish_with_message("done"); 40 | }); 41 | 42 | for i in 0..n { 43 | thread::sleep(Duration::from_millis(15)); 44 | if i == n / 3 { 45 | thread::sleep(Duration::from_secs(2)); 46 | } 47 | pb.inc(1); 48 | let pb2 = pb2.clone(); 49 | threads.push(thread::spawn(move || { 50 | thread::sleep(rand::rng().random_range(Duration::from_secs(1)..Duration::from_secs(5))); 51 | pb2.inc(1); 52 | })); 53 | } 54 | pb.finish_with_message("all jobs started"); 55 | 56 | for thread in threads { 57 | let _ = thread.join(); 58 | } 59 | let _ = h3.join(); 60 | pb2.finish_with_message("all jobs done"); 61 | m.clear().unwrap(); 62 | } 63 | -------------------------------------------------------------------------------- /examples/single.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::ProgressBar; 5 | 6 | fn main() { 7 | let pb = ProgressBar::new(1024); 8 | for _ in 0..1024 { 9 | thread::sleep(Duration::from_millis(5)); 10 | pb.inc(1); 11 | } 12 | pb.finish_with_message("done"); 13 | } 14 | -------------------------------------------------------------------------------- /examples/tokio.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use indicatif::ProgressBar; 4 | use tokio::runtime; 5 | use tokio::time::interval; 6 | 7 | fn main() { 8 | // Plain progress bar, totaling 1024 steps. 9 | let steps = 1024; 10 | let pb = ProgressBar::new(steps); 11 | 12 | // Stream of events, triggering every 5ms. 13 | let rt = runtime::Builder::new_current_thread() 14 | .enable_time() 15 | .build() 16 | .expect("failed to create runtime"); 17 | 18 | // Future computation which runs for `steps` interval events, 19 | // incrementing one step of the progress bar each time. 20 | let future = async { 21 | let mut intv = interval(Duration::from_millis(5)); 22 | 23 | for _ in 0..steps { 24 | intv.tick().await; 25 | pb.inc(1); 26 | } 27 | }; 28 | 29 | // Drive the future to completion, blocking until done. 30 | rt.block_on(future); 31 | 32 | // Mark the progress bar as finished. 33 | pb.finish(); 34 | } 35 | -------------------------------------------------------------------------------- /examples/yarnish.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::{Duration, Instant}; 3 | 4 | use console::{style, Emoji}; 5 | use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle}; 6 | use rand::prelude::IndexedRandom; 7 | use rand::Rng; 8 | 9 | static PACKAGES: &[&str] = &[ 10 | "fs-events", 11 | "my-awesome-module", 12 | "emoji-speaker", 13 | "wrap-ansi", 14 | "stream-browserify", 15 | "acorn-dynamic-import", 16 | ]; 17 | 18 | static COMMANDS: &[&str] = &[ 19 | "cmake .", 20 | "make", 21 | "make clean", 22 | "gcc foo.c -o foo", 23 | "gcc bar.c -o bar", 24 | "./helper.sh rebuild-cache", 25 | "make all-clean", 26 | "make test", 27 | ]; 28 | 29 | static LOOKING_GLASS: Emoji<'_, '_> = Emoji("🔍 ", ""); 30 | static TRUCK: Emoji<'_, '_> = Emoji("🚚 ", ""); 31 | static CLIP: Emoji<'_, '_> = Emoji("🔗 ", ""); 32 | static PAPER: Emoji<'_, '_> = Emoji("📃 ", ""); 33 | static SPARKLE: Emoji<'_, '_> = Emoji("✨ ", ":-)"); 34 | 35 | pub fn main() { 36 | let mut rng = rand::rng(); 37 | let started = Instant::now(); 38 | let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}") 39 | .unwrap() 40 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "); 41 | 42 | println!( 43 | "{} {}Resolving packages...", 44 | style("[1/4]").bold().dim(), 45 | LOOKING_GLASS 46 | ); 47 | println!( 48 | "{} {}Fetching packages...", 49 | style("[2/4]").bold().dim(), 50 | TRUCK 51 | ); 52 | 53 | println!( 54 | "{} {}Linking dependencies...", 55 | style("[3/4]").bold().dim(), 56 | CLIP 57 | ); 58 | let deps = 1232; 59 | let pb = ProgressBar::new(deps); 60 | for _ in 0..deps { 61 | thread::sleep(Duration::from_millis(3)); 62 | pb.inc(1); 63 | } 64 | pb.finish_and_clear(); 65 | 66 | println!( 67 | "{} {}Building fresh packages...", 68 | style("[4/4]").bold().dim(), 69 | PAPER 70 | ); 71 | let m = MultiProgress::new(); 72 | let handles: Vec<_> = (0..4u32) 73 | .map(|i| { 74 | let count = rng.random_range(30..80); 75 | let pb = m.add(ProgressBar::new(count)); 76 | pb.set_style(spinner_style.clone()); 77 | pb.set_prefix(format!("[{}/?]", i + 1)); 78 | thread::spawn(move || { 79 | let mut rng = rand::rng(); 80 | let pkg = PACKAGES.choose(&mut rng).unwrap(); 81 | for _ in 0..count { 82 | let cmd = COMMANDS.choose(&mut rng).unwrap(); 83 | thread::sleep(Duration::from_millis(rng.random_range(25..200))); 84 | pb.set_message(format!("{pkg}: {cmd}")); 85 | pb.inc(1); 86 | } 87 | pb.finish_with_message("waiting..."); 88 | }) 89 | }) 90 | .collect(); 91 | for h in handles { 92 | let _ = h.join(); 93 | } 94 | m.clear().unwrap(); 95 | 96 | println!("{} Done in {}", SPARKLE, HumanDuration(started.elapsed())); 97 | } 98 | -------------------------------------------------------------------------------- /screenshots/download.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/console-rs/indicatif/458e4dc391c1b001937e502bd450a0bfc8b4d9f6/screenshots/download.gif -------------------------------------------------------------------------------- /screenshots/multi-progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/console-rs/indicatif/458e4dc391c1b001937e502bd450a0bfc8b4d9f6/screenshots/multi-progress.gif -------------------------------------------------------------------------------- /screenshots/single.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/console-rs/indicatif/458e4dc391c1b001937e502bd450a0bfc8b4d9f6/screenshots/single.gif -------------------------------------------------------------------------------- /screenshots/yarn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/console-rs/indicatif/458e4dc391c1b001937e502bd450a0bfc8b4d9f6/screenshots/yarn.gif -------------------------------------------------------------------------------- /src/draw_target.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::ops::{Add, AddAssign, Sub}; 3 | use std::slice::SliceIndex; 4 | use std::sync::{Arc, RwLock, RwLockWriteGuard}; 5 | use std::thread::panicking; 6 | use std::time::Duration; 7 | #[cfg(not(target_arch = "wasm32"))] 8 | use std::time::Instant; 9 | 10 | use console::{Term, TermTarget}; 11 | #[cfg(target_arch = "wasm32")] 12 | use web_time::Instant; 13 | 14 | use crate::multi::{MultiProgressAlignment, MultiState}; 15 | use crate::TermLike; 16 | 17 | /// Target for draw operations 18 | /// 19 | /// This tells a [`ProgressBar`](crate::ProgressBar) or a 20 | /// [`MultiProgress`](crate::MultiProgress) object where to paint to. 21 | /// The draw target is a stateful wrapper over a drawing destination and 22 | /// internally optimizes how often the state is painted to the output 23 | /// device. 24 | #[derive(Debug)] 25 | pub struct ProgressDrawTarget { 26 | kind: TargetKind, 27 | } 28 | 29 | impl ProgressDrawTarget { 30 | /// Draw to a buffered stdout terminal at a max of 20 times a second. 31 | /// 32 | /// For more information see [`ProgressDrawTarget::term`]. 33 | pub fn stdout() -> Self { 34 | Self::term(Term::buffered_stdout(), 20) 35 | } 36 | 37 | /// Draw to a buffered stderr terminal at a max of 20 times a second. 38 | /// 39 | /// This is the default draw target for progress bars. For more 40 | /// information see [`ProgressDrawTarget::term`]. 41 | pub fn stderr() -> Self { 42 | Self::term(Term::buffered_stderr(), 20) 43 | } 44 | 45 | /// Draw to a buffered stdout terminal at a max of `refresh_rate` times a second. 46 | /// 47 | /// For more information see [`ProgressDrawTarget::term`]. 48 | pub fn stdout_with_hz(refresh_rate: u8) -> Self { 49 | Self::term(Term::buffered_stdout(), refresh_rate) 50 | } 51 | 52 | /// Draw to a buffered stderr terminal at a max of `refresh_rate` times a second. 53 | /// 54 | /// For more information see [`ProgressDrawTarget::term`]. 55 | pub fn stderr_with_hz(refresh_rate: u8) -> Self { 56 | Self::term(Term::buffered_stderr(), refresh_rate) 57 | } 58 | 59 | pub(crate) fn new_remote(state: Arc>, idx: usize) -> Self { 60 | Self { 61 | kind: TargetKind::Multi { state, idx }, 62 | } 63 | } 64 | 65 | /// Draw to a terminal, with a specific refresh rate. 66 | /// 67 | /// Progress bars are by default drawn to terminals however if the 68 | /// terminal is not user attended the entire progress bar will be 69 | /// hidden. This is done so that piping to a file will not produce 70 | /// useless escape codes in that file. 71 | /// 72 | /// Will panic if `refresh_rate` is `0`. 73 | pub fn term(term: Term, refresh_rate: u8) -> Self { 74 | Self { 75 | kind: TargetKind::Term { 76 | term, 77 | last_line_count: VisualLines::default(), 78 | rate_limiter: RateLimiter::new(refresh_rate), 79 | draw_state: DrawState::default(), 80 | }, 81 | } 82 | } 83 | 84 | /// Draw to a boxed object that implements the [`TermLike`] trait. 85 | pub fn term_like(term_like: Box) -> Self { 86 | Self { 87 | kind: TargetKind::TermLike { 88 | inner: term_like, 89 | last_line_count: VisualLines::default(), 90 | rate_limiter: None, 91 | draw_state: DrawState::default(), 92 | }, 93 | } 94 | } 95 | 96 | /// Draw to a boxed object that implements the [`TermLike`] trait, 97 | /// with a specific refresh rate. 98 | pub fn term_like_with_hz(term_like: Box, refresh_rate: u8) -> Self { 99 | Self { 100 | kind: TargetKind::TermLike { 101 | inner: term_like, 102 | last_line_count: VisualLines::default(), 103 | rate_limiter: Option::from(RateLimiter::new(refresh_rate)), 104 | draw_state: DrawState::default(), 105 | }, 106 | } 107 | } 108 | 109 | /// A hidden draw target. 110 | /// 111 | /// This forces a progress bar to be not rendered at all. 112 | pub fn hidden() -> Self { 113 | Self { 114 | kind: TargetKind::Hidden, 115 | } 116 | } 117 | 118 | /// Returns true if the draw target is hidden. 119 | /// 120 | /// This is internally used in progress bars to figure out if overhead 121 | /// from drawing can be prevented. 122 | pub fn is_hidden(&self) -> bool { 123 | match self.kind { 124 | TargetKind::Hidden => true, 125 | TargetKind::Term { ref term, .. } => !term.is_term(), 126 | TargetKind::Multi { ref state, .. } => state.read().unwrap().is_hidden(), 127 | _ => false, 128 | } 129 | } 130 | 131 | /// This is used in progress bars to determine whether to use stdout or stderr 132 | /// for detecting color support. 133 | pub(crate) fn is_stderr(&self) -> bool { 134 | match &self.kind { 135 | TargetKind::Term { term, .. } => matches!(term.target(), TermTarget::Stderr), 136 | _ => false, 137 | } 138 | } 139 | 140 | /// Returns the current width of the draw target. 141 | pub(crate) fn width(&self) -> Option { 142 | match self.kind { 143 | TargetKind::Term { ref term, .. } => Some(term.size().1), 144 | TargetKind::Multi { ref state, .. } => state.read().unwrap().width(), 145 | TargetKind::TermLike { ref inner, .. } => Some(inner.width()), 146 | TargetKind::Hidden => None, 147 | } 148 | } 149 | 150 | /// Notifies the backing `MultiProgress` (if applicable) that the associated progress bar should 151 | /// be marked a zombie. 152 | pub(crate) fn mark_zombie(&self) { 153 | if let TargetKind::Multi { idx, state } = &self.kind { 154 | state.write().unwrap().mark_zombie(*idx); 155 | } 156 | } 157 | 158 | /// Set whether or not to just move cursor instead of clearing lines 159 | pub(crate) fn set_move_cursor(&mut self, move_cursor: bool) { 160 | match &mut self.kind { 161 | TargetKind::Term { draw_state, .. } => draw_state.move_cursor = move_cursor, 162 | TargetKind::TermLike { draw_state, .. } => draw_state.move_cursor = move_cursor, 163 | _ => {} 164 | } 165 | } 166 | 167 | /// Apply the given draw state (draws it). 168 | pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option> { 169 | match &mut self.kind { 170 | TargetKind::Term { 171 | term, 172 | last_line_count, 173 | rate_limiter, 174 | draw_state, 175 | } => { 176 | if !term.is_term() { 177 | return None; 178 | } 179 | 180 | match force_draw || rate_limiter.allow(now) { 181 | true => Some(Drawable::Term { 182 | term, 183 | last_line_count, 184 | draw_state, 185 | }), 186 | false => None, // rate limited 187 | } 188 | } 189 | TargetKind::Multi { idx, state, .. } => { 190 | let state = state.write().unwrap(); 191 | Some(Drawable::Multi { 192 | idx: *idx, 193 | state, 194 | force_draw, 195 | now, 196 | }) 197 | } 198 | TargetKind::TermLike { 199 | inner, 200 | last_line_count, 201 | rate_limiter, 202 | draw_state, 203 | } => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) { 204 | true => Some(Drawable::TermLike { 205 | term_like: &**inner, 206 | last_line_count, 207 | draw_state, 208 | }), 209 | false => None, // rate limited 210 | }, 211 | // Hidden, finished, or no need to refresh yet 212 | _ => None, 213 | } 214 | } 215 | 216 | /// Properly disconnects from the draw target 217 | pub(crate) fn disconnect(&self, now: Instant) { 218 | match self.kind { 219 | TargetKind::Term { .. } => {} 220 | TargetKind::Multi { idx, ref state, .. } => { 221 | let state = state.write().unwrap(); 222 | let _ = Drawable::Multi { 223 | state, 224 | idx, 225 | force_draw: true, 226 | now, 227 | } 228 | .clear(); 229 | } 230 | TargetKind::Hidden => {} 231 | TargetKind::TermLike { .. } => {} 232 | }; 233 | } 234 | 235 | pub(crate) fn remote(&self) -> Option<(&Arc>, usize)> { 236 | match &self.kind { 237 | TargetKind::Multi { state, idx } => Some((state, *idx)), 238 | _ => None, 239 | } 240 | } 241 | 242 | pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) { 243 | self.kind.adjust_last_line_count(adjust); 244 | } 245 | } 246 | 247 | #[derive(Debug)] 248 | enum TargetKind { 249 | Term { 250 | term: Term, 251 | last_line_count: VisualLines, 252 | rate_limiter: RateLimiter, 253 | draw_state: DrawState, 254 | }, 255 | Multi { 256 | state: Arc>, 257 | idx: usize, 258 | }, 259 | Hidden, 260 | TermLike { 261 | inner: Box, 262 | last_line_count: VisualLines, 263 | rate_limiter: Option, 264 | draw_state: DrawState, 265 | }, 266 | } 267 | 268 | impl TargetKind { 269 | /// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines 270 | fn adjust_last_line_count(&mut self, adjust: LineAdjust) { 271 | let last_line_count = match self { 272 | Self::Term { 273 | last_line_count, .. 274 | } => last_line_count, 275 | Self::TermLike { 276 | last_line_count, .. 277 | } => last_line_count, 278 | _ => return, 279 | }; 280 | 281 | match adjust { 282 | LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count), 283 | LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count), 284 | } 285 | } 286 | } 287 | 288 | pub(crate) enum Drawable<'a> { 289 | Term { 290 | term: &'a Term, 291 | last_line_count: &'a mut VisualLines, 292 | draw_state: &'a mut DrawState, 293 | }, 294 | Multi { 295 | state: RwLockWriteGuard<'a, MultiState>, 296 | idx: usize, 297 | force_draw: bool, 298 | now: Instant, 299 | }, 300 | TermLike { 301 | term_like: &'a dyn TermLike, 302 | last_line_count: &'a mut VisualLines, 303 | draw_state: &'a mut DrawState, 304 | }, 305 | } 306 | 307 | impl Drawable<'_> { 308 | /// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines 309 | pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) { 310 | let last_line_count: &mut VisualLines = match self { 311 | Drawable::Term { 312 | last_line_count, .. 313 | } => last_line_count, 314 | Drawable::TermLike { 315 | last_line_count, .. 316 | } => last_line_count, 317 | _ => return, 318 | }; 319 | 320 | match adjust { 321 | LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count), 322 | LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count), 323 | } 324 | } 325 | 326 | pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> { 327 | let mut state = match self { 328 | Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state), 329 | Drawable::Multi { state, idx, .. } => state.draw_state(*idx), 330 | Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state), 331 | }; 332 | 333 | state.reset(); 334 | state 335 | } 336 | 337 | pub(crate) fn clear(mut self) -> io::Result<()> { 338 | let state = self.state(); 339 | drop(state); 340 | self.draw() 341 | } 342 | 343 | pub(crate) fn draw(self) -> io::Result<()> { 344 | match self { 345 | Drawable::Term { 346 | term, 347 | last_line_count, 348 | draw_state, 349 | } => draw_state.draw_to_term(term, last_line_count), 350 | Drawable::Multi { 351 | mut state, 352 | force_draw, 353 | now, 354 | .. 355 | } => state.draw(force_draw, None, now), 356 | Drawable::TermLike { 357 | term_like, 358 | last_line_count, 359 | draw_state, 360 | } => draw_state.draw_to_term(term_like, last_line_count), 361 | } 362 | } 363 | 364 | pub(crate) fn width(&self) -> Option { 365 | match self { 366 | Self::Term { term, .. } => Some(term.size().1), 367 | Self::Multi { state, .. } => state.width(), 368 | Self::TermLike { term_like, .. } => Some(term_like.width()), 369 | } 370 | } 371 | } 372 | 373 | pub(crate) enum LineAdjust { 374 | /// Adds to `last_line_count` so that the next draw also clears those lines 375 | Clear(VisualLines), 376 | /// Subtracts from `last_line_count` so that the next draw retains those lines 377 | Keep(VisualLines), 378 | } 379 | 380 | pub(crate) struct DrawStateWrapper<'a> { 381 | state: &'a mut DrawState, 382 | orphan_lines: Option<&'a mut Vec>, 383 | } 384 | 385 | impl<'a> DrawStateWrapper<'a> { 386 | pub(crate) fn for_term(state: &'a mut DrawState) -> Self { 387 | Self { 388 | state, 389 | orphan_lines: None, 390 | } 391 | } 392 | 393 | pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec) -> Self { 394 | Self { 395 | state, 396 | orphan_lines: Some(orphan_lines), 397 | } 398 | } 399 | } 400 | 401 | impl std::ops::Deref for DrawStateWrapper<'_> { 402 | type Target = DrawState; 403 | 404 | fn deref(&self) -> &Self::Target { 405 | self.state 406 | } 407 | } 408 | 409 | impl std::ops::DerefMut for DrawStateWrapper<'_> { 410 | fn deref_mut(&mut self) -> &mut Self::Target { 411 | self.state 412 | } 413 | } 414 | 415 | impl Drop for DrawStateWrapper<'_> { 416 | fn drop(&mut self) { 417 | if let Some(text_lines) = &mut self.orphan_lines { 418 | // Filter out the lines that do not contain progress information 419 | // Store the filtered out lines in orphaned 420 | let mut lines = Vec::new(); 421 | 422 | for line in self.state.lines.drain(..) { 423 | match &line { 424 | LineType::Text(_) | LineType::Empty => text_lines.push(line), 425 | _ => lines.push(line), 426 | } 427 | } 428 | 429 | self.state.lines = lines; 430 | } 431 | } 432 | } 433 | 434 | #[derive(Debug)] 435 | struct RateLimiter { 436 | interval: u16, // in milliseconds 437 | capacity: u8, 438 | prev: Instant, 439 | } 440 | 441 | /// Rate limit but allow occasional bursts above desired rate 442 | impl RateLimiter { 443 | fn new(rate: u8) -> Self { 444 | Self { 445 | interval: 1000 / (rate as u16), // between 3 and 1000 milliseconds 446 | capacity: MAX_BURST, 447 | prev: Instant::now(), 448 | } 449 | } 450 | 451 | fn allow(&mut self, now: Instant) -> bool { 452 | if now < self.prev { 453 | return false; 454 | } 455 | 456 | let elapsed = now - self.prev; 457 | // If `capacity` is 0 and not enough time (`self.interval` ms) has passed since 458 | // `self.prev` to add new capacity, return `false`. The goal of this method is to 459 | // make this decision as efficient as possible. 460 | if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) { 461 | return false; 462 | } 463 | 464 | // We now calculate `new`, the number of ms, since we last returned `true`, 465 | // and `remainder`, which represents a number of ns less than 1ms which we cannot 466 | // convert into capacity now, so we're saving it for later. 467 | let (new, remainder) = ( 468 | elapsed.as_millis() / self.interval as u128, 469 | elapsed.as_nanos() % (self.interval as u128 * 1_000_000), 470 | ); 471 | 472 | // We add `new` to `capacity`, subtract one for returning `true` from here, 473 | // then make sure it does not exceed a maximum of `MAX_BURST`, then store it. 474 | self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8; 475 | // Store `prev` for the next iteration after subtracting the `remainder`. 476 | // Just use `unwrap` here because it shouldn't be possible for this to underflow. 477 | self.prev = now 478 | .checked_sub(Duration::from_nanos(remainder as u64)) 479 | .unwrap(); 480 | true 481 | } 482 | } 483 | 484 | const MAX_BURST: u8 = 20; 485 | 486 | /// The drawn state of an element. 487 | #[derive(Clone, Debug, Default)] 488 | pub(crate) struct DrawState { 489 | /// The lines to print (can contain ANSI codes) 490 | pub(crate) lines: Vec, 491 | /// True if we should move the cursor up when possible instead of clearing lines. 492 | pub(crate) move_cursor: bool, 493 | /// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top` 494 | pub(crate) alignment: MultiProgressAlignment, 495 | } 496 | 497 | impl DrawState { 498 | /// Draw the current state to the terminal 499 | /// We expect a few things: 500 | /// - self.lines contains n lines of text/empty then m lines of bars 501 | /// - None of those lines contain newlines 502 | fn draw_to_term( 503 | &mut self, 504 | term: &(impl TermLike + ?Sized), 505 | bar_count: &mut VisualLines, // The number of dynamic lines printed at the previous tick 506 | ) -> io::Result<()> { 507 | if panicking() { 508 | return Ok(()); 509 | } 510 | 511 | if !self.lines.is_empty() && self.move_cursor { 512 | // Move up to first line (assuming the last line doesn't contain a '\n') and then move to then front of the line 513 | term.move_cursor_up(bar_count.as_usize().saturating_sub(1))?; 514 | term.write_str("\r")?; 515 | } else { 516 | // Fork of console::clear_last_lines that assumes that the last line doesn't contain a '\n' 517 | let n = bar_count.as_usize(); 518 | term.move_cursor_up(n.saturating_sub(1))?; 519 | for i in 0..n { 520 | term.clear_line()?; 521 | if i + 1 != n { 522 | term.move_cursor_down(1)?; 523 | } 524 | } 525 | term.move_cursor_up(n.saturating_sub(1))?; 526 | } 527 | 528 | let term_width = term.width() as usize; 529 | 530 | // Here we calculate the terminal vertical real estate that the state requires 531 | let full_height = self.visual_line_count(.., term_width); 532 | 533 | let shift = match self.alignment { 534 | // If we align to the bottom and the new height is less than before, clear the lines 535 | // that are not used by the new content. 536 | MultiProgressAlignment::Bottom if full_height < *bar_count => { 537 | let shift = *bar_count - full_height; 538 | for _ in 0..shift.as_usize() { 539 | term.write_line("")?; 540 | } 541 | shift 542 | } 543 | _ => VisualLines::default(), 544 | }; 545 | 546 | // Accumulate the displayed height in here. This differs from `full_height` in that it will 547 | // accurately reflect the number of lines that have been displayed on the terminal, if the 548 | // full height exceeds the terminal height. 549 | let mut real_height = VisualLines::default(); 550 | 551 | for (idx, line) in self.lines.iter().enumerate() { 552 | let line_height = line.wrapped_height(term_width); 553 | 554 | // Check here for bar lines that exceed the terminal height 555 | if matches!(line, LineType::Bar(_)) { 556 | // Stop here if printing this bar would exceed the terminal height 557 | if real_height + line_height > term.height().into() { 558 | break; 559 | } 560 | 561 | real_height += line_height; 562 | } 563 | 564 | // Print a new line if this is not the first line printed this tick 565 | // the first line will automatically wrap due to the filler below 566 | if idx != 0 { 567 | term.write_line("")?; 568 | } 569 | 570 | term.write_str(line.as_ref())?; 571 | 572 | if idx + 1 == self.lines.len() { 573 | // For the last line of the output, keep the cursor on the right terminal 574 | // side so that next user writes/prints will happen on the next line 575 | let last_line_filler = line_height.as_usize() * term_width - line.console_width(); 576 | term.write_str(&" ".repeat(last_line_filler))?; 577 | } 578 | } 579 | 580 | term.flush()?; 581 | *bar_count = real_height + shift; 582 | 583 | Ok(()) 584 | } 585 | 586 | fn reset(&mut self) { 587 | self.lines.clear(); 588 | } 589 | 590 | pub(crate) fn visual_line_count( 591 | &self, 592 | range: impl SliceIndex<[LineType], Output = [LineType]>, 593 | width: usize, 594 | ) -> VisualLines { 595 | visual_line_count(&self.lines[range], width) 596 | } 597 | } 598 | 599 | #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] 600 | pub(crate) struct VisualLines(usize); 601 | 602 | impl VisualLines { 603 | pub(crate) fn saturating_add(&self, other: Self) -> Self { 604 | Self(self.0.saturating_add(other.0)) 605 | } 606 | 607 | pub(crate) fn saturating_sub(&self, other: Self) -> Self { 608 | Self(self.0.saturating_sub(other.0)) 609 | } 610 | 611 | pub(crate) fn as_usize(&self) -> usize { 612 | self.0 613 | } 614 | } 615 | 616 | impl Add for VisualLines { 617 | type Output = Self; 618 | 619 | fn add(self, rhs: Self) -> Self::Output { 620 | Self(self.0 + rhs.0) 621 | } 622 | } 623 | 624 | impl AddAssign for VisualLines { 625 | fn add_assign(&mut self, rhs: Self) { 626 | self.0 += rhs.0; 627 | } 628 | } 629 | 630 | impl> From for VisualLines { 631 | fn from(value: T) -> Self { 632 | Self(value.into()) 633 | } 634 | } 635 | 636 | impl Sub for VisualLines { 637 | type Output = Self; 638 | 639 | fn sub(self, rhs: Self) -> Self::Output { 640 | Self(self.0 - rhs.0) 641 | } 642 | } 643 | 644 | /// Calculate the number of visual lines in the given lines, after 645 | /// accounting for line wrapping and non-printable characters. 646 | pub(crate) fn visual_line_count(lines: &[LineType], width: usize) -> VisualLines { 647 | lines.iter().fold(VisualLines::default(), |acc, line| { 648 | acc.saturating_add(line.wrapped_height(width)) 649 | }) 650 | } 651 | 652 | #[derive(Clone, Debug)] 653 | pub(crate) enum LineType { 654 | Text(String), 655 | Bar(String), 656 | Empty, 657 | } 658 | 659 | impl LineType { 660 | fn wrapped_height(&self, width: usize) -> VisualLines { 661 | // Calculate real length based on terminal width 662 | // This take in account linewrap from terminal 663 | let terminal_len = (self.console_width() as f64 / width as f64).ceil() as usize; 664 | 665 | // If the line is effectively empty (for example when it consists 666 | // solely of ANSI color code sequences, count it the same as a 667 | // new line. If the line is measured to be len = 0, we will 668 | // subtract with overflow later. 669 | usize::max(terminal_len, 1).into() 670 | } 671 | 672 | fn console_width(&self) -> usize { 673 | console::measure_text_width(self.as_ref()) 674 | } 675 | } 676 | 677 | impl AsRef for LineType { 678 | fn as_ref(&self) -> &str { 679 | match self { 680 | LineType::Text(s) | LineType::Bar(s) => s, 681 | LineType::Empty => "", 682 | } 683 | } 684 | } 685 | 686 | impl PartialEq for LineType { 687 | fn eq(&self, other: &str) -> bool { 688 | self.as_ref() == other 689 | } 690 | } 691 | 692 | #[cfg(test)] 693 | mod tests { 694 | use crate::draw_target::LineType; 695 | use crate::{MultiProgress, ProgressBar, ProgressDrawTarget}; 696 | 697 | #[test] 698 | fn multi_is_hidden() { 699 | let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden()); 700 | 701 | let pb = mp.add(ProgressBar::new(100)); 702 | assert!(mp.is_hidden()); 703 | assert!(pb.is_hidden()); 704 | } 705 | 706 | #[test] 707 | fn real_line_count_test() { 708 | #[derive(Debug)] 709 | struct Case { 710 | lines: &'static [&'static str], 711 | expectation: usize, 712 | width: usize, 713 | } 714 | 715 | let lines_and_expectations = [ 716 | Case { 717 | lines: &["1234567890"], 718 | expectation: 1, 719 | width: 10, 720 | }, 721 | Case { 722 | lines: &["1234567890"], 723 | expectation: 2, 724 | width: 5, 725 | }, 726 | Case { 727 | lines: &["1234567890"], 728 | expectation: 3, 729 | width: 4, 730 | }, 731 | Case { 732 | lines: &["1234567890"], 733 | expectation: 4, 734 | width: 3, 735 | }, 736 | Case { 737 | lines: &["1234567890", "", "1234567890"], 738 | expectation: 3, 739 | width: 10, 740 | }, 741 | Case { 742 | lines: &["1234567890", "", "1234567890"], 743 | expectation: 5, 744 | width: 5, 745 | }, 746 | Case { 747 | lines: &["1234567890", "", "1234567890"], 748 | expectation: 7, 749 | width: 4, 750 | }, 751 | Case { 752 | lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"], 753 | expectation: 8, 754 | width: 7, 755 | }, 756 | Case { 757 | lines: &["", "", "", "", ""], 758 | expectation: 5, 759 | width: 6, 760 | }, 761 | Case { 762 | // These lines contain only ANSI escape sequences, so they should only count as 1 line 763 | lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"], 764 | expectation: 2, 765 | width: 5, 766 | }, 767 | Case { 768 | // These lines contain ANSI escape sequences and two effective chars, so they should only count as 1 line still 769 | lines: &[ 770 | "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma", 771 | "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma", 772 | ], 773 | expectation: 2, 774 | width: 5, 775 | }, 776 | Case { 777 | // These lines contain ANSI escape sequences and six effective chars, so they should count as 2 lines each 778 | lines: &[ 779 | "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd", 780 | "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd", 781 | ], 782 | expectation: 4, 783 | width: 5, 784 | }, 785 | ]; 786 | 787 | for case in lines_and_expectations.iter() { 788 | let result = super::visual_line_count( 789 | &case 790 | .lines 791 | .iter() 792 | .map(|s| LineType::Text(s.to_string())) 793 | .collect::>(), 794 | case.width, 795 | ); 796 | assert_eq!(result, case.expectation.into(), "case: {:?}", case); 797 | } 798 | } 799 | } 800 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::Duration; 3 | 4 | use number_prefix::NumberPrefix; 5 | 6 | const SECOND: Duration = Duration::from_secs(1); 7 | const MINUTE: Duration = Duration::from_secs(60); 8 | const HOUR: Duration = Duration::from_secs(60 * 60); 9 | const DAY: Duration = Duration::from_secs(24 * 60 * 60); 10 | const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60); 11 | const YEAR: Duration = Duration::from_secs(365 * 24 * 60 * 60); 12 | 13 | /// Wraps an std duration for human basic formatting. 14 | #[derive(Debug)] 15 | pub struct FormattedDuration(pub Duration); 16 | 17 | /// Wraps an std duration for human readable formatting. 18 | #[derive(Debug)] 19 | pub struct HumanDuration(pub Duration); 20 | 21 | /// Formats bytes for human readability 22 | /// 23 | /// # Examples 24 | /// ```rust 25 | /// # use indicatif::HumanBytes; 26 | /// assert_eq!("15 B", format!("{}", HumanBytes(15))); 27 | /// assert_eq!("1.46 KiB", format!("{}", HumanBytes(1_500))); 28 | /// assert_eq!("1.43 MiB", format!("{}", HumanBytes(1_500_000))); 29 | /// assert_eq!("1.40 GiB", format!("{}", HumanBytes(1_500_000_000))); 30 | /// assert_eq!("1.36 TiB", format!("{}", HumanBytes(1_500_000_000_000))); 31 | /// assert_eq!("1.33 PiB", format!("{}", HumanBytes(1_500_000_000_000_000))); 32 | /// ``` 33 | #[derive(Debug)] 34 | pub struct HumanBytes(pub u64); 35 | 36 | /// Formats bytes for human readability using SI prefixes 37 | /// 38 | /// # Examples 39 | /// ```rust 40 | /// # use indicatif::DecimalBytes; 41 | /// assert_eq!("15 B", format!("{}", DecimalBytes(15))); 42 | /// assert_eq!("1.50 kB", format!("{}", DecimalBytes(1_500))); 43 | /// assert_eq!("1.50 MB", format!("{}", DecimalBytes(1_500_000))); 44 | /// assert_eq!("1.50 GB", format!("{}", DecimalBytes(1_500_000_000))); 45 | /// assert_eq!("1.50 TB", format!("{}", DecimalBytes(1_500_000_000_000))); 46 | /// assert_eq!("1.50 PB", format!("{}", DecimalBytes(1_500_000_000_000_000))); 47 | /// ``` 48 | #[derive(Debug)] 49 | pub struct DecimalBytes(pub u64); 50 | 51 | /// Formats bytes for human readability using ISO/IEC prefixes 52 | /// 53 | /// # Examples 54 | /// ```rust 55 | /// # use indicatif::BinaryBytes; 56 | /// assert_eq!("15 B", format!("{}", BinaryBytes(15))); 57 | /// assert_eq!("1.46 KiB", format!("{}", BinaryBytes(1_500))); 58 | /// assert_eq!("1.43 MiB", format!("{}", BinaryBytes(1_500_000))); 59 | /// assert_eq!("1.40 GiB", format!("{}", BinaryBytes(1_500_000_000))); 60 | /// assert_eq!("1.36 TiB", format!("{}", BinaryBytes(1_500_000_000_000))); 61 | /// assert_eq!("1.33 PiB", format!("{}", BinaryBytes(1_500_000_000_000_000))); 62 | /// ``` 63 | #[derive(Debug)] 64 | pub struct BinaryBytes(pub u64); 65 | 66 | /// Formats counts for human readability using commas 67 | #[derive(Debug)] 68 | pub struct HumanCount(pub u64); 69 | 70 | /// Formats counts for human readability using commas for floats 71 | #[derive(Debug)] 72 | pub struct HumanFloatCount(pub f64); 73 | 74 | impl fmt::Display for FormattedDuration { 75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | let mut t = self.0.as_secs(); 77 | let seconds = t % 60; 78 | t /= 60; 79 | let minutes = t % 60; 80 | t /= 60; 81 | let hours = t % 24; 82 | t /= 24; 83 | if t > 0 { 84 | let days = t; 85 | write!(f, "{days}d {hours:02}:{minutes:02}:{seconds:02}") 86 | } else { 87 | write!(f, "{hours:02}:{minutes:02}:{seconds:02}") 88 | } 89 | } 90 | } 91 | 92 | // `HumanDuration` should be as intuitively understandable as possible. 93 | // So we want to round, not truncate: otherwise 1 hour and 59 minutes 94 | // would display an ETA of "1 hour" which underestimates the time 95 | // remaining by a factor 2. 96 | // 97 | // To make the precision more uniform, we avoid displaying "1 unit" 98 | // (except for seconds), because it would be displayed for a relatively 99 | // long duration compared to the unit itself. Instead, when we arrive 100 | // around 1.5 unit, we change from "2 units" to the next smaller unit 101 | // (e.g. "89 seconds"). 102 | // 103 | // Formally: 104 | // * for n >= 2, we go from "n+1 units" to "n units" exactly at (n + 1/2) units 105 | // * we switch from "2 units" to the next smaller unit at (1.5 unit minus half of the next smaller unit) 106 | 107 | impl fmt::Display for HumanDuration { 108 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 109 | let mut idx = 0; 110 | for (i, &(cur, _, _)) in UNITS.iter().enumerate() { 111 | idx = i; 112 | match UNITS.get(i + 1) { 113 | Some(&next) if self.0.saturating_add(next.0 / 2) >= cur + cur / 2 => break, 114 | _ => continue, 115 | } 116 | } 117 | 118 | let (unit, name, alt) = UNITS[idx]; 119 | // FIXME when `div_duration_f64` is stable 120 | let mut t = (self.0.as_secs_f64() / unit.as_secs_f64()).round() as usize; 121 | if idx < UNITS.len() - 1 { 122 | t = Ord::max(t, 2); 123 | } 124 | 125 | match (f.alternate(), t) { 126 | (true, _) => write!(f, "{t}{alt}"), 127 | (false, 1) => write!(f, "{t} {name}"), 128 | (false, _) => write!(f, "{t} {name}s"), 129 | } 130 | } 131 | } 132 | 133 | const UNITS: &[(Duration, &str, &str)] = &[ 134 | (YEAR, "year", "y"), 135 | (WEEK, "week", "w"), 136 | (DAY, "day", "d"), 137 | (HOUR, "hour", "h"), 138 | (MINUTE, "minute", "m"), 139 | (SECOND, "second", "s"), 140 | ]; 141 | 142 | impl fmt::Display for HumanBytes { 143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 144 | match NumberPrefix::binary(self.0 as f64) { 145 | NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"), 146 | NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"), 147 | } 148 | } 149 | } 150 | 151 | impl fmt::Display for DecimalBytes { 152 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 153 | match NumberPrefix::decimal(self.0 as f64) { 154 | NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"), 155 | NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"), 156 | } 157 | } 158 | } 159 | 160 | impl fmt::Display for BinaryBytes { 161 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 162 | match NumberPrefix::binary(self.0 as f64) { 163 | NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"), 164 | NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"), 165 | } 166 | } 167 | } 168 | 169 | impl fmt::Display for HumanCount { 170 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 171 | use fmt::Write; 172 | 173 | let num = self.0.to_string(); 174 | let len = num.len(); 175 | for (idx, c) in num.chars().enumerate() { 176 | let pos = len - idx - 1; 177 | f.write_char(c)?; 178 | if pos > 0 && pos % 3 == 0 { 179 | f.write_char(',')?; 180 | } 181 | } 182 | Ok(()) 183 | } 184 | } 185 | 186 | impl fmt::Display for HumanFloatCount { 187 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 188 | use fmt::Write; 189 | 190 | // Use formatter's precision if provided, otherwise default to 4 191 | let precision = f.precision().unwrap_or(4); 192 | let num = format!("{:.*}", precision, self.0); 193 | 194 | let (int_part, frac_part) = match num.split_once('.') { 195 | Some((int_str, fract_str)) => (int_str.to_string(), fract_str), 196 | None => (self.0.trunc().to_string(), ""), 197 | }; 198 | let len = int_part.len(); 199 | for (idx, c) in int_part.chars().enumerate() { 200 | let pos = len - idx - 1; 201 | f.write_char(c)?; 202 | if pos > 0 && pos % 3 == 0 { 203 | f.write_char(',')?; 204 | } 205 | } 206 | let frac_trimmed = frac_part.trim_end_matches('0'); 207 | if !frac_trimmed.is_empty() { 208 | f.write_char('.')?; 209 | f.write_str(frac_trimmed)?; 210 | } 211 | Ok(()) 212 | } 213 | } 214 | 215 | #[cfg(test)] 216 | mod tests { 217 | use super::*; 218 | 219 | const MILLI: Duration = Duration::from_millis(1); 220 | 221 | #[test] 222 | fn human_duration_alternate() { 223 | for (unit, _, alt) in UNITS { 224 | assert_eq!(format!("2{alt}"), format!("{:#}", HumanDuration(2 * *unit))); 225 | } 226 | } 227 | 228 | #[test] 229 | fn human_duration_less_than_one_second() { 230 | assert_eq!( 231 | "0 seconds", 232 | format!("{}", HumanDuration(Duration::from_secs(0))) 233 | ); 234 | assert_eq!("0 seconds", format!("{}", HumanDuration(MILLI))); 235 | assert_eq!("0 seconds", format!("{}", HumanDuration(499 * MILLI))); 236 | assert_eq!("1 second", format!("{}", HumanDuration(500 * MILLI))); 237 | assert_eq!("1 second", format!("{}", HumanDuration(999 * MILLI))); 238 | } 239 | 240 | #[test] 241 | fn human_duration_less_than_two_seconds() { 242 | assert_eq!("1 second", format!("{}", HumanDuration(1499 * MILLI))); 243 | assert_eq!("2 seconds", format!("{}", HumanDuration(1500 * MILLI))); 244 | assert_eq!("2 seconds", format!("{}", HumanDuration(1999 * MILLI))); 245 | } 246 | 247 | #[test] 248 | fn human_duration_one_unit() { 249 | assert_eq!("1 second", format!("{}", HumanDuration(SECOND))); 250 | assert_eq!("60 seconds", format!("{}", HumanDuration(MINUTE))); 251 | assert_eq!("60 minutes", format!("{}", HumanDuration(HOUR))); 252 | assert_eq!("24 hours", format!("{}", HumanDuration(DAY))); 253 | assert_eq!("7 days", format!("{}", HumanDuration(WEEK))); 254 | assert_eq!("52 weeks", format!("{}", HumanDuration(YEAR))); 255 | } 256 | 257 | #[test] 258 | fn human_duration_less_than_one_and_a_half_unit() { 259 | // this one is actually done at 1.5 unit - half of the next smaller unit - epsilon 260 | // and should display the next smaller unit 261 | let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI); 262 | assert_eq!("89 seconds", format!("{d}")); 263 | let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI); 264 | assert_eq!("89 minutes", format!("{d}")); 265 | let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI); 266 | assert_eq!("35 hours", format!("{d}")); 267 | let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI); 268 | assert_eq!("10 days", format!("{d}")); 269 | let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI); 270 | assert_eq!("78 weeks", format!("{d}")); 271 | } 272 | 273 | #[test] 274 | fn human_duration_one_and_a_half_unit() { 275 | // this one is actually done at 1.5 unit - half of the next smaller unit 276 | // and should still display "2 units" 277 | let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2); 278 | assert_eq!("2 minutes", format!("{d}")); 279 | let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2); 280 | assert_eq!("2 hours", format!("{d}")); 281 | let d = HumanDuration(DAY + DAY / 2 - HOUR / 2); 282 | assert_eq!("2 days", format!("{d}")); 283 | let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2); 284 | assert_eq!("2 weeks", format!("{d}")); 285 | let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2); 286 | assert_eq!("2 years", format!("{d}")); 287 | } 288 | 289 | #[test] 290 | fn human_duration_two_units() { 291 | assert_eq!("2 seconds", format!("{}", HumanDuration(2 * SECOND))); 292 | assert_eq!("2 minutes", format!("{}", HumanDuration(2 * MINUTE))); 293 | assert_eq!("2 hours", format!("{}", HumanDuration(2 * HOUR))); 294 | assert_eq!("2 days", format!("{}", HumanDuration(2 * DAY))); 295 | assert_eq!("2 weeks", format!("{}", HumanDuration(2 * WEEK))); 296 | assert_eq!("2 years", format!("{}", HumanDuration(2 * YEAR))); 297 | } 298 | 299 | #[test] 300 | fn human_duration_less_than_two_and_a_half_units() { 301 | let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI); 302 | assert_eq!("2 seconds", format!("{d}")); 303 | let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI); 304 | assert_eq!("2 minutes", format!("{d}")); 305 | let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI); 306 | assert_eq!("2 hours", format!("{d}")); 307 | let d = HumanDuration(2 * DAY + DAY / 2 - MILLI); 308 | assert_eq!("2 days", format!("{d}")); 309 | let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI); 310 | assert_eq!("2 weeks", format!("{d}")); 311 | let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI); 312 | assert_eq!("2 years", format!("{d}")); 313 | } 314 | 315 | #[test] 316 | fn human_duration_two_and_a_half_units() { 317 | let d = HumanDuration(2 * SECOND + SECOND / 2); 318 | assert_eq!("3 seconds", format!("{d}")); 319 | let d = HumanDuration(2 * MINUTE + MINUTE / 2); 320 | assert_eq!("3 minutes", format!("{d}")); 321 | let d = HumanDuration(2 * HOUR + HOUR / 2); 322 | assert_eq!("3 hours", format!("{d}")); 323 | let d = HumanDuration(2 * DAY + DAY / 2); 324 | assert_eq!("3 days", format!("{d}")); 325 | let d = HumanDuration(2 * WEEK + WEEK / 2); 326 | assert_eq!("3 weeks", format!("{d}")); 327 | let d = HumanDuration(2 * YEAR + YEAR / 2); 328 | assert_eq!("3 years", format!("{d}")); 329 | } 330 | 331 | #[test] 332 | fn human_duration_three_units() { 333 | assert_eq!("3 seconds", format!("{}", HumanDuration(3 * SECOND))); 334 | assert_eq!("3 minutes", format!("{}", HumanDuration(3 * MINUTE))); 335 | assert_eq!("3 hours", format!("{}", HumanDuration(3 * HOUR))); 336 | assert_eq!("3 days", format!("{}", HumanDuration(3 * DAY))); 337 | assert_eq!("3 weeks", format!("{}", HumanDuration(3 * WEEK))); 338 | assert_eq!("3 years", format!("{}", HumanDuration(3 * YEAR))); 339 | } 340 | 341 | #[test] 342 | fn human_count() { 343 | assert_eq!("42", format!("{}", HumanCount(42))); 344 | assert_eq!("7,654", format!("{}", HumanCount(7654))); 345 | assert_eq!("12,345", format!("{}", HumanCount(12345))); 346 | assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890))); 347 | } 348 | 349 | #[test] 350 | fn human_float_count() { 351 | assert_eq!("42", format!("{}", HumanFloatCount(42.0))); 352 | assert_eq!("7,654", format!("{}", HumanFloatCount(7654.0))); 353 | assert_eq!("12,345", format!("{}", HumanFloatCount(12345.0))); 354 | assert_eq!( 355 | "1,234,567,890", 356 | format!("{}", HumanFloatCount(1234567890.0)) 357 | ); 358 | assert_eq!("42.5", format!("{}", HumanFloatCount(42.5))); 359 | assert_eq!("42.5", format!("{}", HumanFloatCount(42.500012345))); 360 | assert_eq!("42.502", format!("{}", HumanFloatCount(42.502012345))); 361 | assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.321))); 362 | assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.3210123456))); 363 | assert_eq!("12,345.6789", format!("{}", HumanFloatCount(12345.6789))); 364 | assert_eq!( 365 | "1,234,567,890.1235", 366 | format!("{}", HumanFloatCount(1234567890.1234567)) 367 | ); 368 | assert_eq!( 369 | "1,234,567,890.1234", 370 | format!("{}", HumanFloatCount(1234567890.1234321)) 371 | ); 372 | assert_eq!("1,234", format!("{:.0}", HumanFloatCount(1234.1234321))); 373 | assert_eq!("1,234.1", format!("{:.1}", HumanFloatCount(1234.1234321))); 374 | assert_eq!("1,234.12", format!("{:.2}", HumanFloatCount(1234.1234321))); 375 | assert_eq!("1,234.123", format!("{:.3}", HumanFloatCount(1234.1234321))); 376 | assert_eq!( 377 | "1,234.1234320999999454215867445", 378 | format!("{:.25}", HumanFloatCount(1234.1234321)) 379 | ); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/in_memory.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Formatter, Write as _}; 2 | use std::io::Write as _; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use vt100::Parser; 6 | 7 | use crate::TermLike; 8 | 9 | /// A thin wrapper around [`vt100::Parser`]. 10 | /// 11 | /// This is just an [`Arc`] around its internal state, so it can be freely cloned. 12 | #[cfg_attr(docsrs, doc(cfg(feature = "in_memory")))] 13 | #[derive(Debug, Clone)] 14 | pub struct InMemoryTerm { 15 | state: Arc>, 16 | } 17 | 18 | impl InMemoryTerm { 19 | pub fn new(rows: u16, cols: u16) -> InMemoryTerm { 20 | assert!(rows > 0, "rows must be > 0"); 21 | assert!(cols > 0, "cols must be > 0"); 22 | InMemoryTerm { 23 | state: Arc::new(Mutex::new(InMemoryTermState::new(rows, cols))), 24 | } 25 | } 26 | 27 | pub fn reset(&self) { 28 | let mut state = self.state.lock().unwrap(); 29 | *state = InMemoryTermState::new(state.height, state.width); 30 | } 31 | 32 | pub fn contents(&self) -> String { 33 | let state = self.state.lock().unwrap(); 34 | 35 | // For some reason, the `Screen::contents` method doesn't include newlines in what it 36 | // returns, making it useless for our purposes. So we need to manually reconstruct the 37 | // contents by iterating over the rows in the terminal buffer. 38 | let mut rows = state 39 | .parser 40 | .screen() 41 | .rows(0, state.width) 42 | .collect::>(); 43 | 44 | // Reverse the rows and trim empty lines from the end 45 | rows = rows 46 | .into_iter() 47 | .rev() 48 | .skip_while(|line| line.is_empty()) 49 | .map(|line| line.trim_end().to_string()) 50 | .collect(); 51 | 52 | // Un-reverse the rows and join them up with newlines 53 | rows.reverse(); 54 | rows.join("\n") 55 | } 56 | 57 | pub fn contents_formatted(&self) -> Vec { 58 | let state = self.state.lock().unwrap(); 59 | 60 | // For some reason, the `Screen::contents` method doesn't include newlines in what it 61 | // returns, making it useless for our purposes. So we need to manually reconstruct the 62 | // contents by iterating over the rows in the terminal buffer. 63 | let mut rows = state 64 | .parser 65 | .screen() 66 | .rows_formatted(0, state.width) 67 | .collect::>(); 68 | 69 | // Reverse the rows and trim empty lines from the end 70 | rows = rows 71 | .into_iter() 72 | .rev() 73 | .skip_while(|line| line.is_empty()) 74 | .collect(); 75 | 76 | // Un-reverse the rows 77 | rows.reverse(); 78 | 79 | // Calculate buffer size 80 | let reset = b""; 81 | let len = rows.iter().map(|line| line.len() + reset.len() + 1).sum(); 82 | 83 | // Join rows up with reset codes and newlines 84 | let mut contents = rows.iter().fold(Vec::with_capacity(len), |mut acc, cur| { 85 | acc.extend_from_slice(cur); 86 | acc.extend_from_slice(reset); 87 | acc.push(b'\n'); 88 | acc 89 | }); 90 | 91 | // Remove last newline again, but leave the reset code 92 | contents.truncate(len.saturating_sub(1)); 93 | contents 94 | } 95 | 96 | pub fn moves_since_last_check(&self) -> String { 97 | let mut s = String::new(); 98 | for line in std::mem::take(&mut self.state.lock().unwrap().history) { 99 | writeln!(s, "{line:?}").unwrap(); 100 | } 101 | s 102 | } 103 | } 104 | 105 | impl TermLike for InMemoryTerm { 106 | fn width(&self) -> u16 { 107 | self.state.lock().unwrap().width 108 | } 109 | 110 | fn height(&self) -> u16 { 111 | self.state.lock().unwrap().height 112 | } 113 | 114 | fn move_cursor_up(&self, n: usize) -> std::io::Result<()> { 115 | match n { 116 | 0 => Ok(()), 117 | _ => { 118 | let mut state = self.state.lock().unwrap(); 119 | state.history.push(Move::Up(n)); 120 | state.write_str(&format!("\x1b[{n}A")) 121 | } 122 | } 123 | } 124 | 125 | fn move_cursor_down(&self, n: usize) -> std::io::Result<()> { 126 | match n { 127 | 0 => Ok(()), 128 | _ => { 129 | let mut state = self.state.lock().unwrap(); 130 | state.history.push(Move::Down(n)); 131 | state.write_str(&format!("\x1b[{n}B")) 132 | } 133 | } 134 | } 135 | 136 | fn move_cursor_right(&self, n: usize) -> std::io::Result<()> { 137 | match n { 138 | 0 => Ok(()), 139 | _ => { 140 | let mut state = self.state.lock().unwrap(); 141 | state.history.push(Move::Right(n)); 142 | state.write_str(&format!("\x1b[{n}C")) 143 | } 144 | } 145 | } 146 | 147 | fn move_cursor_left(&self, n: usize) -> std::io::Result<()> { 148 | match n { 149 | 0 => Ok(()), 150 | _ => { 151 | let mut state = self.state.lock().unwrap(); 152 | state.history.push(Move::Left(n)); 153 | state.write_str(&format!("\x1b[{n}D")) 154 | } 155 | } 156 | } 157 | 158 | fn write_line(&self, s: &str) -> std::io::Result<()> { 159 | let mut state = self.state.lock().unwrap(); 160 | state.history.push(Move::Str(s.into())); 161 | state.history.push(Move::NewLine); 162 | 163 | // Don't try to handle writing lines with additional newlines embedded in them - it's not 164 | // worth the extra code for something that indicatif doesn't even do. May revisit in future. 165 | debug_assert!( 166 | s.lines().count() <= 1, 167 | "calling write_line with embedded newlines is not allowed" 168 | ); 169 | 170 | // vte100 needs the full \r\n sequence to jump to the next line and reset the cursor to 171 | // the beginning of the line. Be flexible and take either \n or \r\n 172 | state.write_str(s)?; 173 | state.write_str("\r\n") 174 | } 175 | 176 | fn write_str(&self, s: &str) -> std::io::Result<()> { 177 | let mut state = self.state.lock().unwrap(); 178 | state.history.push(Move::Str(s.into())); 179 | state.write_str(s) 180 | } 181 | 182 | fn clear_line(&self) -> std::io::Result<()> { 183 | let mut state = self.state.lock().unwrap(); 184 | state.history.push(Move::Clear); 185 | state.write_str("\r\x1b[2K") 186 | } 187 | 188 | fn flush(&self) -> std::io::Result<()> { 189 | let mut state = self.state.lock().unwrap(); 190 | state.history.push(Move::Flush); 191 | state.parser.flush() 192 | } 193 | } 194 | 195 | struct InMemoryTermState { 196 | width: u16, 197 | height: u16, 198 | parser: vt100::Parser, 199 | history: Vec, 200 | } 201 | 202 | impl InMemoryTermState { 203 | pub(crate) fn new(rows: u16, cols: u16) -> InMemoryTermState { 204 | InMemoryTermState { 205 | width: cols, 206 | height: rows, 207 | parser: Parser::new(rows, cols, 0), 208 | history: vec![], 209 | } 210 | } 211 | 212 | pub(crate) fn write_str(&mut self, s: &str) -> std::io::Result<()> { 213 | self.parser.write_all(s.as_bytes()) 214 | } 215 | } 216 | 217 | impl Debug for InMemoryTermState { 218 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 219 | f.debug_struct("InMemoryTermState").finish_non_exhaustive() 220 | } 221 | } 222 | 223 | #[derive(Debug, PartialEq, Clone)] 224 | enum Move { 225 | Up(usize), 226 | Down(usize), 227 | Left(usize), 228 | Right(usize), 229 | Str(String), 230 | NewLine, 231 | Clear, 232 | Flush, 233 | } 234 | 235 | #[cfg(test)] 236 | mod test { 237 | use super::*; 238 | 239 | fn cursor_pos(in_mem: &InMemoryTerm) -> (u16, u16) { 240 | in_mem 241 | .state 242 | .lock() 243 | .unwrap() 244 | .parser 245 | .screen() 246 | .cursor_position() 247 | } 248 | 249 | #[test] 250 | fn line_wrapping() { 251 | let in_mem = InMemoryTerm::new(10, 5); 252 | assert_eq!(cursor_pos(&in_mem), (0, 0)); 253 | 254 | in_mem.write_str("ABCDE").unwrap(); 255 | assert_eq!(in_mem.contents(), "ABCDE"); 256 | assert_eq!(cursor_pos(&in_mem), (0, 5)); 257 | assert_eq!( 258 | in_mem.moves_since_last_check(), 259 | r#"Str("ABCDE") 260 | "# 261 | ); 262 | 263 | // Should wrap onto next line 264 | in_mem.write_str("FG").unwrap(); 265 | assert_eq!(in_mem.contents(), "ABCDE\nFG"); 266 | assert_eq!(cursor_pos(&in_mem), (1, 2)); 267 | assert_eq!( 268 | in_mem.moves_since_last_check(), 269 | r#"Str("FG") 270 | "# 271 | ); 272 | 273 | in_mem.write_str("HIJ").unwrap(); 274 | assert_eq!(in_mem.contents(), "ABCDE\nFGHIJ"); 275 | assert_eq!(cursor_pos(&in_mem), (1, 5)); 276 | assert_eq!( 277 | in_mem.moves_since_last_check(), 278 | r#"Str("HIJ") 279 | "# 280 | ); 281 | } 282 | 283 | #[test] 284 | fn write_line() { 285 | let in_mem = InMemoryTerm::new(10, 5); 286 | assert_eq!(cursor_pos(&in_mem), (0, 0)); 287 | 288 | in_mem.write_line("A").unwrap(); 289 | assert_eq!(in_mem.contents(), "A"); 290 | assert_eq!(cursor_pos(&in_mem), (1, 0)); 291 | assert_eq!( 292 | in_mem.moves_since_last_check(), 293 | r#"Str("A") 294 | NewLine 295 | "# 296 | ); 297 | 298 | in_mem.write_line("B").unwrap(); 299 | assert_eq!(in_mem.contents(), "A\nB"); 300 | assert_eq!(cursor_pos(&in_mem), (2, 0)); 301 | assert_eq!( 302 | in_mem.moves_since_last_check(), 303 | r#"Str("B") 304 | NewLine 305 | "# 306 | ); 307 | 308 | in_mem.write_line("Longer than cols").unwrap(); 309 | assert_eq!(in_mem.contents(), "A\nB\nLonge\nr tha\nn col\ns"); 310 | assert_eq!(cursor_pos(&in_mem), (6, 0)); 311 | assert_eq!( 312 | in_mem.moves_since_last_check(), 313 | r#"Str("Longer than cols") 314 | NewLine 315 | "# 316 | ); 317 | } 318 | 319 | #[test] 320 | fn basic_functionality() { 321 | let in_mem = InMemoryTerm::new(10, 80); 322 | 323 | in_mem.write_line("This is a test line").unwrap(); 324 | assert_eq!(in_mem.contents(), "This is a test line"); 325 | assert_eq!( 326 | in_mem.moves_since_last_check(), 327 | r#"Str("This is a test line") 328 | NewLine 329 | "# 330 | ); 331 | 332 | in_mem.write_line("And another line!").unwrap(); 333 | assert_eq!(in_mem.contents(), "This is a test line\nAnd another line!"); 334 | assert_eq!( 335 | in_mem.moves_since_last_check(), 336 | r#"Str("And another line!") 337 | NewLine 338 | "# 339 | ); 340 | 341 | in_mem.move_cursor_up(1).unwrap(); 342 | in_mem.write_str("TEST").unwrap(); 343 | 344 | assert_eq!(in_mem.contents(), "This is a test line\nTESTanother line!"); 345 | assert_eq!( 346 | in_mem.moves_since_last_check(), 347 | r#"Up(1) 348 | Str("TEST") 349 | "# 350 | ); 351 | } 352 | 353 | #[test] 354 | fn newlines() { 355 | let in_mem = InMemoryTerm::new(10, 10); 356 | in_mem.write_line("LINE ONE").unwrap(); 357 | in_mem.write_line("LINE TWO").unwrap(); 358 | in_mem.write_line("").unwrap(); 359 | in_mem.write_line("LINE FOUR").unwrap(); 360 | 361 | assert_eq!(in_mem.contents(), "LINE ONE\nLINE TWO\n\nLINE FOUR"); 362 | 363 | assert_eq!( 364 | in_mem.moves_since_last_check(), 365 | r#"Str("LINE ONE") 366 | NewLine 367 | Str("LINE TWO") 368 | NewLine 369 | Str("") 370 | NewLine 371 | Str("LINE FOUR") 372 | NewLine 373 | "# 374 | ); 375 | } 376 | 377 | #[test] 378 | fn cursor_zero_movement() { 379 | let in_mem = InMemoryTerm::new(10, 80); 380 | in_mem.write_line("LINE ONE").unwrap(); 381 | assert_eq!(cursor_pos(&in_mem), (1, 0)); 382 | 383 | // Check that moving zero rows/cols does not actually move cursor 384 | in_mem.move_cursor_up(0).unwrap(); 385 | assert_eq!(cursor_pos(&in_mem), (1, 0)); 386 | 387 | in_mem.move_cursor_down(0).unwrap(); 388 | assert_eq!(cursor_pos(&in_mem), (1, 0)); 389 | 390 | in_mem.move_cursor_right(1).unwrap(); 391 | assert_eq!(cursor_pos(&in_mem), (1, 1)); 392 | 393 | in_mem.move_cursor_left(0).unwrap(); 394 | assert_eq!(cursor_pos(&in_mem), (1, 1)); 395 | 396 | in_mem.move_cursor_right(0).unwrap(); 397 | assert_eq!(cursor_pos(&in_mem), (1, 1)); 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/iter.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{self, IoSliceMut}; 3 | use std::iter::FusedIterator; 4 | #[cfg(feature = "tokio")] 5 | use std::pin::Pin; 6 | #[cfg(feature = "tokio")] 7 | use std::task::{Context, Poll}; 8 | use std::time::Duration; 9 | 10 | #[cfg(feature = "tokio")] 11 | use tokio::io::{ReadBuf, SeekFrom}; 12 | 13 | use crate::progress_bar::ProgressBar; 14 | use crate::state::ProgressFinish; 15 | use crate::style::ProgressStyle; 16 | 17 | /// Wraps an iterator to display its progress. 18 | pub trait ProgressIterator 19 | where 20 | Self: Sized + Iterator, 21 | { 22 | /// Wrap an iterator with default styling. Uses [`Iterator::size_hint()`] to get length. 23 | /// Returns `Some(..)` only if `size_hint.1` is [`Some`]. If you want to create a progress bar 24 | /// even if `size_hint.1` returns [`None`] use [`progress_count()`](ProgressIterator::progress_count) 25 | /// or [`progress_with()`](ProgressIterator::progress_with) instead. 26 | fn try_progress(self) -> Option> { 27 | self.size_hint() 28 | .1 29 | .map(|len| self.progress_count(u64::try_from(len).unwrap())) 30 | } 31 | 32 | /// Wrap an iterator with default styling. 33 | fn progress(self) -> ProgressBarIter 34 | where 35 | Self: ExactSizeIterator, 36 | { 37 | let len = u64::try_from(self.len()).unwrap(); 38 | self.progress_count(len) 39 | } 40 | 41 | /// Wrap an iterator with an explicit element count. 42 | fn progress_count(self, len: u64) -> ProgressBarIter { 43 | self.progress_with(ProgressBar::new(len)) 44 | } 45 | 46 | /// Wrap an iterator with a custom progress bar. 47 | fn progress_with(self, progress: ProgressBar) -> ProgressBarIter; 48 | 49 | /// Wrap an iterator with a progress bar and style it. 50 | fn progress_with_style(self, style: crate::ProgressStyle) -> ProgressBarIter 51 | where 52 | Self: ExactSizeIterator, 53 | { 54 | let len = u64::try_from(self.len()).unwrap(); 55 | let bar = ProgressBar::new(len).with_style(style); 56 | self.progress_with(bar) 57 | } 58 | } 59 | 60 | /// Wraps an iterator to display its progress. 61 | #[derive(Debug)] 62 | pub struct ProgressBarIter { 63 | pub(crate) it: T, 64 | pub progress: ProgressBar, 65 | } 66 | 67 | impl ProgressBarIter { 68 | /// Builder-like function for setting underlying progress bar's style. 69 | /// 70 | /// See [`ProgressBar::with_style()`]. 71 | pub fn with_style(mut self, style: ProgressStyle) -> Self { 72 | self.progress = self.progress.with_style(style); 73 | self 74 | } 75 | 76 | /// Builder-like function for setting underlying progress bar's prefix. 77 | /// 78 | /// See [`ProgressBar::with_prefix()`]. 79 | pub fn with_prefix(mut self, prefix: impl Into>) -> Self { 80 | self.progress = self.progress.with_prefix(prefix); 81 | self 82 | } 83 | 84 | /// Builder-like function for setting underlying progress bar's message. 85 | /// 86 | /// See [`ProgressBar::with_message()`]. 87 | pub fn with_message(mut self, message: impl Into>) -> Self { 88 | self.progress = self.progress.with_message(message); 89 | self 90 | } 91 | 92 | /// Builder-like function for setting underlying progress bar's position. 93 | /// 94 | /// See [`ProgressBar::with_position()`]. 95 | pub fn with_position(mut self, position: u64) -> Self { 96 | self.progress = self.progress.with_position(position); 97 | self 98 | } 99 | 100 | /// Builder-like function for setting underlying progress bar's elapsed time. 101 | /// 102 | /// See [`ProgressBar::with_elapsed()`]. 103 | pub fn with_elapsed(mut self, elapsed: Duration) -> Self { 104 | self.progress = self.progress.with_elapsed(elapsed); 105 | self 106 | } 107 | 108 | /// Builder-like function for setting underlying progress bar's finish behavior. 109 | /// 110 | /// See [`ProgressBar::with_finish()`]. 111 | pub fn with_finish(mut self, finish: ProgressFinish) -> Self { 112 | self.progress = self.progress.with_finish(finish); 113 | self 114 | } 115 | } 116 | 117 | impl> Iterator for ProgressBarIter { 118 | type Item = S; 119 | 120 | fn next(&mut self) -> Option { 121 | let item = self.it.next(); 122 | 123 | if item.is_some() { 124 | self.progress.inc(1); 125 | } else if !self.progress.is_finished() { 126 | self.progress.finish_using_style(); 127 | } 128 | 129 | item 130 | } 131 | } 132 | 133 | impl ExactSizeIterator for ProgressBarIter { 134 | fn len(&self) -> usize { 135 | self.it.len() 136 | } 137 | } 138 | 139 | impl DoubleEndedIterator for ProgressBarIter { 140 | fn next_back(&mut self) -> Option { 141 | let item = self.it.next_back(); 142 | 143 | if item.is_some() { 144 | self.progress.inc(1); 145 | } else if !self.progress.is_finished() { 146 | self.progress.finish_using_style(); 147 | } 148 | 149 | item 150 | } 151 | } 152 | 153 | impl FusedIterator for ProgressBarIter {} 154 | 155 | impl io::Read for ProgressBarIter { 156 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 157 | let inc = self.it.read(buf)?; 158 | self.progress.inc(inc as u64); 159 | Ok(inc) 160 | } 161 | 162 | fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { 163 | let inc = self.it.read_vectored(bufs)?; 164 | self.progress.inc(inc as u64); 165 | Ok(inc) 166 | } 167 | 168 | fn read_to_string(&mut self, buf: &mut String) -> io::Result { 169 | let inc = self.it.read_to_string(buf)?; 170 | self.progress.inc(inc as u64); 171 | Ok(inc) 172 | } 173 | 174 | fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { 175 | self.it.read_exact(buf)?; 176 | self.progress.inc(buf.len() as u64); 177 | Ok(()) 178 | } 179 | } 180 | 181 | impl io::BufRead for ProgressBarIter { 182 | fn fill_buf(&mut self) -> io::Result<&[u8]> { 183 | self.it.fill_buf() 184 | } 185 | 186 | fn consume(&mut self, amt: usize) { 187 | self.it.consume(amt); 188 | self.progress.inc(amt as u64); 189 | } 190 | } 191 | 192 | impl io::Seek for ProgressBarIter { 193 | fn seek(&mut self, f: io::SeekFrom) -> io::Result { 194 | self.it.seek(f).map(|pos| { 195 | self.progress.set_position(pos); 196 | pos 197 | }) 198 | } 199 | // Pass this through to preserve optimizations that the inner I/O object may use here 200 | // Also avoid sending a set_position update when the position hasn't changed 201 | fn stream_position(&mut self) -> io::Result { 202 | self.it.stream_position() 203 | } 204 | } 205 | 206 | #[cfg(feature = "tokio")] 207 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 208 | impl tokio::io::AsyncWrite for ProgressBarIter { 209 | fn poll_write( 210 | mut self: Pin<&mut Self>, 211 | cx: &mut Context<'_>, 212 | buf: &[u8], 213 | ) -> Poll> { 214 | Pin::new(&mut self.it).poll_write(cx, buf).map(|poll| { 215 | poll.map(|inc| { 216 | self.progress.inc(inc as u64); 217 | inc 218 | }) 219 | }) 220 | } 221 | 222 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 223 | Pin::new(&mut self.it).poll_flush(cx) 224 | } 225 | 226 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 227 | Pin::new(&mut self.it).poll_shutdown(cx) 228 | } 229 | } 230 | 231 | #[cfg(feature = "tokio")] 232 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 233 | impl tokio::io::AsyncRead for ProgressBarIter { 234 | fn poll_read( 235 | mut self: Pin<&mut Self>, 236 | cx: &mut Context<'_>, 237 | buf: &mut ReadBuf<'_>, 238 | ) -> Poll> { 239 | let prev_len = buf.filled().len() as u64; 240 | if let Poll::Ready(e) = Pin::new(&mut self.it).poll_read(cx, buf) { 241 | self.progress.inc(buf.filled().len() as u64 - prev_len); 242 | Poll::Ready(e) 243 | } else { 244 | Poll::Pending 245 | } 246 | } 247 | } 248 | 249 | #[cfg(feature = "tokio")] 250 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 251 | impl tokio::io::AsyncSeek for ProgressBarIter { 252 | fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { 253 | Pin::new(&mut self.it).start_seek(position) 254 | } 255 | 256 | fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 257 | Pin::new(&mut self.it).poll_complete(cx) 258 | } 259 | } 260 | 261 | #[cfg(feature = "tokio")] 262 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 263 | impl tokio::io::AsyncBufRead 264 | for ProgressBarIter 265 | { 266 | fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 267 | let this = self.get_mut(); 268 | let result = Pin::new(&mut this.it).poll_fill_buf(cx); 269 | if let Poll::Ready(Ok(buf)) = &result { 270 | this.progress.inc(buf.len() as u64); 271 | } 272 | result 273 | } 274 | 275 | fn consume(mut self: Pin<&mut Self>, amt: usize) { 276 | Pin::new(&mut self.it).consume(amt); 277 | } 278 | } 279 | 280 | #[cfg(feature = "futures")] 281 | #[cfg_attr(docsrs, doc(cfg(feature = "futures")))] 282 | impl futures_core::Stream for ProgressBarIter { 283 | type Item = S::Item; 284 | 285 | fn poll_next( 286 | self: std::pin::Pin<&mut Self>, 287 | cx: &mut std::task::Context<'_>, 288 | ) -> std::task::Poll> { 289 | let this = self.get_mut(); 290 | let item = std::pin::Pin::new(&mut this.it).poll_next(cx); 291 | match &item { 292 | std::task::Poll::Ready(Some(_)) => this.progress.inc(1), 293 | std::task::Poll::Ready(None) => this.progress.finish_using_style(), 294 | std::task::Poll::Pending => {} 295 | } 296 | item 297 | } 298 | } 299 | 300 | impl io::Write for ProgressBarIter { 301 | fn write(&mut self, buf: &[u8]) -> io::Result { 302 | self.it.write(buf).map(|inc| { 303 | self.progress.inc(inc as u64); 304 | inc 305 | }) 306 | } 307 | 308 | fn write_vectored(&mut self, bufs: &[io::IoSlice]) -> io::Result { 309 | self.it.write_vectored(bufs).map(|inc| { 310 | self.progress.inc(inc as u64); 311 | inc 312 | }) 313 | } 314 | 315 | fn flush(&mut self) -> io::Result<()> { 316 | self.it.flush() 317 | } 318 | 319 | // write_fmt can not be captured with reasonable effort. 320 | // as it uses write_all internally by default that should not be a problem. 321 | // fn write_fmt(&mut self, fmt: fmt::Arguments) -> io::Result<()>; 322 | } 323 | 324 | impl> ProgressIterator for T { 325 | fn progress_with(self, progress: ProgressBar) -> ProgressBarIter { 326 | ProgressBarIter { it: self, progress } 327 | } 328 | } 329 | 330 | #[cfg(test)] 331 | mod test { 332 | use crate::iter::{ProgressBarIter, ProgressIterator}; 333 | use crate::progress_bar::ProgressBar; 334 | use crate::ProgressStyle; 335 | 336 | #[test] 337 | fn it_can_wrap_an_iterator() { 338 | let v = [1, 2, 3]; 339 | let wrap = |it: ProgressBarIter<_>| { 340 | assert_eq!(it.map(|x| x * 2).collect::>(), vec![2, 4, 6]); 341 | }; 342 | 343 | wrap(v.iter().progress()); 344 | wrap(v.iter().progress_count(3)); 345 | wrap({ 346 | let pb = ProgressBar::new(v.len() as u64); 347 | v.iter().progress_with(pb) 348 | }); 349 | wrap({ 350 | let style = ProgressStyle::default_bar() 351 | .template("{wide_bar:.red} {percent}/100%") 352 | .unwrap(); 353 | v.iter().progress_with_style(style) 354 | }); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! indicatif is a library for Rust that helps you build command line 2 | //! interfaces that report progress to users. It comes with various 3 | //! tools and utilities for formatting anything that indicates progress. 4 | //! 5 | //! Platform support: 6 | //! 7 | //! * Linux 8 | //! * macOS 9 | //! * Windows (colors require Windows 10) 10 | //! 11 | //! Best paired with other libraries in the family: 12 | //! 13 | //! * [console](https://docs.rs/console) 14 | //! * [dialoguer](https://docs.rs/dialoguer) 15 | //! 16 | //! # Crate Contents 17 | //! 18 | //! * **Progress bars** 19 | //! * [`ProgressBar`](struct.ProgressBar.html) for bars and spinners 20 | //! * [`MultiProgress`](struct.MultiProgress.html) for multiple bars 21 | //! * **Data Formatting** 22 | //! * [`HumanBytes`](struct.HumanBytes.html) for formatting bytes 23 | //! * [`DecimalBytes`](struct.DecimalBytes.html) for formatting bytes using SI prefixes 24 | //! * [`BinaryBytes`](struct.BinaryBytes.html) for formatting bytes using ISO/IEC prefixes 25 | //! * [`HumanDuration`](struct.HumanDuration.html) for formatting durations 26 | //! * [`HumanCount`](struct.HumanCount.html) for formatting large counts 27 | //! * [`HumanFloatCount`](struct.HumanFloatCount.html) for formatting large float counts 28 | //! 29 | //! # Progress Bars and Spinners 30 | //! 31 | //! indicatif comes with a [`ProgressBar`] type that supports both bounded 32 | //! progress bar uses as well as unbounded "spinner" type progress reports. 33 | //! Progress bars are [`Sync`] and [`Send`] objects which means that they are 34 | //! internally locked and can be passed from thread to thread. 35 | //! 36 | //! Additionally a [`MultiProgress`] utility is provided that can manage 37 | //! rendering multiple progress bars at once (eg: from multiple threads). 38 | //! 39 | //! To whet your appetite, this is what this can look like: 40 | //! 41 | //! 42 | //! 43 | //! Progress bars are manually advanced and by default draw to stderr. 44 | //! When you are done, the progress bar can be finished either visibly 45 | //! (eg: the progress bar stays on the screen) or cleared (the progress 46 | //! bar will be removed). 47 | //! 48 | //! ```rust 49 | //! use indicatif::ProgressBar; 50 | //! 51 | //! let bar = ProgressBar::new(1000); 52 | //! for _ in 0..1000 { 53 | //! bar.inc(1); 54 | //! // ... 55 | //! } 56 | //! bar.finish(); 57 | //! ``` 58 | //! 59 | //! Spinners can be manually advanced with [`tick`](ProgressBar::tick), or you can set them up 60 | //! to spin automatically with [`enable_steady_tick`](ProgressBar::enable_steady_tick): 61 | //! 62 | //! ```rust 63 | //! use std::time::Duration; 64 | //! use indicatif::ProgressBar; 65 | //! 66 | //! let bar = ProgressBar::new_spinner(); 67 | //! bar.enable_steady_tick(Duration::from_millis(100)); 68 | //! // ... do some work 69 | //! bar.finish(); 70 | //! ``` 71 | //! 72 | //! General progress bar behaviors: 73 | //! 74 | //! * if a non terminal is detected the progress bar will be completely 75 | //! hidden. This makes piping programs to logfiles make sense out of 76 | //! the box. 77 | //! * a progress bar only starts drawing when [`set_message`](ProgressBar::set_message), 78 | //! [`inc`](ProgressBar::inc), [`set_position`](ProgressBar::set_position) 79 | //! or [`tick`](ProgressBar::tick) are called. In some situations you 80 | //! might have to call [`tick`](ProgressBar::tick) once to draw it. 81 | //! * progress bars should be explicitly finished to reset the rendering 82 | //! for others. Either by also clearing them or by replacing them with 83 | //! a new message / retaining the current message. 84 | //! * the default template renders neither message nor prefix. 85 | //! 86 | //! # Iterators 87 | //! 88 | //! Similar to [tqdm](https://github.com/tqdm/tqdm), progress bars can be 89 | //! associated with an iterator. For example: 90 | //! 91 | //! ```rust 92 | //! use indicatif::ProgressIterator; 93 | //! 94 | //! for _ in (0..1000).progress() { 95 | //! // ... 96 | //! } 97 | //! ``` 98 | //! 99 | //! See the [`ProgressIterator`](trait.ProgressIterator.html) trait for more 100 | //! methods to configure the number of elements in the iterator or change 101 | //! the progress bar style. Indicatif also has optional support for parallel 102 | //! iterators with [Rayon](https://github.com/rayon-rs/rayon). In your 103 | //! `Cargo.toml`, use the "rayon" feature: 104 | //! 105 | //! ```toml 106 | //! [dependencies] 107 | //! indicatif = {version = "*", features = ["rayon"]} 108 | //! ``` 109 | //! 110 | //! And then use it like this: 111 | //! 112 | //! ```rust,ignore 113 | //! # extern crate rayon; 114 | //! use indicatif::ParallelProgressIterator; 115 | //! use rayon::iter::{ParallelIterator, IntoParallelRefIterator}; 116 | //! 117 | //! let v: Vec<_> = (0..100000).collect(); 118 | //! let v2: Vec<_> = v.par_iter().progress_count(v.len() as u64).map(|i| i + 1).collect(); 119 | //! assert_eq!(v2[0], 1); 120 | //! ``` 121 | //! 122 | //! Or if you'd like to customize the progress bar: 123 | //! 124 | //! ```rust,ignore 125 | //! # extern crate rayon; 126 | //! use indicatif::{ProgressBar, ParallelProgressIterator, ProgressStyle}; 127 | //! use rayon::iter::{ParallelIterator, IntoParallelRefIterator}; 128 | //! 129 | //! // Alternatively, use `ProgressBar::new().with_style()` 130 | //! let style = ProgressStyle::default_bar(); 131 | //! let v: Vec<_> = (0..100000).collect(); 132 | //! let v2: Vec<_> = v.par_iter().progress_with_style(style).map(|i| i + 1).collect(); 133 | //! assert_eq!(v2[0], 1); 134 | //! ``` 135 | //! 136 | //! # Templates 137 | //! 138 | //! Progress bars can be styled with simple format strings similar to the 139 | //! ones in Rust itself. The format for a placeholder is `{key:options}` 140 | //! where the `options` part is optional. If provided the format is this: 141 | //! 142 | //! ```text 143 | //! <^> for an optional alignment specification (left, center and right respectively) 144 | //! WIDTH an optional width as positive integer 145 | //! ! an optional exclamation mark to enable truncation 146 | //! .STYLE an optional dot separated style string 147 | //! /STYLE an optional dot separated alternative style string 148 | //! ``` 149 | //! 150 | //! For the style component see [`Style::from_dotted_str`](https://docs.rs/console/0.7.5/console/struct.Style.html#method.from_dotted_str) 151 | //! for more information. Indicatif uses the `console` base crate for all 152 | //! colorization and formatting options. 153 | //! 154 | //! Some examples for templates: 155 | //! 156 | //! ```text 157 | //! [{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg} 158 | //! ``` 159 | //! 160 | //! This sets a progress bar that is 40 characters wide and has cyan 161 | //! as primary style color and blue as alternative style color. 162 | //! Alternative styles are currently only used for progress bars. 163 | //! 164 | //! Example configuration: 165 | //! 166 | //! ```rust 167 | //! # use indicatif::{ProgressBar, ProgressStyle}; 168 | //! # let bar = ProgressBar::new(0); 169 | //! bar.set_style(ProgressStyle::with_template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}") 170 | //! .unwrap() 171 | //! .progress_chars("##-")); 172 | //! ``` 173 | //! 174 | //! The following keys exist: 175 | //! 176 | //! * `bar`: renders a progress bar. By default 20 characters wide. The 177 | //! style string is used to color the elapsed part, the alternative 178 | //! style is used for the bar that is yet to render. 179 | //! * `wide_bar`: like `bar` but always fills the remaining space. It should not be used with `wide_msg`. 180 | //! * `spinner`: renders the spinner (current tick string). Note that spinners do not automatically tick by default. You either 181 | //! need to call `enable_steady_tick` or manually call `tick`. 182 | //! * `prefix`: renders the prefix set on the progress bar. 183 | //! * `msg`: renders the currently set message on the progress bar. 184 | //! * `wide_msg`: like `msg` but always fills the remaining space and truncates. It should not be used 185 | //! with `wide_bar`. 186 | //! * `pos`: renders the current position of the bar as integer 187 | //! * `human_pos`: renders the current position of the bar as an integer, with commas as the 188 | //! thousands separator. 189 | //! * `len`: renders the amount of work to be done as an integer 190 | //! * `human_len`: renders the total length of the bar as an integer, with commas as the thousands 191 | //! separator. 192 | //! * `percent`: renders the current position of the bar as a percentage of the total length (as an integer). 193 | //! * `percent_precise`: renders the current position of the bar as a percentage of the total length (with 3 fraction digits). 194 | //! * `bytes`: renders the current position of the bar as bytes (alias of `binary_bytes`). 195 | //! * `total_bytes`: renders the total length of the bar as bytes (alias of `binary_total_bytes`). 196 | //! * `decimal_bytes`: renders the current position of the bar as bytes using 197 | //! power-of-10 units, i.e. `MB`, `kB`, etc. 198 | //! * `decimal_total_bytes`: renders the total length of the bar as bytes using 199 | //! power-of-10 units, i.e. `MB`, `kB`, etc. 200 | //! * `binary_bytes`: renders the current position of the bar as bytes using 201 | //! power-of-two units, i.e. `MiB`, `KiB`, etc. 202 | //! * `binary_total_bytes`: renders the total length of the bar as bytes using 203 | //! power-of-two units, i.e. `MiB`, `KiB`, etc. 204 | //! * `elapsed_precise`: renders the elapsed time as `HH:MM:SS`. 205 | //! * `elapsed`: renders the elapsed time as `42s`, `1m` etc. 206 | //! * `per_sec`: renders the speed in steps per second. 207 | //! * `bytes_per_sec`: renders the speed in bytes per second (alias of `binary_bytes_per_sec`). 208 | //! * `decimal_bytes_per_sec`: renders the speed in bytes per second using 209 | //! power-of-10 units, i.e. `MB`, `kB`, etc. 210 | //! * `binary_bytes_per_sec`: renders the speed in bytes per second using 211 | //! power-of-two units, i.e. `MiB`, `KiB`, etc. 212 | //! * `eta_precise`: the remaining time (like `elapsed_precise`). 213 | //! * `eta`: the remaining time (like `elapsed`). 214 | //! * `duration_precise`: the extrapolated total duration (like `elapsed_precise`). 215 | //! * `duration`: the extrapolated total duration time (like `elapsed`). 216 | //! 217 | //! If the list above does not contain the value you need, consider creating a custom 218 | //! [`ProgressTracker`][crate::style::ProgressTracker] implementation. 219 | //! 220 | //! The design of the progress bar can be altered with the integrated 221 | //! template functionality. The template can be set by changing a 222 | //! [`ProgressStyle`] and attaching it to the progress bar. 223 | //! 224 | //! # Human Readable Formatting 225 | //! 226 | //! There are some formatting wrappers for showing elapsed time and 227 | //! file sizes for human users: 228 | //! 229 | //! ```rust 230 | //! # use std::time::Duration; 231 | //! use indicatif::{HumanBytes, HumanCount, HumanDuration, HumanFloatCount}; 232 | //! 233 | //! assert_eq!("3.00 MiB", HumanBytes(3*1024*1024).to_string()); 234 | //! assert_eq!("8 seconds", HumanDuration(Duration::from_secs(8)).to_string()); 235 | //! assert_eq!("33,857,009", HumanCount(33857009).to_string()); 236 | //! assert_eq!("33,857,009.1235", HumanFloatCount(33857009.123456).to_string()); 237 | //! ``` 238 | //! 239 | //! # Feature Flags 240 | //! 241 | //! * `rayon`: adds rayon support 242 | //! * `improved_unicode`: adds improved unicode support (graphemes, better width calculation) 243 | 244 | #![cfg_attr(docsrs, feature(doc_cfg))] 245 | #![warn(unreachable_pub)] 246 | 247 | mod draw_target; 248 | mod format; 249 | #[cfg(feature = "in_memory")] 250 | mod in_memory; 251 | mod iter; 252 | mod multi; 253 | mod progress_bar; 254 | #[cfg(feature = "rayon")] 255 | mod rayon; 256 | mod state; 257 | pub mod style; 258 | mod term_like; 259 | 260 | pub use crate::draw_target::ProgressDrawTarget; 261 | pub use crate::format::{ 262 | BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration, 263 | HumanFloatCount, 264 | }; 265 | #[cfg(feature = "in_memory")] 266 | pub use crate::in_memory::InMemoryTerm; 267 | pub use crate::iter::{ProgressBarIter, ProgressIterator}; 268 | pub use crate::multi::{MultiProgress, MultiProgressAlignment}; 269 | pub use crate::progress_bar::{ProgressBar, WeakProgressBar}; 270 | #[cfg(feature = "rayon")] 271 | pub use crate::rayon::ParallelProgressIterator; 272 | pub use crate::state::{ProgressFinish, ProgressState}; 273 | pub use crate::style::ProgressStyle; 274 | pub use crate::term_like::TermLike; 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | use super::*; 279 | 280 | #[allow(dead_code)] 281 | trait MustBeThreadSafe: Send + Sync {} 282 | 283 | // Ensure that the following types are `Send + Sync` 284 | impl MustBeThreadSafe for MultiProgress {} 285 | impl MustBeThreadSafe for MultiProgressAlignment {} 286 | impl MustBeThreadSafe for ProgressBar {} 287 | impl MustBeThreadSafe for ProgressBarIter<()> {} 288 | impl MustBeThreadSafe for ProgressFinish {} 289 | impl MustBeThreadSafe for ProgressState {} 290 | impl MustBeThreadSafe for ProgressStyle {} 291 | impl MustBeThreadSafe for WeakProgressBar {} 292 | } 293 | -------------------------------------------------------------------------------- /src/multi.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Formatter}; 2 | use std::io; 3 | use std::sync::{Arc, RwLock}; 4 | use std::thread::panicking; 5 | #[cfg(not(target_arch = "wasm32"))] 6 | use std::time::Instant; 7 | 8 | use crate::draw_target::{ 9 | visual_line_count, DrawState, DrawStateWrapper, LineAdjust, LineType, ProgressDrawTarget, 10 | VisualLines, 11 | }; 12 | use crate::progress_bar::ProgressBar; 13 | #[cfg(target_arch = "wasm32")] 14 | use web_time::Instant; 15 | 16 | /// Manages multiple progress bars from different threads 17 | #[derive(Debug, Clone)] 18 | pub struct MultiProgress { 19 | pub(crate) state: Arc>, 20 | } 21 | 22 | impl Default for MultiProgress { 23 | fn default() -> Self { 24 | Self::with_draw_target(ProgressDrawTarget::stderr()) 25 | } 26 | } 27 | 28 | impl MultiProgress { 29 | /// Creates a new multi progress object. 30 | /// 31 | /// Progress bars added to this object by default draw directly to stderr, and refresh 32 | /// a maximum of 15 times a second. To change the refresh rate [set] the [draw target] to 33 | /// one with a different refresh rate. 34 | /// 35 | /// [set]: MultiProgress::set_draw_target 36 | /// [draw target]: ProgressDrawTarget 37 | pub fn new() -> Self { 38 | Self::default() 39 | } 40 | 41 | /// Creates a new multi progress object with the given draw target. 42 | pub fn with_draw_target(draw_target: ProgressDrawTarget) -> Self { 43 | Self { 44 | state: Arc::new(RwLock::new(MultiState::new(draw_target))), 45 | } 46 | } 47 | 48 | /// Sets a different draw target for the multiprogress bar. 49 | /// 50 | /// Use [`MultiProgress::with_draw_target`] to set the draw target during creation. 51 | pub fn set_draw_target(&self, target: ProgressDrawTarget) { 52 | let mut state = self.state.write().unwrap(); 53 | state.draw_target.disconnect(Instant::now()); 54 | state.draw_target = target; 55 | } 56 | 57 | /// Set whether we should try to move the cursor when possible instead of clearing lines. 58 | /// 59 | /// This can reduce flickering, but do not enable it if you intend to change the number of 60 | /// progress bars. 61 | pub fn set_move_cursor(&self, move_cursor: bool) { 62 | self.state 63 | .write() 64 | .unwrap() 65 | .draw_target 66 | .set_move_cursor(move_cursor); 67 | } 68 | 69 | /// Set alignment flag 70 | pub fn set_alignment(&self, alignment: MultiProgressAlignment) { 71 | self.state.write().unwrap().alignment = alignment; 72 | } 73 | 74 | /// Adds a progress bar. 75 | /// 76 | /// The progress bar added will have the draw target changed to a 77 | /// remote draw target that is intercepted by the multi progress 78 | /// object overriding custom [`ProgressDrawTarget`] settings. 79 | /// 80 | /// The progress bar will be positioned below all other bars currently 81 | /// in the [`MultiProgress`]. 82 | /// 83 | /// Adding a progress bar that is already a member of the [`MultiProgress`] 84 | /// will have no effect. 85 | pub fn add(&self, pb: ProgressBar) -> ProgressBar { 86 | self.internalize(InsertLocation::End, pb) 87 | } 88 | 89 | /// Inserts a progress bar. 90 | /// 91 | /// The progress bar inserted at position `index` will have the draw 92 | /// target changed to a remote draw target that is intercepted by the 93 | /// multi progress object overriding custom [`ProgressDrawTarget`] settings. 94 | /// 95 | /// If `index >= MultiProgressState::objects.len()`, the progress bar 96 | /// is added to the end of the list. 97 | /// 98 | /// Inserting a progress bar that is already a member of the [`MultiProgress`] 99 | /// will have no effect. 100 | pub fn insert(&self, index: usize, pb: ProgressBar) -> ProgressBar { 101 | self.internalize(InsertLocation::Index(index), pb) 102 | } 103 | 104 | /// Inserts a progress bar from the back. 105 | /// 106 | /// The progress bar inserted at position `MultiProgressState::objects.len() - index` 107 | /// will have the draw target changed to a remote draw target that is 108 | /// intercepted by the multi progress object overriding custom 109 | /// [`ProgressDrawTarget`] settings. 110 | /// 111 | /// If `index >= MultiProgressState::objects.len()`, the progress bar 112 | /// is added to the start of the list. 113 | /// 114 | /// Inserting a progress bar that is already a member of the [`MultiProgress`] 115 | /// will have no effect. 116 | pub fn insert_from_back(&self, index: usize, pb: ProgressBar) -> ProgressBar { 117 | self.internalize(InsertLocation::IndexFromBack(index), pb) 118 | } 119 | 120 | /// Inserts a progress bar before an existing one. 121 | /// 122 | /// The progress bar added will have the draw target changed to a 123 | /// remote draw target that is intercepted by the multi progress 124 | /// object overriding custom [`ProgressDrawTarget`] settings. 125 | /// 126 | /// Inserting a progress bar that is already a member of the [`MultiProgress`] 127 | /// will have no effect. 128 | pub fn insert_before(&self, before: &ProgressBar, pb: ProgressBar) -> ProgressBar { 129 | self.internalize(InsertLocation::Before(before.index().unwrap()), pb) 130 | } 131 | 132 | /// Inserts a progress bar after an existing one. 133 | /// 134 | /// The progress bar added will have the draw target changed to a 135 | /// remote draw target that is intercepted by the multi progress 136 | /// object overriding custom [`ProgressDrawTarget`] settings. 137 | /// 138 | /// Inserting a progress bar that is already a member of the [`MultiProgress`] 139 | /// will have no effect. 140 | pub fn insert_after(&self, after: &ProgressBar, pb: ProgressBar) -> ProgressBar { 141 | self.internalize(InsertLocation::After(after.index().unwrap()), pb) 142 | } 143 | 144 | /// Removes a progress bar. 145 | /// 146 | /// The progress bar is removed only if it was previously inserted or added 147 | /// by the methods [`MultiProgress::insert`] or [`MultiProgress::add`]. 148 | /// If the passed progress bar does not satisfy the condition above, 149 | /// the `remove` method does nothing. 150 | pub fn remove(&self, pb: &ProgressBar) { 151 | let mut state = pb.state(); 152 | let idx = match &state.draw_target.remote() { 153 | Some((state, idx)) => { 154 | // Check that this progress bar is owned by the current MultiProgress. 155 | assert!(Arc::ptr_eq(&self.state, state)); 156 | *idx 157 | } 158 | _ => return, 159 | }; 160 | 161 | state.draw_target = ProgressDrawTarget::hidden(); 162 | self.state.write().unwrap().remove_idx(idx); 163 | } 164 | 165 | fn internalize(&self, location: InsertLocation, pb: ProgressBar) -> ProgressBar { 166 | let mut state = self.state.write().unwrap(); 167 | let idx = state.insert(location); 168 | drop(state); 169 | 170 | pb.set_draw_target(ProgressDrawTarget::new_remote(self.state.clone(), idx)); 171 | pb 172 | } 173 | 174 | /// Print a log line above all progress bars in the [`MultiProgress`] 175 | /// 176 | /// If the draw target is hidden (e.g. when standard output is not a terminal), `println()` 177 | /// will not do anything. 178 | pub fn println>(&self, msg: I) -> io::Result<()> { 179 | let mut state = self.state.write().unwrap(); 180 | state.println(msg, Instant::now()) 181 | } 182 | 183 | /// Hide all progress bars temporarily, execute `f`, then redraw the [`MultiProgress`] 184 | /// 185 | /// Executes 'f' even if the draw target is hidden. 186 | /// 187 | /// Useful for external code that writes to the standard output. 188 | /// 189 | /// **Note:** The internal lock is held while `f` is executed. Other threads trying to print 190 | /// anything on the progress bar will be blocked until `f` finishes. 191 | /// Therefore, it is recommended to avoid long-running operations in `f`. 192 | pub fn suspend R, R>(&self, f: F) -> R { 193 | let mut state = self.state.write().unwrap(); 194 | state.suspend(f, Instant::now()) 195 | } 196 | 197 | pub fn clear(&self) -> io::Result<()> { 198 | self.state.write().unwrap().clear(Instant::now()) 199 | } 200 | 201 | pub fn is_hidden(&self) -> bool { 202 | self.state.read().unwrap().is_hidden() 203 | } 204 | } 205 | 206 | #[derive(Debug)] 207 | pub(crate) struct MultiState { 208 | /// The collection of states corresponding to progress bars 209 | members: Vec, 210 | /// Set of removed bars, should have corresponding members in the `members` vector with a 211 | /// `draw_state` of `None`. 212 | free_set: Vec, 213 | /// Indices to the `draw_states` to maintain correct visual order 214 | ordering: Vec, 215 | /// Target for draw operation for MultiProgress 216 | draw_target: ProgressDrawTarget, 217 | /// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top` 218 | alignment: MultiProgressAlignment, 219 | /// Lines to be drawn above everything else in the MultiProgress. These specifically come from 220 | /// calling `ProgressBar::println` on a pb that is connected to a `MultiProgress`. 221 | orphan_lines: Vec, 222 | /// The count of currently visible zombie lines. 223 | zombie_lines_count: VisualLines, 224 | } 225 | 226 | impl MultiState { 227 | fn new(draw_target: ProgressDrawTarget) -> Self { 228 | Self { 229 | members: vec![], 230 | free_set: vec![], 231 | ordering: vec![], 232 | draw_target, 233 | alignment: MultiProgressAlignment::default(), 234 | orphan_lines: Vec::new(), 235 | zombie_lines_count: VisualLines::default(), 236 | } 237 | } 238 | 239 | pub(crate) fn mark_zombie(&mut self, index: usize) { 240 | let width = self.width().map(usize::from); 241 | 242 | let member = &mut self.members[index]; 243 | 244 | // If the zombie is the first visual bar then we can reap it right now instead of 245 | // deferring it to the next draw. 246 | if index != self.ordering.first().copied().unwrap() { 247 | member.is_zombie = true; 248 | return; 249 | } 250 | 251 | let line_count = member 252 | .draw_state 253 | .as_ref() 254 | .zip(width) 255 | .map(|(d, width)| d.visual_line_count(.., width)) 256 | .unwrap_or_default(); 257 | 258 | // Track the total number of zombie lines on the screen 259 | self.zombie_lines_count = self.zombie_lines_count.saturating_add(line_count); 260 | 261 | // Make `DrawTarget` forget about the zombie lines so that they aren't cleared on next draw. 262 | self.draw_target 263 | .adjust_last_line_count(LineAdjust::Keep(line_count)); 264 | 265 | self.remove_idx(index); 266 | } 267 | 268 | pub(crate) fn draw( 269 | &mut self, 270 | mut force_draw: bool, 271 | extra_lines: Option>, 272 | now: Instant, 273 | ) -> io::Result<()> { 274 | if panicking() { 275 | return Ok(()); 276 | } 277 | 278 | let width = match self.width() { 279 | Some(width) => width as usize, 280 | None => return Ok(()), 281 | }; 282 | 283 | // Assumption: if extra_lines is not None, then it has at least one line 284 | debug_assert_eq!( 285 | extra_lines.is_some(), 286 | extra_lines.as_ref().map(Vec::len).unwrap_or_default() > 0 287 | ); 288 | 289 | let mut reap_indices = vec![]; 290 | 291 | // Reap all consecutive 'zombie' progress bars from head of the list. 292 | let mut adjust = VisualLines::default(); 293 | for &index in &self.ordering { 294 | let member = &self.members[index]; 295 | if !member.is_zombie { 296 | break; 297 | } 298 | 299 | let line_count = member 300 | .draw_state 301 | .as_ref() 302 | .map(|d| d.visual_line_count(.., width)) 303 | .unwrap_or_default(); 304 | // Track the total number of zombie lines on the screen. 305 | self.zombie_lines_count += line_count; 306 | 307 | // Track the number of zombie lines that will be drawn by this call to draw. 308 | adjust += line_count; 309 | 310 | reap_indices.push(index); 311 | } 312 | 313 | // If this draw is due to a `println`, then we need to erase all the zombie lines. 314 | // This is because `println` is supposed to appear above all other elements in the 315 | // `MultiProgress`. 316 | if extra_lines.is_some() { 317 | self.draw_target 318 | .adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count)); 319 | self.zombie_lines_count = VisualLines::default(); 320 | } 321 | 322 | let orphan_visual_line_count = visual_line_count(&self.orphan_lines, width); 323 | force_draw |= orphan_visual_line_count > VisualLines::default(); 324 | let mut drawable = match self.draw_target.drawable(force_draw, now) { 325 | Some(drawable) => drawable, 326 | None => return Ok(()), 327 | }; 328 | 329 | let mut draw_state = drawable.state(); 330 | draw_state.alignment = self.alignment; 331 | 332 | if let Some(extra_lines) = &extra_lines { 333 | draw_state.lines.extend_from_slice(extra_lines.as_slice()); 334 | } 335 | 336 | // Add lines from `ProgressBar::println` call. 337 | draw_state.lines.append(&mut self.orphan_lines); 338 | 339 | for index in &self.ordering { 340 | let member = &self.members[*index]; 341 | if let Some(state) = &member.draw_state { 342 | draw_state.lines.extend_from_slice(&state.lines[..]); 343 | } 344 | } 345 | 346 | drop(draw_state); 347 | let drawable = drawable.draw(); 348 | 349 | for index in reap_indices { 350 | self.remove_idx(index); 351 | } 352 | 353 | // The zombie lines were drawn for the last time, so make `DrawTarget` forget about them 354 | // so they aren't cleared on next draw. 355 | if extra_lines.is_none() { 356 | self.draw_target 357 | .adjust_last_line_count(LineAdjust::Keep(adjust)); 358 | } 359 | 360 | drawable 361 | } 362 | 363 | pub(crate) fn println>(&mut self, msg: I, now: Instant) -> io::Result<()> { 364 | let msg = msg.as_ref(); 365 | 366 | // If msg is "", make sure a line is still printed 367 | let lines: Vec = match msg.is_empty() { 368 | false => msg.lines().map(|l| LineType::Text(Into::into(l))).collect(), 369 | true => vec![LineType::Empty], 370 | }; 371 | 372 | self.draw(true, Some(lines), now) 373 | } 374 | 375 | pub(crate) fn draw_state(&mut self, idx: usize) -> DrawStateWrapper<'_> { 376 | let member = self.members.get_mut(idx).unwrap(); 377 | // alignment is handled by the `MultiProgress`'s underlying draw target, so there is no 378 | // point in propagating it here. 379 | let state = member.draw_state.get_or_insert(DrawState::default()); 380 | 381 | DrawStateWrapper::for_multi(state, &mut self.orphan_lines) 382 | } 383 | 384 | pub(crate) fn is_hidden(&self) -> bool { 385 | self.draw_target.is_hidden() 386 | } 387 | 388 | pub(crate) fn suspend R, R>(&mut self, f: F, now: Instant) -> R { 389 | self.clear(now).unwrap(); 390 | let ret = f(); 391 | self.draw(true, None, Instant::now()).unwrap(); 392 | ret 393 | } 394 | 395 | pub(crate) fn width(&self) -> Option { 396 | self.draw_target.width() 397 | } 398 | 399 | fn insert(&mut self, location: InsertLocation) -> usize { 400 | let idx = if let Some(idx) = self.free_set.pop() { 401 | self.members[idx] = MultiStateMember::default(); 402 | idx 403 | } else { 404 | self.members.push(MultiStateMember::default()); 405 | self.members.len() - 1 406 | }; 407 | 408 | match location { 409 | InsertLocation::End => self.ordering.push(idx), 410 | InsertLocation::Index(pos) => { 411 | let pos = Ord::min(pos, self.ordering.len()); 412 | self.ordering.insert(pos, idx); 413 | } 414 | InsertLocation::IndexFromBack(pos) => { 415 | let pos = self.ordering.len().saturating_sub(pos); 416 | self.ordering.insert(pos, idx); 417 | } 418 | InsertLocation::After(after_idx) => { 419 | let pos = self.ordering.iter().position(|i| *i == after_idx).unwrap(); 420 | self.ordering.insert(pos + 1, idx); 421 | } 422 | InsertLocation::Before(before_idx) => { 423 | let pos = self.ordering.iter().position(|i| *i == before_idx).unwrap(); 424 | self.ordering.insert(pos, idx); 425 | } 426 | } 427 | 428 | assert_eq!( 429 | self.len(), 430 | self.ordering.len(), 431 | "Draw state is inconsistent" 432 | ); 433 | 434 | idx 435 | } 436 | 437 | fn clear(&mut self, now: Instant) -> io::Result<()> { 438 | match self.draw_target.drawable(true, now) { 439 | Some(mut drawable) => { 440 | // Make the clear operation also wipe out zombie lines 441 | drawable.adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count)); 442 | self.zombie_lines_count = VisualLines::default(); 443 | drawable.clear() 444 | } 445 | None => Ok(()), 446 | } 447 | } 448 | 449 | fn remove_idx(&mut self, idx: usize) { 450 | if self.free_set.contains(&idx) { 451 | return; 452 | } 453 | 454 | self.members[idx] = MultiStateMember::default(); 455 | self.free_set.push(idx); 456 | self.ordering.retain(|&x| x != idx); 457 | 458 | assert_eq!( 459 | self.len(), 460 | self.ordering.len(), 461 | "Draw state is inconsistent" 462 | ); 463 | } 464 | 465 | fn len(&self) -> usize { 466 | self.members.len() - self.free_set.len() 467 | } 468 | } 469 | 470 | #[derive(Default)] 471 | struct MultiStateMember { 472 | /// Draw state will be `None` for members that haven't been drawn before, or for entries that 473 | /// correspond to something in the free set. 474 | draw_state: Option, 475 | /// Whether the corresponding progress bar (more precisely, `BarState`) has been dropped. 476 | is_zombie: bool, 477 | } 478 | 479 | impl Debug for MultiStateMember { 480 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 481 | f.debug_struct("MultiStateElement") 482 | .field("draw_state", &self.draw_state) 483 | .field("is_zombie", &self.is_zombie) 484 | .finish_non_exhaustive() 485 | } 486 | } 487 | 488 | /// Vertical alignment of a multi progress. 489 | /// 490 | /// The alignment controls how the multi progress is aligned if some of its progress bars get removed. 491 | /// E.g. [`Top`](MultiProgressAlignment::Top) alignment (default), when _progress bar 2_ is removed: 492 | /// ```ignore 493 | /// [0/100] progress bar 1 [0/100] progress bar 1 494 | /// [0/100] progress bar 2 => [0/100] progress bar 3 495 | /// [0/100] progress bar 3 496 | /// ``` 497 | /// 498 | /// [`Bottom`](MultiProgressAlignment::Bottom) alignment 499 | /// ```ignore 500 | /// [0/100] progress bar 1 501 | /// [0/100] progress bar 2 => [0/100] progress bar 1 502 | /// [0/100] progress bar 3 [0/100] progress bar 3 503 | /// ``` 504 | #[derive(Debug, Copy, Clone)] 505 | pub enum MultiProgressAlignment { 506 | Top, 507 | Bottom, 508 | } 509 | 510 | impl Default for MultiProgressAlignment { 511 | fn default() -> Self { 512 | Self::Top 513 | } 514 | } 515 | 516 | enum InsertLocation { 517 | End, 518 | Index(usize), 519 | IndexFromBack(usize), 520 | After(usize), 521 | Before(usize), 522 | } 523 | 524 | #[cfg(test)] 525 | mod tests { 526 | use crate::{MultiProgress, ProgressBar, ProgressDrawTarget}; 527 | 528 | #[test] 529 | fn late_pb_drop() { 530 | let pb = ProgressBar::new(10); 531 | let mpb = MultiProgress::new(); 532 | // This clone call is required to trigger a now fixed bug. 533 | // See for context 534 | #[allow(clippy::redundant_clone)] 535 | mpb.add(pb.clone()); 536 | } 537 | 538 | #[test] 539 | fn progress_bar_sync_send() { 540 | let _: Box = Box::new(ProgressBar::new(1)); 541 | let _: Box = Box::new(ProgressBar::new(1)); 542 | let _: Box = Box::new(MultiProgress::new()); 543 | let _: Box = Box::new(MultiProgress::new()); 544 | } 545 | 546 | #[test] 547 | fn multi_progress_hidden() { 548 | let mpb = MultiProgress::with_draw_target(ProgressDrawTarget::hidden()); 549 | let pb = mpb.add(ProgressBar::new(123)); 550 | pb.finish(); 551 | } 552 | 553 | #[test] 554 | fn multi_progress_modifications() { 555 | let mp = MultiProgress::new(); 556 | let p0 = mp.add(ProgressBar::new(1)); 557 | let p1 = mp.add(ProgressBar::new(1)); 558 | let p2 = mp.add(ProgressBar::new(1)); 559 | let p3 = mp.add(ProgressBar::new(1)); 560 | mp.remove(&p2); 561 | mp.remove(&p1); 562 | let p4 = mp.insert(1, ProgressBar::new(1)); 563 | 564 | let state = mp.state.read().unwrap(); 565 | // the removed place for p1 is reused 566 | assert_eq!(state.members.len(), 4); 567 | assert_eq!(state.len(), 3); 568 | 569 | // free_set may contain 1 or 2 570 | match state.free_set.last() { 571 | Some(1) => { 572 | assert_eq!(state.ordering, vec![0, 2, 3]); 573 | assert!(state.members[1].draw_state.is_none()); 574 | assert_eq!(p4.index().unwrap(), 2); 575 | } 576 | Some(2) => { 577 | assert_eq!(state.ordering, vec![0, 1, 3]); 578 | assert!(state.members[2].draw_state.is_none()); 579 | assert_eq!(p4.index().unwrap(), 1); 580 | } 581 | _ => unreachable!(), 582 | } 583 | 584 | assert_eq!(p0.index().unwrap(), 0); 585 | assert_eq!(p1.index(), None); 586 | assert_eq!(p2.index(), None); 587 | assert_eq!(p3.index().unwrap(), 3); 588 | } 589 | 590 | #[test] 591 | fn multi_progress_insert_from_back() { 592 | let mp = MultiProgress::new(); 593 | let p0 = mp.add(ProgressBar::new(1)); 594 | let p1 = mp.add(ProgressBar::new(1)); 595 | let p2 = mp.add(ProgressBar::new(1)); 596 | let p3 = mp.insert_from_back(1, ProgressBar::new(1)); 597 | let p4 = mp.insert_from_back(10, ProgressBar::new(1)); 598 | 599 | let state = mp.state.read().unwrap(); 600 | assert_eq!(state.ordering, vec![4, 0, 1, 3, 2]); 601 | assert_eq!(p0.index().unwrap(), 0); 602 | assert_eq!(p1.index().unwrap(), 1); 603 | assert_eq!(p2.index().unwrap(), 2); 604 | assert_eq!(p3.index().unwrap(), 3); 605 | assert_eq!(p4.index().unwrap(), 4); 606 | } 607 | 608 | #[test] 609 | fn multi_progress_insert_after() { 610 | let mp = MultiProgress::new(); 611 | let p0 = mp.add(ProgressBar::new(1)); 612 | let p1 = mp.add(ProgressBar::new(1)); 613 | let p2 = mp.add(ProgressBar::new(1)); 614 | let p3 = mp.insert_after(&p2, ProgressBar::new(1)); 615 | let p4 = mp.insert_after(&p0, ProgressBar::new(1)); 616 | 617 | let state = mp.state.read().unwrap(); 618 | assert_eq!(state.ordering, vec![0, 4, 1, 2, 3]); 619 | assert_eq!(p0.index().unwrap(), 0); 620 | assert_eq!(p1.index().unwrap(), 1); 621 | assert_eq!(p2.index().unwrap(), 2); 622 | assert_eq!(p3.index().unwrap(), 3); 623 | assert_eq!(p4.index().unwrap(), 4); 624 | } 625 | 626 | #[test] 627 | fn multi_progress_insert_before() { 628 | let mp = MultiProgress::new(); 629 | let p0 = mp.add(ProgressBar::new(1)); 630 | let p1 = mp.add(ProgressBar::new(1)); 631 | let p2 = mp.add(ProgressBar::new(1)); 632 | let p3 = mp.insert_before(&p0, ProgressBar::new(1)); 633 | let p4 = mp.insert_before(&p2, ProgressBar::new(1)); 634 | 635 | let state = mp.state.read().unwrap(); 636 | assert_eq!(state.ordering, vec![3, 0, 1, 4, 2]); 637 | assert_eq!(p0.index().unwrap(), 0); 638 | assert_eq!(p1.index().unwrap(), 1); 639 | assert_eq!(p2.index().unwrap(), 2); 640 | assert_eq!(p3.index().unwrap(), 3); 641 | assert_eq!(p4.index().unwrap(), 4); 642 | } 643 | 644 | #[test] 645 | fn multi_progress_insert_before_and_after() { 646 | let mp = MultiProgress::new(); 647 | let p0 = mp.add(ProgressBar::new(1)); 648 | let p1 = mp.add(ProgressBar::new(1)); 649 | let p2 = mp.add(ProgressBar::new(1)); 650 | let p3 = mp.insert_before(&p0, ProgressBar::new(1)); 651 | let p4 = mp.insert_after(&p3, ProgressBar::new(1)); 652 | let p5 = mp.insert_after(&p3, ProgressBar::new(1)); 653 | let p6 = mp.insert_before(&p1, ProgressBar::new(1)); 654 | 655 | let state = mp.state.read().unwrap(); 656 | assert_eq!(state.ordering, vec![3, 5, 4, 0, 6, 1, 2]); 657 | assert_eq!(p0.index().unwrap(), 0); 658 | assert_eq!(p1.index().unwrap(), 1); 659 | assert_eq!(p2.index().unwrap(), 2); 660 | assert_eq!(p3.index().unwrap(), 3); 661 | assert_eq!(p4.index().unwrap(), 4); 662 | assert_eq!(p5.index().unwrap(), 5); 663 | assert_eq!(p6.index().unwrap(), 6); 664 | } 665 | 666 | #[test] 667 | fn multi_progress_multiple_remove() { 668 | let mp = MultiProgress::new(); 669 | let p0 = mp.add(ProgressBar::new(1)); 670 | let p1 = mp.add(ProgressBar::new(1)); 671 | // double remove beyond the first one have no effect 672 | mp.remove(&p0); 673 | mp.remove(&p0); 674 | mp.remove(&p0); 675 | 676 | let state = mp.state.read().unwrap(); 677 | // the removed place for p1 is reused 678 | assert_eq!(state.members.len(), 2); 679 | assert_eq!(state.free_set.len(), 1); 680 | assert_eq!(state.len(), 1); 681 | assert!(state.members[0].draw_state.is_none()); 682 | assert_eq!(state.free_set.last(), Some(&0)); 683 | 684 | assert_eq!(state.ordering, vec![1]); 685 | assert_eq!(p0.index(), None); 686 | assert_eq!(p1.index().unwrap(), 1); 687 | } 688 | 689 | #[test] 690 | fn mp_no_crash_double_add() { 691 | let mp = MultiProgress::new(); 692 | let pb = mp.add(ProgressBar::new(10)); 693 | mp.add(pb); 694 | } 695 | } 696 | -------------------------------------------------------------------------------- /src/progress_bar.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use portable_atomic::{AtomicBool, Ordering}; 3 | use std::borrow::Cow; 4 | use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak}; 5 | use std::time::Duration; 6 | #[cfg(not(target_arch = "wasm32"))] 7 | use std::time::Instant; 8 | use std::{fmt, io, thread}; 9 | 10 | #[cfg(test)] 11 | use once_cell::sync::Lazy; 12 | #[cfg(target_arch = "wasm32")] 13 | use web_time::Instant; 14 | 15 | use crate::draw_target::ProgressDrawTarget; 16 | use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset, TabExpandedString}; 17 | use crate::style::ProgressStyle; 18 | use crate::{ProgressBarIter, ProgressIterator, ProgressState}; 19 | 20 | /// A progress bar or spinner 21 | /// 22 | /// The progress bar is an [`Arc`] around its internal state. When the progress bar is cloned it 23 | /// just increments the refcount (so the original and its clone share the same state). 24 | #[derive(Clone)] 25 | pub struct ProgressBar { 26 | state: Arc>, 27 | pos: Arc, 28 | ticker: Arc>>, 29 | } 30 | 31 | impl fmt::Debug for ProgressBar { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | f.debug_struct("ProgressBar").finish() 34 | } 35 | } 36 | 37 | impl ProgressBar { 38 | /// Creates a new progress bar with a given length 39 | /// 40 | /// This progress bar by default draws directly to stderr, and refreshes a maximum of 20 times 41 | /// a second. To change the refresh rate, [set] the [draw target] to one with a different refresh 42 | /// rate. 43 | /// 44 | /// [set]: ProgressBar::set_draw_target 45 | /// [draw target]: ProgressDrawTarget 46 | pub fn new(len: u64) -> Self { 47 | Self::with_draw_target(Some(len), ProgressDrawTarget::stderr()) 48 | } 49 | 50 | /// Creates a new progress bar without a specified length 51 | /// 52 | /// This progress bar by default draws directly to stderr, and refreshes a maximum of 20 times 53 | /// a second. To change the refresh rate, [set] the [draw target] to one with a different refresh 54 | /// rate. 55 | /// 56 | /// [set]: ProgressBar::set_draw_target 57 | /// [draw target]: ProgressDrawTarget 58 | pub fn no_length() -> Self { 59 | Self::with_draw_target(None, ProgressDrawTarget::stderr()) 60 | } 61 | 62 | /// Creates a completely hidden progress bar 63 | /// 64 | /// This progress bar still responds to API changes but it does not have a length or render in 65 | /// any way. 66 | pub fn hidden() -> Self { 67 | Self::with_draw_target(None, ProgressDrawTarget::hidden()) 68 | } 69 | 70 | /// Creates a new progress bar with a given length and draw target 71 | pub fn with_draw_target(len: Option, draw_target: ProgressDrawTarget) -> Self { 72 | let pos = Arc::new(AtomicPosition::new()); 73 | Self { 74 | state: Arc::new(Mutex::new(BarState::new(len, draw_target, pos.clone()))), 75 | pos, 76 | ticker: Arc::new(Mutex::new(None)), 77 | } 78 | } 79 | 80 | /// Get a clone of the current progress bar style. 81 | pub fn style(&self) -> ProgressStyle { 82 | self.state().style.clone() 83 | } 84 | 85 | /// A convenience builder-like function for a progress bar with a given style 86 | pub fn with_style(self, style: ProgressStyle) -> Self { 87 | self.set_style(style); 88 | self 89 | } 90 | 91 | /// A convenience builder-like function for a progress bar with a given tab width 92 | pub fn with_tab_width(self, tab_width: usize) -> Self { 93 | self.state().set_tab_width(tab_width); 94 | self 95 | } 96 | 97 | /// A convenience builder-like function for a progress bar with a given prefix 98 | /// 99 | /// For the prefix to be visible, the `{prefix}` placeholder must be present in the template 100 | /// (see [`ProgressStyle`]). 101 | pub fn with_prefix(self, prefix: impl Into>) -> Self { 102 | let mut state = self.state(); 103 | state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width); 104 | drop(state); 105 | self 106 | } 107 | 108 | /// A convenience builder-like function for a progress bar with a given message 109 | /// 110 | /// For the message to be visible, the `{msg}` placeholder must be present in the template (see 111 | /// [`ProgressStyle`]). 112 | pub fn with_message(self, message: impl Into>) -> Self { 113 | let mut state = self.state(); 114 | state.state.message = TabExpandedString::new(message.into(), state.tab_width); 115 | drop(state); 116 | self 117 | } 118 | 119 | /// A convenience builder-like function for a progress bar with a given position 120 | pub fn with_position(self, pos: u64) -> Self { 121 | self.state().state.set_pos(pos); 122 | self 123 | } 124 | 125 | /// A convenience builder-like function for a progress bar with a given elapsed time 126 | pub fn with_elapsed(self, elapsed: Duration) -> Self { 127 | self.state().state.started = Instant::now().checked_sub(elapsed).unwrap(); 128 | self 129 | } 130 | 131 | /// Sets the finish behavior for the progress bar 132 | /// 133 | /// This behavior is invoked when [`ProgressBar`] or 134 | /// [`ProgressBarIter`] completes and 135 | /// [`ProgressBar::is_finished()`] is false. 136 | /// If you don't want the progress bar to be automatically finished then 137 | /// call `with_finish(Abandon)`. 138 | /// 139 | /// [`ProgressBar`]: crate::ProgressBar 140 | /// [`ProgressBarIter`]: crate::ProgressBarIter 141 | /// [`ProgressBar::is_finished()`]: crate::ProgressBar::is_finished 142 | pub fn with_finish(self, finish: ProgressFinish) -> Self { 143 | self.state().on_finish = finish; 144 | self 145 | } 146 | 147 | /// Creates a new spinner 148 | /// 149 | /// This spinner by default draws directly to stderr. This adds the default spinner style to it. 150 | pub fn new_spinner() -> Self { 151 | let rv = Self::with_draw_target(None, ProgressDrawTarget::stderr()); 152 | rv.set_style(ProgressStyle::default_spinner()); 153 | rv 154 | } 155 | 156 | /// Overrides the stored style 157 | /// 158 | /// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it. 159 | pub fn set_style(&self, mut style: ProgressStyle) { 160 | let mut state = self.state(); 161 | if state.draw_target.is_stderr() { 162 | style.set_for_stderr() 163 | }; 164 | state.set_style(style); 165 | } 166 | 167 | /// Sets the tab width (default: 8). All tabs will be expanded to this many spaces. 168 | pub fn set_tab_width(&self, tab_width: usize) { 169 | let mut state = self.state(); 170 | state.set_tab_width(tab_width); 171 | state.draw(true, Instant::now()).unwrap(); 172 | } 173 | 174 | /// Spawns a background thread to tick the progress bar 175 | /// 176 | /// When this is enabled a background thread will regularly tick the progress bar in the given 177 | /// interval. This is useful to advance progress bars that are very slow by themselves. 178 | /// 179 | /// When steady ticks are enabled, calling [`ProgressBar::tick()`] on a progress bar does not 180 | /// have any effect. 181 | pub fn enable_steady_tick(&self, interval: Duration) { 182 | // The way we test for ticker termination is with a single static `AtomicBool`. Since cargo 183 | // runs tests concurrently, we have a `TICKER_TEST` lock to make sure tests using ticker 184 | // don't step on each other. This check catches attempts to use tickers in tests without 185 | // acquiring the lock. 186 | #[cfg(test)] 187 | { 188 | let guard = TICKER_TEST.try_lock(); 189 | let lock_acquired = guard.is_ok(); 190 | // Drop the guard before panicking to avoid poisoning the lock (which would cause other 191 | // ticker tests to fail) 192 | drop(guard); 193 | if lock_acquired { 194 | panic!("you must acquire the TICKER_TEST lock in your test to use this method"); 195 | } 196 | } 197 | 198 | if interval.is_zero() { 199 | return; 200 | } 201 | 202 | self.stop_and_replace_ticker(Some(interval)); 203 | } 204 | 205 | /// Undoes [`ProgressBar::enable_steady_tick()`] 206 | pub fn disable_steady_tick(&self) { 207 | self.stop_and_replace_ticker(None); 208 | } 209 | 210 | fn stop_and_replace_ticker(&self, interval: Option) { 211 | let mut ticker_state = self.ticker.lock().unwrap(); 212 | if let Some(ticker) = ticker_state.take() { 213 | ticker.stop(); 214 | } 215 | 216 | *ticker_state = interval.map(|interval| Ticker::new(interval, &self.state)); 217 | } 218 | 219 | /// Manually ticks the spinner or progress bar 220 | /// 221 | /// This automatically happens on any other change to a progress bar. 222 | pub fn tick(&self) { 223 | self.tick_inner(Instant::now()); 224 | } 225 | 226 | fn tick_inner(&self, now: Instant) { 227 | // Only tick if a `Ticker` isn't installed 228 | if self.ticker.lock().unwrap().is_none() { 229 | self.state().tick(now); 230 | } 231 | } 232 | 233 | /// Advances the position of the progress bar by `delta` 234 | pub fn inc(&self, delta: u64) { 235 | self.pos.inc(delta); 236 | let now = Instant::now(); 237 | if self.pos.allow(now) { 238 | self.tick_inner(now); 239 | } 240 | } 241 | 242 | /// Decrease the position of the progress bar by `delta` 243 | pub fn dec(&self, delta: u64) { 244 | self.pos.dec(delta); 245 | let now = Instant::now(); 246 | if self.pos.allow(now) { 247 | self.tick_inner(now); 248 | } 249 | } 250 | 251 | /// A quick convenience check if the progress bar is hidden 252 | pub fn is_hidden(&self) -> bool { 253 | self.state().draw_target.is_hidden() 254 | } 255 | 256 | /// Indicates that the progress bar finished 257 | pub fn is_finished(&self) -> bool { 258 | self.state().state.is_finished() 259 | } 260 | 261 | /// Print a log line above the progress bar 262 | /// 263 | /// If the progress bar is hidden (e.g. when standard output is not a terminal), `println()` 264 | /// will not do anything. If you want to write to the standard output in such cases as well, use 265 | /// [`ProgressBar::suspend()`] instead. 266 | /// 267 | /// If the progress bar was added to a [`MultiProgress`], the log line will be 268 | /// printed above all other progress bars. 269 | /// 270 | /// [`ProgressBar::suspend()`]: ProgressBar::suspend 271 | /// [`MultiProgress`]: crate::MultiProgress 272 | pub fn println>(&self, msg: I) { 273 | self.state().println(Instant::now(), msg.as_ref()); 274 | } 275 | 276 | /// Update the `ProgressBar`'s inner [`ProgressState`] 277 | pub fn update(&self, f: impl FnOnce(&mut ProgressState)) { 278 | self.state() 279 | .update(Instant::now(), f, self.ticker.lock().unwrap().is_none()); 280 | } 281 | 282 | /// Sets the position of the progress bar 283 | pub fn set_position(&self, pos: u64) { 284 | self.pos.set(pos); 285 | let now = Instant::now(); 286 | if self.pos.allow(now) { 287 | self.tick_inner(now); 288 | } 289 | } 290 | 291 | /// Sets the length of the progress bar to `None` 292 | pub fn unset_length(&self) { 293 | self.state().unset_length(Instant::now()); 294 | } 295 | 296 | /// Sets the length of the progress bar 297 | pub fn set_length(&self, len: u64) { 298 | self.state().set_length(Instant::now(), len); 299 | } 300 | 301 | /// Increase the length of the progress bar 302 | pub fn inc_length(&self, delta: u64) { 303 | self.state().inc_length(Instant::now(), delta); 304 | } 305 | 306 | /// Decrease the length of the progress bar 307 | pub fn dec_length(&self, delta: u64) { 308 | self.state().dec_length(Instant::now(), delta); 309 | } 310 | 311 | /// Sets the current prefix of the progress bar 312 | /// 313 | /// For the prefix to be visible, the `{prefix}` placeholder must be present in the template 314 | /// (see [`ProgressStyle`]). 315 | pub fn set_prefix(&self, prefix: impl Into>) { 316 | let mut state = self.state(); 317 | state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width); 318 | state.update_estimate_and_draw(Instant::now()); 319 | } 320 | 321 | /// Sets the current message of the progress bar 322 | /// 323 | /// For the message to be visible, the `{msg}` placeholder must be present in the template (see 324 | /// [`ProgressStyle`]). 325 | pub fn set_message(&self, msg: impl Into>) { 326 | let mut state = self.state(); 327 | state.state.message = TabExpandedString::new(msg.into(), state.tab_width); 328 | state.update_estimate_and_draw(Instant::now()); 329 | } 330 | 331 | /// Creates a new weak reference to this [`ProgressBar`] 332 | pub fn downgrade(&self) -> WeakProgressBar { 333 | WeakProgressBar { 334 | state: Arc::downgrade(&self.state), 335 | pos: Arc::downgrade(&self.pos), 336 | ticker: Arc::downgrade(&self.ticker), 337 | } 338 | } 339 | 340 | /// Resets the ETA calculation 341 | /// 342 | /// This can be useful if the progress bars made a large jump or was paused for a prolonged 343 | /// time. 344 | pub fn reset_eta(&self) { 345 | self.state().reset(Instant::now(), Reset::Eta); 346 | } 347 | 348 | /// Resets elapsed time and the ETA calculation 349 | pub fn reset_elapsed(&self) { 350 | self.state().reset(Instant::now(), Reset::Elapsed); 351 | } 352 | 353 | /// Resets all of the progress bar state 354 | pub fn reset(&self) { 355 | self.state().reset(Instant::now(), Reset::All); 356 | } 357 | 358 | /// Finishes the progress bar and leaves the current message 359 | pub fn finish(&self) { 360 | self.state() 361 | .finish_using_style(Instant::now(), ProgressFinish::AndLeave); 362 | } 363 | 364 | /// Finishes the progress bar and sets a message 365 | /// 366 | /// For the message to be visible, the `{msg}` placeholder must be present in the template (see 367 | /// [`ProgressStyle`]). 368 | pub fn finish_with_message(&self, msg: impl Into>) { 369 | self.state() 370 | .finish_using_style(Instant::now(), ProgressFinish::WithMessage(msg.into())); 371 | } 372 | 373 | /// Finishes the progress bar and completely clears it 374 | pub fn finish_and_clear(&self) { 375 | self.state() 376 | .finish_using_style(Instant::now(), ProgressFinish::AndClear); 377 | } 378 | 379 | /// Finishes the progress bar and leaves the current message and progress 380 | pub fn abandon(&self) { 381 | self.state() 382 | .finish_using_style(Instant::now(), ProgressFinish::Abandon); 383 | } 384 | 385 | /// Finishes the progress bar and sets a message, and leaves the current progress 386 | /// 387 | /// For the message to be visible, the `{msg}` placeholder must be present in the template (see 388 | /// [`ProgressStyle`]). 389 | pub fn abandon_with_message(&self, msg: impl Into>) { 390 | self.state().finish_using_style( 391 | Instant::now(), 392 | ProgressFinish::AbandonWithMessage(msg.into()), 393 | ); 394 | } 395 | 396 | /// Finishes the progress bar using the behavior stored in the [`ProgressStyle`] 397 | /// 398 | /// See [`ProgressBar::with_finish()`]. 399 | pub fn finish_using_style(&self) { 400 | let mut state = self.state(); 401 | let finish = state.on_finish.clone(); 402 | state.finish_using_style(Instant::now(), finish); 403 | } 404 | 405 | /// Sets a different draw target for the progress bar 406 | /// 407 | /// This can be used to draw the progress bar to stderr (this is the default): 408 | /// 409 | /// ```rust,no_run 410 | /// # use indicatif::{ProgressBar, ProgressDrawTarget}; 411 | /// let pb = ProgressBar::new(100); 412 | /// pb.set_draw_target(ProgressDrawTarget::stderr()); 413 | /// ``` 414 | /// 415 | /// **Note:** Calling this method on a [`ProgressBar`] linked with a [`MultiProgress`] (after 416 | /// running [`MultiProgress::add()`]) will unlink this progress bar. If you don't want this 417 | /// behavior, call [`MultiProgress::set_draw_target()`] instead. 418 | /// 419 | /// Use [`ProgressBar::with_draw_target()`] to set the draw target during creation. 420 | /// 421 | /// [`MultiProgress`]: crate::MultiProgress 422 | /// [`MultiProgress::add()`]: crate::MultiProgress::add 423 | /// [`MultiProgress::set_draw_target()`]: crate::MultiProgress::set_draw_target 424 | pub fn set_draw_target(&self, target: ProgressDrawTarget) { 425 | let mut state = self.state(); 426 | state.draw_target.disconnect(Instant::now()); 427 | state.draw_target = target; 428 | } 429 | 430 | /// Force a redraw of the progress bar to be in sync with its state 431 | /// 432 | /// For performance reasons the progress bar is not redrawn on each state update. 433 | /// This is normally not an issue, since new updates will eventually trigger rendering. 434 | /// 435 | /// For slow running tasks it is recommended to rely on [`ProgressBar::enable_steady_tick()`] 436 | /// to ensure continued rendering of the progress bar. 437 | pub fn force_draw(&self) { 438 | let _ = self.state().draw(true, Instant::now()); 439 | } 440 | 441 | /// Hide the progress bar temporarily, execute `f`, then redraw the progress bar 442 | /// 443 | /// Useful for external code that writes to the standard output. 444 | /// 445 | /// If the progress bar was added to a [`MultiProgress`], it will suspend the entire [`MultiProgress`]. 446 | /// 447 | /// **Note:** The internal lock is held while `f` is executed. Other threads trying to print 448 | /// anything on the progress bar will be blocked until `f` finishes. 449 | /// Therefore, it is recommended to avoid long-running operations in `f`. 450 | /// 451 | /// ```rust,no_run 452 | /// # use indicatif::ProgressBar; 453 | /// let mut pb = ProgressBar::new(3); 454 | /// pb.suspend(|| { 455 | /// println!("Log message"); 456 | /// }) 457 | /// ``` 458 | /// 459 | /// [`MultiProgress`]: crate::MultiProgress 460 | pub fn suspend R, R>(&self, f: F) -> R { 461 | self.state().suspend(Instant::now(), f) 462 | } 463 | 464 | /// Wraps an [`Iterator`] with the progress bar 465 | /// 466 | /// ```rust,no_run 467 | /// # use indicatif::ProgressBar; 468 | /// let v = vec![1, 2, 3]; 469 | /// let pb = ProgressBar::new(3); 470 | /// for item in pb.wrap_iter(v.iter()) { 471 | /// // ... 472 | /// } 473 | /// ``` 474 | pub fn wrap_iter(&self, it: It) -> ProgressBarIter { 475 | it.progress_with(self.clone()) 476 | } 477 | 478 | /// Wraps an [`io::Read`] with the progress bar 479 | /// 480 | /// ```rust,no_run 481 | /// # use std::fs::File; 482 | /// # use std::io; 483 | /// # use indicatif::ProgressBar; 484 | /// # fn test () -> io::Result<()> { 485 | /// let source = File::open("work.txt")?; 486 | /// let mut target = File::create("done.txt")?; 487 | /// let pb = ProgressBar::new(source.metadata()?.len()); 488 | /// io::copy(&mut pb.wrap_read(source), &mut target); 489 | /// # Ok(()) 490 | /// # } 491 | /// ``` 492 | pub fn wrap_read(&self, read: R) -> ProgressBarIter { 493 | ProgressBarIter { 494 | progress: self.clone(), 495 | it: read, 496 | } 497 | } 498 | 499 | /// Wraps an [`io::Write`] with the progress bar 500 | /// 501 | /// ```rust,no_run 502 | /// # use std::fs::File; 503 | /// # use std::io; 504 | /// # use indicatif::ProgressBar; 505 | /// # fn test () -> io::Result<()> { 506 | /// let mut source = File::open("work.txt")?; 507 | /// let target = File::create("done.txt")?; 508 | /// let pb = ProgressBar::new(source.metadata()?.len()); 509 | /// io::copy(&mut source, &mut pb.wrap_write(target)); 510 | /// # Ok(()) 511 | /// # } 512 | /// ``` 513 | pub fn wrap_write(&self, write: W) -> ProgressBarIter { 514 | ProgressBarIter { 515 | progress: self.clone(), 516 | it: write, 517 | } 518 | } 519 | 520 | #[cfg(feature = "tokio")] 521 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 522 | /// Wraps an [`tokio::io::AsyncWrite`] with the progress bar 523 | /// 524 | /// ```rust,no_run 525 | /// # use tokio::fs::File; 526 | /// # use tokio::io; 527 | /// # use indicatif::ProgressBar; 528 | /// # async fn test() -> io::Result<()> { 529 | /// let mut source = File::open("work.txt").await?; 530 | /// let mut target = File::open("done.txt").await?; 531 | /// let pb = ProgressBar::new(source.metadata().await?.len()); 532 | /// io::copy(&mut source, &mut pb.wrap_async_write(target)).await?; 533 | /// # Ok(()) 534 | /// # } 535 | /// ``` 536 | pub fn wrap_async_write( 537 | &self, 538 | write: W, 539 | ) -> ProgressBarIter { 540 | ProgressBarIter { 541 | progress: self.clone(), 542 | it: write, 543 | } 544 | } 545 | 546 | #[cfg(feature = "tokio")] 547 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 548 | /// Wraps an [`tokio::io::AsyncRead`] with the progress bar 549 | /// 550 | /// ```rust,no_run 551 | /// # use tokio::fs::File; 552 | /// # use tokio::io; 553 | /// # use indicatif::ProgressBar; 554 | /// # async fn test() -> io::Result<()> { 555 | /// let mut source = File::open("work.txt").await?; 556 | /// let mut target = File::open("done.txt").await?; 557 | /// let pb = ProgressBar::new(source.metadata().await?.len()); 558 | /// io::copy(&mut pb.wrap_async_read(source), &mut target).await?; 559 | /// # Ok(()) 560 | /// # } 561 | /// ``` 562 | pub fn wrap_async_read(&self, read: R) -> ProgressBarIter { 563 | ProgressBarIter { 564 | progress: self.clone(), 565 | it: read, 566 | } 567 | } 568 | 569 | /// Wraps a [`futures::Stream`](https://docs.rs/futures/0.3/futures/stream/trait.StreamExt.html) with the progress bar 570 | /// 571 | /// ``` 572 | /// # use indicatif::ProgressBar; 573 | /// # futures::executor::block_on(async { 574 | /// use futures::stream::{self, StreamExt}; 575 | /// let pb = ProgressBar::new(10); 576 | /// let mut stream = pb.wrap_stream(stream::iter('a'..='z')); 577 | /// 578 | /// assert_eq!(stream.next().await, Some('a')); 579 | /// assert_eq!(stream.count().await, 25); 580 | /// # }); // block_on 581 | /// ``` 582 | #[cfg(feature = "futures")] 583 | #[cfg_attr(docsrs, doc(cfg(feature = "futures")))] 584 | pub fn wrap_stream(&self, stream: S) -> ProgressBarIter { 585 | ProgressBarIter { 586 | progress: self.clone(), 587 | it: stream, 588 | } 589 | } 590 | 591 | /// Returns the current position 592 | pub fn position(&self) -> u64 { 593 | self.state().state.pos() 594 | } 595 | 596 | /// Returns the current length 597 | pub fn length(&self) -> Option { 598 | self.state().state.len() 599 | } 600 | 601 | /// Returns the current ETA 602 | pub fn eta(&self) -> Duration { 603 | self.state().state.eta() 604 | } 605 | 606 | /// Returns the current rate of progress 607 | pub fn per_sec(&self) -> f64 { 608 | self.state().state.per_sec() 609 | } 610 | 611 | /// Returns the current expected duration 612 | pub fn duration(&self) -> Duration { 613 | self.state().state.duration() 614 | } 615 | 616 | /// Returns the current elapsed time 617 | pub fn elapsed(&self) -> Duration { 618 | self.state().state.elapsed() 619 | } 620 | 621 | /// Index in the `MultiState` 622 | pub(crate) fn index(&self) -> Option { 623 | self.state().draw_target.remote().map(|(_, idx)| idx) 624 | } 625 | 626 | /// Current message 627 | pub fn message(&self) -> String { 628 | self.state().state.message.expanded().to_string() 629 | } 630 | 631 | /// Current prefix 632 | pub fn prefix(&self) -> String { 633 | self.state().state.prefix.expanded().to_string() 634 | } 635 | 636 | #[inline] 637 | pub(crate) fn state(&self) -> MutexGuard<'_, BarState> { 638 | self.state.lock().unwrap() 639 | } 640 | } 641 | 642 | /// A weak reference to a [`ProgressBar`]. 643 | /// 644 | /// Useful for creating custom steady tick implementations 645 | #[derive(Clone, Default)] 646 | pub struct WeakProgressBar { 647 | state: Weak>, 648 | pos: Weak, 649 | ticker: Weak>>, 650 | } 651 | 652 | impl WeakProgressBar { 653 | /// Create a new [`WeakProgressBar`] that returns `None` when [`upgrade()`] is called. 654 | /// 655 | /// [`upgrade()`]: WeakProgressBar::upgrade 656 | pub fn new() -> Self { 657 | Self::default() 658 | } 659 | 660 | /// Attempts to upgrade the Weak pointer to a [`ProgressBar`], delaying dropping of the inner 661 | /// value if successful. Returns [`None`] if the inner value has since been dropped. 662 | /// 663 | /// [`ProgressBar`]: struct.ProgressBar.html 664 | pub fn upgrade(&self) -> Option { 665 | let state = self.state.upgrade()?; 666 | let pos = self.pos.upgrade()?; 667 | let ticker = self.ticker.upgrade()?; 668 | Some(ProgressBar { state, pos, ticker }) 669 | } 670 | } 671 | 672 | pub(crate) struct Ticker { 673 | stopping: Arc<(Mutex, Condvar)>, 674 | join_handle: Option>, 675 | } 676 | 677 | impl Drop for Ticker { 678 | fn drop(&mut self) { 679 | self.stop(); 680 | self.join_handle.take().map(|handle| handle.join()); 681 | } 682 | } 683 | 684 | #[cfg(test)] 685 | static TICKER_RUNNING: AtomicBool = AtomicBool::new(false); 686 | 687 | impl Ticker { 688 | pub(crate) fn new(interval: Duration, bar_state: &Arc>) -> Self { 689 | debug_assert!(!interval.is_zero()); 690 | 691 | // A `Mutex` is used as a flag to indicate whether the ticker was requested to stop. 692 | // The `Condvar` is used a notification mechanism: when the ticker is dropped, we notify 693 | // the thread and interrupt the ticker wait. 694 | #[allow(clippy::mutex_atomic)] 695 | let stopping = Arc::new((Mutex::new(false), Condvar::new())); 696 | let control = TickerControl { 697 | stopping: stopping.clone(), 698 | state: Arc::downgrade(bar_state), 699 | }; 700 | 701 | let join_handle = thread::spawn(move || control.run(interval)); 702 | Self { 703 | stopping, 704 | join_handle: Some(join_handle), 705 | } 706 | } 707 | 708 | pub(crate) fn stop(&self) { 709 | *self.stopping.0.lock().unwrap() = true; 710 | self.stopping.1.notify_one(); 711 | } 712 | } 713 | 714 | struct TickerControl { 715 | stopping: Arc<(Mutex, Condvar)>, 716 | state: Weak>, 717 | } 718 | 719 | impl TickerControl { 720 | fn run(&self, interval: Duration) { 721 | #[cfg(test)] 722 | TICKER_RUNNING.store(true, Ordering::SeqCst); 723 | 724 | while let Some(arc) = self.state.upgrade() { 725 | let mut state = arc.lock().unwrap(); 726 | if state.state.is_finished() { 727 | break; 728 | } 729 | 730 | state.tick(Instant::now()); 731 | 732 | drop(state); // Don't forget to drop the lock before sleeping 733 | drop(arc); // Also need to drop Arc otherwise BarState won't be dropped 734 | 735 | // Wait for `interval` but return early if we are notified to stop 736 | let result = self 737 | .stopping 738 | .1 739 | .wait_timeout_while(self.stopping.0.lock().unwrap(), interval, |stopped| { 740 | !*stopped 741 | }) 742 | .unwrap(); 743 | 744 | // If the wait didn't time out, it means we were notified to stop 745 | if !result.1.timed_out() { 746 | break; 747 | } 748 | } 749 | 750 | #[cfg(test)] 751 | TICKER_RUNNING.store(false, Ordering::SeqCst); 752 | } 753 | } 754 | 755 | // Tests using the global TICKER_RUNNING flag need to be serialized 756 | #[cfg(test)] 757 | pub(crate) static TICKER_TEST: Lazy> = Lazy::new(Mutex::default); 758 | 759 | #[cfg(test)] 760 | mod tests { 761 | use super::*; 762 | 763 | #[allow(clippy::float_cmp)] 764 | #[test] 765 | fn test_pbar_zero() { 766 | let pb = ProgressBar::new(0); 767 | assert_eq!(pb.state().state.fraction(), 1.0); 768 | } 769 | 770 | #[allow(clippy::float_cmp)] 771 | #[test] 772 | fn test_pbar_maxu64() { 773 | let pb = ProgressBar::new(!0); 774 | assert_eq!(pb.state().state.fraction(), 0.0); 775 | } 776 | 777 | #[test] 778 | fn test_pbar_overflow() { 779 | let pb = ProgressBar::new(1); 780 | pb.set_draw_target(ProgressDrawTarget::hidden()); 781 | pb.inc(2); 782 | pb.finish(); 783 | } 784 | 785 | #[test] 786 | fn test_get_position() { 787 | let pb = ProgressBar::new(1); 788 | pb.set_draw_target(ProgressDrawTarget::hidden()); 789 | pb.inc(2); 790 | let pos = pb.position(); 791 | assert_eq!(pos, 2); 792 | } 793 | 794 | #[test] 795 | fn test_weak_pb() { 796 | let pb = ProgressBar::new(0); 797 | let weak = pb.downgrade(); 798 | assert!(weak.upgrade().is_some()); 799 | ::std::mem::drop(pb); 800 | assert!(weak.upgrade().is_none()); 801 | } 802 | 803 | #[test] 804 | fn it_can_wrap_a_reader() { 805 | let bytes = &b"I am an implementation of io::Read"[..]; 806 | let pb = ProgressBar::new(bytes.len() as u64); 807 | let mut reader = pb.wrap_read(bytes); 808 | let mut writer = Vec::new(); 809 | io::copy(&mut reader, &mut writer).unwrap(); 810 | assert_eq!(writer, bytes); 811 | } 812 | 813 | #[test] 814 | fn it_can_wrap_a_writer() { 815 | let bytes = b"implementation of io::Read"; 816 | let mut reader = &bytes[..]; 817 | let pb = ProgressBar::new(bytes.len() as u64); 818 | let writer = Vec::new(); 819 | let mut writer = pb.wrap_write(writer); 820 | io::copy(&mut reader, &mut writer).unwrap(); 821 | assert_eq!(writer.it, bytes); 822 | } 823 | 824 | #[test] 825 | fn ticker_thread_terminates_on_drop() { 826 | let _guard = TICKER_TEST.lock().unwrap(); 827 | assert!(!TICKER_RUNNING.load(Ordering::SeqCst)); 828 | 829 | let pb = ProgressBar::new_spinner(); 830 | pb.enable_steady_tick(Duration::from_millis(50)); 831 | 832 | // Give the thread time to start up 833 | thread::sleep(Duration::from_millis(250)); 834 | 835 | assert!(TICKER_RUNNING.load(Ordering::SeqCst)); 836 | 837 | drop(pb); 838 | assert!(!TICKER_RUNNING.load(Ordering::SeqCst)); 839 | } 840 | 841 | #[test] 842 | fn ticker_thread_terminates_on_drop_2() { 843 | let _guard = TICKER_TEST.lock().unwrap(); 844 | assert!(!TICKER_RUNNING.load(Ordering::SeqCst)); 845 | 846 | let pb = ProgressBar::new_spinner(); 847 | pb.enable_steady_tick(Duration::from_millis(50)); 848 | let pb2 = pb.clone(); 849 | 850 | // Give the thread time to start up 851 | thread::sleep(Duration::from_millis(250)); 852 | 853 | assert!(TICKER_RUNNING.load(Ordering::SeqCst)); 854 | 855 | drop(pb); 856 | assert!(TICKER_RUNNING.load(Ordering::SeqCst)); 857 | 858 | drop(pb2); 859 | assert!(!TICKER_RUNNING.load(Ordering::SeqCst)); 860 | } 861 | } 862 | -------------------------------------------------------------------------------- /src/rayon.rs: -------------------------------------------------------------------------------- 1 | use rayon::iter::plumbing::{Consumer, Folder, Producer, ProducerCallback, UnindexedConsumer}; 2 | use rayon::iter::{IndexedParallelIterator, ParallelIterator}; 3 | 4 | use crate::{ProgressBar, ProgressBarIter}; 5 | 6 | /// Wraps a Rayon parallel iterator. 7 | /// 8 | /// See [`ProgressIterator`](trait.ProgressIterator.html) for method 9 | /// documentation. 10 | #[cfg_attr(docsrs, doc(cfg(feature = "rayon")))] 11 | pub trait ParallelProgressIterator 12 | where 13 | Self: Sized + ParallelIterator, 14 | { 15 | /// Wrap an iterator with a custom progress bar. 16 | fn progress_with(self, progress: ProgressBar) -> ProgressBarIter; 17 | 18 | /// Wrap an iterator with an explicit element count. 19 | fn progress_count(self, len: u64) -> ProgressBarIter { 20 | self.progress_with(ProgressBar::new(len)) 21 | } 22 | 23 | fn progress(self) -> ProgressBarIter 24 | where 25 | Self: IndexedParallelIterator, 26 | { 27 | let len = u64::try_from(self.len()).unwrap(); 28 | self.progress_count(len) 29 | } 30 | 31 | /// Wrap an iterator with a progress bar and style it. 32 | fn progress_with_style(self, style: crate::ProgressStyle) -> ProgressBarIter 33 | where 34 | Self: IndexedParallelIterator, 35 | { 36 | let len = u64::try_from(self.len()).unwrap(); 37 | let bar = ProgressBar::new(len).with_style(style); 38 | self.progress_with(bar) 39 | } 40 | } 41 | 42 | impl> ParallelProgressIterator for T { 43 | fn progress_with(self, progress: ProgressBar) -> ProgressBarIter { 44 | ProgressBarIter { it: self, progress } 45 | } 46 | } 47 | 48 | impl> IndexedParallelIterator for ProgressBarIter { 49 | fn len(&self) -> usize { 50 | self.it.len() 51 | } 52 | 53 | fn drive>(self, consumer: C) -> >::Result { 54 | let consumer = ProgressConsumer::new(consumer, self.progress); 55 | self.it.drive(consumer) 56 | } 57 | 58 | fn with_producer>( 59 | self, 60 | callback: CB, 61 | ) -> >::Output { 62 | return self.it.with_producer(Callback { 63 | callback, 64 | progress: self.progress, 65 | }); 66 | 67 | struct Callback { 68 | callback: CB, 69 | progress: ProgressBar, 70 | } 71 | 72 | impl> ProducerCallback for Callback { 73 | type Output = CB::Output; 74 | 75 | fn callback

(self, base: P) -> CB::Output 76 | where 77 | P: Producer, 78 | { 79 | let producer = ProgressProducer { 80 | base, 81 | progress: self.progress, 82 | }; 83 | self.callback.callback(producer) 84 | } 85 | } 86 | } 87 | } 88 | 89 | struct ProgressProducer { 90 | base: T, 91 | progress: ProgressBar, 92 | } 93 | 94 | impl> Producer for ProgressProducer

{ 95 | type Item = T; 96 | type IntoIter = ProgressBarIter; 97 | 98 | fn into_iter(self) -> Self::IntoIter { 99 | ProgressBarIter { 100 | it: self.base.into_iter(), 101 | progress: self.progress, 102 | } 103 | } 104 | 105 | fn min_len(&self) -> usize { 106 | self.base.min_len() 107 | } 108 | 109 | fn max_len(&self) -> usize { 110 | self.base.max_len() 111 | } 112 | 113 | fn split_at(self, index: usize) -> (Self, Self) { 114 | let (left, right) = self.base.split_at(index); 115 | ( 116 | ProgressProducer { 117 | base: left, 118 | progress: self.progress.clone(), 119 | }, 120 | ProgressProducer { 121 | base: right, 122 | progress: self.progress, 123 | }, 124 | ) 125 | } 126 | } 127 | 128 | struct ProgressConsumer { 129 | base: C, 130 | progress: ProgressBar, 131 | } 132 | 133 | impl ProgressConsumer { 134 | fn new(base: C, progress: ProgressBar) -> Self { 135 | ProgressConsumer { base, progress } 136 | } 137 | } 138 | 139 | impl> Consumer for ProgressConsumer { 140 | type Folder = ProgressFolder; 141 | type Reducer = C::Reducer; 142 | type Result = C::Result; 143 | 144 | fn split_at(self, index: usize) -> (Self, Self, Self::Reducer) { 145 | let (left, right, reducer) = self.base.split_at(index); 146 | ( 147 | ProgressConsumer::new(left, self.progress.clone()), 148 | ProgressConsumer::new(right, self.progress), 149 | reducer, 150 | ) 151 | } 152 | 153 | fn into_folder(self) -> Self::Folder { 154 | ProgressFolder { 155 | base: self.base.into_folder(), 156 | progress: self.progress, 157 | } 158 | } 159 | 160 | fn full(&self) -> bool { 161 | self.base.full() 162 | } 163 | } 164 | 165 | impl> UnindexedConsumer for ProgressConsumer { 166 | fn split_off_left(&self) -> Self { 167 | ProgressConsumer::new(self.base.split_off_left(), self.progress.clone()) 168 | } 169 | 170 | fn to_reducer(&self) -> Self::Reducer { 171 | self.base.to_reducer() 172 | } 173 | } 174 | 175 | struct ProgressFolder { 176 | base: C, 177 | progress: ProgressBar, 178 | } 179 | 180 | impl> Folder for ProgressFolder { 181 | type Result = C::Result; 182 | 183 | fn consume(self, item: T) -> Self { 184 | self.progress.inc(1); 185 | ProgressFolder { 186 | base: self.base.consume(item), 187 | progress: self.progress, 188 | } 189 | } 190 | 191 | fn complete(self) -> C::Result { 192 | self.base.complete() 193 | } 194 | 195 | fn full(&self) -> bool { 196 | self.base.full() 197 | } 198 | } 199 | 200 | impl> ParallelIterator for ProgressBarIter { 201 | type Item = S; 202 | 203 | fn drive_unindexed>(self, consumer: C) -> C::Result { 204 | let consumer1 = ProgressConsumer::new(consumer, self.progress.clone()); 205 | self.it.drive_unindexed(consumer1) 206 | } 207 | } 208 | 209 | #[cfg(test)] 210 | mod test { 211 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 212 | 213 | use crate::{ParallelProgressIterator, ProgressBar, ProgressBarIter, ProgressStyle}; 214 | 215 | #[test] 216 | fn it_can_wrap_a_parallel_iterator() { 217 | let v = vec![1, 2, 3]; 218 | fn wrap<'a, T: ParallelIterator>(it: ProgressBarIter) { 219 | assert_eq!(it.map(|x| x * 2).collect::>(), vec![2, 4, 6]); 220 | } 221 | 222 | wrap(v.par_iter().progress_count(3)); 223 | wrap({ 224 | let pb = ProgressBar::new(v.len() as u64); 225 | v.par_iter().progress_with(pb) 226 | }); 227 | 228 | wrap({ 229 | let style = ProgressStyle::default_bar() 230 | .template("{wide_bar:.red} {percent}/100%") 231 | .unwrap(); 232 | v.par_iter().progress_with_style(style) 233 | }); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io; 3 | use std::sync::{Arc, OnceLock}; 4 | use std::time::Duration; 5 | #[cfg(not(target_arch = "wasm32"))] 6 | use std::time::Instant; 7 | 8 | use portable_atomic::{AtomicU64, AtomicU8, Ordering}; 9 | #[cfg(target_arch = "wasm32")] 10 | use web_time::Instant; 11 | 12 | use crate::draw_target::{LineType, ProgressDrawTarget}; 13 | use crate::style::ProgressStyle; 14 | 15 | pub(crate) struct BarState { 16 | pub(crate) draw_target: ProgressDrawTarget, 17 | pub(crate) on_finish: ProgressFinish, 18 | pub(crate) style: ProgressStyle, 19 | pub(crate) state: ProgressState, 20 | pub(crate) tab_width: usize, 21 | } 22 | 23 | impl BarState { 24 | pub(crate) fn new( 25 | len: Option, 26 | draw_target: ProgressDrawTarget, 27 | pos: Arc, 28 | ) -> Self { 29 | Self { 30 | draw_target, 31 | on_finish: ProgressFinish::default(), 32 | style: ProgressStyle::default_bar(), 33 | state: ProgressState::new(len, pos), 34 | tab_width: DEFAULT_TAB_WIDTH, 35 | } 36 | } 37 | 38 | /// Finishes the progress bar using the [`ProgressFinish`] behavior stored 39 | /// in the [`ProgressStyle`]. 40 | pub(crate) fn finish_using_style(&mut self, now: Instant, finish: ProgressFinish) { 41 | self.state.status = Status::DoneVisible; 42 | match finish { 43 | ProgressFinish::AndLeave => { 44 | if let Some(len) = self.state.len { 45 | self.state.pos.set(len); 46 | } 47 | } 48 | ProgressFinish::WithMessage(msg) => { 49 | if let Some(len) = self.state.len { 50 | self.state.pos.set(len); 51 | } 52 | self.state.message = TabExpandedString::new(msg, self.tab_width); 53 | } 54 | ProgressFinish::AndClear => { 55 | if let Some(len) = self.state.len { 56 | self.state.pos.set(len); 57 | } 58 | self.state.status = Status::DoneHidden; 59 | } 60 | ProgressFinish::Abandon => {} 61 | ProgressFinish::AbandonWithMessage(msg) => { 62 | self.state.message = TabExpandedString::new(msg, self.tab_width); 63 | } 64 | } 65 | 66 | // There's no need to update the estimate here; once the `status` is no longer 67 | // `InProgress`, we will use the length and elapsed time to estimate. 68 | let _ = self.draw(true, now); 69 | } 70 | 71 | pub(crate) fn reset(&mut self, now: Instant, mode: Reset) { 72 | // Always reset the estimator; this is the only reset that will occur if mode is 73 | // `Reset::Eta`. 74 | self.state.est.reset(now); 75 | 76 | if let Reset::Elapsed | Reset::All = mode { 77 | self.state.started = now; 78 | } 79 | 80 | if let Reset::All = mode { 81 | self.state.pos.reset(now); 82 | self.state.status = Status::InProgress; 83 | 84 | for tracker in self.style.format_map.values_mut() { 85 | tracker.reset(&self.state, now); 86 | } 87 | 88 | let _ = self.draw(false, now); 89 | } 90 | } 91 | 92 | pub(crate) fn update(&mut self, now: Instant, f: impl FnOnce(&mut ProgressState), tick: bool) { 93 | f(&mut self.state); 94 | if tick { 95 | self.tick(now); 96 | } 97 | } 98 | 99 | pub(crate) fn unset_length(&mut self, now: Instant) { 100 | self.state.len = None; 101 | self.update_estimate_and_draw(now); 102 | } 103 | 104 | pub(crate) fn set_length(&mut self, now: Instant, len: u64) { 105 | self.state.len = Some(len); 106 | self.update_estimate_and_draw(now); 107 | } 108 | 109 | pub(crate) fn inc_length(&mut self, now: Instant, delta: u64) { 110 | if let Some(len) = self.state.len { 111 | self.state.len = Some(len.saturating_add(delta)); 112 | } 113 | self.update_estimate_and_draw(now); 114 | } 115 | 116 | pub(crate) fn dec_length(&mut self, now: Instant, delta: u64) { 117 | if let Some(len) = self.state.len { 118 | self.state.len = Some(len.saturating_sub(delta)); 119 | } 120 | self.update_estimate_and_draw(now); 121 | } 122 | 123 | pub(crate) fn set_tab_width(&mut self, tab_width: usize) { 124 | self.tab_width = tab_width; 125 | self.state.message.set_tab_width(tab_width); 126 | self.state.prefix.set_tab_width(tab_width); 127 | self.style.set_tab_width(tab_width); 128 | } 129 | 130 | pub(crate) fn set_style(&mut self, style: ProgressStyle) { 131 | self.style = style; 132 | self.style.set_tab_width(self.tab_width); 133 | } 134 | 135 | pub(crate) fn tick(&mut self, now: Instant) { 136 | self.state.tick = self.state.tick.saturating_add(1); 137 | self.update_estimate_and_draw(now); 138 | } 139 | 140 | pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) { 141 | let pos = self.state.pos.pos.load(Ordering::Relaxed); 142 | self.state.est.record(pos, now); 143 | 144 | for tracker in self.style.format_map.values_mut() { 145 | tracker.tick(&self.state, now); 146 | } 147 | 148 | let _ = self.draw(false, now); 149 | } 150 | 151 | pub(crate) fn println(&mut self, now: Instant, msg: &str) { 152 | let width = self.draw_target.width(); 153 | let mut drawable = match self.draw_target.drawable(true, now) { 154 | Some(drawable) => drawable, 155 | None => return, 156 | }; 157 | 158 | let mut draw_state = drawable.state(); 159 | let lines: Vec = msg.lines().map(|l| LineType::Text(Into::into(l))).collect(); 160 | // Empty msg should trigger newline as we are in println 161 | if lines.is_empty() { 162 | draw_state.lines.push(LineType::Empty); 163 | } else { 164 | draw_state.lines.extend(lines); 165 | } 166 | 167 | if let Some(width) = width { 168 | if !matches!(self.state.status, Status::DoneHidden) { 169 | self.style 170 | .format_state(&self.state, &mut draw_state.lines, width); 171 | } 172 | } 173 | 174 | drop(draw_state); 175 | let _ = drawable.draw(); 176 | } 177 | 178 | pub(crate) fn suspend R, R>(&mut self, now: Instant, f: F) -> R { 179 | if let Some((state, _)) = self.draw_target.remote() { 180 | return state.write().unwrap().suspend(f, now); 181 | } 182 | 183 | if let Some(drawable) = self.draw_target.drawable(true, now) { 184 | let _ = drawable.clear(); 185 | } 186 | 187 | let ret = f(); 188 | let _ = self.draw(true, Instant::now()); 189 | ret 190 | } 191 | 192 | pub(crate) fn draw(&mut self, mut force_draw: bool, now: Instant) -> io::Result<()> { 193 | // `|= self.is_finished()` should not be needed here, but we used to always draw for 194 | // finished progress bars, so it's kept as to not cause compatibility issues in weird cases. 195 | force_draw |= self.state.is_finished(); 196 | let mut drawable = match self.draw_target.drawable(force_draw, now) { 197 | Some(drawable) => drawable, 198 | None => return Ok(()), 199 | }; 200 | 201 | // Getting the width can be expensive; thus this should happen after checking drawable. 202 | let width = drawable.width(); 203 | 204 | let mut draw_state = drawable.state(); 205 | 206 | if let Some(width) = width { 207 | if !matches!(self.state.status, Status::DoneHidden) { 208 | self.style 209 | .format_state(&self.state, &mut draw_state.lines, width); 210 | } 211 | } 212 | 213 | drop(draw_state); 214 | drawable.draw() 215 | } 216 | } 217 | 218 | impl Drop for BarState { 219 | fn drop(&mut self) { 220 | // Progress bar is already finished. Do not need to do anything other than notify 221 | // the `MultiProgress` that we're now a zombie. 222 | if self.state.is_finished() { 223 | self.draw_target.mark_zombie(); 224 | return; 225 | } 226 | 227 | self.finish_using_style(Instant::now(), self.on_finish.clone()); 228 | 229 | // Notify the `MultiProgress` that we're now a zombie. 230 | self.draw_target.mark_zombie(); 231 | } 232 | } 233 | 234 | pub(crate) enum Reset { 235 | Eta, 236 | Elapsed, 237 | All, 238 | } 239 | 240 | /// The state of a progress bar at a moment in time. 241 | #[non_exhaustive] 242 | pub struct ProgressState { 243 | pos: Arc, 244 | len: Option, 245 | pub(crate) tick: u64, 246 | pub(crate) started: Instant, 247 | status: Status, 248 | est: Estimator, 249 | pub(crate) message: TabExpandedString, 250 | pub(crate) prefix: TabExpandedString, 251 | } 252 | 253 | impl ProgressState { 254 | pub(crate) fn new(len: Option, pos: Arc) -> Self { 255 | let now = Instant::now(); 256 | Self { 257 | pos, 258 | len, 259 | tick: 0, 260 | status: Status::InProgress, 261 | started: now, 262 | est: Estimator::new(now), 263 | message: TabExpandedString::NoTabs("".into()), 264 | prefix: TabExpandedString::NoTabs("".into()), 265 | } 266 | } 267 | 268 | /// Indicates that the progress bar finished. 269 | pub fn is_finished(&self) -> bool { 270 | match self.status { 271 | Status::InProgress => false, 272 | Status::DoneVisible => true, 273 | Status::DoneHidden => true, 274 | } 275 | } 276 | 277 | /// Returns the completion as a floating-point number between 0 and 1 278 | pub fn fraction(&self) -> f32 { 279 | let pos = self.pos.pos.load(Ordering::Relaxed); 280 | let pct = match (pos, self.len) { 281 | (_, None) => 0.0, 282 | (_, Some(0)) => 1.0, 283 | (0, _) => 0.0, 284 | (pos, Some(len)) => pos as f32 / len as f32, 285 | }; 286 | pct.clamp(0.0, 1.0) 287 | } 288 | 289 | /// The expected ETA 290 | pub fn eta(&self) -> Duration { 291 | if self.is_finished() { 292 | return Duration::new(0, 0); 293 | } 294 | 295 | let len = match self.len { 296 | Some(len) => len, 297 | None => return Duration::new(0, 0), 298 | }; 299 | 300 | let pos = self.pos.pos.load(Ordering::Relaxed); 301 | 302 | let sps = self.est.steps_per_second(Instant::now()); 303 | 304 | // Infinite duration should only ever happen at the beginning, so in this case it's okay to 305 | // just show an ETA of 0 until progress starts to occur. 306 | if sps == 0.0 { 307 | return Duration::new(0, 0); 308 | } 309 | 310 | secs_to_duration(len.saturating_sub(pos) as f64 / sps) 311 | } 312 | 313 | /// The expected total duration (that is, elapsed time + expected ETA) 314 | pub fn duration(&self) -> Duration { 315 | if self.len.is_none() || self.is_finished() { 316 | return Duration::new(0, 0); 317 | } 318 | self.started.elapsed().saturating_add(self.eta()) 319 | } 320 | 321 | /// The number of steps per second 322 | pub fn per_sec(&self) -> f64 { 323 | if let Status::InProgress = self.status { 324 | self.est.steps_per_second(Instant::now()) 325 | } else { 326 | self.pos() as f64 / self.started.elapsed().as_secs_f64() 327 | } 328 | } 329 | 330 | pub fn elapsed(&self) -> Duration { 331 | self.started.elapsed() 332 | } 333 | 334 | pub fn pos(&self) -> u64 { 335 | self.pos.pos.load(Ordering::Relaxed) 336 | } 337 | 338 | pub fn set_pos(&mut self, pos: u64) { 339 | self.pos.set(pos); 340 | } 341 | 342 | #[allow(clippy::len_without_is_empty)] 343 | pub fn len(&self) -> Option { 344 | self.len 345 | } 346 | 347 | pub fn set_len(&mut self, len: u64) { 348 | self.len = Some(len); 349 | } 350 | } 351 | 352 | #[derive(Debug, PartialEq, Eq, Clone)] 353 | pub(crate) enum TabExpandedString { 354 | NoTabs(Cow<'static, str>), 355 | WithTabs { 356 | original: Cow<'static, str>, 357 | expanded: OnceLock, 358 | tab_width: usize, 359 | }, 360 | } 361 | 362 | impl TabExpandedString { 363 | pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self { 364 | if !s.contains('\t') { 365 | Self::NoTabs(s) 366 | } else { 367 | Self::WithTabs { 368 | original: s, 369 | tab_width, 370 | expanded: OnceLock::new(), 371 | } 372 | } 373 | } 374 | 375 | pub(crate) fn expanded(&self) -> &str { 376 | match &self { 377 | Self::NoTabs(s) => { 378 | debug_assert!(!s.contains('\t')); 379 | s 380 | } 381 | Self::WithTabs { 382 | original, 383 | tab_width, 384 | expanded, 385 | } => expanded.get_or_init(|| original.replace('\t', &" ".repeat(*tab_width))), 386 | } 387 | } 388 | 389 | pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) { 390 | if let Self::WithTabs { 391 | expanded, 392 | tab_width, 393 | .. 394 | } = self 395 | { 396 | if *tab_width != new_tab_width { 397 | *tab_width = new_tab_width; 398 | expanded.take(); 399 | } 400 | } 401 | } 402 | } 403 | 404 | /// Double-smoothed exponentially weighted estimator 405 | /// 406 | /// This uses an exponentially weighted *time-based* estimator, meaning that it exponentially 407 | /// downweights old data based on its age. The rate at which this occurs is currently a constant 408 | /// value of 15 seconds for 90% weighting. This means that all data older than 15 seconds has a 409 | /// collective weight of 0.1 in the estimate, and all data older than 30 seconds has a collective 410 | /// weight of 0.01, and so on. 411 | /// 412 | /// The primary value exposed by `Estimator` is `steps_per_second`. This value is doubly-smoothed, 413 | /// meaning that is the result of using an exponentially weighted estimator (as described above) to 414 | /// estimate the value of another exponentially weighted estimator, which estimates the value of 415 | /// the raw data. 416 | /// 417 | /// The purpose of this extra smoothing step is to reduce instantaneous fluctations in the estimate 418 | /// when large updates are received. Without this, estimates might have a large spike followed by a 419 | /// slow asymptotic approach to zero (until the next spike). 420 | #[derive(Debug)] 421 | pub(crate) struct Estimator { 422 | smoothed_steps_per_sec: f64, 423 | double_smoothed_steps_per_sec: f64, 424 | prev_steps: u64, 425 | prev_time: Instant, 426 | start_time: Instant, 427 | } 428 | 429 | impl Estimator { 430 | fn new(now: Instant) -> Self { 431 | Self { 432 | smoothed_steps_per_sec: 0.0, 433 | double_smoothed_steps_per_sec: 0.0, 434 | prev_steps: 0, 435 | prev_time: now, 436 | start_time: now, 437 | } 438 | } 439 | 440 | fn record(&mut self, new_steps: u64, now: Instant) { 441 | // sanity check: don't record data if time or steps have not advanced 442 | if new_steps <= self.prev_steps || now <= self.prev_time { 443 | // Reset on backwards seek to prevent breakage from seeking to the end for length determination 444 | // See https://github.com/console-rs/indicatif/issues/480 445 | if new_steps < self.prev_steps { 446 | self.prev_steps = new_steps; 447 | self.reset(now); 448 | } 449 | return; 450 | } 451 | 452 | let delta_steps = new_steps - self.prev_steps; 453 | let delta_t = duration_to_secs(now - self.prev_time); 454 | 455 | // the rate of steps we saw in this update 456 | let new_steps_per_second = delta_steps as f64 / delta_t; 457 | 458 | // update the estimate: a weighted average of the old estimate and new data 459 | let weight = estimator_weight(delta_t); 460 | self.smoothed_steps_per_sec = 461 | self.smoothed_steps_per_sec * weight + new_steps_per_second * (1.0 - weight); 462 | 463 | // An iterative estimate like `smoothed_steps_per_sec` is supposed to be an exponentially 464 | // weighted average from t=0 back to t=-inf; Since we initialize it to 0, we neglect the 465 | // (non-existent) samples in the weighted average prior to the first one, so the resulting 466 | // average must be normalized. We normalize the single estimate here in order to use it as 467 | // a source for the double smoothed estimate. See comment on normalization in 468 | // `steps_per_second` for details. 469 | let delta_t_start = duration_to_secs(now - self.start_time); 470 | let total_weight = 1.0 - estimator_weight(delta_t_start); 471 | let normalized_smoothed_steps_per_sec = self.smoothed_steps_per_sec / total_weight; 472 | 473 | // determine the double smoothed value (EWA smoothing of the single EWA) 474 | self.double_smoothed_steps_per_sec = self.double_smoothed_steps_per_sec * weight 475 | + normalized_smoothed_steps_per_sec * (1.0 - weight); 476 | 477 | self.prev_steps = new_steps; 478 | self.prev_time = now; 479 | } 480 | 481 | /// Reset the state of the estimator. Once reset, estimates will not depend on any data prior 482 | /// to `now`. This does not reset the stored position of the progress bar. 483 | pub(crate) fn reset(&mut self, now: Instant) { 484 | self.smoothed_steps_per_sec = 0.0; 485 | self.double_smoothed_steps_per_sec = 0.0; 486 | 487 | // only reset prev_time, not prev_steps 488 | self.prev_time = now; 489 | self.start_time = now; 490 | } 491 | 492 | /// Average time per step in seconds, using double exponential smoothing 493 | fn steps_per_second(&self, now: Instant) -> f64 { 494 | // Because the value stored in the Estimator is only updated when the Estimator receives an 495 | // update, this value will become stuck if progress stalls. To return an accurate estimate, 496 | // we determine how much time has passed since the last update, and treat this as a 497 | // pseudo-update with 0 steps. 498 | let delta_t = duration_to_secs(now - self.prev_time); 499 | let reweight = estimator_weight(delta_t); 500 | 501 | // Normalization of estimates: 502 | // 503 | // The raw estimate is a single value (smoothed_steps_per_second) that is iteratively 504 | // updated. At each update, the previous value of the estimate is downweighted according to 505 | // its age, receiving the iterative weight W(t) = 0.1 ^ (t/15). 506 | // 507 | // Since W(Sum(t_n)) = Prod(W(t_n)), the total weight of a sample after a series of 508 | // iterative steps is simply W(t_e) - W(t_b), where t_e is the time since the end of the 509 | // sample, and t_b is the time since the beginning. The resulting estimate is therefore a 510 | // weighted average with sample weights W(t_e) - W(t_b). 511 | // 512 | // Notice that the weighting function generates sample weights that sum to 1 only when the 513 | // sample times span from t=0 to t=inf; but this is not the case. We have a first sample 514 | // with finite, positive t_b = t_f. In the raw estimate, we handle times prior to t_f by 515 | // setting an initial value of 0, meaning that these (non-existent) samples have no weight. 516 | // 517 | // Therefore, the raw estimate must be normalized by dividing it by the sum of the weights 518 | // in the weighted average. This sum is just W(0) - W(t_f), where t_f is the time since the 519 | // first sample, and W(0) = 1. 520 | let delta_t_start = duration_to_secs(now - self.start_time); 521 | let total_weight = 1.0 - estimator_weight(delta_t_start); 522 | 523 | // Generate updated values for `smoothed_steps_per_sec` and `double_smoothed_steps_per_sec` 524 | // (sps and dsps) without storing them. Note that we normalize sps when using it as a 525 | // source to update dsps, and then normalize dsps itself before returning it. 526 | let sps = self.smoothed_steps_per_sec * reweight / total_weight; 527 | let dsps = self.double_smoothed_steps_per_sec * reweight + sps * (1.0 - reweight); 528 | dsps / total_weight 529 | } 530 | } 531 | 532 | pub(crate) struct AtomicPosition { 533 | pub(crate) pos: AtomicU64, 534 | capacity: AtomicU8, 535 | prev: AtomicU64, 536 | start: Instant, 537 | } 538 | 539 | impl AtomicPosition { 540 | pub(crate) fn new() -> Self { 541 | Self { 542 | pos: AtomicU64::new(0), 543 | capacity: AtomicU8::new(MAX_BURST), 544 | prev: AtomicU64::new(0), 545 | start: Instant::now(), 546 | } 547 | } 548 | 549 | pub(crate) fn allow(&self, now: Instant) -> bool { 550 | if now < self.start { 551 | return false; 552 | } 553 | 554 | let mut capacity = self.capacity.load(Ordering::Acquire); 555 | // `prev` is the number of ns after `self.started` we last returned `true` 556 | let prev = self.prev.load(Ordering::Acquire); 557 | // `elapsed` is the number of ns since `self.started` 558 | let elapsed = (now - self.start).as_nanos() as u64; 559 | // `diff` is the number of ns since we last returned `true` 560 | let diff = elapsed.saturating_sub(prev); 561 | 562 | // If `capacity` is 0 and not enough time (1ms) has passed since `prev` 563 | // to add new capacity, return `false`. The goal of this method is to 564 | // make this decision as efficient as possible. 565 | if capacity == 0 && diff < INTERVAL { 566 | return false; 567 | } 568 | 569 | // We now calculate `new`, the number of INTERVALs since we last returned `true`, 570 | // and `remainder`, which represents a number of ns less than INTERVAL which we cannot 571 | // convert into capacity now, so we're saving it for later. We do this by 572 | // subtracting this from `elapsed` before storing it into `self.prev`. 573 | let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL)); 574 | // We add `new` to `capacity`, subtract one for returning `true` from here, 575 | // then make sure it does not exceed a maximum of `MAX_BURST`. 576 | capacity = Ord::min(MAX_BURST as u128, (capacity as u128) + (new as u128) - 1) as u8; 577 | 578 | // Then, we just store `capacity` and `prev` atomically for the next iteration 579 | self.capacity.store(capacity, Ordering::Release); 580 | self.prev.store(elapsed - remainder, Ordering::Release); 581 | true 582 | } 583 | 584 | fn reset(&self, now: Instant) { 585 | self.set(0); 586 | let elapsed = (now.saturating_duration_since(self.start)).as_nanos() as u64; 587 | self.prev.store(elapsed, Ordering::Release); 588 | } 589 | 590 | pub(crate) fn inc(&self, delta: u64) { 591 | self.pos.fetch_add(delta, Ordering::SeqCst); 592 | } 593 | 594 | pub(crate) fn dec(&self, delta: u64) { 595 | self.pos.fetch_sub(delta, Ordering::SeqCst); 596 | } 597 | 598 | pub(crate) fn set(&self, pos: u64) { 599 | self.pos.store(pos, Ordering::Release); 600 | } 601 | } 602 | 603 | const INTERVAL: u64 = 1_000_000; 604 | const MAX_BURST: u8 = 10; 605 | 606 | /// Behavior of a progress bar when it is finished 607 | /// 608 | /// This is invoked when a [`ProgressBar`] or [`ProgressBarIter`] completes and 609 | /// [`ProgressBar::is_finished`] is false. 610 | /// 611 | /// [`ProgressBar`]: crate::ProgressBar 612 | /// [`ProgressBarIter`]: crate::ProgressBarIter 613 | /// [`ProgressBar::is_finished`]: crate::ProgressBar::is_finished 614 | #[derive(Clone, Debug)] 615 | pub enum ProgressFinish { 616 | /// Finishes the progress bar and leaves the current message 617 | /// 618 | /// Same behavior as calling [`ProgressBar::finish()`](crate::ProgressBar::finish). 619 | AndLeave, 620 | /// Finishes the progress bar and sets a message 621 | /// 622 | /// Same behavior as calling [`ProgressBar::finish_with_message()`](crate::ProgressBar::finish_with_message). 623 | WithMessage(Cow<'static, str>), 624 | /// Finishes the progress bar and completely clears it (this is the default) 625 | /// 626 | /// Same behavior as calling [`ProgressBar::finish_and_clear()`](crate::ProgressBar::finish_and_clear). 627 | AndClear, 628 | /// Finishes the progress bar and leaves the current message and progress 629 | /// 630 | /// Same behavior as calling [`ProgressBar::abandon()`](crate::ProgressBar::abandon). 631 | Abandon, 632 | /// Finishes the progress bar and sets a message, and leaves the current progress 633 | /// 634 | /// Same behavior as calling [`ProgressBar::abandon_with_message()`](crate::ProgressBar::abandon_with_message). 635 | AbandonWithMessage(Cow<'static, str>), 636 | } 637 | 638 | impl Default for ProgressFinish { 639 | fn default() -> Self { 640 | Self::AndClear 641 | } 642 | } 643 | 644 | /// Get the appropriate dilution weight for Estimator data given the data's age (in seconds) 645 | /// 646 | /// Whenever an update occurs, we will create a new estimate using a weight `w_i` like so: 647 | /// 648 | /// ```math 649 | /// = * w_i + * (1 - w_i) 650 | /// ``` 651 | /// 652 | /// In other words, the new estimate is a weighted average of the previous estimate and the new 653 | /// data. We want to choose weights such that for any set of samples where `t_0, t_1, ...` are 654 | /// the durations of the samples: 655 | /// 656 | /// ```math 657 | /// Sum(t_i) = ews ==> Prod(w_i) = 0.1 658 | /// ``` 659 | /// 660 | /// With this constraint it is easy to show that 661 | /// 662 | /// ```math 663 | /// w_i = 0.1 ^ (t_i / ews) 664 | /// ``` 665 | /// 666 | /// Notice that the constraint implies that estimates are independent of the durations of the 667 | /// samples, a very useful feature. 668 | fn estimator_weight(age: f64) -> f64 { 669 | const EXPONENTIAL_WEIGHTING_SECONDS: f64 = 15.0; 670 | 0.1_f64.powf(age / EXPONENTIAL_WEIGHTING_SECONDS) 671 | } 672 | 673 | fn duration_to_secs(d: Duration) -> f64 { 674 | d.as_secs() as f64 + f64::from(d.subsec_nanos()) / 1_000_000_000f64 675 | } 676 | 677 | fn secs_to_duration(s: f64) -> Duration { 678 | let secs = s.trunc() as u64; 679 | let nanos = (s.fract() * 1_000_000_000f64) as u32; 680 | Duration::new(secs, nanos) 681 | } 682 | 683 | #[derive(Debug)] 684 | pub(crate) enum Status { 685 | InProgress, 686 | DoneVisible, 687 | DoneHidden, 688 | } 689 | 690 | pub(crate) const DEFAULT_TAB_WIDTH: usize = 8; 691 | 692 | #[cfg(test)] 693 | mod tests { 694 | use super::*; 695 | use crate::ProgressBar; 696 | 697 | // https://github.com/rust-lang/rust-clippy/issues/10281 698 | #[allow(clippy::uninlined_format_args)] 699 | #[test] 700 | fn test_steps_per_second() { 701 | let test_rate = |items_per_second| { 702 | let mut now = Instant::now(); 703 | let mut est = Estimator::new(now); 704 | let mut pos = 0; 705 | 706 | for _ in 0..20 { 707 | pos += items_per_second; 708 | now += Duration::from_secs(1); 709 | est.record(pos, now); 710 | } 711 | let avg_steps_per_second = est.steps_per_second(now); 712 | 713 | assert!(avg_steps_per_second > 0.0); 714 | assert!(avg_steps_per_second.is_finite()); 715 | 716 | let absolute_error = (avg_steps_per_second - items_per_second as f64).abs(); 717 | let relative_error = absolute_error / items_per_second as f64; 718 | assert!( 719 | relative_error < 1.0 / 1e9, 720 | "Expected rate: {}, actual: {}, relative error: {}", 721 | items_per_second, 722 | avg_steps_per_second, 723 | relative_error 724 | ); 725 | }; 726 | 727 | test_rate(1); 728 | test_rate(1_000); 729 | test_rate(1_000_000); 730 | test_rate(1_000_000_000); 731 | test_rate(1_000_000_001); 732 | test_rate(100_000_000_000); 733 | test_rate(1_000_000_000_000); 734 | test_rate(100_000_000_000_000); 735 | test_rate(1_000_000_000_000_000); 736 | } 737 | 738 | #[test] 739 | fn test_double_exponential_ave() { 740 | let mut now = Instant::now(); 741 | let mut est = Estimator::new(now); 742 | let mut pos = 0; 743 | 744 | // note: this is the default weight set in the Estimator 745 | let weight = 15; 746 | 747 | for _ in 0..weight { 748 | pos += 1; 749 | now += Duration::from_secs(1); 750 | est.record(pos, now); 751 | } 752 | now += Duration::from_secs(weight); 753 | 754 | // The first level EWA: 755 | // -> 90% weight @ 0 eps, 9% weight @ 1 eps, 1% weight @ 0 eps 756 | // -> then normalized by deweighting the 1% weight (before -30 seconds) 757 | let single_target = 0.09 / 0.99; 758 | 759 | // The second level EWA: 760 | // -> same logic as above, but using the first level EWA as the source 761 | let double_target = (0.9 * single_target + 0.09) / 0.99; 762 | assert_eq!(est.steps_per_second(now), double_target); 763 | } 764 | 765 | #[test] 766 | fn test_estimator_rewind_position() { 767 | let mut now = Instant::now(); 768 | let mut est = Estimator::new(now); 769 | 770 | now += Duration::from_secs(1); 771 | est.record(1, now); 772 | 773 | // should not panic 774 | now += Duration::from_secs(1); 775 | est.record(0, now); 776 | 777 | // check that reset occurred (estimator at 1 event per sec) 778 | now += Duration::from_secs(1); 779 | est.record(1, now); 780 | assert_eq!(est.steps_per_second(now), 1.0); 781 | 782 | // check that progress bar handles manual seeking 783 | let pb = ProgressBar::hidden(); 784 | pb.set_length(10); 785 | pb.set_position(1); 786 | pb.tick(); 787 | // Should not panic. 788 | pb.set_position(0); 789 | } 790 | 791 | #[test] 792 | fn test_reset_eta() { 793 | let mut now = Instant::now(); 794 | let mut est = Estimator::new(now); 795 | 796 | // two per second, then reset 797 | now += Duration::from_secs(1); 798 | est.record(2, now); 799 | est.reset(now); 800 | 801 | // now one per second, and verify 802 | now += Duration::from_secs(1); 803 | est.record(3, now); 804 | assert_eq!(est.steps_per_second(now), 1.0); 805 | } 806 | 807 | #[test] 808 | fn test_duration_stuff() { 809 | let duration = Duration::new(42, 100_000_000); 810 | let secs = duration_to_secs(duration); 811 | assert_eq!(secs_to_duration(secs), duration); 812 | } 813 | 814 | #[test] 815 | fn test_atomic_position_large_time_difference() { 816 | let atomic_position = AtomicPosition::new(); 817 | let later = atomic_position.start + Duration::from_nanos(INTERVAL * u64::from(u8::MAX)); 818 | // Should not panic. 819 | atomic_position.allow(later); 820 | } 821 | 822 | #[test] 823 | fn test_atomic_position_reset() { 824 | const ELAPSE_TIME: Duration = Duration::from_millis(20); 825 | let mut pos = AtomicPosition::new(); 826 | pos.reset(pos.start + ELAPSE_TIME); 827 | 828 | // prev should be exactly ELAPSE_TIME after reset 829 | assert_eq!(*pos.pos.get_mut(), 0); 830 | assert_eq!(*pos.prev.get_mut(), ELAPSE_TIME.as_nanos() as u64); 831 | } 832 | } 833 | -------------------------------------------------------------------------------- /src/term_like.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::io; 3 | 4 | use console::Term; 5 | 6 | /// A trait for minimal terminal-like behavior. 7 | /// 8 | /// Anything that implements this trait can be used a draw target via [`ProgressDrawTarget::term_like`]. 9 | /// 10 | /// [`ProgressDrawTarget::term_like`]: crate::ProgressDrawTarget::term_like 11 | pub trait TermLike: Debug + Send + Sync { 12 | /// Return the terminal width 13 | fn width(&self) -> u16; 14 | /// Return the terminal height 15 | fn height(&self) -> u16 { 16 | // FIXME: remove this default impl in the next major version bump 17 | 20 // sensible default 18 | } 19 | 20 | /// Move the cursor up by `n` lines 21 | fn move_cursor_up(&self, n: usize) -> io::Result<()>; 22 | /// Move the cursor down by `n` lines 23 | fn move_cursor_down(&self, n: usize) -> io::Result<()>; 24 | /// Move the cursor right by `n` chars 25 | fn move_cursor_right(&self, n: usize) -> io::Result<()>; 26 | /// Move the cursor left by `n` chars 27 | fn move_cursor_left(&self, n: usize) -> io::Result<()>; 28 | 29 | /// Write a string and add a newline. 30 | fn write_line(&self, s: &str) -> io::Result<()>; 31 | /// Write a string 32 | fn write_str(&self, s: &str) -> io::Result<()>; 33 | /// Clear the current line and reset the cursor to beginning of the line 34 | fn clear_line(&self) -> io::Result<()>; 35 | 36 | fn flush(&self) -> io::Result<()>; 37 | } 38 | 39 | impl TermLike for Term { 40 | fn width(&self) -> u16 { 41 | self.size().1 42 | } 43 | 44 | fn height(&self) -> u16 { 45 | self.size().0 46 | } 47 | 48 | fn move_cursor_up(&self, n: usize) -> io::Result<()> { 49 | self.move_cursor_up(n) 50 | } 51 | 52 | fn move_cursor_down(&self, n: usize) -> io::Result<()> { 53 | self.move_cursor_down(n) 54 | } 55 | 56 | fn move_cursor_right(&self, n: usize) -> io::Result<()> { 57 | self.move_cursor_right(n) 58 | } 59 | 60 | fn move_cursor_left(&self, n: usize) -> io::Result<()> { 61 | self.move_cursor_left(n) 62 | } 63 | 64 | fn write_line(&self, s: &str) -> io::Result<()> { 65 | self.write_line(s) 66 | } 67 | 68 | fn write_str(&self, s: &str) -> io::Result<()> { 69 | self.write_str(s) 70 | } 71 | 72 | fn clear_line(&self) -> io::Result<()> { 73 | self.clear_line() 74 | } 75 | 76 | fn flush(&self) -> io::Result<()> { 77 | self.flush() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/multi-autodrop.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use indicatif::{MultiProgress, ProgressBar}; 5 | 6 | #[test] 7 | fn main() { 8 | let pb = { 9 | let m = MultiProgress::new(); 10 | m.add(ProgressBar::new(10)) 11 | // The MultiProgress is dropped here. 12 | }; 13 | 14 | { 15 | #[allow(clippy::redundant_clone)] 16 | let pb2 = pb.clone(); 17 | for _ in 0..10 { 18 | pb2.inc(1); 19 | thread::sleep(Duration::from_millis(50)); 20 | } 21 | } 22 | 23 | pb.set_message("Done"); 24 | pb.finish(); 25 | 26 | println!("Done with MultiProgress"); 27 | } 28 | --------------------------------------------------------------------------------