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