feat(widget): add help widget and improve score widget

- Introduced a new `Help` widget to display user instructions.
- Improved the `ScoreListWidget` by removing unnecessary execution rendering methods and simplifying state transitions.
- Cleaned up unused imports and refactored code for better readability.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-01-29 15:34:16 -05:00
parent 6628e193e0
commit f1f2c796c4
7 changed files with 78 additions and 45 deletions

View File

@ -31,7 +31,7 @@ impl std::fmt::Display for InterpretName {
}
#[async_trait]
pub trait Interpret: std::fmt::Debug {
pub trait Interpret: std::fmt::Debug + Send {
async fn execute(
&self,
inventory: &Inventory,

View File

@ -35,7 +35,7 @@ impl Maestro {
score_mut.append(&mut scores);
}
pub async fn interpret<S: Score>(&self, score: &S) -> Result<Outcome, InterpretError> {
pub async fn interpret(&self, score: Box<dyn Score>) -> Result<Outcome, InterpretError> {
info!("Running score {score:?}");
let interpret = score.create_interpret();
info!("Launching interpret {interpret:?}");

View File

@ -52,7 +52,7 @@ where
}
#[derive(Debug)]
pub struct K8sResourceInterpret<K: Resource + std::fmt::Debug + Sync> {
pub struct K8sResourceInterpret<K: Resource + std::fmt::Debug + Sync + Send> {
pub score: K8sResourceScore<K>,
}
@ -64,6 +64,7 @@ impl<
+ DeserializeOwned
+ serde::Serialize
+ Default
+ Send
+ Sync,
> Interpret for K8sResourceInterpret<K>
where

View File

@ -1,10 +1,11 @@
mod ratatui_utils;
mod widget;
use log::{debug, error, info, trace, warn};
use log::{debug, info};
use tokio::sync::mpsc;
use tokio_stream::StreamExt;
use widget::score::ScoreListWidget;
use tui_logger::{TuiWidgetEvent, TuiWidgetState};
use widget::{help::HelpWidget, score::ScoreListWidget};
use std::{sync::Arc, time::Duration};
@ -48,10 +49,10 @@ pub async fn init(maestro: Maestro) -> Result<(), Box<dyn std::error::Error>> {
}
pub struct HarmonyTUI {
maestro: Maestro,
score: ScoreListWidget,
should_quit: bool,
channel_handle: tokio::task::JoinHandle<()>,
tui_state: TuiWidgetState,
}
#[derive(Debug)]
@ -61,23 +62,34 @@ enum HarmonyTuiEvent {
impl HarmonyTUI {
pub fn new(maestro: Maestro) -> Self {
let (handle, sender) = Self::start_channel();
let maestro = Arc::new(maestro);
let (handle, sender) = Self::start_channel(maestro.clone());
let score = ScoreListWidget::new(Self::scores_list(&maestro), sender);
HarmonyTUI {
maestro,
should_quit: false,
score,
channel_handle: handle,
tui_state: TuiWidgetState::new(),
}
}
fn start_channel() -> (tokio::task::JoinHandle<()>, mpsc::Sender<HarmonyTuiEvent>) {
fn start_channel(
maestro: Arc<Maestro>,
) -> (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:#?}");
match event {
HarmonyTuiEvent::LaunchScore(score_item) => {
info!(
"Interpretation result {:#?}",
maestro.interpret(score_item.0).await
)
}
}
}
info!("STOPPING message channel receiver loop");
});
@ -86,9 +98,9 @@ impl HarmonyTUI {
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();
tui_logger::init_logger(log::LevelFilter::Info).unwrap();
// Set default level for unknown targets to Trace
tui_logger::set_default_level(log::LevelFilter::Trace);
tui_logger::set_default_level(log::LevelFilter::Info);
color_eyre::install()?;
let mut terminal = ratatui::init();
@ -116,22 +128,30 @@ impl HarmonyTUI {
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, output_area] =
Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)])
.areas(frame.area());
.areas(app_area);
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,
)
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, output_area)
}
fn scores_list(maestro: &Maestro) -> Vec<ScoreItem> {
@ -144,10 +164,14 @@ impl HarmonyTUI {
}
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') => self.tui_state.transition(TuiWidgetEvent::EscapeKey),
_ => self.score.handle_event(event).await,
}
}

View File

@ -0,0 +1,20 @@
use ratatui::widgets::{Paragraph, Widget, Wrap};
pub(crate) struct HelpWidget;
impl HelpWidget {
pub(crate) fn new() -> Self {
Self
}
}
impl Widget for HelpWidget {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized {
let text = Paragraph::new("Usage => q/Esc: Quit | j/↑ :Select UP | k/↓: Select Down | Enter: Launch Score | PageUp/PageDown: Scroll Logs | g/Home: Logs top | Shift+G/End: Logs bottom")
.centered()
.wrap(Wrap { trim: false });
Widget::render(text, area, buf)
}
}

View File

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

View File

@ -1,26 +1,22 @@
use std::sync::{ Arc, RwLock};
use std::sync::{Arc, RwLock};
use crossterm::event::{Event, KeyCode, KeyEventKind};
use log::{debug, error, info, trace, warn};
use log::{info, warn};
use ratatui::{
layout::{Constraint, Rect},
layout::Rect,
style::{Style, Stylize},
widgets::{Block, Clear, List, ListState, Paragraph, StatefulWidget, Widget},
widgets::{List, ListState, StatefulWidget, Widget},
Frame,
};
use tokio::sync::mpsc;
use crate::{HarmonyTuiEvent, ScoreItem};
use crate::ratatui_utils::center;
#[derive(Debug)]
enum ExecutionState {
INITIATED,
CONFIRMED,
CANCELED,
RUNNING,
COMPLETED,
CANCELED,
}
#[derive(Debug)]
@ -39,7 +35,7 @@ pub(crate) struct ScoreListWidget {
}
impl ScoreListWidget {
pub(crate) fn new(scores: Vec<ScoreItem>, sender : mpsc::Sender<HarmonyTuiEvent>) -> 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));
@ -80,16 +76,8 @@ impl ScoreListWidget {
pub(crate) fn render(&self, area: Rect, frame: &mut Frame) {
frame.render_widget(self, area);
self.render_execution(frame);
}
pub(crate) fn render_execution(&self, frame: &mut Frame) {
if let None = self.execution {
return;
}
let execution = self.execution.as_ref().unwrap();
}
fn clear_execution(&mut self) {
match self.execution.take() {
@ -104,13 +92,12 @@ impl ScoreListWidget {
if let Some(execution) = &mut self.execution {
match confirm {
true => {
execution.state = ExecutionState::CONFIRMED;
execution.state = ExecutionState::RUNNING;
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");
self.sender
.send(HarmonyTuiEvent::LaunchScore(execution.score.clone()))
.await
.expect("Should be able to send message");
}
false => {
execution.state = ExecutionState::CANCELED;