1#![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 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}