use clap::Parser;
use core::time::Duration;
use eyre::{Result, WrapErr};
use nodo_runtime::{
proto::nodo as nodo_pb, ConfigureClient, ConfigureReply, ConfigureRequest, InspectorClient,
};
use ratatui::{
crossterm::event::{self, KeyCode},
layout::{Constraint, Layout},
prelude::Direction,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, TableState, Tabs},
};
use std::{cell::RefCell, sync::Arc, time::Instant};
mod configure;
mod connection_indicator;
mod help_view;
mod monitors_view;
mod nodo_app_tree;
mod schedule_view;
mod signals_view;
mod statistics;
mod style;
use configure::*;
use connection_indicator::*;
use help_view::*;
use monitors_view::*;
use nodo_app_tree::*;
use schedule_view::*;
use signals_view::*;
use statistics::*;
pub const PAGE_SCROLL_STEP: usize = 16;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(long, default_value = "localhost")]
host: String,
#[arg(long)]
disable_tui: bool,
}
fn main() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
let mut terminal = (!cli.disable_tui).then(|| ratatui::init());
let model = Arc::new(RefCell::new(InspectorModel::new(&cli.host)?));
let mut stats_ctrl = StatisticsController {
model: model.clone(),
};
let stats_view = StatisticsView {
model: model.clone(),
};
let mut config_ctrl = ConfigureController {
model: model.clone(),
};
let config_view = ConfigureView {
model: model.clone(),
};
let mut signals_ctrl = SignalsController {
model: model.clone(),
};
let signals_view = SignalsView {
model: model.clone(),
};
let mut monitors_ctrl = MonitorsController {
model: model.clone(),
};
let monitors_view = MonitorsView {
model: model.clone(),
};
let mut schedules_ctrl = ScheduleController {
model: model.clone(),
};
let schedule_view = ScheduleView {
model: model.clone(),
};
let help_view = HelpView {
model: model.clone(),
};
let mut selected_tab: usize = 0;
const TAB_COUNT: usize = 6;
const TAB_TITLES: [&str; TAB_COUNT] = [
"Statistics",
"Config",
"Signals",
"Monitors",
"Schedules",
"Help",
];
loop {
let maybe_error = {
let mut model = model.borrow_mut();
if let Err(err) = model.on_step() {
model.error.set(format!("{err:?}"));
}
model.error.get().map(|s| s.clone())
};
if let Some(terminal) = terminal.as_mut() {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.area());
{
let titles: Vec<_> = TAB_TITLES.iter().cloned().map(Line::from).collect();
let model = model.borrow();
let tabs = Tabs::new(titles)
.block(
Block::default()
.borders(Borders::ALL)
.title(Line::from(vec![
Span::styled(
" NODO INSPECTOR",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::from(" ── "),
model.connection_indicator.span(),
Span::from(" ("),
if model.connection_indicator.is_connected() {
Span::styled(&model.host, Color::Green)
} else {
Span::styled(&model.host, Color::DarkGray)
},
Span::from(")"),
if model.connection_indicator.is_connected() {
Span::styled(
format!(" [{:.0} kB/s]", model.datarate / 1024.0),
Style::default().fg(Color::White),
)
} else {
Span::default()
},
Span::from(" ── Press 'h' for help ──"),
{
match maybe_error {
Some(err) => Span::styled(
format!("Error: {err}"),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::REVERSED),
),
None => Span::from(""),
}
},
])),
)
.style(Color::Yellow)
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::REVERSED),
)
.select(selected_tab);
f.render_widget(tabs, chunks[0]);
}
match selected_tab {
0 => f.render_widget(&stats_view, chunks[1]),
1 => f.render_widget(&config_view, chunks[1]),
2 => f.render_widget(&signals_view, chunks[1]),
3 => f.render_widget(&monitors_view, chunks[1]),
4 => f.render_widget(&schedule_view, chunks[1]),
5 => f.render_widget(&help_view, chunks[1]),
TAB_COUNT.. => unreachable!("invalid tab: {selected_tab}"),
}
})?;
if event::poll(Duration::from_millis(50))? {
match event::read()? {
event::Event::Key(key) => match key.code {
KeyCode::Char('q') => break,
KeyCode::Char('h') => selected_tab = TAB_COUNT - 1,
KeyCode::Left => {
selected_tab = selected_tab.wrapping_sub(1).min(TAB_COUNT - 1)
}
KeyCode::Right => {
selected_tab = {
let n = selected_tab + 1;
if n == TAB_COUNT {
0
} else {
n
}
}
}
key => match selected_tab {
0 => stats_ctrl.on_key(key),
1 => config_ctrl.on_key(key),
2 => signals_ctrl.on_key(key),
3 => monitors_ctrl.on_key(key),
4 => schedules_ctrl.on_key(key),
5 => {}
TAB_COUNT.. => unreachable!("invalid tab: {selected_tab}"),
},
},
_ => {}
}
}
}
}
ratatui::restore();
Ok(())
}
pub struct InspectorModel {
host: String,
inspector_client: InspectorClient,
configure_client: ConfigureClient,
connection_indicator: ConnectionIndicator,
tree: NodoAppTree,
datarate: f64,
report: Option<nodo_pb::Report>,
report_time: Option<Instant>,
report_table_state: TableState,
config_table_state: TableState,
signals_table_state: TableState,
monitors_table_state: TableState,
schedule_table_state: TableState,
error: TimeoutValue<String>,
}
pub struct TimeoutValue<T> {
value: Option<T>,
timeout: Duration,
invalidation: Instant,
}
impl<T> TimeoutValue<T> {
pub fn new(timeout: Duration) -> Self {
Self {
value: None,
timeout,
invalidation: Instant::now(),
}
}
pub fn set(&mut self, value: T) {
self.value = Some(value);
self.invalidation = Instant::now() + self.timeout;
}
pub fn get(&mut self) -> Option<&T> {
let now = Instant::now();
if now > self.invalidation {
self.value = None;
}
self.value.as_ref()
}
}
impl InspectorModel {
pub fn new(host: &str) -> Result<Self> {
let inspector_client = InspectorClient::dial(&format!("tcp://{}:54399", host))?;
let configure_client = ConfigureClient::dial(&format!("tcp://{}:54398", host))?;
Ok(Self {
host: host.into(),
inspector_client,
configure_client,
connection_indicator: Default::default(),
tree: Default::default(),
datarate: Default::default(),
report: Default::default(),
report_time: Default::default(),
report_table_state: Default::default(),
config_table_state: Default::default(),
signals_table_state: Default::default(),
monitors_table_state: Default::default(),
schedule_table_state: Default::default(),
error: TimeoutValue::new(Duration::from_secs(3)),
})
}
pub fn on_step(&mut self) -> Result<()> {
self.update_config().context("failed to update config")?;
self.update_report().context("failed to update report")?;
if self
.report_time
.map_or(false, |last| (Instant::now() - last).as_secs_f32() < 1.0)
{
self.connection_indicator.on_connect();
} else {
self.connection_indicator.on_connecting();
};
self.connection_indicator.on_step();
Ok(())
}
fn update_config(&mut self) -> Result<()> {
if let Ok(reply) = self.configure_client.send_request(&ConfigureRequest::List) {
match reply {
ConfigureReply::List(list) => self.tree.update_config_parameters(list),
_ => {}
}
}
Ok(())
}
fn update_report(&mut self) -> Result<()> {
if let Some(report) = self.inspector_client.try_recv_report()? {
self.tree.update(report.clone())?;
self.report = Some(report);
self.report_time = Some(Instant::now());
}
self.datarate = self.inspector_client.datarate();
Ok(())
}
}