1#[cfg(feature = "debug_pane")]
2use arboard::Clipboard;
3use color_eyre::config::HookBuilder;
4#[cfg(feature = "debug_pane")]
5use cu_tuimon::MonitorLogCapture;
6pub use cu_tuimon::{
7 MonitorModel, MonitorScreen, MonitorUi, MonitorUiAction, MonitorUiEvent, MonitorUiKey,
8 MonitorUiOptions, ScrollDirection,
9};
10use cu29::context::CuContext;
11use cu29::monitoring::{
12 ComponentId, CopperListIoStats, CopperListView, CuComponentState, CuMonitor,
13 CuMonitoringMetadata, CuMonitoringRuntime, Decision,
14};
15use cu29::{CuError, CuResult};
16use ratatui::backend::CrosstermBackend;
17use ratatui::crossterm::event::{
18 DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEventKind,
19};
20use ratatui::crossterm::terminal::{
21 EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
22};
23use ratatui::crossterm::tty::IsTty;
24use ratatui::crossterm::{event, execute};
25use ratatui::{Terminal, TerminalOptions, Viewport};
26use std::backtrace::Backtrace;
27use std::io::{Write, stdin, stdout};
28use std::process;
29#[cfg(feature = "debug_pane")]
30use std::sync::Mutex;
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::sync::{Arc, OnceLock};
33use std::thread::JoinHandle;
34use std::time::Duration;
35use std::{io, thread};
36
37pub struct CuConsoleMon {
39 model: MonitorModel,
40 ui_handle: Option<JoinHandle<()>>,
41 quitting: Arc<AtomicBool>,
42 #[cfg(feature = "debug_pane")]
43 log_capture: Option<Mutex<MonitorLogCapture>>,
44}
45
46impl CuConsoleMon {
47 pub fn model(&self) -> MonitorModel {
48 self.model.clone()
49 }
50}
51
52impl Drop for CuConsoleMon {
53 fn drop(&mut self) {
54 self.quitting.store(true, Ordering::SeqCst);
55 let _ = restore_terminal();
56 if let Some(handle) = self.ui_handle.take() {
57 let _ = handle.join();
58 }
59 }
60}
61
62struct UI {
63 monitor_ui: MonitorUi,
64 quitting: Arc<AtomicBool>,
65 #[cfg(feature = "debug_pane")]
66 clipboard: Option<Clipboard>,
67}
68
69impl UI {
70 fn new(model: MonitorModel, quitting: Arc<AtomicBool>) -> Self {
71 init_error_hooks();
72 Self {
73 monitor_ui: MonitorUi::new(
74 model,
75 MonitorUiOptions {
76 show_quit_hint: true,
77 },
78 ),
79 quitting,
80 #[cfg(feature = "debug_pane")]
81 clipboard: None,
82 }
83 }
84
85 fn draw(&mut self, frame: &mut ratatui::Frame) {
86 self.monitor_ui.draw(frame);
87 }
88
89 fn handle_action(&mut self, action: MonitorUiAction) -> bool {
90 match action {
91 MonitorUiAction::None => false,
92 MonitorUiAction::QuitRequested => {
93 self.quitting.store(true, Ordering::SeqCst);
94 true
95 }
96 #[cfg(feature = "debug_pane")]
97 MonitorUiAction::CopyLogSelection(text) => {
98 self.copy_text(text);
99 false
100 }
101 }
102 }
103
104 fn handle_key(&mut self, key: KeyCode) -> bool {
105 let action = match key {
106 KeyCode::Char(ch) => {
107 self.monitor_ui
108 .handle_event(MonitorUiEvent::Key(MonitorUiKey::Char(
109 ch.to_ascii_lowercase(),
110 )))
111 }
112 KeyCode::Left => self
113 .monitor_ui
114 .handle_event(MonitorUiEvent::Key(MonitorUiKey::Left)),
115 KeyCode::Right => self
116 .monitor_ui
117 .handle_event(MonitorUiEvent::Key(MonitorUiKey::Right)),
118 KeyCode::Up => self
119 .monitor_ui
120 .handle_event(MonitorUiEvent::Key(MonitorUiKey::Up)),
121 KeyCode::Down => self
122 .monitor_ui
123 .handle_event(MonitorUiEvent::Key(MonitorUiKey::Down)),
124 _ => MonitorUiAction::None,
125 };
126
127 self.handle_action(action)
128 }
129
130 fn handle_mouse_event(&mut self, mouse: event::MouseEvent) {
131 let action = match mouse.kind {
132 MouseEventKind::Down(MouseButton::Left) => {
133 self.monitor_ui.handle_event(MonitorUiEvent::MouseDown {
134 col: mouse.column,
135 row: mouse.row,
136 })
137 }
138 #[cfg(feature = "debug_pane")]
139 MouseEventKind::Drag(MouseButton::Left) => {
140 self.monitor_ui.handle_event(MonitorUiEvent::MouseDrag {
141 col: mouse.column,
142 row: mouse.row,
143 })
144 }
145 #[cfg(feature = "debug_pane")]
146 MouseEventKind::Up(MouseButton::Left) => {
147 self.monitor_ui.handle_event(MonitorUiEvent::MouseUp {
148 col: mouse.column,
149 row: mouse.row,
150 })
151 }
152 MouseEventKind::ScrollDown => self.monitor_ui.handle_event(MonitorUiEvent::Scroll {
153 direction: ScrollDirection::Down,
154 steps: 1,
155 }),
156 MouseEventKind::ScrollUp => self.monitor_ui.handle_event(MonitorUiEvent::Scroll {
157 direction: ScrollDirection::Up,
158 steps: 1,
159 }),
160 MouseEventKind::ScrollLeft => self.monitor_ui.handle_event(MonitorUiEvent::Scroll {
161 direction: ScrollDirection::Left,
162 steps: 5,
163 }),
164 MouseEventKind::ScrollRight => self.monitor_ui.handle_event(MonitorUiEvent::Scroll {
165 direction: ScrollDirection::Right,
166 steps: 5,
167 }),
168 _ => MonitorUiAction::None,
169 };
170
171 let _ = self.handle_action(action);
172 }
173
174 #[cfg(feature = "debug_pane")]
175 fn copy_text(&mut self, text: String) {
176 if text.is_empty() {
177 return;
178 }
179 if self.clipboard.is_none() {
180 match Clipboard::new() {
181 Ok(clipboard) => self.clipboard = Some(clipboard),
182 Err(err) => {
183 eprintln!("CuConsoleMon clipboard init failed: {err}");
184 return;
185 }
186 }
187 }
188 if let Some(clipboard) = self.clipboard.as_mut()
189 && let Err(err) = clipboard.set_text(text)
190 {
191 eprintln!("CuConsoleMon clipboard copy failed: {err}");
192 }
193 }
194
195 fn run_app<B: ratatui::prelude::Backend<Error = io::Error>>(
196 &mut self,
197 terminal: &mut Terminal<B>,
198 ) -> io::Result<()> {
199 loop {
200 if self.quitting.load(Ordering::SeqCst) {
201 break;
202 }
203
204 terminal.draw(|frame| {
205 self.draw(frame);
206 })?;
207
208 if event::poll(Duration::from_millis(50))? {
209 match event::read()? {
210 Event::Key(key) if self.handle_key(key.code) => {
211 break;
212 }
213 Event::Mouse(mouse) => self.handle_mouse_event(mouse),
214 Event::Resize(_, _) => self.monitor_ui.mark_graph_dirty(),
215 _ => {}
216 }
217 }
218 }
219 Ok(())
220 }
221}
222
223impl CuMonitor for CuConsoleMon {
224 fn new(metadata: CuMonitoringMetadata, _runtime: CuMonitoringRuntime) -> CuResult<Self> {
225 Ok(Self {
226 model: MonitorModel::from_metadata(&metadata),
227 ui_handle: None,
228 quitting: Arc::new(AtomicBool::new(false)),
229 #[cfg(feature = "debug_pane")]
230 log_capture: None,
231 })
232 }
233
234 fn observe_copperlist_io(&self, stats: CopperListIoStats) {
235 self.model.observe_copperlist_io(stats);
236 }
237
238 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
239 #[cfg(feature = "debug_pane")]
240 {
241 self.log_capture = Some(Mutex::new(if should_start_ui() {
242 MonitorLogCapture::to_model(self.model.clone())
243 } else {
244 MonitorLogCapture::to_stdout()
245 }));
246 }
247
248 if !should_start_ui() {
249 return Ok(());
250 }
251
252 let model = self.model.clone();
253 let quitting = self.quitting.clone();
254 let handle = thread::spawn(move || {
255 let backend = CrosstermBackend::new(stdout());
256 let _terminal_guard = TerminalRestoreGuard;
257
258 if let Err(err) = setup_terminal() {
259 eprintln!("Failed to prepare terminal UI: {err}");
260 return;
261 }
262
263 let mut terminal = match Terminal::with_options(
264 backend,
265 TerminalOptions {
266 viewport: Viewport::Fullscreen,
267 },
268 ) {
269 Ok(terminal) => terminal,
270 Err(err) => {
271 eprintln!("Failed to initialize terminal backend: {err}");
272 return;
273 }
274 };
275
276 let mut ui = UI::new(model, quitting.clone());
277 if let Err(err) = ui.run_app(&mut terminal) {
278 let _ = restore_terminal();
279 eprintln!("CuConsoleMon UI exited with error: {err}");
280 return;
281 }
282
283 quitting.store(true, Ordering::SeqCst);
284 let _ = restore_terminal();
285 });
286
287 self.ui_handle = Some(handle);
288 Ok(())
289 }
290
291 fn process_copperlist(&self, ctx: &CuContext, view: CopperListView<'_>) -> CuResult<()> {
292 #[cfg(feature = "debug_pane")]
293 if let Some(log_capture) = &self.log_capture {
294 let mut log_capture = log_capture.lock().unwrap_or_else(|err| err.into_inner());
295 log_capture.poll();
296 }
297
298 self.model.process_copperlist(ctx.cl_id(), view);
299 if self.quitting.load(Ordering::SeqCst) {
300 return Err("Exiting...".into());
301 }
302 Ok(())
303 }
304
305 fn process_error(
306 &self,
307 component_id: ComponentId,
308 step: CuComponentState,
309 error: &CuError,
310 ) -> Decision {
311 self.model
312 .set_component_error(component_id, error.to_string());
313 match step {
314 CuComponentState::Start => Decision::Shutdown,
315 CuComponentState::Preprocess => Decision::Abort,
316 CuComponentState::Process => Decision::Ignore,
317 CuComponentState::Postprocess => Decision::Ignore,
318 CuComponentState::Stop => Decision::Shutdown,
319 }
320 }
321
322 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
323 self.quitting.store(true, Ordering::SeqCst);
324 let _ = restore_terminal();
325
326 if let Some(handle) = self.ui_handle.take() {
327 let _ = handle.join();
328 }
329
330 #[cfg(feature = "debug_pane")]
331 {
332 self.log_capture = None;
333 }
334
335 self.model.reset_latency();
336 Ok(())
337 }
338}
339
340struct TerminalRestoreGuard;
341
342impl Drop for TerminalRestoreGuard {
343 fn drop(&mut self) {
344 let _ = restore_terminal();
345 }
346}
347
348fn init_error_hooks() {
349 static ONCE: OnceLock<()> = OnceLock::new();
350 if ONCE.get().is_some() {
351 return;
352 }
353
354 let (_panic_hook, error) = HookBuilder::default().into_hooks();
355 let error = error.into_eyre_hook();
356 color_eyre::eyre::set_hook(Box::new(move |err| {
357 let _ = restore_terminal();
358 error(err)
359 }))
360 .unwrap();
361
362 std::panic::set_hook(Box::new(move |info| {
363 let _ = restore_terminal();
364 let backtrace = Backtrace::force_capture();
365 println!("CuConsoleMon panic: {info}");
366 println!("Backtrace:\n{backtrace}");
367 let _ = stdout().flush();
368 process::exit(1);
369 }));
370
371 let _ = ONCE.set(());
372}
373
374fn setup_terminal() -> io::Result<()> {
375 enable_raw_mode()?;
376 execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?;
377 Ok(())
378}
379
380fn restore_terminal() -> io::Result<()> {
381 execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
382 disable_raw_mode()
383}
384
385fn should_start_ui() -> bool {
386 if !stdout().is_tty() || !stdin().is_tty() {
387 return false;
388 }
389
390 #[cfg(unix)]
391 {
392 use std::os::unix::io::AsRawFd;
393
394 let stdin_fd = stdin().as_raw_fd();
395 let fg_pgrp = unsafe { libc::tcgetpgrp(stdin_fd) };
396 if fg_pgrp == -1 {
397 return false;
398 }
399
400 let pgrp = unsafe { libc::getpgrp() };
401 if fg_pgrp != pgrp {
402 return false;
403 }
404 }
405
406 true
407}