Creating a simple progress bar

Hi,
This is my very first day of Rust (hope it is not going to be my last :slight_smile: )
I am trying to create a very simple progress bar. (I set myself some small challenges like this one to learn the language)

Below is the code I wrote.

use std::time::Duration;
use std::thread::sleep;
fn main() {
  //let pbstr = "\u{25A0}".repeat(20).to_string();
  let pbstr = "=".repeat(20).to_string();
  let pbwid = "-".repeat(20).to_string();
  let mut perc;
  let mut lpad;
  for k in 1..4 {
    for i in 1..21 {
      perc = (i as f64)/20.0;
      lpad = (perc * 20.0).floor();
      sleep(Duration::from_millis(100));
      print!("\r Processing data {} of 3: [{}{}]{}%",k,&pbstr[0..(lpad.trunc() as usize)], &pbwid[0..((20.0-lpad).trunc() as usize)],(perc*100.0).trunc());
    }
    print!("\n");
  }
}

Question:
1/ When I run the following code, I don't see the bars moving. Any idea what I did wrong?
2/ If I want to use let pbstr = "\u{25A0}".repeat(20).to_string(); instead of the other line, my code does not work. I get the following error:
"thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '■' (bytes 0..3)"
Any idea what I would need to do to correct that?
3/ In C I also have 2 additional lines:
printf("\e[?25l");
printf("\e[?25h");
before and after the main for loop to avoid seeing the cursor move all the time. What is the equivalent code in Rust?

I can post the equivalent C code if that helps.
I know that some Rust package have implemented progress bars so it is more a learning exercise for me. Please feel free to correct my code if you notice anything bad.
Thank you
Kind regards.

1/ Seems to be line buffering. Throwing in a std::io::stdout().flush().unwrap(); helps.

1 Like

2/ This is due to what &pbstr[0..n] does. A string in Rust is always UTF-8 encoded which is a variable-length encoding where everything except ASCII characters will take up more than one byte per character. Slicing of strings is based on byte indices however (anything else couldn’t be implemented as a constant-time operation; by the way, this is also (one of) the reason(s) why strings don’t support indexing), and in that case it’s illegal to slice up a string in the middle of a character.

You could solve that e.g. by factoring in the length of '\u{25A0}', which can be determined with the .len_utf8() method

use std::io::{stdout, Write};
use std::thread::sleep;
use std::time::Duration;
fn main() {
    let pbstr = "\u{25A0}".repeat(20).to_string();
    let pbwid = "-".repeat(20).to_string();
    let mut perc;
    let mut lpad;
    for k in 1..4 {
        for i in 1..21 {
            perc = (i as f64) / 20.0;
            lpad = (perc * 20.0).floor();
            sleep(Duration::from_millis(100));
            print!(
                "\r Processing data {} of 3: [{}{}]{}%",
                k,
                &pbstr[0..'\u{25A0}'.len_utf8()*(lpad.trunc() as usize)],
                &pbwid[0..((20.0 - lpad).trunc() as usize)],
                (perc * 100.0).trunc()
            );
            stdout().flush().unwrap();
        }
        print!("\n");
    }
}
1 Like

3/ That’s a terminal thing, so these codes should work the same in Rust. The character that the escape sequence \e produces in C is ESC, so ASCII number 27, which in hex is 1b. So you can write print!("\x1b[?25l"); in Rust. Note that when the program is terminated by e.g. ctrl+c, the terminal is not instructed to make the cursor visible again. For that you’d need to set up a handler. You could e.g. use the ctrlc crate for an easy start:

use std::io::{stdout, Write};
use std::thread::sleep;
use std::{process::exit, time::Duration};
fn main() {
    let pbstr = "\u{25A0}".repeat(20).to_string();
    let pbwid = "-".repeat(20).to_string();
    let mut perc;
    let mut lpad;
    print!("\x1b[?25l");
    ctrlc::set_handler(|| {
        println!("\x1b[?25h");
        exit(130)
    })
    .expect("Failed setting up ctrl-c handler");

    for k in 1..4 {
        for i in 1..21 {
            perc = (i as f64) / 20.0;
            lpad = (perc * 20.0).floor();
            sleep(Duration::from_millis(100));
            print!(
                "\r Processing data {} of 3: [{}{}]{}%",
                k,
                &pbstr[0..'\u{25A0}'.len_utf8() * (lpad.trunc() as usize)],
                &pbwid[0..((20.0 - lpad).trunc() as usize)],
                (perc * 100.0).trunc()
            );
            stdout().flush().unwrap();
        }
        print!("\n");
    }
    print!("\x1b[?25h");
}

and in Cargo.toml

[dependencies]
ctrlc = "3.1.9"
1 Like

FYI you can use e.g. crossterm::cursor instead of using ANSI sequences manually. One of the advantages is that your code will be cross-platform.