mfform_lib/
form.rs

1use crossterm::{
2    cursor,
3    event::{Event, KeyCode},
4    style,
5    terminal::{self, ClearType},
6    QueueableCommand,
7};
8use log::debug;
9use std::{
10    cmp::Ordering,
11    io::{self, Stdout, Write},
12};
13
14use crate::{
15    app::{EventHandlerResult, EventResult},
16    input::{Input, Select},
17    label::Label,
18    pos::Pos,
19    select_form::SelectForm,
20};
21
22/// Normal input form
23#[derive(Debug, Clone)]
24pub struct Form {
25    pub(crate) labels: Vec<Label>,
26    pub(crate) inputs: Vec<Input>,
27    pub(crate) current_pos: Pos,
28    pub(crate) size: Pos,
29    pub(crate) select_form: Option<SelectForm>,
30}
31
32impl Form {
33    /// Construct a Form object, given the input dimensions.
34    pub fn new(size: impl Into<Pos>) -> io::Result<Self> {
35        Ok(Self {
36            labels: Default::default(),
37            inputs: Default::default(),
38            current_pos: (0, 0).into(),
39            size: size.into(),
40            select_form: None,
41        })
42    }
43
44    // Input handling
45    pub(crate) fn event_handler(&mut self, event: &Event) -> io::Result<EventHandlerResult> {
46        // Popup input handling
47        if let Some(select_form) = self.select_form.as_mut() {
48            let result = select_form.event_handler(event)?;
49            match result {
50                EventHandlerResult::Handled(EventResult::Submit) => {
51                    let selected = select_form.get_selection();
52                    if select_form.select_type == Select::Single {
53                        debug!("Selected: {:?}", selected);
54                        assert!(
55                            selected.len() <= 1,
56                            "Single should only return none or one item"
57                        );
58                    }
59                    self.select_form = None;
60                    if let Some(f) = self.current_field() {
61                        f.value = selected
62                            .first()
63                            .map(|s| s.as_str())
64                            .unwrap_or_default()
65                            .to_string();
66                    };
67                    self.current_pos = self
68                        .current_field()
69                        .map(|f| f.pos)
70                        .unwrap_or(self.current_pos);
71                    return Ok(EventHandlerResult::Handled(EventResult::None));
72                }
73                EventHandlerResult::Handled(EventResult::Abort) => {
74                    debug!("Aborted");
75                    self.select_form = None;
76                    return Ok(EventHandlerResult::Handled(EventResult::None));
77                }
78                _ => return Ok(result),
79            };
80        }
81
82        let mut current_pos = self.current_pos;
83
84        if let Some(current_field) = self.current_field() {
85            if let EventHandlerResult::Handled(result) =
86                current_field.event_handler(event, &mut current_pos)?
87            {
88                self.current_pos = current_pos;
89                return Ok(EventHandlerResult::Handled(result));
90            }
91        }
92
93        match event {
94            Event::Key(k) if k.code == KeyCode::Esc => {
95                return Ok(EventHandlerResult::Handled(EventResult::Abort));
96            }
97            Event::Key(k) if k.code == KeyCode::Enter => {
98                return Ok(EventHandlerResult::Handled(EventResult::Submit));
99            }
100            Event::Key(k) if k.code == KeyCode::Left => {
101                self.move_event(k.code);
102            }
103            Event::Key(k) if k.code == KeyCode::Right => {
104                self.move_event(k.code);
105            }
106            Event::Key(k) if k.code == KeyCode::Up => {
107                self.move_event(k.code);
108            }
109            Event::Key(k) if k.code == KeyCode::Down => {
110                self.move_event(k.code);
111            }
112            Event::Key(k) if k.code == KeyCode::Tab => {
113                self.next_input();
114            }
115            Event::Key(k) if k.code == KeyCode::BackTab => {
116                self.prev_input();
117            }
118            Event::Key(k) if k.code == KeyCode::F(4) => {
119                debug!("Display select form");
120                let Some(current_field) = self.current_field() else {
121                    return Ok(EventHandlerResult::Handled(EventResult::None));
122                };
123
124                if current_field.select == Select::Single {
125                    let mut select_form =
126                        SelectForm::new(&current_field.select_static, (80, 24), Select::Single)?;
127                    select_form.display(&mut std::io::stdout())?;
128
129                    self.select_form = Some(select_form);
130                }
131            }
132            _ => return Ok(EventHandlerResult::NotHandled),
133        }
134
135        Ok(EventHandlerResult::Handled(EventResult::None))
136    }
137
138    pub(crate) fn display(&mut self, stdout: &mut Stdout) -> io::Result<()> {
139        if let Some(select_form) = self.select_form.as_mut() {
140            return select_form.display(stdout);
141        }
142
143        // Clear dialog
144        stdout
145            .queue(cursor::MoveTo(self.size.x, self.size.y))?
146            .queue(terminal::Clear(ClearType::FromCursorUp))?;
147
148        // Border
149        stdout.queue(style::SetForegroundColor(style::Color::DarkGreen))?;
150        for y in 0..24 {
151            stdout
152                .queue(cursor::MoveTo(80, y))?
153                .queue(style::Print('│'))?;
154        }
155        stdout
156            .queue(cursor::MoveTo(0, 24))?
157            .queue(style::Print("─".repeat(80)))?
158            .queue(style::Print('┘'))?;
159
160        stdout
161            .queue(cursor::MoveTo(2, 24))?
162            .queue(style::Print(" Esc=Abort "))?
163            .queue(style::Print('─'))?
164            .queue(style::Print(" Enter=Submit "))?;
165
166        if self
167            .inputs
168            .iter()
169            .find(|i| i.has_focus(self.current_pos))
170            .filter(|i| i.select != Select::None)
171            .is_some()
172        {
173            stdout
174                .queue(cursor::MoveTo(82 - 6 - 10, 24))?
175                .queue(style::SetForegroundColor(style::Color::DarkGreen))?
176                .queue(style::Print(" F4 - Select "))?;
177        }
178
179        for label in self.labels.clone() {
180            stdout
181                .queue(cursor::MoveTo(label.pos.x, label.pos.y))?
182                .queue(style::SetForegroundColor(style::Color::White))?
183                .queue(style::Print(label.text))?;
184        }
185
186        for input in self.inputs.clone() {
187            display_generic(stdout, &input)?;
188        }
189
190        stdout.queue(cursor::MoveTo(self.current_pos.x, self.current_pos.y))?;
191        stdout.queue(cursor::SetCursorStyle::SteadyUnderScore)?;
192
193        stdout.flush()
194    }
195
196    pub(crate) fn move_event(&mut self, code: KeyCode) {
197        self.current_pos = match code {
198            KeyCode::Left => Pos {
199                x: self.current_pos.x.checked_sub(1).unwrap_or_default(),
200                y: self.current_pos.y,
201            },
202            KeyCode::Right => Pos {
203                x: self.current_pos.x + 1,
204                y: self.current_pos.y,
205            },
206            KeyCode::Up => Pos {
207                x: self.current_pos.x,
208                y: self.current_pos.y.checked_sub(1).unwrap_or_default(),
209            },
210            KeyCode::Down => Pos {
211                x: self.current_pos.x,
212                y: self.current_pos.y + 1,
213            },
214            _ => self.current_pos,
215        }
216        .constrain(self.size)
217    }
218
219    pub(crate) fn find_next_input(&mut self) -> Option<Pos> {
220        let mut inputs = self.inputs.clone();
221        inputs.sort();
222
223        let mut i = inputs.iter();
224        let mut first_pos = None;
225
226        loop {
227            // Return None if no input fields
228            let Some(input) = i.next() else {
229                return first_pos;
230            };
231
232            // Set first pos if none set
233            if first_pos.is_none() {
234                first_pos = Some(input.pos);
235            }
236
237            match self.current_pos.cmp(&input.pos) {
238                // Current pos is before the input pos
239                Ordering::Less => {
240                    return Some(input.pos);
241                }
242
243                // We are on the first character if an input field
244                // We should return the next input field
245                Ordering::Equal => {
246                    if let Some(next) = i.next() {
247                        // There is a next field available, return it's pos
248                        return Some(next.pos);
249                    } else {
250                        // No next field, wrap around to first_pos
251                        return first_pos;
252                    }
253                }
254                _ => (),
255            }
256        }
257    }
258
259    pub(crate) fn find_prev_input(&mut self) -> Option<Pos> {
260        let mut inputs = self.inputs.clone();
261        inputs.sort();
262
263        let mut i = inputs.iter().rev();
264
265        loop {
266            // Return None if no input fields
267            let Some(input) = i.next() else {
268                debug!("Returning last");
269                return inputs.last().map(|w| w.pos);
270            };
271
272            debug!("Comparing {:?} to {:?}", self.current_pos, input.pos);
273
274            match self.current_pos.cmp(&input.pos) {
275                // Current pos is before the input pos
276                Ordering::Greater => {
277                    return Some(input.pos);
278                }
279
280                // We are on the first character if an input field
281                // We should return the next input field
282                Ordering::Equal => {
283                    if let Some(next) = i.next() {
284                        // There is a next field available, return it's pos
285                        return Some(next.pos);
286                    } else {
287                        // No next field, wrap around to first_pos
288                        return inputs.iter().map(|w| w.pos).last();
289                    }
290                }
291                _ => (),
292            }
293        }
294    }
295
296    /// Move cursor to next input
297    pub fn next_input(&mut self) {
298        if let Some(pos) = self.find_next_input() {
299            self.current_pos = pos;
300        }
301    }
302
303    /// Move cursor to previous input
304    pub fn prev_input(&mut self) {
305        if let Some(pos) = self.find_prev_input() {
306            self.current_pos = pos;
307        }
308    }
309
310    /// Get input field under cursor
311    pub fn current_field(&mut self) -> Option<&mut Input> {
312        self.inputs
313            .iter_mut()
314            .find(|f| f.has_focus(self.current_pos))
315    }
316
317    /// Add text label to form at specified position with supplied text
318    #[allow(dead_code)]
319    pub fn add_text(mut self, pos: impl Into<Pos>, text: impl Into<String>) -> Self {
320        self.labels.push(Label::new_label(pos, text));
321
322        self
323    }
324
325    /// Add a label to the form, label must be created beforehand
326    pub fn add_label(&mut self, label: Label) {
327        self.labels.push(label);
328    }
329
330    /// Add an input to the form, label must be created beforehand
331    pub fn add_input(&mut self, input: Input) {
332        self.inputs.push(input);
333    }
334
335    /// Add select option to an imput field in the form.
336    ///
337    /// Adding an option to a field enables the 'Select (F4)' function on
338    /// the input field.  This in effect changes the input field to have
339    /// SingleSelct behavior.  Options will be displayed in the order they
340    /// are added.
341    pub fn add_select(&mut self, input: String, id: String, value: String) {
342        let input = self.inputs.iter_mut().find(|i| i.name == input);
343
344        if let Some(input) = input {
345            if input.select == Select::None {
346                input.select = Select::Single;
347            }
348
349            input.select_static.push((id, value));
350            debug!("List: {:?}", input.select_static);
351        } else {
352            panic!("Input not found");
353        }
354    }
355
356    /// Place the cursor on the next available input, or on 0,0 if no inputs
357    /// are present.
358    pub fn place_cursor(mut self) -> Self {
359        self.current_pos = self.find_next_input().unwrap_or((0, 0).into());
360
361        self
362    }
363
364    #[allow(dead_code)]
365    fn get_input(&self, field_name: &'static str) -> Option<String> {
366        self.inputs.iter().find_map(|input| {
367            if input.name == field_name {
368                Some(input.value.to_string())
369            } else {
370                None
371            }
372        })
373    }
374
375    /// Return an array of input field name and values
376    pub fn get_field_and_data(&self) -> Vec<(&str, &str)> {
377        let mut output = Vec::new();
378
379        for input in &self.inputs {
380            output.push((input.name.as_str(), input.value.as_str()));
381        }
382
383        output
384    }
385}
386
387fn display_string(stdout: &mut Stdout, input: &Input) -> io::Result<()> {
388    stdout
389        .queue(cursor::MoveTo(input.pos.x, input.pos.y))?
390        .queue(style::SetAttribute(style::Attribute::Underlined))?;
391    for i in 0..(input.length as usize) {
392        match (
393            input.value.chars().nth(i),
394            input.default_value.chars().nth(i),
395        ) {
396            (Some(s), Some(d)) if s != d => {
397                stdout
398                    .queue(style::SetForegroundColor(style::Color::DarkRed))?
399                    .queue(style::Print(s))?;
400            }
401            (Some(s), Some(_)) => {
402                stdout
403                    .queue(style::SetForegroundColor(style::Color::DarkGreen))?
404                    .queue(style::Print(s))?;
405            }
406            (Some(s), None) => {
407                stdout
408                    .queue(style::SetForegroundColor(style::Color::DarkRed))?
409                    .queue(style::Print(s))?;
410            }
411            _ => {
412                stdout
413                    .queue(style::SetForegroundColor(style::Color::DarkGreen))?
414                    .queue(style::Print(" "))?;
415            }
416        }
417    }
418
419    stdout.queue(style::SetAttribute(style::Attribute::NoUnderline))?;
420
421    Ok(())
422}
423
424fn display_password(stdout: &mut Stdout, input: &Input) -> io::Result<()> {
425    let pass_len = input.value.chars().count();
426
427    // We only get called if there is a mask_char
428    let mask_char = input.mask_char.unwrap();
429    stdout
430        .queue(cursor::MoveTo(input.pos.x, input.pos.y))?
431        .queue(style::SetAttribute(style::Attribute::Underlined))?
432        .queue(style::SetForegroundColor(style::Color::DarkRed))?
433        .queue(style::Print(mask_char.to_string().repeat(pass_len)))?
434        .queue(style::SetForegroundColor(style::Color::DarkGreen))?
435        .queue(style::Print(" ".repeat(input.length as usize - pass_len)))?
436        .queue(style::SetAttribute(style::Attribute::NoUnderline))?;
437
438    Ok(())
439}
440
441fn display_generic(stdout: &mut Stdout, input: &Input) -> io::Result<()> {
442    if input.mask_char.is_some() {
443        display_password(stdout, input)?;
444    } else {
445        display_string(stdout, input)?;
446    }
447
448    Ok(())
449}