1use color_eyre::eyre::Result;
6use crossterm::{
7 event::{DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream},
8 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
9 ExecutableCommand,
10};
11use futures::StreamExt;
12use ratatui::{backend::CrosstermBackend, Terminal};
13use std::io::{self, Stdout};
14use tokio::{
15 sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
16 task::JoinHandle,
17 time::{self, Duration},
18};
19use tokio_util::sync::CancellationToken;
20
21#[derive(Debug, Clone)]
23pub enum Event {
24 Crossterm(CrosstermEvent),
26 Tick,
28 Render,
30 Resize(u16, u16),
32}
33
34pub struct Tui {
36 pub terminal: Terminal<CrosstermBackend<Stdout>>,
38 task: JoinHandle<()>,
40 cancellation_token: CancellationToken,
42 event_rx: UnboundedReceiver<Event>,
44 _event_tx: UnboundedSender<Event>,
46 #[allow(dead_code)]
48 frame_rate: f64,
49 #[allow(dead_code)]
51 tick_rate: f64,
52}
53
54impl Tui {
55 pub fn new() -> Result<Self> {
57 let frame_rate = 60.0; let tick_rate = 4.0; let (event_tx, event_rx) = mpsc::unbounded_channel();
61 let cancellation_token = CancellationToken::new();
62
63 let task = {
65 let event_tx = event_tx.clone();
66 let cancellation_token = cancellation_token.clone();
67 let tick_duration = Duration::from_secs_f64(1.0 / tick_rate);
68 let render_duration = Duration::from_secs_f64(1.0 / frame_rate);
69
70 tokio::spawn(async move {
71 let mut reader = EventStream::new();
72 let mut tick_interval = time::interval(tick_duration);
73 let mut render_interval = time::interval(render_duration);
74
75 loop {
76 tokio::select! {
77 biased;
78
79 _ = cancellation_token.cancelled() => {
80 break;
81 }
82 maybe_event = reader.next() => {
83 match maybe_event {
84 Some(Ok(evt)) => {
85 if let CrosstermEvent::Resize(w, h) = evt {
87 let _ = event_tx.send(Event::Resize(w, h));
88 }
89 let _ = event_tx.send(Event::Crossterm(evt));
90 }
91 Some(Err(_)) => {}
92 None => break,
93 }
94 }
95 _ = tick_interval.tick() => {
96 let _ = event_tx.send(Event::Tick);
97 }
98 _ = render_interval.tick() => {
99 let _ = event_tx.send(Event::Render);
100 }
101 }
102 }
103 })
104 };
105
106 let terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
108
109 Ok(Self {
110 terminal,
111 task,
112 cancellation_token,
113 event_rx,
114 _event_tx: event_tx,
115 frame_rate,
116 tick_rate,
117 })
118 }
119
120 pub fn enter(&mut self) -> Result<()> {
122 enable_raw_mode()?;
123 io::stdout().execute(EnterAlternateScreen)?;
124 io::stdout().execute(EnableMouseCapture)?;
125 self.terminal.hide_cursor()?;
126 self.terminal.clear()?;
127 Ok(())
128 }
129
130 pub fn exit(&mut self) -> Result<()> {
132 self.terminal.show_cursor()?;
133 io::stdout().execute(DisableMouseCapture)?;
134 io::stdout().execute(LeaveAlternateScreen)?;
135 disable_raw_mode()?;
136 Ok(())
137 }
138
139 pub fn cancel(&self) {
141 self.cancellation_token.cancel();
142 }
143
144 pub async fn next(&mut self) -> Option<Event> {
146 self.event_rx.recv().await
147 }
148
149 #[allow(dead_code)]
151 pub fn frame_rate(&self) -> f64 {
152 self.frame_rate
153 }
154
155 #[allow(dead_code)]
157 pub fn tick_rate(&self) -> f64 {
158 self.tick_rate
159 }
160}
161
162impl Drop for Tui {
163 fn drop(&mut self) {
164 self.cancel();
165 let _ = self.exit();
166 self.task.abort();
167 }
168}