use std::sync::{Arc, Mutex};
use std::time::Duration;
use colored::Colorize;
use comfy_table::{ContentArrangement, Table};
use indicatif::{ProgressBar, ProgressStyle};
use crate::steps::{ProgressReporter, StepOutput, StepStatus};
use crate::utils::sanitize_message;
pub struct OutputManager {
active_spinner: Arc<Mutex<Option<ProgressBar>>>,
interactive: bool,
to_stderr: bool,
}
impl OutputManager {
pub fn new() -> Self {
Self {
active_spinner: Arc::new(Mutex::new(None)),
interactive: console::Term::stderr().is_term(),
to_stderr: false,
}
}
pub fn stderr() -> Self {
Self {
to_stderr: true,
..Self::new()
}
}
fn line(&self, content: String) {
if self.to_stderr {
eprintln!("{}", content);
} else {
println!("{}", content);
}
}
pub fn progress_reporter(&self) -> Arc<dyn ProgressReporter> {
Arc::new(SpinnerProgress {
active_spinner: self.active_spinner.clone(),
})
}
fn start_spinner(&self, message: String) {
if !self.interactive {
return;
}
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::with_template(" {spinner:.cyan} {msg}")
.expect("static spinner template is valid"),
);
spinner.set_message(message);
spinner.enable_steady_tick(Duration::from_millis(80));
if let Ok(mut guard) = self.active_spinner.lock() {
if let Some(old) = guard.take() {
old.finish_and_clear();
}
*guard = Some(spinner);
}
}
fn clear_spinner(&self) {
if let Ok(mut guard) = self.active_spinner.lock() {
if let Some(spinner) = guard.take() {
spinner.finish_and_clear();
}
}
}
pub fn section(&self, title: &str) {
self.clear_spinner();
self.line(format!("\n{}", format!("▸ {}", title).bold().cyan()));
}
pub fn step_status(&self, name: &str, status: &str) {
if self.interactive {
self.start_spinner(format!("{} {}", name, status));
} else {
self.line(format!(" {} {}", name.bold(), status.dimmed()));
}
}
pub fn step_ok(&self, name: &str) {
self.clear_spinner();
self.line(format!(" {} {}", "✓".green().bold(), name));
}
pub fn step_done(&self, name: &str, output: &StepOutput) {
self.clear_spinner();
let icon = match output.status {
StepStatus::Success => "✓".green().bold(),
StepStatus::Skipped => "⊘".yellow().bold(),
StepStatus::Failed => "✗".red().bold(),
};
let timing = format!("({}ms)", output.duration_ms).dimmed();
self.line(format!(
" {} {} {} {}",
icon,
name.bold(),
output.message,
timing
));
if let Some(ref details) = output.dry_run_details {
self.print_dry_run_details(details);
}
}
fn print_dry_run_details(&self, details: &crate::steps::DryRunDetails) {
for change in &details.file_changes {
let op_icon = match change.operation {
crate::steps::FileOperation::Create => "+",
crate::steps::FileOperation::Modify => "~",
crate::steps::FileOperation::Delete => "-",
};
self.line(format!(
" {} {} {} {}",
"├─".dimmed(),
op_icon.yellow(),
change.path.dimmed(),
format!("({:?})", change.operation).dimmed()
));
if let Some(ref diff) = change.diff {
for line in diff.lines() {
self.line(format!(" {} {}", "│".dimmed(), line.cyan()));
}
}
}
if let Some(ref docker) = details.docker_preview {
self.line(format!(
" {} {} {}",
"├─".dimmed(),
"📦".to_string().yellow(),
format!("Docker image: {}", docker.image_name).dimmed()
));
self.line(format!(
" {} {} {}",
"│".dimmed(),
"🏷️".dimmed(),
format!("Tags: {}", docker.tags.join(", ")).dimmed()
));
if let Some(layers) = docker.layers_estimate {
self.line(format!(
" {} {} {}",
"│".dimmed(),
"📚".dimmed(),
format!("Estimated layers: {}", layers).dimmed()
));
}
}
for note in &details.notes {
self.line(format!(
" {} {} {}",
"├─".dimmed(),
"ℹ".dimmed(),
note.dimmed()
));
}
}
pub fn step_fail(&self, name: &str, error: &str) {
self.clear_spinner();
let safe_error = sanitize_message(error);
self.line(format!(
" {} {} {}",
"✗".red().bold(),
name.bold(),
safe_error.red()
));
}
pub fn blank_line(&self) {
self.clear_spinner();
self.line(String::new());
}
pub fn summary_table(&self, outputs: &[(&str, &StepOutput)]) {
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec!["Step", "Status", "Duration", "Message"]);
for (name, out) in outputs {
table.add_row(vec![
name.to_string(),
out.status.to_string(),
format!("{}ms", out.duration_ms),
out.message.clone(),
]);
}
self.line(format!("{table}"));
}
pub fn success(&self, msg: &str) {
self.line(format!("\n{}", format!(" ✓ {}", msg).green().bold()));
}
pub fn error(&self, msg: &str) {
let safe_msg = sanitize_message(msg);
self.line(format!("\n{}", format!(" ✗ {}", safe_msg).red().bold()));
}
pub fn info(&self, msg: &str) {
self.line(format!(" {}", msg));
}
pub fn warn(&self, msg: &str) {
self.line(format!(" {}", format!("⚠ {}", msg).yellow()));
}
}
impl Default for OutputManager {
fn default() -> Self {
Self::new()
}
}
struct SpinnerProgress {
active_spinner: Arc<Mutex<Option<ProgressBar>>>,
}
impl ProgressReporter for SpinnerProgress {
fn set_message(&self, message: &str) {
if let Ok(guard) = self.active_spinner.lock() {
if let Some(ref spinner) = *guard {
let one_line = message.lines().next().unwrap_or("");
let truncated: String = one_line.chars().take(120).collect();
spinner.set_message(truncated);
}
}
}
}