1#![allow(dead_code)] use 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(); 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 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, }
137 Some(Err(_)) => Event::Error,
138 None => break, },
140 };
141 if event_tx.send(event).is_err() {
142 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}