nodo_inspector 0.18.5

Telemetry terminal UI for NODO
// Copyright 2025 David Weikersdorfer

use crate::{lifecycle_status_color, style::*, InspectorModel, KeyCode, PAGE_SCROLL_STEP};
use nodo_runtime::proto::nodo as nodo_pb;
use ratatui::{
    prelude::{Buffer, Constraint, Rect},
    style::{Color, Modifier, Style},
    text::Span,
    widgets::{Cell, Row, StatefulWidget, Table, Widget},
};
use std::{cell::RefCell, sync::Arc};

pub struct ScheduleController {
    pub model: Arc<RefCell<InspectorModel>>,
}

impl ScheduleController {
    pub fn on_key(&mut self, key: KeyCode) {
        match key {
            KeyCode::Down => self.select_next(),
            KeyCode::Up => self.select_previous(),
            KeyCode::PageDown => self.on_page_down(),
            KeyCode::PageUp => self.on_page_up(),
            KeyCode::Enter => {}
            _ => {}
        }
    }

    fn select_next(&mut self) {
        self.model.borrow_mut().schedule_table_state.select_next();
    }

    fn select_previous(&mut self) {
        self.model
            .borrow_mut()
            .schedule_table_state
            .select_previous();
    }

    fn on_page_down(&mut self) {
        for _ in 0..PAGE_SCROLL_STEP {
            self.select_next()
        }
    }

    fn on_page_up(&mut self) {
        for _ in 0..PAGE_SCROLL_STEP {
            self.select_previous()
        }
    }
}

pub struct ScheduleView {
    pub model: Arc<RefCell<InspectorModel>>,
}

impl Widget for &ScheduleView {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let mut model = self.model.borrow_mut();

        let combined_rows: Vec<_> = model
            .tree
            .schedules
            .iter()
            .map(|(_, sched)| {
                Row::new(vec![
                    {
                        Cell::from(Span::styled(
                            format!("{}", sched.id.0),
                            Style::default().fg(STYLE_COLOR_VALUE_DEFAULT),
                        ))
                    },
                    {
                        Cell::from(Span::styled(
                            format!("{}", sched.name),
                            Style::default().fg(STYLE_COLOR_VALUE_DEFAULT),
                        ))
                    },
                    {
                        if sched.status.has_panicked {
                            Cell::from(Span::styled("PANIC", Style::default().fg(STYLE_COLOR_ERR)))
                        } else if sched.status.has_finished {
                            Cell::from(Span::styled(
                                "FINISHED",
                                Style::default().fg(STYLE_COLOR_UNAVAILABLE),
                            ))
                        } else {
                            let lifecycle_status: nodo_pb::LifecycleStatus = sched
                                .status
                                .lifecycle_status
                                .try_into()
                                .unwrap_or(nodo_pb::LifecycleStatus::Unspecified);

                            Cell::from(Span::styled(
                                format!("{lifecycle_status:?}"),
                                Style::default().fg(lifecycle_status_color(lifecycle_status)),
                            ))
                        }
                    },
                    {
                        match sched.target_period {
                            Some(period) => Cell::from(Span::styled(
                                format!("{} ms", 1000. * period),
                                Style::default().fg(STYLE_COLOR_VALUE_DEFAULT),
                            )),
                            None => Cell::from(Span::styled(
                                "N/A",
                                Style::default().fg(STYLE_COLOR_UNAVAILABLE),
                            )),
                        }
                    },
                    {
                        match sched.status.last_period {
                            Some(period) => Cell::from(Span::styled(
                                format!("{} ms", 1000. * period),
                                Style::default().fg(STYLE_COLOR_VALUE_DEFAULT),
                            )),
                            None => Cell::from(Span::styled(
                                "N/A",
                                Style::default().fg(STYLE_COLOR_UNAVAILABLE),
                            )),
                        }
                    },
                    {
                        match &sched.status.last_error {
                            Some(text) => Cell::from(Span::styled(
                                text.clone(),
                                Style::default().fg(STYLE_COLOR_ERR),
                            )),
                            None => Cell::from(Span::styled(
                                "NONE",
                                Style::default().fg(STYLE_COLOR_UNAVAILABLE),
                            )),
                        }
                    },
                ])
            })
            .collect();

        // Create the combined table.
        let table = Table::new(
            combined_rows,
            &[
                Constraint::Fill(1),
                Constraint::Fill(2),
                Constraint::Fill(2),
                Constraint::Fill(2),
                Constraint::Fill(2),
                Constraint::Fill(5),
            ],
        )
        .header(
            Row::new(vec![
                "ID".into(),
                "Name".into(),
                "Status".into(),
                "Target Period".to_string(),
                "Actual Period".to_string(),
                "Last Error".to_string(),
            ])
            .style(
                Style::default()
                    .add_modifier(Modifier::BOLD)
                    .add_modifier(Modifier::REVERSED),
            ),
        )
        .style(Style::new().fg(Color::White))
        .row_highlight_style(Style::new().add_modifier(Modifier::REVERSED));

        // Render the combined table.
        StatefulWidget::render(table, area, buf, &mut model.schedule_table_state);
    }
}