use std::io::{IsTerminal, Write};
use std::time::Duration;
use crossterm::{
cursor, queue,
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{Clear, ClearType},
};
use tokio::time::Instant;
use crate::args::TesterArgs;
use crate::shutdown::ShutdownSender;
const PROGRESS_BAR_WIDTH: usize = 30;
const PROGRESS_TICK_INTERVAL: Duration = Duration::from_millis(250);
const MIN_TARGET_SECS: u64 = 1;
const MIN_PROGRESS_UNITS: usize = 1;
const MS_PER_SEC: u128 = 1000;
const TENTH_MS: u128 = 100;
const TENTHS_PER_SEC: u128 = 10;
const PERCENT_SCALE: u128 = 10_000;
const PERCENT_DIVISOR: u128 = 100;
pub(crate) fn setup_progress_indicator(
args: &TesterArgs,
run_start: Instant,
shutdown_tx: &ShutdownSender,
) -> tokio::task::JoinHandle<()> {
let mut shutdown_rx = shutdown_tx.subscribe();
let target_secs = args.target_duration.get();
let goal = usize::try_from(target_secs.max(MIN_TARGET_SECS)).unwrap_or(MIN_PROGRESS_UNITS);
let style = ProgressStyle::new(PROGRESS_BAR_WIDTH);
let no_color = args.no_color;
tokio::spawn(async move {
if !std::io::stderr().is_terminal() {
return;
}
let mut ticker = tokio::time::interval(PROGRESS_TICK_INTERVAL);
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
let elapsed_ms = u128::from(target_secs).saturating_mul(MS_PER_SEC);
if render_progress_line(&style, goal, elapsed_ms, no_color).is_err() {
break;
}
if finish_progress_line().is_err() {
break;
}
break;
}
_ = ticker.tick() => {
let elapsed_ms = run_start.elapsed().as_millis();
if render_progress_line(&style, goal, elapsed_ms, no_color).is_err() {
break;
}
}
}
}
})
}
fn render_progress_line(
style: &ProgressStyle,
goal: usize,
elapsed_ms: u128,
no_color: bool,
) -> Result<(), std::io::Error> {
let elapsed_secs = elapsed_ms.checked_div(MS_PER_SEC).unwrap_or(0);
let current = usize::try_from(elapsed_secs).unwrap_or(goal);
let current = current.min(goal);
let line = build_progress_line(style, current, goal, elapsed_ms, no_color);
let mut out = std::io::stderr();
queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine))?;
for segment in line {
if no_color {
queue!(out, Print(&segment.text))?;
} else if let Some(color) = segment.color {
queue!(
out,
SetForegroundColor(color),
Print(&segment.text),
ResetColor
)?;
} else {
queue!(out, Print(&segment.text))?;
}
}
out.flush()?;
Ok(())
}
fn finish_progress_line() -> Result<(), std::io::Error> {
let mut out = std::io::stderr();
out.write_all(b"\n")?;
out.flush()?;
Ok(())
}
fn build_progress_line(
style: &ProgressStyle,
current: usize,
goal: usize,
elapsed_ms: u128,
no_color: bool,
) -> Vec<ProgressSegment> {
let size = style.size.max(MIN_PROGRESS_UNITS);
let goal = goal.max(MIN_PROGRESS_UNITS);
let current = current.min(goal);
let current_u128 = u128::from(u64::try_from(current).unwrap_or(u64::MAX));
let size_u128 = u128::from(u64::try_from(size).unwrap_or(u64::MAX));
let goal_u128 = u128::from(u64::try_from(goal).unwrap_or(u64::MAX));
let scaled = current_u128
.saturating_mul(size_u128)
.checked_div(goal_u128)
.unwrap_or(0);
let complete_size = usize::try_from(scaled).unwrap_or(size).min(size);
let incomplete_size = size.saturating_sub(complete_size);
let percent_x100 = current_u128
.saturating_mul(PERCENT_SCALE)
.checked_div(goal_u128)
.unwrap_or(0);
let percent_whole = percent_x100.checked_div(PERCENT_DIVISOR).unwrap_or(0);
let percent_frac = percent_x100.checked_rem(PERCENT_DIVISOR).unwrap_or(0);
let percent_text = format!(" {}.{:02}%", percent_whole, percent_frac);
let elapsed_tenths = elapsed_ms.checked_div(TENTH_MS).unwrap_or(0);
let secs = elapsed_tenths.checked_div(TENTHS_PER_SEC).unwrap_or(0);
let tenths = elapsed_tenths.checked_rem(TENTHS_PER_SEC).unwrap_or(0);
let time_text = format!(" | {}.{}s / {}s", secs, tenths, goal);
let progress_bar = format!(
"{}{}{}{}",
style.begin,
style.fill.repeat(complete_size),
style.empty.repeat(incomplete_size),
style.end
);
if no_color {
vec![
ProgressSegment::plain(progress_bar),
ProgressSegment::plain(percent_text),
ProgressSegment::plain(time_text),
]
} else {
vec![
ProgressSegment::plain(progress_bar),
ProgressSegment::colored(percent_text, Color::Cyan),
ProgressSegment::colored(time_text, Color::Yellow),
]
}
}
struct ProgressStyle {
size: usize,
begin: String,
end: String,
fill: String,
empty: String,
}
impl ProgressStyle {
fn new(size: usize) -> Self {
Self {
size,
begin: "[".to_owned(),
end: "]".to_owned(),
fill: "#".to_owned(),
empty: "-".to_owned(),
}
}
}
struct ProgressSegment {
text: String,
color: Option<Color>,
}
impl ProgressSegment {
const fn plain(text: String) -> Self {
Self { text, color: None }
}
const fn colored(text: String, color: Color) -> Self {
Self {
text,
color: Some(color),
}
}
}