cli_prompts/prompts/options/
selection.rs

1use crate::{
2    engine::CommandBuffer,
3    input::Key,
4    prompts::{options::Options, AbortReason, EventOutcome, Prompt},
5    style::SelectionStyle,
6};
7
8use super::multioption_prompt::MultiOptionPrompt;
9
10const DEFAULT_OPTIONS_COUNT: u16 = 5;
11
12/// Prompt that allows to select one option from the given list.
13/// Supports filtering and moving the selection with arrow keys.
14///
15/// ```rust
16/// use cli_prompts::{
17///      prompts::{Selection, AbortReason},
18///      DisplayPrompt,
19/// };
20///
21/// fn main() {
22///     let social_media = [
23///         "Facebook",
24///         "Instagram",
25///         "Twitter",
26///         "Snapchat"
27///     ];
28///
29///     let prompt = Selection::new("Where you want to post?", social_media.into_iter())
30///                     .displayed_options_count(3);
31///     let selection : Result<&str, AbortReason> = prompt.display();
32///     match selection {
33///         Ok(media) => println!("Posting to {}", media),
34///         Err(abort_reason) => println!("Prompt is aborted because of {:?}", abort_reason),
35///     }
36/// }
37/// ```
38pub struct Selection<T> {
39    label: String,
40    options: Options<T>,
41    current_selection: usize,
42    max_options: u16,
43    current_filter: String,
44    is_submitted: bool,
45    style: SelectionStyle,
46}
47
48impl<T> Selection<T> {
49
50    /// Create new prompt with the given label and the iterator over a type that is convertable to
51    /// `String`
52    pub fn new<S, I>(label: S, options: I) -> Self
53    where
54        T: Into<String> + Clone,
55        S: Into<String>,
56        I: Iterator<Item = T>,
57    {
58        let options = Options::from_iter(options);
59        Self::new_internal(label.into(), options)
60    }
61}
62
63impl<T> Selection<T> {
64
65    /// Create new prompt with the given label and a transformation function that will convert the
66    /// iterator items to strings
67    pub fn new_with_transformation<S, I, F>(label: S, options: I, transformation: F) -> Self
68    where
69        S: Into<String>,
70        I: Iterator<Item = T>,
71        F: Fn(&T) -> String,
72    {
73        let options = Options::from_iter_transformed(options, transformation);
74        Self::new_internal(label.into(), options)
75    }
76
77    /// Set maximum number of options that can be displayed on the screen
78    pub fn displayed_options_count(mut self, options_count: u16) -> Self {
79        self.max_options = options_count;
80        self
81    }
82
83    /// Set the prompt style
84    pub fn style(mut self, style: SelectionStyle) -> Self {
85        self.style = style;
86        self
87    }
88
89    fn new_internal(label: String, options: Options<T>) -> Self {
90        Selection {
91            label,
92            options,
93            current_selection: 0_usize,
94            max_options: DEFAULT_OPTIONS_COUNT,
95            current_filter: String::new(),
96            is_submitted: false,
97            style: SelectionStyle::default(),
98        }
99    }
100}
101
102impl<T> MultiOptionPrompt<T> for Selection<T> {
103    fn max_options_count(&self) -> u16 {
104        self.max_options
105    }
106
107    fn options(&self) -> &Options<T> {
108        &self.options
109    }
110
111    fn currently_selected_index(&self) -> usize {
112        self.current_selection
113    }
114
115    fn draw_option(
116        &self,
117        _: usize,
118        option_label: &str,
119        is_selected: bool,
120        cmd_buffer: &mut impl CommandBuffer,
121    ) {
122        if is_selected {
123            self.style.selected_marker.print(cmd_buffer);
124            self.style
125                .selected_option_formatting
126                .print(option_label, cmd_buffer);
127        } else {
128            self.style.not_selected_marker.print(cmd_buffer);
129            self.style
130                .option_formatting
131                .print(option_label, cmd_buffer)
132        }
133    }
134
135    fn draw_header(&self, commands: &mut impl CommandBuffer, is_submitted: bool) {
136        if is_submitted {
137            let selected_option_index = self.options.filtered_options()[self.current_selection];
138            let selected_option = &self.options.transformed_options()[selected_option_index];
139            self.style
140                .submitted_formatting
141                .print(selected_option, commands);
142        } else {
143            self.style
144                .filter_formatting
145                .print(&self.current_filter, commands);
146        }
147    }
148}
149
150impl<T> Prompt<T> for Selection<T> {
151    fn draw(&self, commands: &mut impl CommandBuffer) {
152        self.draw_multioption(
153            &self.label,
154            self.is_submitted,
155            &self.style.label_style,
156            commands,
157        )
158    }
159
160    fn on_key_pressed(&mut self, key: Key) -> EventOutcome<T> {
161        match key {
162            Key::Char(c) => {
163                self.current_filter.push(c);
164                self.options.filter(&self.current_filter);
165                self.current_selection = 0;
166                EventOutcome::Continue
167            }
168            Key::Backspace if self.current_filter.len() > 0 => {
169                self.current_filter.pop();
170                self.options.filter(&self.current_filter);
171                self.current_selection = 0;
172                EventOutcome::Continue
173            }
174            Key::Up if self.current_selection > 0 => {
175                self.current_selection -= 1;
176                EventOutcome::Continue
177            }
178            Key::Down if self.current_selection < self.options.filtered_options().len() - 1 => {
179                self.current_selection += 1;
180                EventOutcome::Continue
181            }
182            Key::Enter if self.options.filtered_options().len() > 0 => {
183                self.is_submitted = true;
184                let selected_option_index = self.options.filtered_options()[self.current_selection];
185                let result = self.options.all_options_mut().remove(selected_option_index);
186                EventOutcome::Done(result)
187            }
188            Key::Esc => EventOutcome::Abort(AbortReason::Interrupt),
189            _ => EventOutcome::Continue,
190        }
191    }
192}