tracexec_tui/
lib.rs

1// Copyright (c) 2023 Ratatui Developers
2// Copyright (c) 2024 Levi Zim
3
4// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5// associated documentation files (the "Software"), to deal in the Software without restriction,
6// including without limitation the rights to use, copy, modify, merge, publish, distribute,
7// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9
10// The above copyright notice and this permission notice shall be included in all copies or substantial
11// portions of the Software.
12
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
16// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
17// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
19use std::{
20  ops::{
21    Deref,
22    DerefMut,
23  },
24  time::Duration,
25};
26
27use color_eyre::eyre::Result;
28use crossterm::{
29  cursor,
30  event::{
31    Event as CrosstermEvent,
32    KeyEventKind,
33  },
34  terminal::{
35    EnterAlternateScreen,
36    LeaveAlternateScreen,
37  },
38};
39use futures::{
40  FutureExt,
41  StreamExt,
42};
43use ratatui::backend::CrosstermBackend as Backend;
44use tokio::{
45  sync::mpsc::{
46    self,
47    UnboundedReceiver,
48    UnboundedSender,
49  },
50  task::JoinHandle,
51};
52use tokio_util::sync::CancellationToken;
53use tracexec_core::event::{
54  Event,
55  TracerMessage,
56};
57use tracing::{
58  error,
59  trace,
60};
61
62pub mod action;
63pub mod app;
64pub mod backtrace_popup;
65mod breakpoint_manager;
66pub mod copy_popup;
67pub mod details_popup;
68pub mod error_popup;
69mod event;
70pub mod event_line;
71mod event_list;
72pub mod help;
73mod hit_manager;
74mod output;
75mod partial_line;
76mod pseudo_term;
77pub mod query;
78mod sized_paragraph;
79pub mod theme;
80mod ui;
81
82pub use event::TracerEventDetailsTuiExt;
83
84pub struct Tui {
85  pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
86  pub task: JoinHandle<()>,
87  pub cancellation_token: CancellationToken,
88  pub event_rx: UnboundedReceiver<Event>,
89  pub event_tx: UnboundedSender<Event>,
90  pub frame_rate: f64,
91}
92
93pub fn init_tui() -> Result<()> {
94  crossterm::terminal::enable_raw_mode()?;
95  crossterm::execute!(std::io::stdout(), EnterAlternateScreen, cursor::Hide)?;
96  Ok(())
97}
98
99pub fn restore_tui() -> Result<()> {
100  crossterm::execute!(std::io::stdout(), LeaveAlternateScreen, cursor::Show)?;
101  crossterm::terminal::disable_raw_mode()?;
102  Ok(())
103}
104
105impl Tui {
106  pub fn new() -> Result<Self> {
107    let frame_rate = 30.0;
108    let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
109    let (event_tx, event_rx) = mpsc::unbounded_channel();
110    let cancellation_token = CancellationToken::new();
111    let task = tokio::spawn(async {});
112    Ok(Self {
113      terminal,
114      task,
115      cancellation_token,
116      event_rx,
117      event_tx,
118      frame_rate,
119    })
120  }
121
122  pub fn frame_rate(mut self, frame_rate: f64) -> Self {
123    self.frame_rate = frame_rate;
124    self
125  }
126
127  pub fn start(&mut self, mut tracer_rx: UnboundedReceiver<TracerMessage>) {
128    let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
129    self.cancel();
130    self.cancellation_token = CancellationToken::new();
131    let _cancellation_token = self.cancellation_token.clone();
132    let event_tx = self.event_tx.clone();
133    self.task = tokio::spawn(async move {
134      let mut reader = crossterm::event::EventStream::new();
135      let mut render_interval = tokio::time::interval(render_delay);
136      event_tx.send(Event::Init).unwrap();
137      loop {
138        let render_delay = render_interval.tick();
139        let crossterm_event = reader.next().fuse();
140        let tracer_event = tracer_rx.recv();
141        tokio::select! {
142          biased;
143          () = _cancellation_token.cancelled() => {
144            break;
145          }
146          Some(event) = crossterm_event => {
147            #[cfg(debug_assertions)]
148            trace!("TUI event: crossterm event {event:?}!");
149            match event {
150              Ok(evt) => {
151                match evt {
152                  CrosstermEvent::Key(key) => {
153                      if key.kind == KeyEventKind::Press {
154                          event_tx.send(Event::Key(key)).unwrap();
155                      }
156                  },
157                  CrosstermEvent::Resize(cols, rows) => {
158                      event_tx.send(Event::Resize{
159                          width: cols,
160                          height: rows,
161                      }).unwrap();
162                  },
163                  _ => {},
164                }
165              }
166              Err(_) => {
167                event_tx.send(Event::Error).unwrap();
168              }
169            }
170          },
171          Some(tracer_event) = tracer_event => {
172            trace!("TUI event: tracer message!");
173            event_tx.send(Event::Tracer(tracer_event)).unwrap();
174          }
175          _ = render_delay => {
176            // log::trace!("TUI event: Render!");
177            event_tx.send(Event::Render).unwrap();
178          },
179        }
180      }
181    });
182  }
183
184  pub fn stop(&self) -> Result<()> {
185    self.cancel();
186    let mut counter = 0;
187    while !self.task.is_finished() {
188      std::thread::sleep(Duration::from_millis(1));
189      counter += 1;
190      if counter > 50 {
191        self.task.abort();
192      }
193      if counter > 100 {
194        error!("Failed to abort task in 100 milliseconds for unknown reason");
195        break;
196      }
197    }
198    Ok(())
199  }
200
201  pub fn enter(&mut self, tracer_rx: UnboundedReceiver<TracerMessage>) -> Result<()> {
202    init_tui()?;
203    self.start(tracer_rx);
204    Ok(())
205  }
206
207  pub fn exit(&mut self) -> Result<()> {
208    self.stop()?;
209    if crossterm::terminal::is_raw_mode_enabled()? {
210      self.flush()?;
211      restore_tui()?;
212    }
213    Ok(())
214  }
215
216  pub fn cancel(&self) {
217    self.cancellation_token.cancel();
218  }
219
220  pub async fn next(&mut self) -> Option<Event> {
221    self.event_rx.recv().await
222  }
223}
224
225impl Deref for Tui {
226  type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
227
228  fn deref(&self) -> &Self::Target {
229    &self.terminal
230  }
231}
232
233impl DerefMut for Tui {
234  fn deref_mut(&mut self) -> &mut Self::Target {
235    &mut self.terminal
236  }
237}
238
239impl Drop for Tui {
240  fn drop(&mut self) {
241    self.exit().unwrap();
242  }
243}