tty_form/
form.rs

1use crossterm::event::{Event, KeyCode, KeyModifiers};
2use tty_interface::{pos, Interface, Position};
3
4use crate::{
5    dependency::DependencyState,
6    device::InputDevice,
7    step::{InputResult, Step},
8    utility::render_segment,
9    Error, Result,
10};
11
12/// A TTY-based form with multiple steps and inputs.
13///
14/// # Examples
15/// ```
16/// # use tty_interface::{Interface, test::VirtualDevice};
17/// # use tty_form::{Error, test::VirtualInputDevice};
18/// # let mut device = VirtualDevice::new();
19/// # let mut interface = Interface::new_relative(&mut device)?;
20/// # let mut stdin = VirtualInputDevice;
21/// use tty_form::{
22///     Form,
23///     step::{Step, CompoundStep, TextBlockStep},
24///     control::{Control, TextInput},
25/// };
26///
27/// let mut form = Form::new();
28///
29/// let mut name_step = CompoundStep::new();
30/// TextInput::new("Enter a name:", false).add_to(&mut name_step);
31/// name_step.add_to(&mut form);
32///
33/// TextBlockStep::new("Enter a description of this person:").add_to(&mut form);
34///
35/// let submission = form.execute(&mut interface, &mut stdin)?;
36/// # Ok::<(), Error>(())
37/// ```
38pub struct Form {
39    steps: Vec<Box<dyn Step>>,
40
41    /// The currently-focused step.
42    active_step: usize,
43
44    /// The furthest step the user has reached so far.
45    max_step: usize,
46
47    /// The last render's height.
48    last_height: u16,
49}
50
51impl Default for Form {
52    /// Create a new, default terminal form.
53    fn default() -> Self {
54        Self {
55            steps: Vec::new(),
56            active_step: 0,
57            max_step: 0,
58            last_height: 0,
59        }
60    }
61}
62
63impl Form {
64    /// Create a new, default terminal form.
65    pub fn new() -> Form {
66        Self::default()
67    }
68
69    /// Append and return a compound step with multiple component controls.
70    pub fn add_step(&mut self, step: Box<dyn Step>) {
71        self.steps.push(step);
72    }
73
74    /// Execute the provided form and return its WYSIWYG result.
75    pub fn execute<D: InputDevice>(
76        mut self,
77        interface: &mut Interface,
78        input_device: &mut D,
79    ) -> Result<String> {
80        let mut dependency_state = DependencyState::new();
81
82        for (step_index, step) in self.steps.iter_mut().enumerate() {
83            step.initialize(&mut dependency_state, step_index);
84        }
85
86        self.render_form(interface, &dependency_state);
87        interface.apply()?;
88
89        loop {
90            interface.set_cursor(None);
91
92            if let Event::Key(key_event) = input_device.read()? {
93                if (KeyModifiers::CONTROL, KeyCode::Char('c'))
94                    == (key_event.modifiers, key_event.code)
95                {
96                    return self.cancel_form(interface, &dependency_state);
97                }
98
99                if let Some(action) =
100                    self.steps[self.active_step].update(&mut dependency_state, key_event)
101                {
102                    match action {
103                        InputResult::AdvanceForm => {
104                            if self.advance() {
105                                break;
106                            }
107                        }
108                        InputResult::RetreatForm => {
109                            if self.retreat() {
110                                return self.cancel_form(interface, &dependency_state);
111                            }
112                        }
113                    }
114                }
115            }
116
117            self.render_form(interface, &dependency_state);
118            interface.apply()?;
119        }
120
121        self.render_form(interface, &dependency_state);
122        interface.apply()?;
123
124        let mut result = String::new();
125
126        for step in self.steps {
127            result.push_str(&step.result(&dependency_state));
128        }
129
130        result = result.trim().to_string();
131
132        Ok(result)
133    }
134
135    /// Exits the form early by performing a final, unfocused render and returning a cancelation code.
136    fn cancel_form(
137        &mut self,
138        interface: &mut Interface,
139        dependency_state: &DependencyState,
140    ) -> Result<String> {
141        self.active_step = usize::MAX;
142        self.render_form(interface, &dependency_state);
143        interface.apply()?;
144
145        return Err(Error::Canceled);
146    }
147
148    /// Advance the form to its next step. Returns whether we've finished the form.
149    fn advance(&mut self) -> bool {
150        let is_last_step = self.active_step + 1 == self.steps.len();
151        if !is_last_step {
152            self.active_step += 1;
153
154            if self.active_step > self.max_step {
155                self.max_step = self.active_step;
156            }
157        }
158
159        is_last_step
160    }
161
162    /// Retreat the form to its previous step. Returns whether we're at the first step.
163    fn retreat(&mut self) -> bool {
164        let is_first_step = self.active_step == 0;
165        if !is_first_step {
166            self.active_step -= 1;
167        }
168
169        is_first_step
170    }
171
172    /// Re-render the form's updated state.
173    fn render_form(&mut self, interface: &mut Interface, dependency_state: &DependencyState) {
174        for line in 0..self.last_height {
175            interface.clear_line(line);
176        }
177
178        let mut drawer = None;
179        let mut line = 1;
180        for (step_index, step) in self.steps.iter().enumerate() {
181            if step_index > self.max_step {
182                break;
183            }
184
185            let step_height = step.render(
186                interface,
187                dependency_state,
188                pos!(0, line),
189                step_index == self.active_step,
190            );
191
192            line += step_height;
193
194            if step_index == self.active_step {
195                render_segment(interface, pos!(0, 0), step.help());
196                drawer = step.drawer();
197            }
198        }
199
200        if let Some(drawer) = drawer {
201            for item in drawer {
202                render_segment(interface, pos!(0, line), item);
203                line += 1;
204            }
205        }
206
207        self.last_height = line;
208    }
209}