内置使用实例一:TuiReporter

vielpork提供了一个内置的TUI报告器TuiReporter,用于在终端中显示下载任务的进度。TuiReporter实现了ProgressReporterResultReporter两个trait,用于向外部报告下载任务的进度和结果。

TuiReporter使用indicatif库来显示下载任务的进度。在下载任务开始时,TuiReporter会创建一个新的进度条,并在下载任务结束时更新进度条的状态。TuiReporter还会在下载任务结束时向外部报告下载任务的结果。


#![allow(unused)]

fn main() {
use crate::error::Result;
use crate::base::structs::DownloadProgress;
use crate::base::enums::{DownloadResult, OperationType};
use crate::base::traits::{ProgressReporter, ResultReporter};
use async_trait::async_trait;


#[cfg(feature = "tui")]
use indicatif::{ProgressBar, ProgressStyle,ProgressDrawTarget,MultiProgress};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

const MAX_CONCURRENT_BARS: usize = 10 ;

// TUI 实现
#[cfg(feature = "tui")]
#[derive(Debug)]
pub struct TuiReporter {
    mp: MultiProgress,
    bars: Arc<Mutex<HashMap<u32, ProgressBar>>>,
}

impl TuiReporter {
    pub fn new() -> Self {
        let mp = Self::setup_global_progress();
        Self {
            mp,
            bars: Arc::new(Mutex::new(HashMap::new())),
        }
    }
    fn setup_global_progress() -> MultiProgress {
        let mp = MultiProgress::new();
        mp.set_draw_target(ProgressDrawTarget::stdout());
        mp
    }

    // 私有方法用于获取或创建进度条
    async fn get_or_create_bar(&self, task_id: u32, total: u64) -> ProgressBar {
        let mut bars = self.bars.lock().await;
        
        if bars.len() >= MAX_CONCURRENT_BARS {
            bars.retain(|_, bar| !bar.is_finished());
        }

        bars.entry(task_id)
            .or_insert_with(|| {
                let bar = ProgressBar::new(total);
                // 关键修改:将进度条添加到 MultiProgress 系统
                let bar = self.mp.add(bar); // 这行是核心修改
                bar.set_style(ProgressStyle::with_template(&format!(
                    "{{spinner:.green}} [{{bar:.cyan/blue}}] {{bytes}}/{{total_bytes}} ({}) {{msg}}",
                    task_id
                ))
                .unwrap_or(ProgressStyle::default_bar())
                .progress_chars("#>-"));
                bar
            });
        
        bars.get(&task_id).unwrap_or(&ProgressBar::hidden()).clone()
    }
}


#[async_trait]
impl ProgressReporter for TuiReporter {
    async fn start_task(&self, task_id: u32, total: u64) -> Result<()> {
        let bar = self.get_or_create_bar(task_id, total).await;
        bar.set_message("Downloading...");
        Ok(())
    }

    async fn update_progress(&self, task_id: u32, progress: &DownloadProgress) -> Result<()> {
        let bar = self.get_or_create_bar(task_id, progress.total_bytes).await;
        bar.set_position(progress.bytes_downloaded);

        let speed = if progress.rate > 1_000_000.0 {
            format!("{:.2} MB/s", progress.rate / 1_000_000.0)
        } else if progress.rate > 1_000.0 {
            format!("{:.2} KB/s", progress.rate / 1_000.0)
        } else {
            format!("{:.0} B/s", progress.rate)
        };

        // 格式化剩余时间
        let eta = if progress.remaining_time.as_secs() > 60 {
            format!("{}m {}s", 
                progress.remaining_time.as_secs() / 60,
                progress.remaining_time.as_secs() % 60)
        } else {
            format!("{}s", progress.remaining_time.as_secs())
        };
        
        bar.set_message(format!("Speed: {} | ETA: {}", speed,eta));

        
        Ok(())
    }

    async fn finish_task(&self, task_id: u32,result: DownloadResult) -> Result<()> {
        let mut bars = self.bars.lock().await;
        if let Some(bar) = bars.remove(&task_id) {
            match result {
                DownloadResult::Success { path ,duration ,.. } => {
                    // 条的颜色变成绿色,还是#>-
                    bar.set_style(ProgressStyle::with_template(&format!(
                        "{{spinner:.green}} [{{bar:.green/blue}}] {{bytes}}/{{total_bytes}} ({}): {{msg}}",
                        task_id
                    ))?.progress_chars("#>-"));
                    let success_message = format!("✅ Done in {}s, saved to {}",duration.as_secs(),path.display());
                    bar.finish_with_message(success_message)
                },
                DownloadResult::Failed { error,.. } => {
                    // 条变成红色
                    bar.set_style(ProgressStyle::default_bar().template(&format!(
                        "{{spinner:.red}} [{{bar:.red/blue}}] {{bytes}}/{{total_bytes}} ({}): {{msg}}",
                        task_id
                    ))?.progress_chars("#>-"));
                    let error_message = format!("❌ Error: {}", error);
                    bar.abandon_with_message(error_message)
                },
                DownloadResult::Canceled => {
                    // 条变成黄色
                    bar.set_style(ProgressStyle::default_bar().template(&format!(
                        "{{spinner:.red}} [{{bar:.yellow/blue}}] {{bytes}}/{{total_bytes}} ({}): {{msg}}",
                        task_id
                    ))?.progress_chars("#>-"));
                    bar.abandon_with_message("⛔ Canceled")
                },
            }
        }
        Ok(())
    }
}

#[async_trait]
impl ResultReporter for TuiReporter {
    async fn operation_result(&self, operation: OperationType, code: u32, message: String) -> Result<()> {
        if code == 200 {
            println!("{}: {}", operation, message);
        } else {
            eprintln!("{}: {}", operation, message);
        }
        Ok(())
    }
}

}