Skip to main content

cu_consolemon/
lib.rs

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
34/// A TUI based realtime console for Copper.
35pub 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}