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}