backup_suite/ui/interactive.rs
1use anyhow::Result;
2use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
3
4/// インタラクティブな確認プロンプト
5///
6/// dialoguerライブラリを使用してユーザーに確認を求めます。
7///
8/// # 機能
9///
10/// - Yes/No確認プロンプト
11/// - 選択メニュー
12/// - テキスト入力
13///
14/// # 使用例
15///
16/// ```no_run
17/// use backup_suite::ui::interactive::confirm;
18///
19/// if confirm("実行しますか?", true)? {
20/// println!("実行します");
21/// }
22/// # Ok::<(), anyhow::Error>(())
23/// ```
24/// Yes/No確認プロンプトを表示
25///
26/// # 引数
27///
28/// * `message` - 確認メッセージ
29/// * `default` - デフォルト値(trueならYesがデフォルト)
30///
31/// # 戻り値
32///
33/// ユーザーの選択(true: Yes, false: No)
34///
35/// # Errors
36///
37/// 次の場合にエラーを返します:
38/// - 標準入出力へのアクセスに失敗した場合
39/// - ユーザーが対話を中断した場合(Ctrl-C等)
40/// - ターミナルが利用できない環境で実行した場合
41///
42/// # 使用例
43///
44/// ```no_run
45/// use backup_suite::ui::interactive::confirm;
46///
47/// if confirm("バックアップを実行しますか?", true)? {
48/// println!("バックアップを開始します");
49/// } else {
50/// println!("キャンセルされました");
51/// }
52/// # Ok::<(), anyhow::Error>(())
53/// ```
54pub fn confirm(message: &str, default: bool) -> Result<bool> {
55 // CI環境での自動確認サポート
56 // BACKUP_SUITE_YES=true または BACKUP_SUITE_YES=1 で自動的にtrueを返す
57 if let Ok(auto_yes) = std::env::var("BACKUP_SUITE_YES") {
58 if auto_yes == "true" || auto_yes == "1" {
59 eprintln!("[CI MODE] Auto-confirming: {}", message);
60 return Ok(true);
61 }
62 }
63
64 let result = Confirm::with_theme(&ColorfulTheme::default())
65 .with_prompt(message)
66 .default(default)
67 .interact()?;
68
69 Ok(result)
70}
71
72/// Yes/No確認プロンプト(カスタマイズ可能版)
73///
74/// # 引数
75///
76/// * `message` - 確認メッセージ
77/// * `default` - デフォルト値
78/// * `yes_text` - Yesボタンのテキスト
79/// * `no_text` - Noボタンのテキスト
80///
81/// # 戻り値
82///
83/// ユーザーの選択
84///
85/// # Errors
86///
87/// 次の場合にエラーを返します:
88/// - 標準入出力へのアクセスに失敗した場合
89/// - ユーザーが対話を中断した場合(Ctrl-C等)
90/// - ターミナルが利用できない環境で実行した場合
91///
92/// # 使用例
93///
94/// ```no_run
95/// use backup_suite::ui::interactive::confirm_with_text;
96///
97/// if confirm_with_text(
98/// "古いバックアップを削除しますか?",
99/// false,
100/// "削除する",
101/// "保持する"
102/// )? {
103/// println!("削除します");
104/// }
105/// # Ok::<(), anyhow::Error>(())
106/// ```
107pub fn confirm_with_text(
108 message: &str,
109 default: bool,
110 yes_text: &str,
111 no_text: &str,
112) -> Result<bool> {
113 println!("\n{message}");
114 println!(" {yes_text} / {no_text}");
115
116 let result = Confirm::with_theme(&ColorfulTheme::default())
117 .with_prompt("選択してください")
118 .default(default)
119 .interact()?;
120
121 Ok(result)
122}
123
124/// 選択メニューを表示
125///
126/// # 引数
127///
128/// * `message` - プロンプトメッセージ
129/// * `items` - 選択肢のリスト
130///
131/// # 戻り値
132///
133/// 選択されたインデックス
134///
135/// # Errors
136///
137/// 次の場合にエラーを返します:
138/// - 標準入出力へのアクセスに失敗した場合
139/// - ユーザーが対話を中断した場合(Ctrl-C等)
140/// - ターミナルが利用できない環境で実行した場合
141///
142/// # 使用例
143///
144/// ```no_run
145/// use backup_suite::ui::interactive::select;
146///
147/// let items = vec!["オプション1", "オプション2", "オプション3"];
148/// let selection = select("選択してください", &items)?;
149/// println!("選択: {}", items[selection]);
150/// # Ok::<(), anyhow::Error>(())
151/// ```
152pub fn select(message: &str, items: &[&str]) -> Result<usize> {
153 let selection = Select::with_theme(&ColorfulTheme::default())
154 .with_prompt(message)
155 .items(items)
156 .default(0)
157 .interact()?;
158
159 Ok(selection)
160}
161
162/// 選択メニュー(デフォルト位置指定可能版)
163///
164/// # 引数
165///
166/// * `message` - プロンプトメッセージ
167/// * `items` - 選択肢のリスト
168/// * `default` - デフォルトの選択位置
169///
170/// # 戻り値
171///
172/// 選択されたインデックス
173///
174/// # Errors
175///
176/// 次の場合にエラーを返します:
177/// - 標準入出力へのアクセスに失敗した場合
178/// - ユーザーが対話を中断した場合(Ctrl-C等)
179/// - ターミナルが利用できない環境で実行した場合
180///
181/// # 使用例
182///
183/// ```no_run
184/// use backup_suite::ui::interactive::select_with_default;
185///
186/// let priorities = vec!["高", "中", "低"];
187/// let selection = select_with_default("優先度を選択", &priorities, 1)?;
188/// # Ok::<(), anyhow::Error>(())
189/// ```
190pub fn select_with_default(message: &str, items: &[&str], default: usize) -> Result<usize> {
191 let selection = Select::with_theme(&ColorfulTheme::default())
192 .with_prompt(message)
193 .items(items)
194 .default(default)
195 .interact()?;
196
197 Ok(selection)
198}
199
200/// テキスト入力プロンプト
201///
202/// # 引数
203///
204/// * `message` - プロンプトメッセージ
205/// * `default` - デフォルト値(オプション)
206///
207/// # 戻り値
208///
209/// 入力されたテキスト
210///
211/// # Errors
212///
213/// 次の場合にエラーを返します:
214/// - 標準入出力へのアクセスに失敗した場合
215/// - ユーザーが対話を中断した場合(Ctrl-C等)
216/// - ターミナルが利用できない環境で実行した場合
217///
218/// # 使用例
219///
220/// ```no_run
221/// use backup_suite::ui::interactive::input;
222///
223/// let name = input("バックアップ名を入力", Some("backup_1"))?;
224/// println!("バックアップ名: {}", name);
225/// # Ok::<(), anyhow::Error>(())
226/// ```
227pub fn input(message: &str, default: Option<&str>) -> Result<String> {
228 let theme = ColorfulTheme::default();
229 let mut input_builder = Input::<String>::with_theme(&theme).with_prompt(message);
230
231 if let Some(default_value) = default {
232 input_builder = input_builder.default(default_value.to_string());
233 }
234
235 let result = input_builder.interact_text()?;
236 Ok(result)
237}
238
239/// パス入力プロンプト
240///
241/// パス入力に特化したプロンプト。存在確認は行わない。
242///
243/// # 引数
244///
245/// * `message` - プロンプトメッセージ
246/// * `default` - デフォルトパス(オプション)
247///
248/// # 戻り値
249///
250/// 入力されたパス文字列
251///
252/// # Errors
253///
254/// 次の場合にエラーを返します:
255/// - 標準入出力へのアクセスに失敗した場合
256/// - ユーザーが対話を中断した場合(Ctrl-C等)
257/// - ターミナルが利用できない環境で実行した場合
258///
259/// # 使用例
260///
261/// ```no_run
262/// use backup_suite::ui::interactive::input_path;
263///
264/// let path = input_path("バックアップ先を入力", Some("/tmp/backup"))?;
265/// println!("パス: {}", path);
266/// # Ok::<(), anyhow::Error>(())
267/// ```
268pub fn input_path(message: &str, default: Option<&str>) -> Result<String> {
269 input(message, default)
270}
271
272/// 複数選択メニュー(実装予定)
273///
274/// 現在はselectのエイリアス。将来的に複数選択対応予定。
275///
276/// # 引数
277///
278/// * `message` - プロンプトメッセージ
279/// * `items` - 選択肢のリスト
280///
281/// # 戻り値
282///
283/// 選択されたインデックスのベクター
284///
285/// # Errors
286///
287/// 次の場合にエラーを返します:
288/// - 標準入出力へのアクセスに失敗した場合
289/// - ユーザーが対話を中断した場合(Ctrl-C等)
290/// - ターミナルが利用できない環境で実行した場合
291pub fn multi_select(message: &str, items: &[&str]) -> Result<Vec<usize>> {
292 // 現在は単一選択のみサポート
293 let selection = select(message, items)?;
294 Ok(vec![selection])
295}
296
297/// バックアップ開始前の最終確認
298///
299/// バックアップ実行前の標準的な確認プロンプト。
300///
301/// # 引数
302///
303/// * `file_count` - バックアップ対象のファイル数
304/// * `destination` - バックアップ先パス
305///
306/// # 戻り値
307///
308/// ユーザーの確認結果
309///
310/// # Errors
311///
312/// 次の場合にエラーを返します:
313/// - 標準入出力へのアクセスに失敗した場合
314/// - ユーザーが対話を中断した場合(Ctrl-C等)
315/// - ターミナルが利用できない環境で実行した場合
316///
317/// # 使用例
318///
319/// ```no_run
320/// use backup_suite::ui::interactive::confirm_backup;
321/// use backup_suite::i18n::Language;
322///
323/// if confirm_backup(150, "/backup/destination", Language::Japanese)? {
324/// println!("バックアップを開始します");
325/// }
326/// # Ok::<(), anyhow::Error>(())
327/// ```
328pub fn confirm_backup(
329 file_count: usize,
330 destination: &str,
331 lang: crate::i18n::Language,
332) -> Result<bool> {
333 use crate::i18n::{get_message, MessageKey};
334
335 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
336 println!("{}", get_message(MessageKey::ConfirmBackupTitle, lang));
337 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
338 println!(
339 "{}",
340 get_message(MessageKey::ConfirmBackupTargetFiles, lang)
341 .replace("{}", &file_count.to_string())
342 );
343 println!(
344 "{}",
345 get_message(MessageKey::ConfirmBackupDestination, lang).replace("{}", destination)
346 );
347 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
348
349 confirm(get_message(MessageKey::PromptBackupConfirm, lang), true)
350}
351
352/// 古いバックアップの削除確認
353///
354/// # 引数
355///
356/// * `count` - 削除対象のバックアップ数
357/// * `keep_days` - 保持日数
358///
359/// # 戻り値
360///
361/// ユーザーの確認結果
362///
363/// # Errors
364///
365/// 次の場合にエラーを返します:
366/// - 標準入出力へのアクセスに失敗した場合
367/// - ユーザーが対話を中断した場合(Ctrl-C等)
368/// - ターミナルが利用できない環境で実行した場合
369///
370/// # 使用例
371///
372/// ```no_run
373/// use backup_suite::ui::interactive::confirm_cleanup;
374/// use backup_suite::i18n::Language;
375///
376/// if confirm_cleanup(5, 30, Language::Japanese)? {
377/// println!("古いバックアップを削除します");
378/// }
379/// # Ok::<(), anyhow::Error>(())
380/// ```
381pub fn confirm_cleanup(count: usize, keep_days: u32, lang: crate::i18n::Language) -> Result<bool> {
382 use crate::i18n::{get_message, MessageKey};
383
384 println!("\n{}", get_message(MessageKey::ConfirmCleanupTitle, lang));
385 println!(
386 "{}",
387 get_message(MessageKey::ConfirmCleanupTargetCount, lang).replace("{}", &count.to_string())
388 );
389 println!(
390 "{}",
391 get_message(MessageKey::ConfirmCleanupRetentionDays, lang)
392 .replace("{}", &keep_days.to_string())
393 );
394
395 confirm(get_message(MessageKey::PromptConfirmDelete, lang), false)
396}
397
398/// 優先度選択プロンプト
399///
400/// バックアップ対象の優先度を選択。
401///
402/// # 戻り値
403///
404/// 選択された優先度("high", "medium", "low")
405///
406/// # Errors
407///
408/// 次の場合にエラーを返します:
409/// - 標準入出力へのアクセスに失敗した場合
410/// - ユーザーが対話を中断した場合(Ctrl-C等)
411/// - ターミナルが利用できない環境で実行した場合
412///
413/// # 使用例
414///
415/// ```no_run
416/// use backup_suite::ui::interactive::select_priority;
417///
418/// let priority = select_priority()?;
419/// println!("選択された優先度: {}", priority);
420/// # Ok::<(), anyhow::Error>(())
421/// ```
422pub fn select_priority() -> Result<String> {
423 let priorities = vec![
424 "高 (High) - 毎日バックアップ",
425 "中 (Medium) - 週次バックアップ",
426 "低 (Low) - 月次バックアップ",
427 ];
428
429 let selection = select("優先度を選択してください", &priorities)?;
430
431 let priority = match selection {
432 0 => "high",
433 1 => "medium",
434 2 => "low",
435 _ => "medium",
436 };
437
438 Ok(priority.to_string())
439}
440
441#[cfg(test)]
442mod tests {
443 // インタラクティブなテストは自動テスト困難なため、
444 // 基本的な関数の存在確認のみ
445
446 #[test]
447 fn test_select_priority_values() {
448 // 優先度の値が正しいことを確認
449 let priorities = ["high", "medium", "low"];
450 assert!(priorities.contains(&"high"));
451 assert!(priorities.contains(&"medium"));
452 assert!(priorities.contains(&"low"));
453 }
454
455 #[test]
456 fn test_multi_select_returns_vec() {
457 // multi_selectが将来的にVec<usize>を返すことを確認
458 // 実際の対話的テストは手動で実施
459 }
460}