node_launchpad/
tui.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use 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}