mod ratatui_utils; mod widget; use log::{debug, error, info}; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tui_logger::{TuiWidgetEvent, TuiWidgetState}; use widget::{help::HelpWidget, score::ScoreListWidget}; use std::{panic, sync::Arc, time::Duration}; use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; use harmony::{maestro::Maestro, score::Score}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, style::{Color, Style}, widgets::{Block, Borders, ListItem}, }; pub mod tui { // Export any necessary modules or types from the internal tui module } /// Initializes the terminal UI with the provided Maestro instance. /// /// # Arguments /// /// * `maestro` - A reference to the Maestro instance containing registered scores. /// /// # Example /// /// ```rust /// use harmony; /// use harmony_tui::init; /// /// #[harmony::main] /// pub async fn main(maestro: harmony::Maestro) { /// maestro.register(DeploymentScore::new("nginx-test", "nginx")); /// maestro.register(OKDLoadBalancerScore::new(&maestro.inventory, &maestro.topology)); /// // Register other scores as needed /// /// init(maestro).await.unwrap(); /// } /// ``` pub async fn init(maestro: Maestro) -> Result<(), Box> { HarmonyTUI::new(maestro).init().await } pub struct HarmonyTUI { score: ScoreListWidget, should_quit: bool, tui_state: TuiWidgetState, } #[derive(Debug)] enum HarmonyTuiEvent { LaunchScore(ScoreItem), } impl HarmonyTUI { pub fn new(maestro: Maestro) -> Self { let maestro = Arc::new(maestro); let (_handle, sender) = Self::start_channel(maestro.clone()); let score = ScoreListWidget::new(Self::scores_list(&maestro), sender); HarmonyTUI { should_quit: false, score, tui_state: TuiWidgetState::new(), } } fn start_channel( maestro: Arc, ) -> (tokio::task::JoinHandle<()>, mpsc::Sender) { let (sender, mut receiver) = mpsc::channel::(32); let handle = tokio::spawn(async move { info!("Starting message channel receiver loop"); while let Some(event) = receiver.recv().await { info!("Received event {event:#?}"); match event { HarmonyTuiEvent::LaunchScore(score_item) => { let maestro = maestro.clone(); let joinhandle_result = tokio::spawn(async move { maestro.interpret(score_item.0).await }) .await; match joinhandle_result { Ok(interpretation_result) => match interpretation_result { Ok(success) => info!("Score execution successful {success:?}"), Err(error) => error!("Score failed {error:?}"), }, Err(joinhandle_failure) => { error!("Score execution TASK FAILED! {:#?}", joinhandle_failure); } } } } } info!("STOPPING message channel receiver loop"); }); (handle, sender) } pub async fn init(mut self) -> Result<(), Box> { // Set max_log_level to Trace tui_logger::init_logger(log::LevelFilter::Info).unwrap(); // Set default level for unknown targets to Trace tui_logger::set_default_level(log::LevelFilter::Info); tui_logger::set_log_file("harmony.log").unwrap(); color_eyre::install()?; let mut terminal = ratatui::init(); log_panics::init(); terminal.hide_cursor()?; // TODO improve performance here // Refreshing the entire terminal 10 times a second does not seem very smart let period = Duration::from_secs_f32(1.0 / 10.0); let mut interval = tokio::time::interval(period); let mut events = EventStream::new(); while !self.should_quit { tokio::select! { _ = interval.tick() => { terminal.draw(|frame| self.render(frame))?; }, Some(Ok(event)) = events.next() => self.handle_event(&event).await, } } ratatui::restore(); Ok(()) } fn render(&self, frame: &mut Frame) { let size = frame.area(); frame.set_cursor_position(Position::new(size.x / 2, size.y / 2)); let [app_area, help_area] = Layout::vertical([Constraint::Percentage(100), Constraint::Min(4)]).areas(frame.area()); let help_block = Block::default().borders(Borders::TOP); frame.render_widget(&help_block, help_area); frame.render_widget(HelpWidget::new(), help_block.inner(help_area)); let [list_area, output_area] = Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]).areas(app_area); let block = Block::default().borders(Borders::RIGHT); frame.render_widget(&block, list_area); self.score.render(list_area, frame); let tui_logger = tui_logger::TuiLoggerWidget::default() .style_error(Style::default().fg(Color::Red)) .style_warn(Style::default().fg(Color::LightRed)) .style_info(Style::default().fg(Color::LightGreen)) .style_debug(Style::default().fg(Color::Gray)) .style_trace(Style::default().fg(Color::Gray)) .state(&self.tui_state); frame.render_widget(tui_logger, output_area) } fn scores_list(maestro: &Maestro) -> Vec { let scores = maestro.scores(); let scores_read = scores.read().expect("Should be able to read scores"); scores_read .iter() .map(|s| ScoreItem(s.clone_box())) .collect() } async fn handle_event(&mut self, event: &Event) { debug!("Got event {event:?}"); if let Event::Key(key) = event { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true, KeyCode::PageUp => self.tui_state.transition(TuiWidgetEvent::PrevPageKey), KeyCode::PageDown => self.tui_state.transition(TuiWidgetEvent::NextPageKey), KeyCode::Char('G') | KeyCode::End => { self.tui_state.transition(TuiWidgetEvent::EscapeKey) } _ => self.score.handle_event(event).await, } } } } } #[derive(Debug)] struct ScoreItem(Box); impl ScoreItem { pub fn clone(&self) -> Self { Self(self.0.clone_box()) } } impl Into> for &ScoreItem { fn into(self) -> ListItem<'static> { ListItem::new(self.0.name()) } }