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::new(
130                io::ErrorKind::Other,
131                format!(
132                    "Invalid terminal {:?}. Both width and height must be larger than 0",
133                    self.size
134                ),
135            ))
136        } else {
137            Ok(())
138        }
139    }
140
141    fn init(&mut self) -> io::Result<()> {
142        self.backend.init()?;
143        self.base_row = self.backend.get_cursor_pos()?.1;
144        self.render()
145    }
146
147    fn adjust_scrollback(&mut self, height: u16) -> io::Result<u16> {
148        let th = self.size.height;
149
150        let mut base_row = self.base_row;
151
152        if self.base_row > th.saturating_sub(height) {
153            let dist = self.base_row - th.saturating_sub(height);
154            base_row -= dist;
155            self.backend.scroll(-(dist as i16))?;
156            self.backend.move_cursor(MoveDirection::Up(dist))?;
157        }
158
159        Ok(base_row)
160    }
161
162    fn flush(&mut self) -> io::Result<()> {
163        if !self.backend.hide_cursor {
164            let (x, y) = self.prompt.cursor_pos(self.layout());
165
166            if self.render_overflow && y >= self.size.height - 1 {
167                // If the height of the prompt exceeds the height of the terminal a cut-off message
168                // is displayed at the bottom. If the cursor is positioned on this cut-off, then we
169                // hide it.
170                if !self.backend.cursor_hidden {
171                    self.backend.cursor_hidden = true;
172                    self.backend.hide_cursor()?;
173                }
174            } else if self.backend.cursor_hidden {
175                // Otherwise, the cursor should be visible, and currently is not. So, we show it.
176                self.backend.cursor_hidden = false;
177                self.backend.show_cursor()?;
178            }
179
180            self.backend.move_cursor_to(x, y)?;
181        }
182        self.backend.flush()
183    }
184
185    fn render_cutoff_msg(&mut self) -> io::Result<()> {
186        let cross = crate::symbols::current().cross;
187        self.backend.set_fg(crate::style::Color::DarkGrey)?;
188        write!(
189            self.backend,
190            "{0} the window height is too small, the prompt has been cut-off {0}",
191            cross
192        )?;
193        self.backend.set_fg(crate::style::Color::Reset)
194    }
195
196    fn render(&mut self) -> io::Result<()> {
197        self.update_size()?;
198        let height = self.prompt.height(&mut self.layout());
199        self.base_row = self.adjust_scrollback(height)?;
200        self.clear()?;
201
202        self.prompt.render(&mut self.layout(), &mut *self.backend)?;
203        self.render_overflow = height > self.size.height;
204
205        if self.render_overflow {
206            self.backend.move_cursor_to(0, self.size.height - 1)?;
207            self.render_cutoff_msg()?;
208        }
209
210        self.flush()
211    }
212
213    fn clear(&mut self) -> io::Result<()> {
214        self.backend.move_cursor_to(0, self.base_row)?;
215        self.backend.clear(ClearType::FromCursorDown)
216    }
217
218    fn goto_last_line(&mut self, height: u16) -> io::Result<()> {
219        self.base_row = self.adjust_scrollback(height + 1)?;
220        self.backend.move_cursor_to(0, self.base_row + height)
221    }
222
223    fn print_error(&mut self, mut e: P::ValidateErr) -> io::Result<()> {
224        self.update_size()?;
225        let height = self.prompt.height(&mut self.layout());
226        self.base_row = self.adjust_scrollback(height + 1)?;
227        self.clear()?;
228        self.prompt.render(&mut self.layout(), &mut *self.backend)?;
229
230        self.goto_last_line(height)?;
231
232        let mut layout = Layout::new(2, self.size).with_offset(0, self.base_row + height);
233        let err_height = e.height(&mut layout.clone());
234        self.base_row = self.adjust_scrollback(height + err_height)?;
235
236        if self.render_overflow {
237            self.backend
238                .move_cursor_to(0, self.size.height - err_height - 1)?;
239            self.backend.clear(ClearType::FromCursorDown)?;
240            self.render_cutoff_msg()?;
241            self.backend
242                .move_cursor_to(0, self.size.height - err_height)?;
243        }
244
245        self.backend
246            .write_styled(&crate::symbols::current().cross.red())?;
247        self.backend.write_all(b" ")?;
248
249        e.render(&mut layout, &mut *self.backend)?;
250
251        self.flush()
252    }
253
254    fn exit(&mut self) -> io::Result<()> {
255        self.update_size()?;
256        let height = self.prompt.height(&mut self.layout());
257        self.goto_last_line(height)?;
258        self.backend.reset()
259    }
260
261    /// Display the prompt and process events until the user presses `Enter`.
262    ///
263    /// After the user presses `Enter`, [`validate`](Prompt::validate) will be called.
264    pub fn run<E>(mut self, events: &mut E) -> error::Result<Option<P::Output>>
265    where
266        E: EventIterator,
267    {
268        self.init()?;
269
270        loop {
271            let e = events.next_event()?;
272
273            let key_handled = match e.code {
274                KeyCode::Char('c') if e.modifiers.contains(KeyModifiers::CONTROL) => {
275                    self.exit()?;
276                    return Err(error::ErrorKind::Interrupted);
277                }
278                KeyCode::Null => {
279                    self.exit()?;
280                    return Err(error::ErrorKind::Eof);
281                }
282                KeyCode::Esc if self.on_esc == OnEsc::Terminate => {
283                    self.exit()?;
284                    return Err(error::ErrorKind::Aborted);
285                }
286                KeyCode::Esc if self.on_esc == OnEsc::SkipQuestion => {
287                    self.clear()?;
288                    self.backend.reset()?;
289
290                    return Ok(None);
291                }
292                KeyCode::Enter => match self.prompt.validate() {
293                    Ok(Validation::Finish) => {
294                        self.clear()?;
295                        self.backend.reset()?;
296
297                        return Ok(Some(self.prompt.finish()));
298                    }
299                    Ok(Validation::Continue) => true,
300                    Err(e) => {
301                        self.print_error(e)?;
302
303                        continue;
304                    }
305                },
306                _ => self.prompt.handle_key(e),
307            };
308
309            if key_handled {
310                self.render()?;
311            }
312        }
313    }
314}
315
316#[derive(Debug)]
317struct TerminalState<B: Backend> {
318    backend: B,
319    hide_cursor: bool,
320    cursor_hidden: bool,
321    enabled: bool,
322}
323
324impl<B: Backend> TerminalState<B> {
325    fn new(backend: B, hide_cursor: bool) -> Self {
326        Self {
327            backend,
328            enabled: false,
329            hide_cursor,
330            cursor_hidden: false,
331        }
332    }
333
334    fn init(&mut self) -> io::Result<()> {
335        self.enabled = true;
336        if self.hide_cursor && !self.cursor_hidden {
337            self.backend.hide_cursor()?;
338            self.cursor_hidden = true;
339        }
340        self.backend.enable_raw_mode()
341    }
342
343    fn reset(&mut self) -> io::Result<()> {
344        self.enabled = false;
345        if self.cursor_hidden {
346            self.backend.show_cursor()?;
347            self.cursor_hidden = false;
348        }
349        self.backend.disable_raw_mode()
350    }
351}
352
353impl<B: Backend> Drop for TerminalState<B> {
354    fn drop(&mut self) {
355        if self.enabled {
356            let _ = self.reset();
357        }
358    }
359}
360
361impl<B: Backend> Deref for TerminalState<B> {
362    type Target = B;
363
364    fn deref(&self) -> &Self::Target {
365        &self.backend
366    }
367}
368
369impl<B: Backend> DerefMut for TerminalState<B> {
370    fn deref_mut(&mut self) -> &mut Self::Target {
371        &mut self.backend
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::{backend::TestBackend, events::TestEvents};
379
380    #[derive(Debug, Default, Clone, Copy)]
381    struct TestPrompt {
382        height: u16,
383    }
384
385    impl Widget for TestPrompt {
386        fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> io::Result<()> {
387            for i in 0..self.height(layout) {
388                // Not the most efficient but this is a test, and it makes assertions easier
389                backend.write_all(format!("Line {}", i).as_bytes())?;
390                backend.move_cursor(MoveDirection::NextLine(1))?;
391            }
392            Ok(())
393        }
394
395        fn height(&mut self, layout: &mut Layout) -> u16 {
396            layout.offset_y += self.height;
397            self.height
398        }
399
400        fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
401            layout.offset_cursor((0, self.height))
402        }
403
404        fn handle_key(&mut self, key: crate::events::KeyEvent) -> bool {
405            todo!("{:?}", key)
406        }
407    }
408
409    impl Prompt for TestPrompt {
410        type ValidateErr = &'static str;
411
412        type Output = ();
413
414        fn finish(self) -> Self::Output {}
415    }
416
417    #[test]
418    fn test_hide_cursor() {
419        let mut backend = TestBackend::new((100, 20).into());
420        let mut backend = Input::new(TestPrompt::default(), &mut backend)
421            .hide_cursor()
422            .backend;
423
424        backend.init().unwrap();
425
426        crate::assert_backend_snapshot!(*backend);
427    }
428
429    #[test]
430    fn test_adjust_scrollback() {
431        let prompt = TestPrompt::default();
432        let size = (100, 20).into();
433
434        let mut backend = TestBackend::new(size);
435        backend.move_cursor_to(0, 14).unwrap();
436
437        assert_eq!(
438            Input {
439                prompt,
440                on_esc: OnEsc::Ignore,
441                backend: TerminalState::new(&mut backend, false),
442                base_row: 14,
443                size,
444                render_overflow: false,
445            }
446            .adjust_scrollback(3)
447            .unwrap(),
448            14
449        );
450
451        crate::assert_backend_snapshot!(backend);
452
453        assert_eq!(
454            Input {
455                prompt,
456                on_esc: OnEsc::Ignore,
457                backend: TerminalState::new(&mut backend, false),
458                base_row: 14,
459                size,
460                render_overflow: false,
461            }
462            .adjust_scrollback(6)
463            .unwrap(),
464            14
465        );
466        crate::assert_backend_snapshot!(backend);
467
468        assert_eq!(
469            Input {
470                prompt,
471                on_esc: OnEsc::Ignore,
472                backend: TerminalState::new(&mut backend, false),
473                base_row: 14,
474                size,
475                render_overflow: false,
476            }
477            .adjust_scrollback(10)
478            .unwrap(),
479            10
480        );
481        crate::assert_backend_snapshot!(backend);
482    }
483
484    #[test]
485    fn test_render() {
486        let prompt = TestPrompt { height: 5 };
487        let size = (100, 20).into();
488        let mut backend = TestBackend::new(size);
489        backend.move_cursor_to(0, 5).unwrap();
490
491        assert!(Input {
492            prompt,
493            on_esc: OnEsc::Ignore,
494            backend: TerminalState::new(&mut backend, false),
495            size,
496            base_row: 5,
497            render_overflow: false,
498        }
499        .render()
500        .is_ok());
501
502        crate::assert_backend_snapshot!(backend);
503    }
504
505    #[test]
506    fn test_goto_last_line() {
507        let size = (100, 20).into();
508        let mut backend = TestBackend::new(size);
509        backend.move_cursor_to(0, 15).unwrap();
510
511        let mut input = Input {
512            prompt: TestPrompt::default(),
513            on_esc: OnEsc::Ignore,
514            backend: TerminalState::new(&mut backend, false),
515            size,
516            base_row: 15,
517            render_overflow: false,
518        };
519
520        assert!(input.goto_last_line(9).is_ok());
521        assert_eq!(input.base_row, 10);
522        drop(input);
523
524        crate::assert_backend_snapshot!(backend);
525    }
526
527    #[test]
528    fn test_print_error() {
529        let error = "error text";
530        let size = (100, 20).into();
531        let mut backend = TestBackend::new(size);
532
533        assert!(Input {
534            prompt: TestPrompt { height: 5 },
535            on_esc: OnEsc::Ignore,
536            backend: TerminalState::new(&mut backend, true),
537            base_row: 0,
538            size,
539            render_overflow: false,
540        }
541        .print_error(error)
542        .is_ok());
543
544        crate::assert_backend_snapshot!(backend);
545    }
546
547    #[test]
548    fn test_zero_size() {
549        let mut backend = TestBackend::new((20, 0).into());
550        let err = Input::new(TestPrompt::default(), &mut backend)
551            .run(&mut TestEvents::new([]))
552            .expect_err("zero size should error");
553
554        let err = match err {
555            crate::ErrorKind::IoError(err) => err,
556            err => panic!("expected io error, got {:?}", err),
557        };
558
559        assert_eq!(err.kind(), io::ErrorKind::Other);
560        assert_eq!(
561            format!("{}", err),
562            "Invalid terminal Size { width: 20, height: 0 }. Both width and height must be larger than 0"
563        );
564
565        let mut backend = TestBackend::new((0, 20).into());
566        let err = Input::new(TestPrompt::default(), &mut backend)
567            .run(&mut TestEvents::new([]))
568            .expect_err("zero size should error");
569
570        let err = match err {
571            crate::ErrorKind::IoError(err) => err,
572            err => panic!("expected io error, got {:?}", err),
573        };
574
575        assert_eq!(err.kind(), io::ErrorKind::Other);
576        assert_eq!(
577            format!("{}", err),
578            "Invalid terminal Size { width: 0, height: 20 }. Both width and height must be larger than 0"
579        );
580    }
581}