use std::{process::exit, env::Args, io::{stdin, Read}, sync::mpsc, thread, time::Instant}; static DEFAULT_MAX: usize = 100; static DEFAULT_WIDTH: usize = 80; fn main() { let args = std::env::args(); let config = match Config::new(args) { Ok(v ) => v, Err(e) => err(&e) }; if let Some(val) = config.val { let bar = generate_bar(val, config.max, config.width, &config.chars, None); println!("{}", bar); } else { let (tx, rx) = mpsc::sync_channel(256); thread::spawn(move || { let mut stdin = stdin(); let mut out = [0; 1]; loop { if let Ok(count) = stdin.read(&mut out) { tx.send(count); } } }); let mut val = 0; let mut spin = 0; let now = Instant::now(); loop { if let Ok(count) = rx.try_recv() { val += count; } let bar = generate_bar_clamp(val, config.max, config.width, &config.chars, Some(spin)); print!("\r{}", bar); if val >= config.max { break; } spin = ((now.elapsed().as_millis() / 100) % 6) as usize; } } } fn generate_bar(val: usize, max: usize, width: usize, chars: &Chars, spin: Option) -> String { let percent = val * 100 / max; let percent_str = percent.to_string(); let after_bar = if let Some(spin) = spin { format!(" {} {}% {} ", "-".repeat(6 - percent_str.len()), percent_str, if val == max { chars.spin_done() } else { chars.spin(spin) }) } else { format!(" {} {}% ", "-".repeat(6 - percent_str.len()), percent_str) }; let bar_len = width - after_bar.len(); let sep = bar_len * percent / 100; let mut bar = String::new(); bar.push(chars.first(sep > 0)); for i in 1..bar_len-1 { bar.push(chars.mid(sep > i)); } bar.push(chars.last(sep > bar_len - 1)); format!("{}{}", bar, after_bar) } fn generate_bar_clamp(mut val: usize, max: usize, width: usize, chars: &Chars, spin: Option) -> String { if val > max { val = max; } generate_bar(val, max, width, chars, spin) } fn err(msg: &str) -> ! { eprintln!("{}", msg); show_help(); exit(1); } fn show_help() { eprintln!("Usage: progress [] [--max ] [--width ] [--fira]"); } struct Chars { first: (char, char), mid: (char, char), last: (char, char), spin: [char; 6], spin_done: char } impl Chars { fn new(first: (char, char), mid: (char, char), last: (char, char), spin: [char; 6], spin_done: char) -> Self { Self { first, mid, last, spin, spin_done } } fn first(&self, filled: bool) -> char { if filled { self.first.1 } else { self.first.0 } } fn mid(&self, filled: bool) -> char { if filled { self.mid.1 } else { self.mid.0 } } fn last(&self, filled: bool) -> char { if filled { self.last.1 } else { self.last.0 } } fn spin(&self, n: usize) -> char { self.spin[n % 6] } fn spin_done(&self) -> char { self.spin_done } } struct Config { val: Option, max: usize, width: usize, chars: Chars } impl Config { fn new(mut args: Args) -> Result { args.next(); let mut val = None; let mut max = None; let mut width = None; let mut first_char = ('[', '['); let mut mid_char = ('.', '#'); let mut last_char = (']', ']'); let mut spin = ['/', '-', '\\', '/', '-', '\\']; while let Some(next) = args.next() { if next.starts_with("--") { if next == "--fira" { first_char = ('\u{ee00}', '\u{ee03}'); mid_char = ('\u{ee01}', '\u{ee04}'); last_char = ('\u{ee02}', '\u{ee05}'); spin = ['\u{ee06}', '\u{ee07}', '\u{ee08}', '\u{ee09}', '\u{ee0a}', '\u{ee0b}']; } else if next == "--max" { if let Some(_) = max { return Err("can only provide 1 max".to_string()) } let next = match args.next() { Some(v) => v, None => return Err("must provide a max after --max".to_string()) }; let max_result = next.parse(); match max_result { Ok(v) => max = Some(v), Err(_) => return Err("max must be a number".to_string()) } } else if next == "--width" { if let Some(_) = width { return Err("can only provide 1 width".to_string()) } let next = match args.next() { Some(v) => v, None => return Err("must provide a width after --width".to_string()) }; let max_result = next.parse(); match max_result { Ok(v) => width = Some(v), Err(_) => return Err("width must be a number".to_string()) } } else { return Err(format!("unknown option `{}`", next)); } } else { if let Some(_) = val { return Err("can only provide 1 value".to_string()) } let val_result = next.parse(); match val_result { Ok(v) => val = Some(v), Err(_) => return Err("max must be a number".to_string()) } } } let max = max.unwrap_or(DEFAULT_MAX); let width = width.unwrap_or_else(|| termsize::get().map_or(DEFAULT_WIDTH, |v| v.cols.into())); if width < 11 { return Err("window not wide enough".to_string()); } Ok(Config { val, max, width, chars: Chars::new(first_char, mid_char, last_char, spin, '✓'), }) } }