mod widget; use tokio_stream::StreamExt; use widget::score::ScoreListWidget; use std::time::Duration; use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; use harmony::{maestro::Maestro, score::Score}; use ratatui::{ self, layout::{Constraint, Layout, Position}, widgets::{Block, Borders, ListItem}, Frame, }; 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 { maestro: Maestro, score: ScoreListWidget, should_quit: bool, } impl HarmonyTUI { pub fn new(maestro: Maestro) -> Self { let score = ScoreListWidget::new(Self::scores_list(&maestro)); HarmonyTUI { maestro, should_quit: false, score, } } pub async fn init(mut self) -> Result<(), Box> { color_eyre::install()?; let mut terminal = ratatui::init(); // 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), } } 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 [list_area, output_area] = Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]) .areas(frame.area()); let block = Block::default().borders(Borders::RIGHT); frame.render_widget(&block, list_area); self.score.render(list_area, frame); } 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() } fn handle_event(&mut self, 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::Char('j') | KeyCode::Down => self.score.scroll_down(), KeyCode::Char('k') | KeyCode::Up => self.score.scroll_up(), KeyCode::Enter => self.score.launch_execution(), KeyCode::Char('y') => self.score.confirm(true), KeyCode::Char('n') => self.score.confirm(false), _ => {} } } } } } #[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()) } }