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 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 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}