use std::collections::VecDeque;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use crossterm::{
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use modbus_rs::gateway::UnitRouteTable;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use tokio::sync::broadcast;
use tracing::info;
use crate::capture::CaptureState;
use crate::error::AppResult;
use crate::metrics::{MetricsCollector, TrafficEvent};
use crate::orchestrator::GatewayOrchestrator;
use super::event::{AppEvent, is_quit, next_event};
use super::ui;
const MAX_ROUTES: usize = 64;
const TRAFFIC_HISTORY: usize = 200;
const TICK_RATE: Duration = Duration::from_millis(250);
pub struct TuiApp {
metrics: Arc<MetricsCollector>,
#[allow(dead_code)] router: Arc<RwLock<UnitRouteTable<MAX_ROUTES>>>,
downstream_names: Vec<String>,
traffic: VecDeque<TrafficEvent>,
traffic_rx: Option<broadcast::Receiver<TrafficEvent>>,
#[allow(dead_code)] log_messages: VecDeque<String>,
capture_state: CaptureState,
focus: usize,
show_help: bool,
version: &'static str,
}
impl TuiApp {
pub fn from_orchestrator(orch: GatewayOrchestrator) -> Self {
Self {
metrics: orch.metrics,
router: orch.router,
downstream_names: orch.downstream_names,
traffic: VecDeque::with_capacity(TRAFFIC_HISTORY),
traffic_rx: orch.traffic_rx,
log_messages: VecDeque::with_capacity(200),
capture_state: orch.capture_state,
focus: 0,
show_help: false,
version: env!("CARGO_PKG_VERSION"),
}
}
pub fn run(mut self, on_quit: impl Fn()) -> AppResult<()> {
enable_raw_mode().map_err(crate::error::AppError::Io)?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen).map_err(crate::error::AppError::Io)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)
.map_err(crate::error::AppError::Io)?;
let result = self.event_loop(&mut terminal, on_quit);
disable_raw_mode().ok();
execute!(terminal.backend_mut(), LeaveAlternateScreen).ok();
terminal.show_cursor().ok();
result
}
fn event_loop(
&mut self,
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
on_quit: impl Fn(),
) -> AppResult<()> {
loop {
self.drain_traffic();
terminal
.draw(|frame| ui::draw(frame, self))
.map_err(crate::error::AppError::Io)?;
match next_event(TICK_RATE)? {
AppEvent::Key(key) => {
if is_quit(&key) {
info!("quit requested from TUI");
on_quit();
break;
}
self.handle_key(key.code);
}
AppEvent::Resize(_, _) => {
}
AppEvent::Tick => {}
}
}
Ok(())
}
fn drain_traffic(&mut self) {
if let Some(rx) = &mut self.traffic_rx {
loop {
match rx.try_recv() {
Ok(event) => {
if self.traffic.len() >= TRAFFIC_HISTORY {
self.traffic.pop_front();
}
self.traffic.push_back(event);
}
Err(broadcast::error::TryRecvError::Lagged(n)) => {
info!("TUI traffic display lagged by {n} events (channel full)");
}
Err(broadcast::error::TryRecvError::Empty) => break,
Err(broadcast::error::TryRecvError::Closed) => break,
}
}
}
}
fn handle_key(&mut self, code: crossterm::event::KeyCode) {
use crossterm::event::KeyCode::*;
match code {
Tab => {
self.focus = (self.focus + 1) % 3;
}
Char('?') => {
self.show_help = !self.show_help;
}
Char('p') => {
let new_state = self.capture_state.toggle_pcap();
info!(enabled = new_state, "PCAP capture toggled");
}
Char('c') => {
let new_state = self.capture_state.toggle_csv();
info!(enabled = new_state, "CSV capture toggled");
}
Char('l') => {
}
_ => {}
}
}
pub fn metrics(&self) -> &MetricsCollector {
&self.metrics
}
pub fn traffic_events(&self) -> &VecDeque<TrafficEvent> {
&self.traffic
}
pub fn downstream_names(&self) -> &[String] {
&self.downstream_names
}
pub fn focus(&self) -> usize {
self.focus
}
pub fn show_help(&self) -> bool {
self.show_help
}
pub fn version(&self) -> &str {
self.version
}
pub fn capture_state(&self) -> &CaptureState {
&self.capture_state
}
}