1use 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 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}