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
19#![allow(clippy::future_not_send)]
20
21use std::{
22  ops::{
23    Deref,
24    DerefMut,
25  },
26  time::Duration,
27};
28
29use color_eyre::eyre::Result;
30use crossterm::{
31  cursor,
32  event::{
33    Event as CrosstermEvent,
34    KeyEventKind,
35  },
36  terminal::{
37    EnterAlternateScreen,
38    LeaveAlternateScreen,
39  },
40};
41use futures::{
42  FutureExt,
43  StreamExt,
44};
45use ratatui::backend::CrosstermBackend as Backend;
46use tokio::{
47  sync::mpsc::{
48    self,
49    UnboundedReceiver,
50    UnboundedSender,
51  },
52  task::JoinHandle,
53};
54use tokio_util::sync::CancellationToken;
55use tracexec_core::event::{
56  Event,
57  TracerMessage,
58};
59use tracing::{
60  error,
61  trace,
62};
63
64pub mod action;
65pub mod app;
66pub mod backtrace_popup;
67mod breakpoint_manager;
68pub mod copy_popup;
69pub mod details_popup;
70pub mod error_popup;
71mod event;
72pub mod event_line;
73mod event_list;
74pub mod help;
75mod hit_manager;
76mod output;
77mod partial_line;
78mod pseudo_term;
79pub mod query;
80mod sized_paragraph;
81pub mod theme;
82mod ui;
83
84pub use event::TracerEventDetailsTuiExt;
85
86pub struct Tui {
87  pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
88  pub task: JoinHandle<()>,
89  pub cancellation_token: CancellationToken,
90  pub event_rx: UnboundedReceiver<Event>,
91  pub event_tx: UnboundedSender<Event>,
92  pub frame_rate: f64,
93}
94
95pub fn init_tui() -> Result<()> {
96  crossterm::terminal::enable_raw_mode()?;
97  crossterm::execute!(std::io::stdout(), EnterAlternateScreen, cursor::Hide)?;
98  Ok(())
99}
100
101pub fn restore_tui() -> Result<()> {
102  crossterm::execute!(std::io::stdout(), LeaveAlternateScreen, cursor::Show)?;
103  crossterm::terminal::disable_raw_mode()?;
104  Ok(())
105}
106
107impl Tui {
108  pub fn new() -> Result<Self> {
109    let frame_rate = 30.0;
110    let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
111    let (event_tx, event_rx) = mpsc::unbounded_channel();
112    let cancellation_token = CancellationToken::new();
113    let task = tokio::spawn(async {});
114    Ok(Self {
115      terminal,
116      task,
117      cancellation_token,
118      event_rx,
119      event_tx,
120      frame_rate,
121    })
122  }
123
124  pub fn frame_rate(mut self, frame_rate: f64) -> Self {
125    self.frame_rate = frame_rate;
126    self
127  }
128
129  pub fn start(&mut self, mut tracer_rx: UnboundedReceiver<TracerMessage>) {
130    let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
131    self.cancel();
132    self.cancellation_token = CancellationToken::new();
133    let _cancellation_token = self.cancellation_token.clone();
134    let event_tx = self.event_tx.clone();
135    self.task = tokio::spawn(async move {
136      let mut reader = crossterm::event::EventStream::new();
137      let mut render_interval = tokio::time::interval(render_delay);
138      event_tx.send(Event::Init).unwrap();
139      loop {
140        let render_delay = render_interval.tick();
141        let crossterm_event = reader.next().fuse();
142        let tracer_event = tracer_rx.recv();
143        tokio::select! {
144          biased;
145          () = _cancellation_token.cancelled() => {
146            break;
147          }
148          Some(event) = crossterm_event => {
149            #[cfg(debug_assertions)]
150            trace!("TUI event: crossterm event {event:?}!");
151            match event {
152              Ok(evt) => {
153                match evt {
154                  CrosstermEvent::Key(key) => {
155                      if key.kind == KeyEventKind::Press {
156                          event_tx.send(Event::Key(key)).unwrap();
157                      }
158                  },
159                  CrosstermEvent::Resize(cols, rows) => {
160                      event_tx.send(Event::Resize{
161                          width: cols,
162                          height: rows,
163                      }).unwrap();
164                  },
165                  _ => {},
166                }
167              }
168              Err(_) => {
169                event_tx.send(Event::Error).unwrap();
170              }
171            }
172          },
173          Some(tracer_event) = tracer_event => {
174            trace!("TUI event: tracer message!");
175            event_tx.send(Event::Tracer(tracer_event)).unwrap();
176          }
177          _ = render_delay => {
178            // log::trace!("TUI event: Render!");
179            event_tx.send(Event::Render).unwrap();
180          },
181        }
182      }
183    });
184  }
185
186  pub fn stop(&self) -> Result<()> {
187    self.cancel();
188    let mut counter = 0;
189    while !self.task.is_finished() {
190      std::thread::sleep(Duration::from_millis(1));
191      counter += 1;
192      if counter > 50 {
193        self.task.abort();
194      }
195      if counter > 100 {
196        error!("Failed to abort task in 100 milliseconds for unknown reason");
197        break;
198      }
199    }
200    Ok(())
201  }
202
203  pub fn enter(&mut self, tracer_rx: UnboundedReceiver<TracerMessage>) -> Result<()> {
204    init_tui()?;
205    self.start(tracer_rx);
206    Ok(())
207  }
208
209  pub fn exit(&mut self) -> Result<()> {
210    self.stop()?;
211    if crossterm::terminal::is_raw_mode_enabled()? {
212      self.flush()?;
213      restore_tui()?;
214    }
215    Ok(())
216  }
217
218  pub fn cancel(&self) {
219    self.cancellation_token.cancel();
220  }
221
222  pub async fn next(&mut self) -> Option<Event> {
223    self.event_rx.recv().await
224  }
225}
226
227impl Deref for Tui {
228  type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
229
230  fn deref(&self) -> &Self::Target {
231    &self.terminal
232  }
233}
234
235impl DerefMut for Tui {
236  fn deref_mut(&mut self) -> &mut Self::Target {
237    &mut self.terminal
238  }
239}
240
241impl Drop for Tui {
242  fn drop(&mut self) {
243    self.exit().unwrap();
244  }
245}