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}