backup_suite/ui/
progress.rs

1use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::i18n::{get_message, MessageKey};
6
7/// バックアップ進捗表示機能
8///
9/// indicatifライブラリを使用してリアルタイムの進捗状況を表示します。
10///
11/// # 機能
12///
13/// - メインプログレスバー: 全体の進捗を表示
14/// - 詳細プログレスバー: 現在処理中のファイル情報を表示
15/// - 経過時間と推定残り時間(ETA)を表示
16/// - 処理速度表示(ファイル/秒、MB/秒)
17///
18/// # 使用例
19///
20/// ```no_run
21/// use backup_suite::ui::progress::BackupProgress;
22///
23/// let progress = BackupProgress::new(100);
24/// progress.set_message("処理中: /path/to/file.txt");
25/// progress.inc(1);
26/// progress.finish("バックアップ完了");
27/// ```
28#[derive(Clone)]
29pub struct BackupProgress {
30    #[allow(dead_code)]
31    multi: Arc<MultiProgress>,
32    main_bar: ProgressBar,
33    detail_bar: ProgressBar,
34    stats_bar: ProgressBar,
35    lang: crate::i18n::Language,
36}
37
38impl BackupProgress {
39    /// 新しいBackupProgressインスタンスを作成
40    ///
41    /// # 引数
42    ///
43    /// * `total_files` - バックアップ対象の総ファイル数
44    ///
45    /// # 戻り値
46    ///
47    /// BackupProgressインスタンス
48    ///
49    /// # 使用例
50    ///
51    /// ```no_run
52    /// use backup_suite::ui::progress::BackupProgress;
53    ///
54    /// let progress = BackupProgress::new(100);
55    /// ```
56    #[must_use]
57    pub fn new(total_files: u64) -> Self {
58        let lang = crate::i18n::Language::detect();
59        Self::with_language(total_files, lang)
60    }
61
62    /// 言語指定付きでBackupProgressインスタンスを作成
63    #[must_use]
64    pub fn with_language(total_files: u64, lang: crate::i18n::Language) -> Self {
65        let multi = Arc::new(MultiProgress::new());
66
67        // メインプログレスバー(改善版:ETA付き、国際化対応)
68        let main_bar = multi.add(ProgressBar::new(total_files));
69        let files_label = get_message(MessageKey::Files, lang);
70        let template = format!(
71            "[{{elapsed_precise}}] {{bar:40.cyan/blue}} {{pos}}/{{len}} {} ({{percent}}%) ETA: {{eta}} {{msg}}",
72            files_label
73        );
74        main_bar.set_style(
75            ProgressStyle::default_bar()
76                .template(&template)
77                .unwrap()
78                .progress_chars("█▉▊▋▌▍▎▏  "),
79        );
80
81        // 詳細プログレスバー(現在のファイル)
82        let detail_bar = multi.add(ProgressBar::new(0));
83        detail_bar.set_style(
84            ProgressStyle::default_bar()
85                .template("  📄 {wide_msg}")
86                .unwrap(),
87        );
88
89        // 統計プログレスバー(速度表示)
90        let stats_bar = multi.add(ProgressBar::new(0));
91        stats_bar.set_style(
92            ProgressStyle::default_bar()
93                .template("  📊 {wide_msg}")
94                .unwrap(),
95        );
96
97        Self {
98            multi,
99            main_bar,
100            detail_bar,
101            stats_bar,
102            lang,
103        }
104    }
105
106    /// プログレスバーを指定量進める
107    ///
108    /// # 引数
109    ///
110    /// * `delta` - 進める量(通常は1)
111    ///
112    /// # 使用例
113    ///
114    /// ```no_run
115    /// use backup_suite::ui::progress::BackupProgress;
116    ///
117    /// let progress = BackupProgress::new(100);
118    /// progress.inc(1); // 1つ進める
119    /// ```
120    pub fn inc(&self, delta: u64) {
121        self.main_bar.inc(delta);
122    }
123
124    /// 詳細メッセージを設定
125    ///
126    /// 現在処理中のファイルや操作の詳細を表示します。
127    ///
128    /// # 引数
129    ///
130    /// * `msg` - 表示するメッセージ
131    ///
132    /// # 使用例
133    ///
134    /// ```no_run
135    /// use backup_suite::ui::progress::BackupProgress;
136    ///
137    /// let progress = BackupProgress::new(100);
138    /// progress.set_message("処理中: /path/to/file.txt");
139    /// ```
140    pub fn set_message(&self, msg: &str) {
141        self.detail_bar.set_message(msg.to_string());
142    }
143
144    /// メインプログレスバーのメッセージを設定
145    ///
146    /// # 引数
147    ///
148    /// * `msg` - 表示するメッセージ
149    ///
150    /// # 使用例
151    ///
152    /// ```no_run
153    /// use backup_suite::ui::progress::BackupProgress;
154    ///
155    /// let progress = BackupProgress::new(100);
156    /// progress.set_main_message("高優先度ファイル処理中");
157    /// ```
158    pub fn set_main_message(&self, msg: &str) {
159        self.main_bar.set_message(msg.to_string());
160    }
161
162    /// 統計メッセージを設定
163    ///
164    /// 処理速度やデータ量などの統計情報を表示します。
165    ///
166    /// # 引数
167    ///
168    /// * `msg` - 表示するメッセージ
169    ///
170    /// # 使用例
171    ///
172    /// ```no_run
173    /// use backup_suite::ui::progress::BackupProgress;
174    ///
175    /// let progress = BackupProgress::new(100);
176    /// progress.set_stats("速度: 15.2 MB/s | 合計: 1.5 GB");
177    /// ```
178    pub fn set_stats(&self, msg: &str) {
179        self.stats_bar.set_message(msg.to_string());
180    }
181
182    /// プログレスバーを完了させる
183    ///
184    /// 最終メッセージを表示してプログレスバーを終了します。
185    ///
186    /// # 引数
187    ///
188    /// * `msg` - 完了メッセージ
189    ///
190    /// # 使用例
191    ///
192    /// ```no_run
193    /// use backup_suite::ui::progress::BackupProgress;
194    ///
195    /// let progress = BackupProgress::new(100);
196    /// // ... 処理 ...
197    /// progress.finish("バックアップ完了!");
198    /// ```
199    pub fn finish(&self, msg: &str) {
200        self.main_bar.finish_with_message(msg.to_string());
201        self.detail_bar.finish_and_clear();
202        self.stats_bar.finish_and_clear();
203    }
204
205    /// 現在の位置を設定
206    ///
207    /// # 引数
208    ///
209    /// * `pos` - 新しい位置
210    ///
211    /// # 使用例
212    ///
213    /// ```no_run
214    /// use backup_suite::ui::progress::BackupProgress;
215    ///
216    /// let progress = BackupProgress::new(100);
217    /// progress.set_position(50); // 50%に設定
218    /// ```
219    pub fn set_position(&self, pos: u64) {
220        self.main_bar.set_position(pos);
221    }
222
223    /// 総数を設定
224    ///
225    /// 処理中に総ファイル数が判明した場合に使用します。
226    ///
227    /// # 引数
228    ///
229    /// * `len` - 新しい総数
230    ///
231    /// # 使用例
232    ///
233    /// ```no_run
234    /// use backup_suite::ui::progress::BackupProgress;
235    ///
236    /// let progress = BackupProgress::new(0);
237    /// progress.set_length(150); // 実際の総数が判明
238    /// ```
239    pub fn set_length(&self, len: u64) {
240        self.main_bar.set_length(len);
241    }
242
243    /// プログレスバーを非表示にして完了
244    ///
245    /// メッセージを表示せずに終了します。
246    ///
247    /// # 使用例
248    ///
249    /// ```no_run
250    /// use backup_suite::ui::progress::BackupProgress;
251    ///
252    /// let progress = BackupProgress::new(100);
253    /// progress.finish_and_clear();
254    /// ```
255    pub fn finish_and_clear(&self) {
256        self.main_bar.finish_and_clear();
257        self.detail_bar.finish_and_clear();
258        self.stats_bar.finish_and_clear();
259    }
260
261    /// スピナーモードのプログレスバーを作成
262    ///
263    /// ファイル数が不明な場合に使用するスピナー表示。
264    ///
265    /// # 戻り値
266    ///
267    /// スピナーモードのBackupProgress
268    ///
269    /// # 使用例
270    ///
271    /// ```no_run
272    /// use backup_suite::ui::progress::BackupProgress;
273    ///
274    /// let progress = BackupProgress::new_spinner();
275    /// progress.set_message("ファイル検索中...");
276    /// // ... 処理 ...
277    /// progress.finish("検索完了");
278    /// ```
279    #[must_use]
280    pub fn new_spinner() -> Self {
281        let lang = crate::i18n::Language::detect();
282        Self::new_spinner_with_language(lang)
283    }
284
285    /// 言語指定付きでスピナーを作成
286    #[must_use]
287    pub fn new_spinner_with_language(lang: crate::i18n::Language) -> Self {
288        let multi = Arc::new(MultiProgress::new());
289
290        let main_bar = multi.add(ProgressBar::new_spinner());
291        main_bar.set_style(
292            ProgressStyle::default_spinner()
293                .template("{spinner:.cyan} {msg}")
294                .unwrap()
295                .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
296        );
297        main_bar.enable_steady_tick(Duration::from_millis(120));
298
299        let detail_bar = multi.add(ProgressBar::new(0));
300        detail_bar.set_style(
301            ProgressStyle::default_bar()
302                .template("  {wide_msg}")
303                .unwrap(),
304        );
305
306        let stats_bar = multi.add(ProgressBar::new(0));
307        stats_bar.set_style(
308            ProgressStyle::default_bar()
309                .template("  📊 {wide_msg}")
310                .unwrap(),
311        );
312
313        Self {
314            multi,
315            main_bar,
316            detail_bar,
317            stats_bar,
318            lang,
319        }
320    }
321
322    /// 処理速度を計算して統計情報を更新
323    ///
324    /// # 引数
325    ///
326    /// * `processed_files` - 処理済みファイル数
327    /// * `total_bytes` - 処理済みバイト数
328    /// * `elapsed_secs` - 経過秒数
329    ///
330    /// # 使用例
331    ///
332    /// ```no_run
333    /// use backup_suite::ui::progress::BackupProgress;
334    ///
335    /// let progress = BackupProgress::new(100);
336    /// progress.update_stats(50, 52_428_800, 10.5); // 50ファイル, 50MB, 10.5秒
337    /// ```
338    #[allow(clippy::cast_precision_loss)]
339    pub fn update_stats(&self, processed_files: u64, total_bytes: u64, elapsed_secs: f64) {
340        if elapsed_secs > 0.0 {
341            let files_per_sec = processed_files as f64 / elapsed_secs;
342            let bytes_per_sec = total_bytes as f64 / elapsed_secs;
343            let mb_per_sec = bytes_per_sec / 1024.0 / 1024.0;
344            let total_mb = total_bytes as f64 / 1024.0 / 1024.0;
345
346            let stats_msg = match self.lang {
347                crate::i18n::Language::Japanese => format!(
348                    "速度: {:.1} ファイル/秒, {:.2} MB/秒 | 合計: {:.2} MB",
349                    files_per_sec, mb_per_sec, total_mb
350                ),
351                crate::i18n::Language::SimplifiedChinese => format!(
352                    "速度: {:.1} 文件/秒, {:.2} MB/秒 | 总计: {:.2} MB",
353                    files_per_sec, mb_per_sec, total_mb
354                ),
355                crate::i18n::Language::TraditionalChinese => format!(
356                    "速度: {:.1} 檔案/秒, {:.2} MB/秒 | 總計: {:.2} MB",
357                    files_per_sec, mb_per_sec, total_mb
358                ),
359                crate::i18n::Language::English => format!(
360                    "Speed: {:.1} files/sec, {:.2} MB/sec | Total: {:.2} MB",
361                    files_per_sec, mb_per_sec, total_mb
362                ),
363            };
364
365            self.set_stats(&stats_msg);
366        }
367    }
368}
369
370/// シンプルなプログレスバーを作成
371///
372/// 単純な進捗表示が必要な場合の便利関数。
373///
374/// # 引数
375///
376/// * `total` - 総数
377/// * `message` - 表示メッセージ
378///
379/// # 戻り値
380///
381/// ProgressBarインスタンス
382///
383/// # 使用例
384///
385/// ```no_run
386/// use backup_suite::ui::progress::create_progress_bar;
387///
388/// let pb = create_progress_bar(100, "処理中");
389/// for _ in 0..100 {
390///     pb.inc(1);
391/// }
392/// pb.finish_with_message("完了");
393/// ```
394#[must_use]
395pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
396    let pb = ProgressBar::new(total);
397    pb.set_style(
398        ProgressStyle::default_bar()
399            .template(&format!(
400                "{{spinner:.green}} {message} [{{elapsed_precise}}] {{bar:40.cyan/blue}} {{pos}}/{{len}} ({{percent}}%) ETA: {{eta}} {{msg}}"
401            ))
402            .unwrap()
403            .progress_chars("█▉▊▋▌▍▎▏  "),
404    );
405    pb
406}
407
408/// スピナーを作成
409///
410/// 不定期間の処理を表示する場合に使用。
411///
412/// # 引数
413///
414/// * `message` - 表示メッセージ
415///
416/// # 戻り値
417///
418/// スピナーのProgressBarインスタンス
419///
420/// # 使用例
421///
422/// ```no_run
423/// use backup_suite::ui::progress::create_spinner;
424///
425/// let spinner = create_spinner("接続中...");
426/// // ... 処理 ...
427/// spinner.finish_with_message("接続完了");
428/// ```
429#[must_use]
430pub fn create_spinner(message: &str) -> ProgressBar {
431    let spinner = ProgressBar::new_spinner();
432    spinner.set_style(
433        ProgressStyle::default_spinner()
434            .template(&format!("{{spinner:.cyan}} {message}"))
435            .unwrap()
436            .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
437    );
438    spinner.enable_steady_tick(Duration::from_millis(120));
439    spinner
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_new_progress() {
448        let progress = BackupProgress::new(100);
449        // 基本的な作成テスト
450        progress.set_message("テスト");
451        progress.inc(1);
452        progress.finish_and_clear();
453    }
454
455    #[test]
456    fn test_new_spinner() {
457        let progress = BackupProgress::new_spinner();
458        progress.set_message("スピナーテスト");
459        progress.finish_and_clear();
460    }
461
462    #[test]
463    fn test_set_position() {
464        let progress = BackupProgress::new(100);
465        progress.set_position(50);
466        progress.finish_and_clear();
467    }
468
469    #[test]
470    fn test_set_length() {
471        let progress = BackupProgress::new(0);
472        progress.set_length(100);
473        progress.inc(10);
474        progress.finish_and_clear();
475    }
476
477    #[test]
478    fn test_create_progress_bar() {
479        let pb = create_progress_bar(100, "テスト");
480        pb.inc(10);
481        pb.finish_and_clear();
482    }
483
484    #[test]
485    fn test_create_spinner() {
486        let spinner = create_spinner("読み込み中");
487        spinner.finish_and_clear();
488    }
489
490    #[test]
491    fn test_progress_messages() {
492        let progress = BackupProgress::new(100);
493        progress.set_main_message("メイン");
494        progress.set_message("詳細");
495        progress.set_stats("統計");
496        progress.inc(1);
497        progress.finish("完了");
498    }
499
500    #[test]
501    fn test_update_stats() {
502        let progress = BackupProgress::new(100);
503        progress.update_stats(50, 52_428_800, 10.5); // 50ファイル, 50MB, 10.5秒
504        progress.finish_and_clear();
505    }
506}