englog/
tui.rs

1use anyhow::Result;
2use crossterm::{
3    event::{
4        DisableMouseCapture, EnableMouseCapture, Event as CrossTermEvent, KeyEvent, KeyEventKind,
5    },
6    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use futures::FutureExt;
9use futures::StreamExt;
10use ratatui::backend::CrosstermBackend;
11use std::io::{self, Stdout};
12use std::time::Duration;
13use tokio::{
14    sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
15    task::JoinHandle,
16};
17use tokio_util::sync::CancellationToken;
18
19use crate::app::App;
20
21type CrosstermTerminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
22
23#[derive(Clone, Copy, Debug)]
24pub enum Loading {
25    Saving(bool),
26    Loading(bool),
27}
28
29#[derive(Clone, Copy, Debug)]
30pub enum Event {
31    Tick,
32    Key(KeyEvent),
33    Loading(Loading),
34    // True means switch to edit screen for current day
35    LoadDays(bool),
36}
37
38pub struct Tui {
39    terminal: CrosstermTerminal,
40    pub event_rx: UnboundedReceiver<Event>,
41    pub event_tx: UnboundedSender<Event>,
42    cancellation_token: CancellationToken,
43    task: JoinHandle<()>,
44}
45
46impl Tui {
47    pub fn new(terminal: CrosstermTerminal) -> Self {
48        let (event_tx, event_rx) = mpsc::unbounded_channel();
49        Self {
50            terminal,
51            cancellation_token: CancellationToken::new(),
52            event_rx,
53            event_tx,
54            task: tokio::spawn(async {}),
55        }
56    }
57
58    pub fn start(&mut self) {
59        let tick_delay = std::time::Duration::from_secs_f64(1.0 / 4.0);
60        self.cancel();
61        self.cancellation_token = CancellationToken::new();
62        let _cancellation_token = self.cancellation_token.clone();
63        let _event_tx = self.event_tx.clone();
64        self.task = tokio::spawn(async move {
65            let mut reader = crossterm::event::EventStream::new();
66            let mut tick_interval = tokio::time::interval(tick_delay);
67            _event_tx
68                .send(Event::LoadDays(true))
69                .expect("Failed to load events");
70            loop {
71                let tick_delay = tick_interval.tick();
72                let crossterm_event = reader.next().fuse();
73                tokio::select! {
74                    _ = _cancellation_token.cancelled() => {
75                        break;
76                    }
77                    maybe_event = crossterm_event => {
78                        if let Some(Ok(CrossTermEvent::Key(key))) = maybe_event {
79                            if key.kind == KeyEventKind::Press {
80                                _event_tx.send(Event::Key(key)).unwrap();
81                            }
82                        }
83                    },
84                    _ = tick_delay => {
85                        _event_tx.send(Event::Tick).unwrap();
86                    },
87                }
88            }
89        });
90    }
91
92    pub fn enter(&mut self) -> Result<()> {
93        terminal::enable_raw_mode()?;
94        crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?;
95
96        // Define a custom panic hook to reset the terminal properties.
97        // This way, you won't have your terminal messed up if an unexpected error happens.
98        let original_hook = std::panic::take_hook();
99        std::panic::set_hook(Box::new(move |panic_info| {
100            crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)
101                .unwrap();
102            crossterm::terminal::disable_raw_mode().unwrap();
103            original_hook(panic_info);
104        }));
105
106        self.terminal.hide_cursor()?;
107        self.terminal.clear()?;
108        self.start();
109        Ok(())
110    }
111
112    pub async fn next(&mut self) -> Option<Event> {
113        self.event_rx.recv().await
114    }
115
116    pub fn stop(&self) -> Result<()> {
117        self.cancel();
118        let mut counter = 0;
119        while !self.task.is_finished() {
120            std::thread::sleep(Duration::from_millis(1));
121            counter += 1;
122            if counter > 50 {
123                self.task.abort();
124            }
125            if counter > 100 {
126                break;
127            }
128        }
129        Ok(())
130    }
131
132    pub fn cancel(&self) {
133        self.cancellation_token.cancel();
134    }
135
136    fn reset() -> Result<()> {
137        terminal::disable_raw_mode()?;
138        crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?;
139        Ok(())
140    }
141
142    pub fn exit(&mut self) -> Result<()> {
143        self.stop()?;
144        Self::reset()?;
145        self.terminal.show_cursor()?;
146        Ok(())
147    }
148}
149
150impl Tui {
151    pub fn draw(&mut self, app: &mut App) -> Result<()> {
152        self.terminal.draw(|f| crate::ui::ui(f, app))?;
153        Ok(())
154    }
155}