use std::collections::HashMap;
use std::io::{self, Write};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepState {
Pending,
Running {
progress: u8,
message: String,
started_at: Instant,
},
Completed {
duration: Duration,
},
Failed {
error: String,
duration: Duration,
},
Skipped {
reason: String,
},
}
impl StepState {
pub fn symbol(&self) -> &'static str {
match self {
Self::Pending => "⏳",
Self::Running { .. } => "▶",
Self::Completed { .. } => "✓",
Self::Failed { .. } => "✗",
Self::Skipped { .. } => "⊘",
}
}
pub fn text(&self) -> &'static str {
match self {
Self::Pending => "PENDING",
Self::Running { .. } => "RUNNING",
Self::Completed { .. } => "COMPLETE",
Self::Failed { .. } => "FAILED",
Self::Skipped { .. } => "SKIPPED",
}
}
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed { .. } | Self::Failed { .. } | Self::Skipped { .. }
)
}
pub fn progress(&self) -> u8 {
match self {
Self::Pending => 0,
Self::Running { progress, .. } => *progress,
Self::Completed { .. } => 100,
Self::Failed { .. } => 0,
Self::Skipped { .. } => 0,
}
}
}
#[derive(Debug, Clone)]
pub struct StepInfo {
pub id: String,
pub name: String,
pub state: StepState,
pub index: usize,
}
impl StepInfo {
pub fn new(id: &str, name: &str, index: usize) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
state: StepState::Pending,
index,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProgressStyle {
Minimal,
#[default]
Standard,
Verbose,
Quiet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExecutionMode {
#[default]
Normal,
DryRun,
Hermetic,
Test,
}
impl ExecutionMode {
pub fn label(&self) -> &'static str {
match self {
Self::Normal => "NORMAL",
Self::DryRun => "DRY-RUN",
Self::Hermetic => "HERMETIC",
Self::Test => "TEST",
}
}
}
#[derive(Debug)]
pub struct InstallerProgress {
name: String,
version: String,
steps: Vec<StepInfo>,
step_index: HashMap<String, usize>,
started_at: Instant,
checkpoint: Option<String>,
mode: ExecutionMode,
artifacts_verified: usize,
artifacts_total: usize,
signatures_verified: bool,
trace_recording: bool,
style: ProgressStyle,
}
impl InstallerProgress {
pub fn new(name: &str, version: &str) -> Self {
Self {
name: name.to_string(),
version: version.to_string(),
steps: Vec::new(),
step_index: HashMap::new(),
started_at: Instant::now(),
checkpoint: None,
mode: ExecutionMode::default(),
artifacts_verified: 0,
artifacts_total: 0,
signatures_verified: false,
trace_recording: false,
style: ProgressStyle::default(),
}
}
pub fn with_style(mut self, style: ProgressStyle) -> Self {
self.style = style;
self
}
pub fn with_mode(mut self, mode: ExecutionMode) -> Self {
self.mode = mode;
self
}
pub fn with_artifacts(mut self, verified: usize, total: usize) -> Self {
self.artifacts_verified = verified;
self.artifacts_total = total;
self
}
pub fn with_signatures(mut self, verified: bool) -> Self {
self.signatures_verified = verified;
self
}
pub fn with_trace(mut self, recording: bool) -> Self {
self.trace_recording = recording;
self
}
pub fn add_step(&mut self, id: &str, name: &str) {
let index = self.steps.len() + 1;
self.step_index.insert(id.to_string(), self.steps.len());
self.steps.push(StepInfo::new(id, name, index));
}
pub fn get_step(&self, id: &str) -> Option<&StepInfo> {
self.step_index.get(id).and_then(|&i| self.steps.get(i))
}
fn get_step_mut(&mut self, id: &str) -> Option<&mut StepInfo> {
if let Some(&i) = self.step_index.get(id) {
self.steps.get_mut(i)
} else {
None
}
}
pub fn start_step(&mut self, id: &str, message: &str) {
if let Some(step) = self.get_step_mut(id) {
step.state = StepState::Running {
progress: 0,
message: message.to_string(),
started_at: Instant::now(),
};
}
}
pub fn update_step(&mut self, id: &str, progress: u8, message: &str) {
if let Some(step) = self.get_step_mut(id) {
if let StepState::Running { started_at, .. } = &step.state {
step.state = StepState::Running {
progress: progress.min(100),
message: message.to_string(),
started_at: *started_at,
};
}
}
}
pub fn complete_step(&mut self, id: &str) {
if let Some(step) = self.get_step_mut(id) {
let duration = if let StepState::Running { started_at, .. } = &step.state {
started_at.elapsed()
} else {
Duration::ZERO
};
step.state = StepState::Completed { duration };
self.checkpoint = Some(id.to_string());
}
}
pub fn fail_step(&mut self, id: &str, error: &str) {
if let Some(step) = self.get_step_mut(id) {
let duration = if let StepState::Running { started_at, .. } = &step.state {
started_at.elapsed()
} else {
Duration::ZERO
};
step.state = StepState::Failed {
error: error.to_string(),
duration,
};
}
}
pub fn skip_step(&mut self, id: &str, reason: &str) {
if let Some(step) = self.get_step_mut(id) {
step.state = StepState::Skipped {
reason: reason.to_string(),
};
}
}
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
pub fn completed_count(&self) -> usize {
self.steps
.iter()
.filter(|s| matches!(s.state, StepState::Completed { .. }))
.count()
}
pub fn failed_count(&self) -> usize {
self.steps
.iter()
.filter(|s| matches!(s.state, StepState::Failed { .. }))
.count()
}
pub fn skipped_count(&self) -> usize {
self.steps
.iter()
.filter(|s| matches!(s.state, StepState::Skipped { .. }))
.count()
}
pub fn estimated_remaining(&self) -> Option<Duration> {
let completed = self.completed_count();
if completed == 0 {
return None;
}
let remaining = self
.steps
.len()
.saturating_sub(completed + self.skipped_count());
if remaining == 0 {
return Some(Duration::ZERO);
}
let elapsed = self.elapsed();
let avg_per_step = elapsed / completed as u32;
Some(avg_per_step * remaining as u32)
}
pub fn is_complete(&self) -> bool {
self.steps.iter().all(|s| s.state.is_terminal())
}
pub fn has_failures(&self) -> bool {
self.steps
.iter()
.any(|s| matches!(s.state, StepState::Failed { .. }))
}
pub fn steps(&self) -> &[StepInfo] {
&self.steps
}
pub fn total_steps(&self) -> usize {
self.steps.len()
}
}
pub trait ProgressRenderer {
fn render_header(&self, progress: &InstallerProgress) -> String;
fn render_step(&self, step: &StepInfo, total: usize) -> String;
fn render_footer(&self, progress: &InstallerProgress) -> String;
fn render(&self, progress: &InstallerProgress) -> String {
let mut output = self.render_header(progress);
for step in progress.steps() {
output.push_str(&self.render_step(step, progress.total_steps()));
}
output.push_str(&self.render_footer(progress));
output
}
}
#[derive(Debug, Default)]
pub struct TerminalRenderer {
width: usize,
}
impl TerminalRenderer {
pub fn new() -> Self {
Self { width: 80 }
}
pub fn with_width(width: usize) -> Self {
Self { width }
}
fn format_duration(d: Duration) -> String {
let secs = d.as_secs();
if secs >= 60 {
format!("{}m {:02}s", secs / 60, secs % 60)
} else if secs > 0 {
format!("{}.{:02}s", secs, d.subsec_millis() / 10)
} else {
format!("{}ms", d.as_millis())
}
}
fn progress_bar(&self, progress: u8, width: usize) -> String {
let filled = (progress as usize * width) / 100;
let empty = width.saturating_sub(filled);
let filled_char = '━';
let partial_char = if progress < 100 && filled < width {
'╸'
} else {
filled_char
};
let empty_char = '━';
if progress >= 100 {
filled_char.to_string().repeat(width)
} else if filled > 0 {
format!(
"{}{}{}",
filled_char.to_string().repeat(filled.saturating_sub(1)),
partial_char,
empty_char.to_string().repeat(empty)
)
} else {
empty_char.to_string().repeat(width)
}
}
}
impl ProgressRenderer for TerminalRenderer {
fn render_header(&self, progress: &InstallerProgress) -> String {
let line = "═".repeat(self.width);
format!("{} v{}\n{}\n\n", progress.name, progress.version, line)
}
}
include!("progress_part2_incl2.rs");