requestty_ui/
input.rs

1use std::{
2    io,
3    ops::{Deref, DerefMut},
4};
5
6use super::Widget;
7use crate::{
8    backend::{Backend, ClearType, MoveDirection, Size},
9    error,
10    events::{EventIterator, KeyCode, KeyModifiers},
11    layout::Layout,
12    style::Stylize,
13};
14
15/// The state of a prompt on validation.
16///
17/// See [`Prompt::validate`]
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Validation {
20    /// If the prompt is ready to finish.
21    Finish,
22    /// If the state is valid, but the prompt should still persist.
23    ///
24    /// Unlike returning an Err, this will not show an error and is a way for the prompt to progress
25    /// its internal state machine.
26    Continue,
27}
28
29/// What to do after receiving `Esc`
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum OnEsc {
32    /// Stop asking the `PromptModule`. Similar effect to `Ctrl+C` except it can be distinguished in
33    /// case different behaviour is needed
34    Terminate,
35    /// Skip the current question and move on to the next question. The question will not be asked
36    /// again.
37    SkipQuestion,
38    /// Pressing `Esc` will not do anything, and will be ignored. This is the default behaviour.
39    Ignore,
40}
41
42/// This trait should be implemented by all 'root' widgets.
43///
44/// It provides the functionality required only by the main controlling widget. For the trait
45/// required for general rendering to terminal, see [`Widget`].
46pub trait Prompt: Widget {
47    /// The error type returned by validate. It can be any widget and the [render cycle] is guaranteed
48    /// to be called only once.
49    ///
50    /// [render cycle]: widgets/trait.Widget.html#render-cycle
51    type ValidateErr: Widget;
52
53    /// The output type returned by [`Input::run`]
54    type Output;
55
56    /// Determine whether the prompt state is ready to be submitted. It is called whenever the user
57    /// presses the enter key.
58    ///
59    /// See [`Validation`]
60    fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
61        Ok(Validation::Finish)
62    }
63    /// The value to return from [`Input::run`]. This will only be called once validation returns
64    /// [`Validation::Finish`]
65    fn finish(self) -> Self::Output;
66}
67
68/// A ui runner which implements the [render cycle].
69///
70/// It renders and processes events with the help of a type that implements [`Prompt`].
71///
72/// See [`run`](Input::run) for more information
73///
74/// [render cycle]: widgets/trait.Widget.html#render-cycle
75#[derive(Debug)]
76pub struct Input<P, B: Backend> {
77    prompt: P,
78    on_esc: OnEsc,
79    backend: TerminalState<B>,
80    base_row: u16,
81    size: Size,
82    render_overflow: bool,
83}
84
85impl<P, B: Backend> Input<P, B> {
86    #[allow(clippy::new_ret_no_self)]
87    /// Creates a new `Input`. This won't do anything until it is [run](Input::run).
88    pub fn new(prompt: P, backend: &mut B) -> Input<P, &mut B> {
89        // The method doesn't return self directly, as its always used with a `&mut B`,
90        // and this tells the compiler that it doesn't need to consume the `&mut B`, but
91        // once the Input has been dropped, it can be used again
92        Input {
93            prompt,
94            on_esc: OnEsc::Ignore,
95            backend: TerminalState::new(backend, false),
96            base_row: 0,
97            size: Size::default(),
98            render_overflow: false,
99        }
100    }
101
102    /// Hides the cursor while running the input. This won't do anything until it is [run](Input::run).
103    pub fn hide_cursor(mut self) -> Self {
104        self.backend.hide_cursor = true;
105        self
106    }
107
108    /// What to do after receiving a `Esc`.
109    ///
110    /// For [`OnEsc::Terminate`] - an [`Error::Aborted`](error::ErrorKind::Aborted) will be returned.
111    /// For [`OnEsc::SkipQuestion`] - the currently shown prompt will be cleared, and `Ok(None)`
112    /// will be returned.
113    /// For [`OnEsc::Ignore`] - no special behaviour will be applied to the `Esc` key. Like other
114    /// keys, the `Esc` key will be passed to the prompt to handle.
115    pub fn on_esc(mut self, on_esc: OnEsc) -> Self {
116        self.on_esc = on_esc;
117        self
118    }
119}
120
121impl<P: Prompt, B: Backend> Input<P, B> {
122    fn layout(&self) -> Layout {
123        Layout::new(0, self.size).with_offset(0, self.base_row)
124    }
125
126    fn update_size(&mut self) -> io::Result<()> {
127        self.size = self.backend.size()?;
128        if self.size.area() == 0 {
129            Err(io::Error::other(format!(
130                "Invalid terminal {:?}. Both width and height must be larger than 0",
131                self.size
132            )))
133        } else {
134            Ok(())
135        }
136    }
137
138    fn init(&mut self) -> io::Result<()> {
139        self.backend.init()?;
140        self.base_row = self.backend.get_cursor_pos()?.1;
141        self.render()
142    }
143
144    fn adjust_scrollback(&mut self, height: u16) -> io::Result<u16> {
145        let th = self.size.height;
146
147        let mut base_row = self.base_row;
148
149        if self.base_row > th.saturating_sub(height) {
150            let dist = self.base_row - th.saturating_sub(height);
151            base_row -= dist;
152            self.backend.scroll(-(dist as i16))?;
153            self.backend.move_cursor(MoveDirection::Up(dist))?;
154        }
155
156        Ok(base_row)
157    }
158
159    fn flush(&mut self) -> io::Result<()> {
160        if !self.backend.hide_cursor {
161            let (x, y) = self.prompt.cursor_pos(self.layout());
162
163            if self.render_overflow && y >= self.size.height - 1 {
164                // If the height of the prompt exceeds the height of the terminal a cut-off message
165                // is displayed at the bottom. If the cursor is positioned on this cut-off, then we
166                // hide it.
167                if !self.backend.cursor_hidden {
168                    self.backend.cursor_hidden = true;
169                    self.backend.hide_cursor()?;
170                }
171            } else if self.backend.cursor_hidden {
172                // Otherwise, the cursor should be visible, and currently is not. So, we show it.
173                self.backend.cursor_hidden = false;
174                self.backend.show_cursor()?;
175            }
176
177            self.backend.move_cursor_to(x, y)?;
178        }
179        self.backend.flush()
180    }
181
182    fn render_cutoff_msg(&mut self) -> io::Result<()> {
183        let cross = crate::symbols::current().cross;
184        self.backend.set_fg(crate::style::Color::DarkGrey)?;
185        write!(
186            self.backend,
187            "{0} the window height is too small, the prompt has been cut-off {0}",
188            cross
189        )?;
190        self.backend.set_fg(crate::style::Color::Reset)
191    }
192
193    fn render(&mut self) -> io::Result<()> {
194        self.update_size()?;
195        let height = self.prompt.height(&mut self.layout());
196        self.base_row = self.adjust_scrollback(height)?;
197        self.clear()?;
198
199        self.prompt.render(&mut self.layout(), &mut *self.backend)?;
200        self.render_overflow = height > self.size.height;
201
202        if self.render_overflow {
203            self.backend.move_cursor_to(0, self.size.height - 1)?;
204            self.render_cutoff_msg()?;
205        }
206
207        self.flush()
208    }
209
210    fn clear(&mut self) -> io::Result<()> {
211        self.backend.move_cursor_to(0, self.base_row)?;
212        self.backend.clear(ClearType::FromCursorDown)
213    }
214
215    fn goto_last_line(&mut self, height: u16) -> io::Result<()> {
216        self.base_row = self.adjust_scrollback(height + 1)?;
217        self.backend.move_cursor_to(0, self.base_row + height)
218    }
219
220    fn print_error(&mut self, mut e: P::ValidateErr) -> io::Result<()> {
221        self.update_size()?;
222        let height = self.prompt.height(&mut self.layout());
223        self.base_row = self.adjust_scrollback(height + 1)?;
224        self.clear()?;
225        self.prompt.render(&mut self.layout(), &mut *self.backend)?;
226
227        self.goto_last_line(height)?;
228
229        let mut layout = Layout::new(2, self.size).with_offset(0, self.base_row + height);
230        let err_height = e.height(&mut layout.clone());
231        self.base_row = self.adjust_scrollback(height + err_height)?;
232
233        if self.render_overflow {
234            self.backend
235                .move_cursor_to(0, self.size.height - err_height - 1)?;
236            self.backend.clear(ClearType::FromCursorDown)?;
237            self.render_cutoff_msg()?;
238            self.backend
239                .move_cursor_to(0, self.size.height - err_height)?;
240        }
241
242        self.backend
243            .write_styled(&crate::symbols::current().cross.red())?;
244        self.backend.write_all(b" ")?;
245
246        e.render(&mut layout, &mut *self.backend)?;
247
248        self.flush()
249    }
250
251    fn exit(&mut self) -> io::Result<()> {
252        self.update_size()?;
253        let height = self.prompt.height(&mut self.layout());
254        self.goto_last_line(height)?;
255        self.backend.reset()
256    }
257
258    /// Display the prompt and process events until the user presses `Enter`.
259    ///
260    /// After the user presses `Enter`, [`validate`](Prompt::validate) will be called.
261    pub fn run<E>(mut self, events: &mut E) -> error::Result<Option<P::Output>>
262    where
263        E: EventIterator,
264    {
265        self.init()?;
266
267        loop {
268            let e = events.next_event()?;
269
270            let key_handled = match e.code {
271                KeyCode::Char('c') if e.modifiers.contains(KeyModifiers::CONTROL) => {
272                    self.exit()?;
273                    return Err(error::ErrorKind::Interrupted);
274                }
275                KeyCode::Null => {
276                    self.exit()?;
277                    return Err(error::ErrorKind::Eof);
278                }
279                KeyCode::Esc if self.on_esc == OnEsc::Terminate => {
280                    self.exit()?;
281                    return Err(error::ErrorKind::Aborted);
282                }
283                KeyCode::Esc if self.on_esc == OnEsc::SkipQuestion => {
284                    self.clear()?;
285                    self.backend.reset()?;
286
287                    return Ok(None);
288                }
289                KeyCode::Enter => match self.prompt.validate() {
290                    Ok(Validation::Finish) => {
291                        self.clear()?;
292                        self.backend.reset()?;
293
294                        return Ok(Some(self.prompt.finish()));
295                    }
296                    Ok(Validation::Continue) => true,
297                    Err(e) => {
298                        self.print_error(e)?;
299
300                        continue;
301                    }
302                },
303                _ => self.prompt.handle_key(e),
304            };
305
306            if key_handled {
307                self.render()?;
308            }
309        }
310    }
311}
312
313#[derive(Debug)]
314struct TerminalState<B: Backend> {
315    backend: B,
316    hide_cursor: bool,
317    cursor_hidden: bool,
318    enabled: bool,
319}
320
321impl<B: Backend> TerminalState<B> {
322    fn new(backend: B, hide_cursor: bool) -> Self {
323        Self {
324            backend,
325            enabled: false,
326            hide_cursor,
327            cursor_hidden: false,
328        }
329    }
330
331    fn init(&mut self) -> io::Result<()> {
332        self.enabled = true;
333        if self.hide_cursor && !self.cursor_hidden {
334            self.backend.hide_cursor()?;
335            self.cursor_hidden = true;
336        }
337        self.backend.enable_raw_mode()
338    }
339
340    fn reset(&mut self) -> io::Result<()> {
341        self.enabled = false;
342        if self.cursor_hidden {
343            self.backend.show_cursor()?;
344            self.cursor_hidden = false;
345        }
346        self.backend.disable_raw_mode()
347    }
348}
349
350impl<B: Backend> Drop for TerminalState<B> {
351    fn drop(&mut self) {
352        if self.enabled {
353            let _ = self.reset();
354        }
355    }
356}
357
358impl<B: Backend> Deref for TerminalState<B> {
359    type Target = B;
360
361    fn deref(&self) -> &Self::Target {
362        &self.backend
363    }
364}
365
366impl<B: Backend> DerefMut for TerminalState<B> {
367    fn deref_mut(&mut self) -> &mut Self::Target {
368        &mut self.backend
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::{backend::TestBackend, events::TestEvents};
376
377    #[derive(Debug, Default, Clone, Copy)]
378    struct TestPrompt {
379        height: u16,
380    }
381
382    impl Widget for TestPrompt {
383        fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> io::Result<()> {
384            for i in 0..self.height(layout) {
385                // Not the most efficient but this is a test, and it makes assertions easier
386                backend.write_all(format!("Line {}", i).as_bytes())?;
387                backend.move_cursor(MoveDirection::NextLine(1))?;
388            }
389            Ok(())
390        }
391
392        fn height(&mut self, layout: &mut Layout) -> u16 {
393            layout.offset_y += self.height;
394            self.height
395        }
396
397        fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
398            layout.offset_cursor((0, self.height))
399        }
400
401        fn handle_key(&mut self, key: crate::events::KeyEvent) -> bool {
402            todo!("{:?}", key)
403        }
404    }
405
406    impl Prompt for TestPrompt {
407        type ValidateErr = &'static str;
408
409        type Output = ();
410
411        fn finish(self) -> Self::Output {}
412    }
413
414    #[test]
415    fn test_hide_cursor() {
416        let mut backend = TestBackend::new((100, 20).into());
417        let mut backend = Input::new(TestPrompt::default(), &mut backend)
418            .hide_cursor()
419            .backend;
420
421        backend.init().unwrap();
422
423        crate::assert_backend_snapshot!(*backend);
424    }
425
426    #[test]
427    fn test_adjust_scrollback() {
428        let prompt = TestPrompt::default();
429        let size = (100, 20).into();
430
431        let mut backend = TestBackend::new(size);
432        backend.move_cursor_to(0, 14).unwrap();
433
434        assert_eq!(
435            Input {
436                prompt,
437                on_esc: OnEsc::Ignore,
438                backend: TerminalState::new(&mut backend, false),
439                base_row: 14,
440                size,
441                render_overflow: false,
442            }
443            .adjust_scrollback(3)
444            .unwrap(),
445            14
446        );
447
448        crate::assert_backend_snapshot!(backend);
449
450        assert_eq!(
451            Input {
452                prompt,
453                on_esc: OnEsc::Ignore,
454                backend: TerminalState::new(&mut backend, false),
455                base_row: 14,
456                size,
457                render_overflow: false,
458            }
459            .adjust_scrollback(6)
460            .unwrap(),
461            14
462        );
463        crate::assert_backend_snapshot!(backend);
464
465        assert_eq!(
466            Input {
467                prompt,
468                on_esc: OnEsc::Ignore,
469                backend: TerminalState::new(&mut backend, false),
470                base_row: 14,
471                size,
472                render_overflow: false,
473            }
474            .adjust_scrollback(10)
475            .unwrap(),
476            10
477        );
478        crate::assert_backend_snapshot!(backend);
479    }
480
481    #[test]
482    fn test_render() {
483        let prompt = TestPrompt { height: 5 };
484        let size = (100, 20).into();
485        let mut backend = TestBackend::new(size);
486        backend.move_cursor_to(0, 5).unwrap();
487
488        assert!(Input {
489            prompt,
490            on_esc: OnEsc::Ignore,
491            backend: TerminalState::new(&mut backend, false),
492            size,
493            base_row: 5,
494            render_overflow: false,
495        }
496        .render()
497        .is_ok());
498
499        crate::assert_backend_snapshot!(backend);
500    }
501
502    #[test]
503    fn test_goto_last_line() {
504        let size = (100, 20).into();
505        let mut backend = TestBackend::new(size);
506        backend.move_cursor_to(0, 15).unwrap();
507
508        let mut input = Input {
509            prompt: TestPrompt::default(),
510            on_esc: OnEsc::Ignore,
511            backend: TerminalState::new(&mut backend, false),
512            size,
513            base_row: 15,
514            render_overflow: false,
515        };
516
517        assert!(input.goto_last_line(9).is_ok());
518        assert_eq!(input.base_row, 10);
519        drop(input);
520
521        crate::assert_backend_snapshot!(backend);
522    }
523
524    #[test]
525    fn test_print_error() {
526        let error = "error text";
527        let size = (100, 20).into();
528        let mut backend = TestBackend::new(size);
529
530        assert!(Input {
531            prompt: TestPrompt { height: 5 },
532            on_esc: OnEsc::Ignore,
533            backend: TerminalState::new(&mut backend, true),
534            base_row: 0,
535            size,
536            render_overflow: false,
537        }
538        .print_error(error)
539        .is_ok());
540
541        crate::assert_backend_snapshot!(backend);
542    }
543
544    #[test]
545    fn test_zero_size() {
546        let mut backend = TestBackend::new((20, 0).into());
547        let err = Input::new(TestPrompt::default(), &mut backend)
548            .run(&mut TestEvents::new([]))
549            .expect_err("zero size should error");
550
551        let err = match err {
552            crate::ErrorKind::IoError(err) => err,
553            err => panic!("expected io error, got {:?}", err),
554        };
555
556        assert_eq!(err.kind(), io::ErrorKind::Other);
557        assert_eq!(
558            format!("{}", err),
559            "Invalid terminal Size { width: 20, height: 0 }. Both width and height must be larger than 0"
560        );
561
562        let mut backend = TestBackend::new((0, 20).into());
563        let err = Input::new(TestPrompt::default(), &mut backend)
564            .run(&mut TestEvents::new([]))
565            .expect_err("zero size should error");
566
567        let err = match err {
568            crate::ErrorKind::IoError(err) => err,
569            err => panic!("expected io error, got {:?}", err),
570        };
571
572        assert_eq!(err.kind(), io::ErrorKind::Other);
573        assert_eq!(
574            format!("{}", err),
575            "Invalid terminal Size { width: 0, height: 20 }. Both width and height must be larger than 0"
576        );
577    }
578}