use crate::localization::{self, keys};
use crate::output_prefs::OutputPrefs;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
use std::io::{self, Write};
use std::sync::Mutex;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct StageNumber(u32);
impl StageNumber {
#[must_use]
pub const fn new_unchecked(value: u32) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct LocalizationKey(&'static str);
impl LocalizationKey {
#[must_use]
pub const fn new(value: &'static str) -> Self {
Self(value)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
}
impl From<&'static str> for LocalizationKey {
fn from(s: &'static str) -> Self {
Self::new(s)
}
}
#[path = "status_pipeline.rs"]
mod pipeline;
#[path = "status_timing.rs"]
mod timing;
use pipeline::PIPELINE_STAGE_TOTAL;
pub use pipeline::{PipelineStage, report_pipeline_stage};
pub use timing::VerboseTimingReporter;
fn stage_label(current: StageNumber, total: StageNumber, description: &str) -> String {
localization::message(keys::STATUS_STAGE_LABEL)
.with_arg("current", current.get().to_string())
.with_arg("total", total.get().to_string())
.with_arg("description", description)
.to_string()
}
fn stage_summary(
state_key: LocalizationKey,
current: StageNumber,
total: StageNumber,
description: &str,
) -> String {
let state = localization::message(state_key.as_str()).to_string();
let label = stage_label(current, total, description);
localization::message(keys::STATUS_STAGE_SUMMARY)
.with_arg("state", state)
.with_arg("label", label)
.to_string()
}
fn task_progress_update(current: u32, total: u32, description: &str) -> String {
let task = localization::message(keys::STATUS_TASK_PROGRESS_LABEL)
.with_arg("current", current.to_string())
.with_arg("total", total.to_string())
.to_string();
if description.is_empty() {
return task;
}
localization::message(keys::STATUS_TASK_PROGRESS_UPDATE)
.with_arg("task", task)
.with_arg("description", description)
.to_string()
}
fn format_completion_line(prefs: OutputPrefs, tool_key: LocalizationKey) -> String {
let tool = localization::message(tool_key.as_str());
let prefix = prefs.success_prefix();
let message = localization::message(keys::STATUS_COMPLETE).with_arg("tool", tool);
format!("{prefix} {message}")
}
pub trait StatusReporter {
fn report_stage(&self, current: StageNumber, total: StageNumber, description: &str);
fn report_task_progress(&self, _current: u32, _total: u32, _description: &str) {}
fn report_complete(&self, tool_key: LocalizationKey);
}
pub struct AccessibleReporter<W: Write + Send = io::Stderr> {
prefs: OutputPrefs,
writer: Mutex<W>,
}
impl AccessibleReporter {
#[must_use]
pub fn new(prefs: OutputPrefs) -> Self {
Self {
prefs,
writer: Mutex::new(io::stderr()),
}
}
}
impl<W: Write + Send> AccessibleReporter<W> {
#[must_use]
pub const fn with_writer(prefs: OutputPrefs, writer: W) -> Self {
Self {
prefs,
writer: Mutex::new(writer),
}
}
}
impl<W: Write + Send> StatusReporter for AccessibleReporter<W> {
fn report_stage(&self, current: StageNumber, total: StageNumber, description: &str) {
let prefix = self.prefs.info_prefix();
let message = stage_label(current, total, description);
let mut w = self
.writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
drop(writeln!(w, "{prefix} {message}"));
}
fn report_complete(&self, tool_key: LocalizationKey) {
let line = format_completion_line(self.prefs, tool_key);
let mut w = self
.writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
drop(writeln!(w, "{line}"));
}
fn report_task_progress(&self, current: u32, total: u32, description: &str) {
let message = task_progress_update(current, total, description);
let mut w = self
.writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
drop(writeln!(w, "{}{message}", self.prefs.task_indent()));
}
}
pub struct SilentReporter;
impl StatusReporter for SilentReporter {
fn report_stage(&self, _current: StageNumber, _total: StageNumber, _description: &str) {}
fn report_complete(&self, _tool_key: LocalizationKey) {}
}
#[derive(Debug)]
struct IndicatifState {
progress: MultiProgress,
bars: Vec<ProgressBar>,
descriptions: Vec<String>,
running_index: Option<usize>,
completed: bool,
is_hidden: bool,
force_text_task_updates: bool,
}
const STAGE6_INDEX: usize = (PipelineStage::NinjaSynthesisAndExecution as usize) - 1;
pub struct IndicatifReporter {
prefs: OutputPrefs,
state: Mutex<IndicatifState>,
}
impl IndicatifReporter {
#[must_use]
pub fn new(prefs: OutputPrefs, force_text_task_updates: bool) -> Self {
let progress = MultiProgress::with_draw_target(ProgressDrawTarget::stderr_with_hz(12));
progress.set_move_cursor(false);
let style = ProgressStyle::with_template("{msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner());
let mut bars = Vec::with_capacity(PipelineStage::ALL.len());
let mut descriptions = Vec::with_capacity(PipelineStage::ALL.len());
for stage in PipelineStage::ALL {
let description = stage.description(None);
let current = stage.index();
let bar = progress.add(ProgressBar::new(1));
bar.set_style(style.clone());
bar.set_message(stage_summary(
LocalizationKey::new(keys::STATUS_STATE_PENDING),
current,
PIPELINE_STAGE_TOTAL,
&description,
));
bars.push(bar);
descriptions.push(description);
}
Self {
prefs,
state: Mutex::new(IndicatifState {
is_hidden: progress.is_hidden(),
progress,
bars,
descriptions,
running_index: None,
completed: false,
force_text_task_updates,
}),
}
}
#[must_use]
pub fn with_force_text_task_updates(prefs: OutputPrefs, force_text_task_updates: bool) -> Self {
Self::new(prefs, force_text_task_updates)
}
fn is_stage6_active(state: &IndicatifState) -> bool {
state.running_index == Some(STAGE6_INDEX) && STAGE6_INDEX < state.bars.len()
}
fn set_stage_state(
state: &mut IndicatifState,
index: usize,
status_key: LocalizationKey,
finish_line: bool,
) {
let Some(current_raw) = u32::try_from(index + 1).ok() else {
return;
};
let current = StageNumber::new_unchecked(current_raw);
let description = state.descriptions.get(index).map_or("", String::as_str);
let message = stage_summary(status_key, current, PIPELINE_STAGE_TOTAL, description);
if state.is_hidden {
drop(writeln!(io::stderr(), "{message}"));
return;
}
if let Some(bar) = state.bars.get(index) {
if finish_line {
bar.finish_with_message(message);
} else {
bar.set_message(message);
}
}
}
}
impl Default for IndicatifReporter {
fn default() -> Self {
Self::with_force_text_task_updates(crate::output_prefs::resolve(None), false)
}
}
impl Drop for IndicatifReporter {
fn drop(&mut self) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.completed {
return;
}
if let Some(index) = state.running_index.take() {
Self::set_stage_state(
&mut state,
index,
LocalizationKey::new(keys::STATUS_STATE_FAILED),
true,
);
}
let _ = &state.progress;
}
}
impl StatusReporter for IndicatifReporter {
fn report_stage(&self, current: StageNumber, _total: StageNumber, description: &str) {
let Ok(index) = usize::try_from(current.get().saturating_sub(1)) else {
return;
};
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if index >= state.bars.len() {
return;
}
if let Some(existing) = state.descriptions.get_mut(index) {
description.clone_into(existing);
}
if let Some(previous) = state.running_index
&& previous != index
{
Self::set_stage_state(
&mut state,
previous,
LocalizationKey::new(keys::STATUS_STATE_DONE),
true,
);
}
Self::set_stage_state(
&mut state,
index,
LocalizationKey::new(keys::STATUS_STATE_RUNNING),
false,
);
state.running_index = Some(index);
}
fn report_task_progress(&self, current: u32, total: u32, description: &str) {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !Self::is_stage6_active(&state) {
return;
}
let stage_index = STAGE6_INDEX;
let task = task_progress_update(current, total, description);
if state.is_hidden || state.force_text_task_updates {
drop(writeln!(io::stderr(), "{task}"));
return;
}
let Some(stage_current_raw) = u32::try_from(stage_index + 1).ok() else {
return;
};
let stage_current = StageNumber::new_unchecked(stage_current_raw);
let stage_description = state
.descriptions
.get(stage_index)
.map_or("", String::as_str);
let state_label = localization::message(keys::STATUS_STATE_RUNNING).to_string();
let stage_line = stage_label(stage_current, PIPELINE_STAGE_TOTAL, stage_description);
let message = localization::message(keys::STATUS_STAGE_SUMMARY_WITH_TASK)
.with_arg("state", state_label)
.with_arg("label", stage_line)
.with_arg("task_progress", &task)
.to_string();
if let Some(bar) = state.bars.get(stage_index) {
bar.set_message(message);
}
}
fn report_complete(&self, tool_key: LocalizationKey) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(index) = state.running_index.take() {
Self::set_stage_state(
&mut state,
index,
LocalizationKey::new(keys::STATUS_STATE_DONE),
true,
);
}
state.completed = true;
let _ = &state.progress;
let line = format_completion_line(self.prefs, tool_key);
drop(writeln!(io::stderr(), "{line}"));
}
}
#[cfg(test)]
#[path = "status_tests.rs"]
mod tests;