hyperfoot 0.2.2

Benchmark the resource footprint of commands
use std::io::{self, IsTerminal, Write};
use std::time::Duration;

use colored::Colorize;

/// Live "N/M runs done" status printed to stderr while runs happen, so a
/// slow benchmark isn't silent for minutes. Stderr (not stdout) so it never
/// ends up in `--export-*` files or piped output. Disabled outright when
/// stderr isn't a terminal (redirected to a file, CI logs) since carriage
/// returns just make those messy.
pub struct Progress {
    enabled: bool,
    label_width: usize,
}

impl Progress {
    pub fn new(commands: &[String]) -> Self {
        Self {
            enabled: io::stderr().is_terminal(),
            label_width: commands
                .iter()
                .map(|c| c.chars().count())
                .max()
                .unwrap_or(0)
                .min(40),
        }
    }

    pub fn warmup(&self, command: &str, position: (usize, usize), current: u32, total: u32) {
        self.render(
            &self.prefix(position),
            command,
            "warmup",
            current,
            total,
            None,
        );
    }

    pub fn run(
        &self,
        command: &str,
        position: (usize, usize),
        current: u32,
        total: u32,
        mean_so_far: Option<Duration>,
    ) {
        self.render(
            &self.prefix(position),
            command,
            "run",
            current,
            total,
            mean_so_far,
        );
    }

    /// Clears the progress line so it doesn't linger above the final report.
    pub fn finish(&self) {
        if self.enabled {
            eprint!("\r\x1b[2K");
            let _ = io::stderr().flush();
        }
    }

    fn prefix(&self, (command_index, total_commands): (usize, usize)) -> String {
        if total_commands > 1 {
            format!("[{}/{}] ", command_index + 1, total_commands)
        } else {
            String::new()
        }
    }

    fn render(
        &self,
        prefix: &str,
        command: &str,
        phase: &str,
        current: u32,
        total: u32,
        mean_so_far: Option<Duration>,
    ) {
        if !self.enabled {
            return;
        }
        let label = truncate(command, self.label_width);
        let eta = mean_so_far
            .filter(|_| current < total)
            .map(|mean| {
                format!(
                    " (~{:.1}s left)",
                    mean.as_secs_f64() * (total - current) as f64
                )
            })
            .unwrap_or_default();
        eprint!(
            "\r\x1b[2K{}{} {} {}/{}{}",
            prefix.dimmed(),
            label.bold(),
            phase.dimmed(),
            current,
            total,
            eta.dimmed()
        );
        let _ = io::stderr().flush();
    }
}

fn truncate(s: &str, max: usize) -> String {
    if max == 0 || s.chars().count() <= max {
        return s.to_string();
    }
    let mut truncated: String = s.chars().take(max.saturating_sub(1)).collect();
    truncated.push('\u{2026}');
    truncated
}