use std::sync::{Arc, RwLock}; use crate::HarmonyTuiEvent; use crossterm::event::{Event, KeyCode, KeyEventKind}; use harmony::{score::Score, topology::Topology}; use log::{info, warn}; use ratatui::{ Frame, layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, }; use tokio::sync::mpsc; #[derive(Debug)] enum ExecutionState { Initiated, Running, Canceled, } struct Execution { state: ExecutionState, score: Box>, } impl std::fmt::Display for Execution { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "Execution of {} status {:?}", self.score.name(), self.state )) } } pub(crate) struct ScoreListWidget { list_state: Arc>, scores: Vec>>, execution: Option>, execution_history: Vec>, sender: mpsc::Sender>, } impl ScoreListWidget { pub(crate) fn new( scores: Vec>>, sender: mpsc::Sender>, ) -> Self { let mut list_state = ListState::default(); list_state.select_first(); let list_state = Arc::new(RwLock::new(list_state)); Self { scores, list_state, execution: None, execution_history: vec![], sender, } } pub(crate) fn launch_execution(&mut self) { if let Some(score) = self.get_selected_score() { self.execution = Some(Execution { state: ExecutionState::Initiated, score: score.clone_box(), }); info!("{}\n\nConfirm Execution (Press y/n)", score.name()); info!("{}", score.print_score_details()); } else { warn!("No Score selected, nothing to launch"); } } pub(crate) fn get_selected_score(&self) -> Option>> { let list_read = self.list_state.read().unwrap(); if let Some(index) = list_read.selected() { self.scores.get(index).map(|s| s.clone_box()) } else { None } } pub(crate) fn scroll_down(&self) { self.list_state.write().unwrap().scroll_down_by(1); } pub(crate) fn scroll_up(&self) { self.list_state.write().unwrap().scroll_up_by(1); } pub(crate) fn render(&self, area: Rect, frame: &mut Frame) { frame.render_widget(self, area); } fn clear_execution(&mut self) { match self.execution.take() { Some(execution) => { self.execution_history.push(execution); } None => warn!("Should never clear execution when no execution exists"), } } pub(crate) async fn confirm(&mut self, confirm: bool) { if let Some(execution) = &mut self.execution { match confirm { true => { execution.state = ExecutionState::Running; info!("Launch execution {execution}"); self.sender .send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box())) .await .expect("Should be able to send message"); } false => { execution.state = ExecutionState::Canceled; info!("Execution cancelled"); self.clear_execution(); } } } } pub(crate) async fn handle_event(&mut self, event: &Event) { if let Event::Key(key) = event { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('j') | KeyCode::Down => self.scroll_down(), KeyCode::Char('k') | KeyCode::Up => self.scroll_up(), KeyCode::Enter => self.launch_execution(), KeyCode::Char('y') => self.confirm(true).await, KeyCode::Char('n') => self.confirm(false).await, _ => {} } } } } } impl Widget for &ScoreListWidget { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let mut list_state = self.list_state.write().unwrap(); let scores_items: Vec> = self .scores .iter() .map(|score| ListItem::new(score.name())) .collect(); let list = List::new(scores_items) .highlight_style(Style::new().bold().italic()) .highlight_symbol("🠊 "); StatefulWidget::render(list, area, buf, &mut list_state) } }