use std::sync::{Arc, RwLock}; 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; use crate::HarmonyTuiEvent; #[derive(Debug)] enum ExecutionState { INITIATED, RUNNING, CANCELED, } #[derive(Debug)] struct Execution { state: ExecutionState, score: Box>, } #[derive(Debug)] 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) { let list_read = self.list_state.read().unwrap(); if let Some(index) = list_read.selected() { let score = self .scores .get(index) .expect("List state should always match with internal Vec"); self.execution = Some(Execution { state: ExecutionState::INITIATED, score: score.clone_box(), }); info!("{:#?}\n\nConfirm Execution (Press y/n)", score); } else { warn!("No Score selected, nothing to launch"); } } 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_to_list_item).collect(); let list = List::new(scores_items) .highlight_style(Style::new().bold().italic()) .highlight_symbol("🠊 "); StatefulWidget::render(list, area, buf, &mut list_state) } } fn score_to_list_item<'a, T: Topology>(score: &'a Box>) -> ListItem<'a> { ListItem::new(score.name()) }