tty_form/step/
yesno.rs

1use crossterm::event::{KeyCode, KeyEvent};
2use tty_interface::{pos, Interface, Position};
3use tty_text::Key;
4
5use crate::{
6    dependency::{DependencyId, DependencyState, Evaluation},
7    style::{help_style, muted_style},
8    text::{DrawerContents, Segment, Text},
9    Form,
10};
11
12use super::{InputResult, Step};
13
14/// A boolean input which, if true, accepts a text description.
15pub struct YesNoStep {
16    prompt: String,
17    prefix: String,
18    omit_if_no: bool,
19    toggle_value: bool,
20    text_prompt: String,
21    text: tty_text::Text,
22    evaluation: Option<(DependencyId, Evaluation)>,
23}
24
25impl YesNoStep {
26    pub fn new(prompt: &str, description_prompt: &str, prefix: &str) -> Self {
27        Self {
28            prompt: prompt.to_string(),
29            prefix: prefix.to_string(),
30            omit_if_no: true,
31            toggle_value: false,
32            text_prompt: description_prompt.to_string(),
33            text: tty_text::Text::new(false),
34            evaluation: None,
35        }
36    }
37
38    pub fn set_omit_if_no(&mut self, omit: bool) {
39        self.omit_if_no = omit;
40    }
41
42    pub fn set_evaluation(&mut self, evaluation: Evaluation) -> DependencyId {
43        let id = DependencyId::new();
44        self.evaluation = Some((id, evaluation));
45        id
46    }
47
48    fn get_display_value(&self) -> String {
49        if !self.text.value().is_empty() {
50            self.text.value()
51        } else if self.toggle_value {
52            String::from("Yes")
53        } else {
54            String::from("No")
55        }
56    }
57}
58
59impl Step for YesNoStep {
60    fn initialize(&mut self, _dependency_state: &mut DependencyState, _index: usize) {}
61
62    fn render(
63        &self,
64        interface: &mut Interface,
65        _dependency_state: &DependencyState,
66        position: Position,
67        is_focused: bool,
68    ) -> u16 {
69        if self.toggle_value || is_focused || !self.omit_if_no {
70            let display_value = self.get_display_value();
71            if !self.toggle_value && (is_focused || !self.omit_if_no) {
72                // Render muted prompt and value
73                interface.set_styled(
74                    position,
75                    &format!("{}: {}", self.prefix, display_value),
76                    muted_style(),
77                );
78            } else if is_focused && self.toggle_value && self.text.value().is_empty() {
79                // Render a white prefix with muted value
80                interface.set(position, &format!("{}:", self.prefix));
81
82                let value_position = pos!(self.prefix.len() as u16 + 2, position.y());
83                interface.set_styled(value_position, &display_value, muted_style());
84            } else if is_focused || self.toggle_value {
85                // Render white prompt and value
86                interface.set(position, &format!("{}: {}", self.prefix, display_value));
87            }
88
89            if is_focused && self.toggle_value {
90                let (cursor_column, _) = self.text.cursor();
91                let cursor = pos!((self.prefix.len() + 2 + cursor_column) as u16, position.y());
92                interface.set_cursor(Some(cursor));
93            }
94
95            return 1;
96        }
97
98        0
99    }
100
101    fn update(
102        &mut self,
103        dependency_state: &mut DependencyState,
104        input: KeyEvent,
105    ) -> Option<InputResult> {
106        match input.code {
107            KeyCode::Esc | KeyCode::BackTab => return Some(InputResult::RetreatForm),
108            KeyCode::Enter | KeyCode::Tab => return Some(InputResult::AdvanceForm),
109            _ => {}
110        };
111
112        if self.text.value().is_empty()
113            && (input.code == KeyCode::Up || input.code == KeyCode::Down)
114        {
115            self.toggle_value = !self.toggle_value;
116        }
117
118        if self.toggle_value {
119            match input.code {
120                KeyCode::Char(ch) => self.text.handle_input(Key::Char(ch)),
121                KeyCode::Backspace => self.text.handle_input(Key::Backspace),
122                KeyCode::Left => self.text.handle_input(Key::Left),
123                KeyCode::Right => self.text.handle_input(Key::Right),
124                _ => {}
125            };
126        }
127
128        if let Some((id, evaluation)) = &self.evaluation {
129            let value = match evaluation {
130                Evaluation::Equal(value) => value == &self.get_display_value(),
131                Evaluation::NotEqual(value) => value != &self.get_display_value(),
132                Evaluation::IsEmpty => false,
133            };
134
135            dependency_state.update_evaluation(&id, value);
136        }
137
138        None
139    }
140
141    fn help(&self) -> Segment {
142        Text::new_styled(
143            if self.toggle_value {
144                self.text_prompt.to_string()
145            } else {
146                self.prompt.to_string()
147            },
148            help_style(),
149        )
150        .as_segment()
151    }
152
153    fn drawer(&self) -> Option<DrawerContents> {
154        None
155    }
156
157    fn result(&self, _dependency_state: &DependencyState) -> String {
158        if self.omit_if_no && !self.toggle_value {
159            return String::new();
160        }
161
162        format!("{}: {}\n", self.prefix, self.get_display_value())
163    }
164
165    fn add_to(self, form: &mut Form) {
166        form.add_step(Box::new(self));
167    }
168}