entrust_dialog/
dialog.rs

1use ratatui::backend::CrosstermBackend;
2use ratatui::crossterm::event;
3use ratatui::crossterm::event::Event;
4use ratatui::{Frame, Terminal, TerminalOptions, Viewport, crossterm};
5use std::io;
6use std::time::{Duration, Instant};
7
8pub trait Dialog: Sized {
9    type Update;
10    type Output;
11    fn update_for_event(event: Event) -> Option<Self::Update>;
12    fn perform_update(&mut self, update: Self::Update) -> io::Result<()>;
13    fn state(&self) -> DialogState;
14    fn output(self) -> Self::Output;
15    fn viewport(&self) -> Viewport;
16    fn draw(&mut self, frame: &mut Frame);
17    fn run(mut self) -> io::Result<Self::Output> {
18        let backend = CrosstermBackend::new(io::stderr());
19        let mut terminal = Terminal::with_options(
20            backend,
21            TerminalOptions {
22                viewport: self.viewport(),
23            },
24        )?;
25        crossterm::terminal::enable_raw_mode()?;
26        let start = Instant::now();
27        let mut up_to_date = false;
28        let mut timed_out = false;
29        while self.state() == DialogState::Pending {
30            if !up_to_date {
31                terminal.draw(|frame| self.draw(frame))?;
32            }
33
34            let event_available = event::poll(self.tick_rate())?;
35            if self
36                .timeout()
37                .is_some_and(|timeout| start.elapsed() > timeout)
38            {
39                timed_out = true;
40                break;
41            }
42            if !event_available {
43                up_to_date = !self.tick();
44                continue;
45            }
46
47            let event = event::read()?;
48            if let Some(update) = Self::update_for_event(event) {
49                self.perform_update(update)?;
50                up_to_date = false;
51            } else {
52                up_to_date = true;
53            }
54        }
55        terminal.clear()?;
56        let result = if timed_out {
57            Err(io::Error::other("timed out"))
58        } else if self.state() == DialogState::Cancelled {
59            Err(io::Error::other("cancelled"))
60        } else {
61            Ok(self.output())
62        };
63        crossterm::terminal::disable_raw_mode()?;
64        result
65    }
66    fn tick(&mut self) -> bool {
67        false
68    }
69    fn tick_rate(&self) -> Duration {
70        Duration::from_millis(500)
71    }
72    fn timeout(&self) -> Option<Duration> {
73        None
74    }
75}
76
77#[derive(Clone, Copy, Debug, Default, PartialEq)]
78pub enum DialogState {
79    #[default]
80    Pending,
81    Completed,
82    Cancelled,
83}