progress/src/main.rs
2022-02-11 18:34:29 -06:00

191 lines
6 KiB
Rust

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<usize>) -> 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<usize>) -> 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 [<value>] [--max <max>] [--width <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<usize>,
max: usize,
width: usize,
chars: Chars
}
impl Config {
fn new(mut args: Args) -> Result<Config, String> {
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, '✓'),
})
}
}