tty_form/control/
selectinput.rs

1use crossterm::event::{KeyCode, KeyEvent};
2
3use crate::{
4    dependency::{Action, DependencyId, Evaluation},
5    step::CompoundStep,
6    style::{drawer_selected_style, drawer_style, help_style},
7    text::{DrawerContents, Segment, Text},
8};
9
10use super::Control;
11
12/// An option selection field.
13///
14/// # Examples
15/// ```
16/// use tty_interface::Style;
17///
18/// use tty_form::{
19///     step::CompoundStep,
20///     control::{Control, SelectInput},
21/// };
22///
23/// let mut step = CompoundStep::new();
24/// SelectInput::new("Select favorite food:", vec![
25///     ("Pizza", "A supreme pizza."),
26///     ("Burgers", "A hamburger with cheese."),
27///     ("Fries", "Simple potato french-fries."),
28/// ]).add_to(&mut step);
29/// ```
30pub struct SelectInput {
31    prompt: String,
32    options: Vec<SelectInputOption>,
33    selected_option: usize,
34}
35
36impl SelectInput {
37    /// Create a new option-selection input with the specified prompt and options.
38    pub fn new(prompt: &str, options: Vec<(&str, &str)>) -> Self {
39        Self {
40            prompt: prompt.to_string(),
41            options: options
42                .iter()
43                .map(|(value, description)| SelectInputOption::new(value, description))
44                .collect(),
45            selected_option: 0,
46        }
47    }
48
49    /// Update this input's prompt text.
50    pub fn set_prompt(&mut self, prompt: &str) {
51        self.prompt = prompt.to_string();
52    }
53
54    /// Add an option to this input's list.
55    pub fn add_option(&mut self, option: SelectInputOption) {
56        self.options.push(option);
57    }
58
59    /// Set this input's options.
60    pub fn set_options(&mut self, options: Vec<SelectInputOption>) {
61        self.options = options;
62    }
63
64    /// The currently-selected option's value.
65    fn selected_option_value(&self) -> &str {
66        &self.options[self.selected_option].value
67    }
68}
69
70impl Control for SelectInput {
71    fn focusable(&self) -> bool {
72        true
73    }
74
75    fn update(&mut self, input: KeyEvent) {
76        match input.code {
77            KeyCode::Up => {
78                if self.selected_option == 0 {
79                    self.selected_option = self.options.len() - 1;
80                } else {
81                    self.selected_option -= 1;
82                }
83            }
84            KeyCode::Down => {
85                if self.selected_option + 1 == self.options.len() {
86                    self.selected_option = 0;
87                } else {
88                    self.selected_option += 1;
89                }
90            }
91            _ => {}
92        }
93    }
94
95    fn help(&self) -> Option<Segment> {
96        Some(Text::new_styled(self.prompt.clone(), help_style()).as_segment())
97    }
98
99    fn text(&self) -> (Segment, Option<u16>) {
100        let value = self.selected_option_value();
101        let segment = Text::new(value.to_string()).as_segment();
102
103        (segment, Some(0))
104    }
105
106    fn drawer(&self) -> Option<DrawerContents> {
107        let mut items = Vec::new();
108
109        for (option_index, option) in self.options.iter().enumerate() {
110            let mut text = format!("   {} - {}", option.value, option.description);
111            let mut style = drawer_style();
112
113            if option_index == self.selected_option {
114                style = drawer_selected_style();
115                text.replace_range(1..2, ">");
116            }
117
118            items.push(Text::new_styled(text, style).as_segment());
119        }
120
121        Some(items)
122    }
123
124    fn evaluation(&self) -> Option<(DependencyId, Evaluation)> {
125        None
126    }
127
128    fn dependency(&self) -> Option<(DependencyId, Action)> {
129        None
130    }
131
132    fn evaluate(&self, evaluation: &Evaluation) -> bool {
133        match evaluation {
134            Evaluation::Equal(value) => self.selected_option_value() == value,
135            Evaluation::NotEqual(value) => self.selected_option_value() != value,
136            Evaluation::IsEmpty => false,
137        }
138    }
139
140    fn add_to(self, step: &mut CompoundStep) {
141        step.add_control(Box::new(self))
142    }
143}
144
145/// A option for an option selection input.
146pub struct SelectInputOption {
147    value: String,
148    description: String,
149}
150
151impl SelectInputOption {
152    /// Create a new option with the specified value and description.
153    pub fn new(value: &str, description: &str) -> Self {
154        Self {
155            value: value.to_string(),
156            description: description.to_string(),
157        }
158    }
159
160    /// This option's value.
161    pub fn value(&self) -> &str {
162        &self.value
163    }
164
165    /// This option's descriptive text.
166    pub fn description(&self) -> &str {
167        &self.description
168    }
169}