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