feat: harmony terminal ui can now browse scores and (almost) launch them

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-01-28 16:51:58 -05:00
parent 3410751463
commit 6628e193e0
8 changed files with 107 additions and 39 deletions

24
harmony-rs/Cargo.lock generated
View File

@ -993,6 +993,15 @@ dependencies = [
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@ -1113,6 +1122,7 @@ dependencies = [
"ratatui",
"tokio",
"tokio-stream",
"tui-logger",
]
[[package]]
@ -3537,6 +3547,20 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tui-logger"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55a5249b5e5df37af5389721794f9839dad0b3f7b92445f24528199acf2f1805"
dependencies = [
"chrono",
"fxhash",
"lazy_static",
"log",
"parking_lot",
"ratatui",
]
[[package]]
name = "typenum"
version = "1.17.0"

View File

@ -1,6 +1,6 @@
use super::interpret::Interpret;
pub trait Score: std::fmt::Debug {
pub trait Score: std::fmt::Debug + Send + Sync {
fn create_interpret(&self) -> Box<dyn Interpret>;
fn name(&self) -> String;
fn clone_box(&self) -> Box<dyn Score>;

View File

@ -32,6 +32,7 @@ impl<
+ Default
+ serde::Serialize
+ 'static
+ Send
+ Clone,
> Score for K8sResourceScore<K>
where

View File

@ -12,3 +12,4 @@ ratatui = "0.29.0"
crossterm = { version = "0.28.1", features = [ "event-stream" ] }
color-eyre = "0.6.3"
tokio-stream = "0.1.17"
tui-logger = "0.14.1"

View File

@ -1,15 +1,19 @@
mod ratatui_utils;
mod widget;
use log::{debug, error, info, trace, warn};
use tokio::sync::mpsc;
use tokio_stream::StreamExt;
use widget::score::ScoreListWidget;
use std::time::Duration;
use std::{sync::Arc, time::Duration};
use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind};
use harmony::{maestro::Maestro, score::Score};
use ratatui::{
self,
layout::{Constraint, Layout, Position},
style::{Color, Style},
widgets::{Block, Borders, ListItem},
Frame,
};
@ -47,22 +51,48 @@ pub struct HarmonyTUI {
maestro: Maestro,
score: ScoreListWidget,
should_quit: bool,
channel_handle: tokio::task::JoinHandle<()>,
}
#[derive(Debug)]
enum HarmonyTuiEvent {
LaunchScore(ScoreItem),
}
impl HarmonyTUI {
pub fn new(maestro: Maestro) -> Self {
let score = ScoreListWidget::new(Self::scores_list(&maestro));
let (handle, sender) = Self::start_channel();
let score = ScoreListWidget::new(Self::scores_list(&maestro), sender);
HarmonyTUI {
maestro,
should_quit: false,
score,
channel_handle: handle,
}
}
fn start_channel() -> (tokio::task::JoinHandle<()>, mpsc::Sender<HarmonyTuiEvent>) {
let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent>(32);
let handle = tokio::spawn(async move {
info!("Starting message channel receiver loop");
while let Some(event) = receiver.recv().await {
info!("Received event {event:#?}");
}
info!("STOPPING message channel receiver loop");
});
(handle, sender)
}
pub async fn init(mut self) -> Result<(), Box<dyn std::error::Error>> {
// Set max_log_level to Trace
tui_logger::init_logger(log::LevelFilter::Trace).unwrap();
// Set default level for unknown targets to Trace
tui_logger::set_default_level(log::LevelFilter::Trace);
color_eyre::install()?;
let mut terminal = ratatui::init();
terminal.hide_cursor()?;
// TODO improve performance here
// Refreshing the entire terminal 10 times a second does not seem very smart
@ -73,7 +103,7 @@ impl HarmonyTUI {
while !self.should_quit {
tokio::select! {
_ = interval.tick() => { terminal.draw(|frame| self.render(frame))?; },
Some(Ok(event)) = events.next() => self.handle_event(&event),
Some(Ok(event)) = events.next() => self.handle_event(&event).await,
}
}
@ -93,6 +123,15 @@ impl HarmonyTUI {
let block = Block::default().borders(Borders::RIGHT);
frame.render_widget(&block, list_area);
self.score.render(list_area, frame);
frame.render_widget(
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)),
output_area,
)
}
fn scores_list(maestro: &Maestro) -> Vec<ScoreItem> {
@ -104,17 +143,12 @@ impl HarmonyTUI {
.collect()
}
fn handle_event(&mut self, event: &Event) {
async 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),
_ => {}
_ => self.score.handle_event(event).await,
}
}
}

View File

@ -1,2 +1 @@
pub mod score;
pub mod ratatui_utils;

View File

@ -1,16 +1,18 @@
use std::sync::{Arc, RwLock};
use std::sync::{ Arc, RwLock};
use log::warn;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use log::{debug, error, info, trace, warn};
use ratatui::{
layout::{Constraint, Rect},
style::{Style, Stylize},
widgets::{Block, Clear, List, ListState, Paragraph, StatefulWidget, Widget},
Frame,
};
use tokio::sync::mpsc;
use crate::ScoreItem;
use crate::{HarmonyTuiEvent, ScoreItem};
use super::ratatui_utils::center;
use crate::ratatui_utils::center;
#[derive(Debug)]
enum ExecutionState {
@ -33,10 +35,11 @@ pub(crate) struct ScoreListWidget {
scores: Vec<ScoreItem>,
execution: Option<Execution>,
execution_history: Vec<Execution>,
sender: mpsc::Sender<HarmonyTuiEvent>,
}
impl ScoreListWidget {
pub(crate) fn new(scores: Vec<ScoreItem>) -> Self {
pub(crate) fn new(scores: Vec<ScoreItem>, sender : mpsc::Sender<HarmonyTuiEvent>) -> Self {
let mut list_state = ListState::default();
list_state.select_first();
let list_state = Arc::new(RwLock::new(list_state));
@ -45,6 +48,7 @@ impl ScoreListWidget {
list_state,
execution: None,
execution_history: vec![],
sender,
}
}
@ -60,6 +64,9 @@ impl ScoreListWidget {
state: ExecutionState::INITIATED,
score: score.clone(),
});
info!("{:#?}\n\nConfirm Execution (Press y/n)", score.0);
} else {
warn!("No Score selected, nothing to launch");
}
}
@ -82,25 +89,6 @@ impl ScoreListWidget {
}
let execution = self.execution.as_ref().unwrap();
// let confirm = Paragraph::new(format!("{:#?}", execution.score.0)).block(
// Block::default()
// .borders(Borders::ALL)
// .title("Confirm Execution")
// .border_type(BorderType::Rounded),
// );
// let rect = frame.area().inner(Margin::new(5, 5));
// frame.render_widget(confirm, rect);
let area = center(
frame.area(),
Constraint::Percentage(80),
Constraint::Percentage(80), // top and bottom border + content
);
let popup = Paragraph::new(format!("{:#?}", execution.score.0))
.block(Block::bordered().title("Confirm Execution (Press y/n)"));
frame.render_widget(Clear, area);
frame.render_widget(popup, area);
}
fn clear_execution(&mut self) {
@ -112,20 +100,41 @@ impl ScoreListWidget {
}
}
pub(crate) fn confirm(&mut self, confirm: bool) {
pub(crate) async fn confirm(&mut self, confirm: bool) {
if let Some(execution) = &mut self.execution {
match confirm {
true => {
execution.state = ExecutionState::CONFIRMED;
todo!("launch execution {:#?}", execution);
},
info!("Launch execution {:?}", execution);
warn!("Launch execution");
error!("Launch execution");
trace!("Launch execution");
debug!("Launch execution");
self.sender.send(HarmonyTuiEvent::LaunchScore(execution.score.clone())).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 {