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