changepacks_cli/
prompter.rs

1use anyhow::Result;
2use changepacks_core::Project;
3use thiserror::Error;
4
5/// Error type for user cancellation (Ctrl+C or ESC)
6#[derive(Debug, Error)]
7#[error("")]
8pub struct UserCancelled;
9
10/// Trait for user input prompts - allows dependency injection for testing
11pub trait Prompter: Send + Sync {
12    fn multi_select<'a>(
13        &self,
14        message: &str,
15        options: Vec<&'a Project>,
16        defaults: Vec<usize>,
17    ) -> Result<Vec<&'a Project>>;
18
19    fn confirm(&self, message: &str) -> Result<bool>;
20
21    fn text(&self, message: &str) -> Result<String>;
22}
23
24/// Real implementation using inquire crate
25#[derive(Default)]
26pub struct InquirePrompter;
27
28impl Prompter for InquirePrompter {
29    fn multi_select<'a>(
30        &self,
31        message: &str,
32        options: Vec<&'a Project>,
33        defaults: Vec<usize>,
34    ) -> Result<Vec<&'a Project>> {
35        let mut selector = inquire::MultiSelect::new(message, options);
36        selector.page_size = 15;
37        selector.default = Some(defaults);
38        selector.scorer = &|_input, option, _string_value, _idx| -> Option<i64> {
39            if option.is_changed() {
40                Some(100)
41            } else {
42                Some(0)
43            }
44        };
45        selector.formatter = &|option| {
46            option
47                .iter()
48                .map(|o| format!("{}", o.value))
49                .collect::<Vec<_>>()
50                .join("\n")
51        };
52        match selector.prompt() {
53            Ok(v) => Ok(v),
54            Err(inquire::InquireError::OperationCanceled)
55            | Err(inquire::InquireError::OperationInterrupted) => Err(UserCancelled.into()),
56            Err(e) => Err(e.into()),
57        }
58    }
59
60    fn confirm(&self, message: &str) -> Result<bool> {
61        match inquire::Confirm::new(message).prompt() {
62            Ok(v) => Ok(v),
63            Err(inquire::InquireError::OperationCanceled)
64            | Err(inquire::InquireError::OperationInterrupted) => Err(UserCancelled.into()),
65            Err(e) => Err(e.into()),
66        }
67    }
68
69    fn text(&self, message: &str) -> Result<String> {
70        match inquire::Text::new(message).prompt() {
71            Ok(v) => Ok(v),
72            Err(inquire::InquireError::OperationCanceled)
73            | Err(inquire::InquireError::OperationInterrupted) => Err(UserCancelled.into()),
74            Err(e) => Err(e.into()),
75        }
76    }
77}
78
79/// Mock implementation that returns predefined values (for testing)
80pub struct MockPrompter {
81    pub select_all: bool,
82    pub confirm_value: bool,
83    pub text_value: String,
84}
85
86impl Default for MockPrompter {
87    fn default() -> Self {
88        Self {
89            select_all: true,
90            confirm_value: true,
91            text_value: "test note".to_string(),
92        }
93    }
94}
95
96impl Prompter for MockPrompter {
97    fn multi_select<'a>(
98        &self,
99        _message: &str,
100        options: Vec<&'a Project>,
101        _defaults: Vec<usize>,
102    ) -> Result<Vec<&'a Project>> {
103        if self.select_all {
104            Ok(options)
105        } else {
106            Ok(vec![])
107        }
108    }
109
110    fn confirm(&self, _message: &str) -> Result<bool> {
111        Ok(self.confirm_value)
112    }
113
114    fn text(&self, _message: &str) -> Result<String> {
115        Ok(self.text_value.clone())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_mock_prompter_default() {
125        let prompter = MockPrompter::default();
126        assert!(prompter.select_all);
127        assert!(prompter.confirm_value);
128        assert_eq!(prompter.text_value, "test note");
129    }
130
131    #[test]
132    fn test_mock_prompter_confirm() {
133        let prompter = MockPrompter {
134            confirm_value: false,
135            ..Default::default()
136        };
137        assert!(!prompter.confirm("test").unwrap());
138    }
139
140    #[test]
141    fn test_mock_prompter_text() {
142        let prompter = MockPrompter {
143            text_value: "custom".to_string(),
144            ..Default::default()
145        };
146        assert_eq!(prompter.text("test").unwrap(), "custom");
147    }
148
149    #[test]
150    fn test_mock_prompter_multi_select_empty() {
151        let prompter = MockPrompter {
152            select_all: false,
153            ..Default::default()
154        };
155        let options: Vec<&Project> = vec![];
156        let result = prompter.multi_select("test", options, vec![]).unwrap();
157        assert!(result.is_empty());
158    }
159}