use std::{collections::HashMap, fmt::Display, sync::OnceLock}; use tokio::sync::watch; pub static PROGRESS_CENTER: OnceLock = OnceLock::new(); const SPECIAL_FLAG_CLEAR: &str = "_clear"; const SPECIAL_FLAG_CLOSE: &str = "_close"; #[derive(Debug)] pub struct ProgressCenter { tx: watch::Sender>, rx: watch::Receiver>, } /// Initialize `ProgressCenter` early in the program /// /// ``` rust /// use just_progress::progress; /// /// // Initialize /// let progress_center = progress::init(); /// ``` pub fn init() -> &'static ProgressCenter { let (tx, rx) = watch::channel::>(HashMap::new()); PROGRESS_CENTER.get_or_init(|| ProgressCenter { tx, rx }) } /// Bind a callback to ProgressCenter and create a Future for receiving and rendering messages /// /// /// ```rust /// use just_progress::progress; /// /// #[tokio::main] /// async fn main() { /// // Initialize /// let progress_center = progress::init(); /// let bind_future = progress::bind(progress_center, |name, state| { /// println!("Update progress `{}` state to `{}`", name, state); /// }); /// } /// ``` pub fn bind( center: &'static ProgressCenter, mut callback: F, ) -> impl std::future::Future + Send where F: FnMut(String, ProgressState) + Send + 'static, { let mut rx = center.rx.clone(); async move { while rx.changed().await.is_ok() { let state = rx.borrow().clone(); // If it's a close flag, break if state.contains_key(SPECIAL_FLAG_CLOSE) { break; } // Clear flag if state.contains_key(SPECIAL_FLAG_CLEAR) { // Send all known names + progress 0 + EmptyState for (name, _) in state.iter().filter(|(k, _)| *k != SPECIAL_FLAG_CLEAR) { callback( name.clone(), ProgressState { progress: 0.0, info: ProgressInfo::Empty, }, ); } // Clear the entire HashMap let _ = center.tx.send(HashMap::new()); } else { for (name, progress_state) in state { callback(name, progress_state); } } } } } #[derive(Debug, Default, Clone, Copy)] pub struct ProgressState { pub(crate) progress: f32, pub(crate) info: ProgressInfo, } impl ProgressState { /// Get the progress value (0.0 to 1.0) pub fn progress(&self) -> f32 { self.progress } /// Get the progress info pub fn info(&self) -> ProgressInfo { self.info } } impl Display for ProgressState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let space = if !matches!(self.info, ProgressInfo::Empty) { " " } else { "" }; write!(f, "{}{}({:.1}%)", self.info, space, self.progress * 100.0) } } #[derive(Debug, Default, Clone, Copy)] pub enum ProgressInfo { #[default] Empty, Info(&'static str), Warning(&'static str), Error(&'static str), } impl Display for ProgressInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ProgressInfo::Empty => write!(f, ""), ProgressInfo::Info(msg) => write!(f, "Info: `{}`", msg), ProgressInfo::Warning(msg) => write!(f, "Warning: `{}`", msg), ProgressInfo::Error(msg) => write!(f, "Error: `{}`", msg), } } } /// Complete an item /// /// Set an item's progress to 100% so it disappears from progress rendering pub fn complete(name: &str) { update_progress(name, 1.); } /// Increase /// /// Increase a progress item's value by a specified amount (clamped to 1.0) pub fn increase(name: &str, amount: f32) { let Some(center) = PROGRESS_CENTER.get() else { return; }; let mut state = center.tx.borrow().clone(); let entry = state .entry(name.to_string()) .or_insert_with(|| ProgressState { progress: 0.0, info: ProgressInfo::default(), }); entry.progress = (entry.progress + amount).clamp(0.0, 1.0); let _ = center.tx.send(state); } /// Reduce /// /// Reduce a progress item's value by a specified amount (clamped to 0.0) pub fn reduce(name: &str, amount: f32) { let Some(center) = PROGRESS_CENTER.get() else { return; }; let mut state = center.tx.borrow().clone(); let entry = state .entry(name.to_string()) .or_insert_with(|| ProgressState { progress: 0.0, info: ProgressInfo::default(), }); entry.progress = (entry.progress - amount).clamp(0.0, 1.0); let _ = center.tx.send(state); } /// Clear all /// /// Send a message to clear all items, ensuring no progress remains pub fn clear_all() { update(SPECIAL_FLAG_CLEAR, 0., ProgressInfo::Empty); } /// Close the channel /// /// Send a close message to stop bind() from running pub fn close() { update(SPECIAL_FLAG_CLOSE, 0., ProgressInfo::Empty); } /// Update /// /// Update a progress item's information pub fn update(name: &str, progress: f32, info: ProgressInfo) { let Some(center) = PROGRESS_CENTER.get() else { return; }; let mut state = center.tx.borrow().clone(); state.insert(name.to_string(), ProgressState { progress, info }); let _ = center.tx.send(state); } /// Update progress /// /// Modify a progress item's value pub fn update_progress(name: &str, progress: f32) { let Some(center) = PROGRESS_CENTER.get() else { return; }; let mut state = center.tx.borrow().clone(); let entry = state .entry(name.to_string()) .or_insert_with(|| ProgressState { progress: 0.0, info: ProgressInfo::default(), }); entry.progress = progress.clamp(0.0, 1.0); let _ = center.tx.send(state); } /// Update info /// /// Modify a progress item's status information pub fn update_info(name: &str, info: ProgressInfo) { let Some(center) = PROGRESS_CENTER.get() else { return; }; let mut state = center.tx.borrow().clone(); let entry = state .entry(name.to_string()) .or_insert_with(|| ProgressState { progress: 0.0, info: ProgressInfo::default(), }); entry.info = info; let _ = center.tx.send(state); }