Skip to main content

bee_tui/
tui.rs

1#![allow(dead_code)] // Remove this once you start using the code
2
3use std::{
4    io::{Stdout, stdout},
5    ops::{Deref, DerefMut},
6    time::Duration,
7};
8
9use crossterm::{
10    cursor,
11    event::{
12        DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
13        Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind, MouseEvent,
14    },
15    terminal::{EnterAlternateScreen, LeaveAlternateScreen},
16};
17use futures::{FutureExt, StreamExt};
18use ratatui::backend::CrosstermBackend as Backend;
19use serde::{Deserialize, Serialize};
20use tokio::{
21    sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
22    task::JoinHandle,
23    time::interval,
24};
25use tokio_util::sync::CancellationToken;
26use tracing::error;
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub enum Event {
30    Init,
31    Quit,
32    Error,
33    Closed,
34    Tick,
35    Render,
36    FocusGained,
37    FocusLost,
38    Paste(String),
39    Key(KeyEvent),
40    Mouse(MouseEvent),
41    Resize(u16, u16),
42}
43
44pub struct Tui {
45    pub terminal: ratatui::Terminal<Backend<Stdout>>,
46    pub task: JoinHandle<()>,
47    pub cancellation_token: CancellationToken,
48    pub event_rx: UnboundedReceiver<Event>,
49    pub event_tx: UnboundedSender<Event>,
50    pub frame_rate: f64,
51    pub tick_rate: f64,
52    pub mouse: bool,
53    pub paste: bool,
54}
55
56impl Tui {
57    pub fn new() -> color_eyre::Result<Self> {
58        let (event_tx, event_rx) = mpsc::unbounded_channel();
59        Ok(Self {
60            terminal: ratatui::Terminal::new(Backend::new(stdout()))?,
61            task: tokio::spawn(async {}),
62            cancellation_token: CancellationToken::new(),
63            event_rx,
64            event_tx,
65            frame_rate: 60.0,
66            tick_rate: 4.0,
67            mouse: false,
68            paste: false,
69        })
70    }
71
72    pub fn tick_rate(mut self, tick_rate: f64) -> Self {
73        self.tick_rate = tick_rate;
74        self
75    }
76
77    pub fn frame_rate(mut self, frame_rate: f64) -> Self {
78        self.frame_rate = frame_rate;
79        self
80    }
81
82    pub fn mouse(mut self, mouse: bool) -> Self {
83        self.mouse = mouse;
84        self
85    }
86
87    pub fn paste(mut self, paste: bool) -> Self {
88        self.paste = paste;
89        self
90    }
91
92    pub fn start(&mut self) {
93        self.cancel(); // Cancel any existing task
94        self.cancellation_token = CancellationToken::new();
95        let event_loop = Self::event_loop(
96            self.event_tx.clone(),
97            self.cancellation_token.clone(),
98            self.tick_rate,
99            self.frame_rate,
100        );
101        self.task = tokio::spawn(async {
102            event_loop.await;
103        });
104    }
105
106    async fn event_loop(
107        event_tx: UnboundedSender<Event>,
108        cancellation_token: CancellationToken,
109        tick_rate: f64,
110        frame_rate: f64,
111    ) {
112        let mut event_stream = EventStream::new();
113        let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate));
114        let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate));
115
116        // if this fails, then it's likely a bug in the calling code
117        event_tx
118            .send(Event::Init)
119            .expect("failed to send init event");
120        loop {
121            let event = tokio::select! {
122                _ = cancellation_token.cancelled() => {
123                    break;
124                }
125                _ = tick_interval.tick() => Event::Tick,
126                _ = render_interval.tick() => Event::Render,
127                crossterm_event = event_stream.next().fuse() => match crossterm_event {
128                    Some(Ok(event)) => match event {
129                        CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key),
130                        CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse),
131                        CrosstermEvent::Resize(x, y) => Event::Resize(x, y),
132                        CrosstermEvent::FocusLost => Event::FocusLost,
133                        CrosstermEvent::FocusGained => Event::FocusGained,
134                        CrosstermEvent::Paste(s) => Event::Paste(s),
135                        _ => continue, // ignore other events
136                    }
137                    Some(Err(_)) => Event::Error,
138                    None => break, // the event stream has stopped and will not produce any more events
139                },
140            };
141            if event_tx.send(event).is_err() {
142                // the receiver has been dropped, so there's no point in continuing the loop
143                break;
144            }
145        }
146        cancellation_token.cancel();
147    }
148
149    pub fn stop(&self) -> color_eyre::Result<()> {
150        self.cancel();
151        let mut counter = 0;
152        while !self.task.is_finished() {
153            std::thread::sleep(Duration::from_millis(1));
154            counter += 1;
155            if counter > 50 {
156                self.task.abort();
157            }
158            if counter > 100 {
159                error!("Failed to abort task in 100 milliseconds for unknown reason");
160                break;
161            }
162        }
163        Ok(())
164    }
165
166    pub fn enter(&mut self) -> color_eyre::Result<()> {
167        crossterm::terminal::enable_raw_mode()?;
168        crossterm::execute!(stdout(), EnterAlternateScreen, cursor::Hide)?;
169        if self.mouse {
170            crossterm::execute!(stdout(), EnableMouseCapture)?;
171        }
172        if self.paste {
173            crossterm::execute!(stdout(), EnableBracketedPaste)?;
174        }
175        self.start();
176        Ok(())
177    }
178
179    pub fn exit(&mut self) -> color_eyre::Result<()> {
180        self.stop()?;
181        if crossterm::terminal::is_raw_mode_enabled()? {
182            self.flush()?;
183            if self.paste {
184                crossterm::execute!(stdout(), DisableBracketedPaste)?;
185            }
186            if self.mouse {
187                crossterm::execute!(stdout(), DisableMouseCapture)?;
188            }
189            crossterm::execute!(stdout(), LeaveAlternateScreen, cursor::Show)?;
190            crossterm::terminal::disable_raw_mode()?;
191        }
192        Ok(())
193    }
194
195    pub fn cancel(&self) {
196        self.cancellation_token.cancel();
197    }
198
199    pub fn suspend(&mut self) -> color_eyre::Result<()> {
200        self.exit()?;
201        #[cfg(not(windows))]
202        signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
203        Ok(())
204    }
205
206    pub fn resume(&mut self) -> color_eyre::Result<()> {
207        self.enter()?;
208        Ok(())
209    }
210
211    pub async fn next_event(&mut self) -> Option<Event> {
212        self.event_rx.recv().await
213    }
214}
215
216impl Deref for Tui {
217    type Target = ratatui::Terminal<Backend<Stdout>>;
218
219    fn deref(&self) -> &Self::Target {
220        &self.terminal
221    }
222}
223
224impl DerefMut for Tui {
225    fn deref_mut(&mut self) -> &mut Self::Target {
226        &mut self.terminal
227    }
228}
229
230impl Drop for Tui {
231    fn drop(&mut self) {
232        self.exit().unwrap();
233    }
234}