mod widget; use log::{debug, error, info}; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tui_logger::{TuiLoggerFile, 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, topology::Topology}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, style::{Color, Style}, widgets::{Block, Borders}, }; 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,no_run /// use harmony::{ /// inventory::Inventory, /// maestro::Maestro, /// modules::dummy::{ErrorScore, PanicScore, SuccessScore}, /// topology::HAClusterTopology, /// }; /// /// #[tokio::main] /// async fn main() { /// let inventory = Inventory::autoload(); /// let topology = HAClusterTopology::autoload(); /// let mut maestro = Maestro::new(inventory, topology); /// /// maestro.register_all(vec![ /// Box::new(SuccessScore {}), /// Box::new(ErrorScore {}), /// Box::new(PanicScore {}), /// ]); /// harmony_tui::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, } enum HarmonyTuiEvent { LaunchScore(Box>), } impl std::fmt::Display for HarmonyTuiEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let output = match self { HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})", score.name()), }; f.write_str(&output) } } 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).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); std::fs::create_dir_all("log")?; tui_logger::set_log_file(TuiLoggerFile::new("log/harmony.log")); 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, logger_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, logger_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| 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, } } } } }