From 6ddf48591b94345edb39aa554b993589c0f96c7c Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 17 Apr 2025 09:42:36 -0400 Subject: [PATCH 01/10] modified tui-example main --- Cargo.lock | 6 +- examples/tui/src/main.rs | 6 +- harmony_tui/Cargo.toml | 2 + harmony_tui/src/lib.rs | 18 ++++- harmony_tui/src/widget/score.rs | 127 +++++++++++++++++++++++++++++++- 5 files changed, 149 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c279f..c571489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1188,6 +1188,8 @@ dependencies = [ "log", "log-panics", "ratatui", + "serde-value", + "serde_json", "tokio", "tokio-stream", "tui-logger", @@ -3026,9 +3028,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 05a768b..d912844 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -1,7 +1,10 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::dummy::{ErrorScore, PanicScore, SuccessScore}, + modules::{ + dns::DnsScore, + dummy::{ErrorScore, PanicScore, SuccessScore}, + }, topology::HAClusterTopology, }; @@ -15,6 +18,7 @@ async fn main() { Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), + Box::new(DnsScore::new(vec![], None)), ]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/harmony_tui/Cargo.toml b/harmony_tui/Cargo.toml index a12e534..6aacedd 100644 --- a/harmony_tui/Cargo.toml +++ b/harmony_tui/Cargo.toml @@ -16,3 +16,5 @@ color-eyre = "0.6.3" tokio-stream = "0.1.17" tui-logger = "0.14.1" log-panics = "2.1.0" +serde-value.workspace = true +serde_json = "1.0.140" diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 11208f0..462591d 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -14,7 +14,7 @@ use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, style::{Color, Style}, - widgets::{Block, Borders}, + widgets::{Block, Borders, Paragraph, Wrap}, }; pub mod tui { @@ -159,12 +159,15 @@ impl HarmonyTUI { frame.render_widget(&help_block, help_area); frame.render_widget(HelpWidget::new(), help_block.inner(help_area)); - let [list_area, output_area] = + let [list_area, right_area] = Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]).areas(app_area); let block = Block::default().borders(Borders::RIGHT); frame.render_widget(&block, list_area); self.score.render(list_area, frame); + + let [logger_area, info_area] = + Layout::vertical([Constraint::Min(30), Constraint::Percentage(100)]).areas(right_area); let tui_logger = tui_logger::TuiLoggerWidget::default() .style_error(Style::default().fg(Color::Red)) .style_warn(Style::default().fg(Color::LightRed)) @@ -172,7 +175,16 @@ impl HarmonyTUI { .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) + + frame.render_widget(tui_logger, logger_area); + + let info = self.score.display_value(); + //let info = self.score.display_value(); + let info_block = Block::default().borders(Borders::ALL).title("Scores Info"); + let info_paragraph = Paragraph::new(info) + .block(info_block) + .wrap(Wrap { trim: true }); + frame.render_widget(info_paragraph, info_area); } fn scores_list(maestro: &Maestro) -> Vec>> { diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index b0d2c27..c4d3d63 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,18 +1,22 @@ +use ratatui::layout::Constraint; +use serde_value::Value; +use std::collections::BTreeMap; 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}, + style::{Modifier, Style, Stylize}, + widgets::{ + Block, Borders, Cell, List, ListItem, ListState, Row, StatefulWidget, Table, Widget, + }, }; use tokio::sync::mpsc; -use crate::HarmonyTuiEvent; - #[derive(Debug)] enum ExecutionState { INITIATED, @@ -64,11 +68,126 @@ impl ScoreListWidget { state: ExecutionState::INITIATED, score: score.clone_box(), }); + //TODO: need to format the output of the score.serialize() + //currently just dumps to MAP() and handle _ + //maybe columns + //https://arcnmx.github.io/serde-value/serde_value/enum.Value.html info!("{:#?}\n\nConfirm Execution (Press y/n)", score); } else { warn!("No Score selected, nothing to launch"); } } + //TODO working on match statement to output serialized value + //want to try and build a fn for a few types that are represtendd in DNSscore + //and output them by calling fn display_values in the about fn + // + //currently outputs info about all available scores, not quite + //looking to output info about the selected score and subscores + // + +fn print_value(&self, val: &Value, indent: usize) -> String { + let pad = " ".repeat(indent * 2); + let mut output = String::new(); + + match val { + Value::Bool(b) => output += &format!("{}{}\n", pad, b), + Value::U8(u) => output += &format!("{}{}\n", pad, u), + Value::U16(u) => output += &format!("{}{}\n", pad, u), + Value::U32(u) => output += &format!("{}{}\n", pad, u), + Value::U64(u) => output += &format!("{}{}\n", pad, u), + Value::I8(i) => output += &format!("{}{}\n", pad, i), + Value::I16(i) => output += &format!("{}{}\n", pad, i), + Value::I32(i) => output += &format!("{}{}\n", pad, i), + Value::I64(i) => output += &format!("{}{}\n", pad, i), + Value::F32(f) => output += &format!("{}{}\n", pad, f), + Value::F64(f) => output += &format!("{}{}\n", pad, f), + Value::Char(c) => output += &format!("{}{}\n", pad, c), + Value::String(s) => output += &format!("{}{}\n", pad, s), + Value::Unit => output += &format!("{}\n", pad), + Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes), + + Value::Option(opt) => match opt { + Some(inner) => { + output += &format!("{}Option:\n", pad); + output += &self.print_value(inner, indent + 1); + } + None => output += &format!("{}None\n", pad), + }, + + Value::Newtype(inner) => { + output += &format!("{}Newtype:\n", pad); + output += &self.print_value(inner, indent + 1); + } + + Value::Seq(seq) => { + if seq.is_empty() { + output += &format!("{}[]\n", pad); + } else { + output += &format!("{}[\n", pad); + for item in seq { + output += &self.print_value(item, indent + 1); + } + output += &format!("{}]\n", pad); + } + } + + Value::Map(map) => { + if map.is_empty() { + output += &format!("{}\n", pad); + } else { + output += &format!( + "\n{}+--------------------------+----------------------------+\n", + pad + ); + output += &format!( + "{}| {:<24} | {:<26} |\n", + pad, "Key", "Value" + ); + output += &format!( + "{}+--------------------------+----------------------------+\n", + pad + ); + + for (k, v) in map { + let key_str = match k { + Value::String(s) => s.clone(), + other => format!("{:?}", other), + }; + let val_str = match v { + Value::String(s) => s.clone(), + Value::Bool(b) => b.to_string(), + Value::Option(Some(inner)) => format!("{:?}", inner), + Value::Option(None) => "None".to_string(), + Value::Seq(seq) => format!("{:?}", seq), + _ => format!("{:?}", v), + }; + + output += &format!( + "{}| {:<24} | {:<26} |\n", + pad, key_str, val_str + ); + } + + output += &format!( + "{}+--------------------------+----------------------------+\n\n", + pad + ); + } + } + } + + output +} + + pub(crate) fn display_value(&self) -> String { + let mut output = String::new(); + output += "SCORE DETAILS"; + for score in &self.scores { + let val = score.serialize(); + output += &self.print_value(&val, 0); + } + output + } pub(crate) fn scroll_down(&self) { self.list_state.write().unwrap().scroll_down_by(1); -- 2.39.5 From 4f8523ab69c82d461647fdf36618298989ab9008 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 11 Apr 2025 15:16:33 -0400 Subject: [PATCH 02/10] wip:made score details box output information for highlighted score, worked on outputing score info in log output --- harmony/src/domain/score.rs | 109 ++++++++++++++++++++++++- harmony_tui/src/lib.rs | 11 ++- harmony_tui/src/widget/score.rs | 138 +++----------------------------- 3 files changed, 126 insertions(+), 132 deletions(-) diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index dbe7aa5..7f3d410 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -4,7 +4,7 @@ use serde_value::Value; use super::{interpret::Interpret, topology::Topology}; pub trait Score: - std::fmt::Debug + Send + Sync + CloneBoxScore + SerializeScore + std::fmt::Debug + DisplayValues + Send + Sync + CloneBoxScore + SerializeScore { fn create_interpret(&self) -> Box>; fn name(&self) -> String; @@ -39,3 +39,110 @@ where Box::new(self.clone()) } } + +pub trait DisplayValues { + fn display_value(&self) -> String; + fn print_value(&self, val: &Value, indent: usize) -> String; +} + +impl DisplayValues for S +where + T: Topology, + S: Score + 'static, +{ + fn display_value(&self) -> String { + let mut output = String::new(); + output += &format!("{}\n", &self.name()); + output += &self.print_value(&self.serialize(), 0); + output += "\n"; + output + } + + fn print_value(&self, val: &Value, indent: usize) -> String { + let pad = " ".repeat(indent * 2); + let mut output = String::new(); + + match val { + Value::Bool(b) => output += &format!("{}{}\n", pad, b), + Value::U8(u) => output += &format!("{}{}\n", pad, u), + Value::U16(u) => output += &format!("{}{}\n", pad, u), + Value::U32(u) => output += &format!("{}{}\n", pad, u), + Value::U64(u) => output += &format!("{}{}\n", pad, u), + Value::I8(i) => output += &format!("{}{}\n", pad, i), + Value::I16(i) => output += &format!("{}{}\n", pad, i), + Value::I32(i) => output += &format!("{}{}\n", pad, i), + Value::I64(i) => output += &format!("{}{}\n", pad, i), + Value::F32(f) => output += &format!("{}{}\n", pad, f), + Value::F64(f) => output += &format!("{}{}\n", pad, f), + Value::Char(c) => output += &format!("{}{}\n", pad, c), + Value::String(s) => output += &format!("{}{}\n", pad, s), + Value::Unit => output += &format!("{}\n", pad), + Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes), + + Value::Option(opt) => match opt { + Some(inner) => { + output += &format!("{}Option:\n", pad); + output += &self.print_value(inner, indent + 1); + } + None => output += &format!("{}None\n", pad), + }, + + Value::Newtype(inner) => { + output += &format!("{}Newtype:\n", pad); + output += &self.print_value(inner, indent + 1); + } + + Value::Seq(seq) => { + if seq.is_empty() { + output += &format!("{}[]\n", pad); + } else { + output += &format!("{}[\n", pad); + for item in seq { + output += &self.print_value(item, indent + 1); + } + output += &format!("{}]\n", pad); + } + } + + Value::Map(map) => { + if map.is_empty() { + output += &format!("{}\n", pad); + } else { + output += &format!( + "{}+--------------------------+----------------------------+\n", + pad + ); + output += &format!("{}| {:<24} | {:<26} |\n", pad, "Key", "Value"); + output += &format!( + "{}+--------------------------+----------------------------+\n", + pad + ); + + for (k, v) in map { + let key_str = match k { + Value::String(s) => s.clone(), + other => format!("{:?}", other), + }; + let val_str = match v { + Value::String(s) => s.clone(), + Value::Bool(b) => b.to_string(), + Value::Option(Some(inner)) => format!("{:?}", inner), + Value::Option(None) => "None".to_string(), + Value::Seq(seq) => format!("{:?}", seq), + _ => format!("{:?}", v), + }; + + output += &format!("{}| {:<24} | {:<26} |\n", pad, key_str, val_str); + } + + output += &format!( + "{}+--------------------------+----------------------------+\n\n", + pad + ); + } + } + } + + output + } +} diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 462591d..33de969 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -177,10 +177,13 @@ impl HarmonyTUI { .state(&self.tui_state); frame.render_widget(tui_logger, logger_area); - - let info = self.score.display_value(); - //let info = self.score.display_value(); - let info_block = Block::default().borders(Borders::ALL).title("Scores Info"); + let info_block = Block::default().borders(Borders::ALL).title("Score Details"); + + let scores = &self.score.get_selected_score(); + let mut info = String::new(); + scores.into_iter().for_each(|score| { + info += &score.display_value(); + }); let info_paragraph = Paragraph::new(info) .block(info_block) .wrap(Wrap { trim: true }); diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index c4d3d63..beae663 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,6 +1,3 @@ -use ratatui::layout::Constraint; -use serde_value::Value; -use std::collections::BTreeMap; use std::sync::{Arc, RwLock}; use crate::HarmonyTuiEvent; @@ -10,10 +7,8 @@ use log::{info, warn}; use ratatui::{ Frame, layout::Rect, - style::{Modifier, Style, Stylize}, - widgets::{ - Block, Borders, Cell, List, ListItem, ListState, Row, StatefulWidget, Table, Widget, - }, + style::{Style, Stylize}, + widgets::{List, ListItem, ListState, StatefulWidget, Widget}, }; use tokio::sync::mpsc; @@ -57,136 +52,25 @@ impl ScoreListWidget { } 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"); - + if let Some(score) = self.get_selected_score() { self.execution = Some(Execution { state: ExecutionState::INITIATED, score: score.clone_box(), }); - //TODO: need to format the output of the score.serialize() - //currently just dumps to MAP() and handle _ - //maybe columns - //https://arcnmx.github.io/serde-value/serde_value/enum.Value.html info!("{:#?}\n\nConfirm Execution (Press y/n)", score); + info!("{:#?}", score.display_value()); } else { warn!("No Score selected, nothing to launch"); } } - //TODO working on match statement to output serialized value - //want to try and build a fn for a few types that are represtendd in DNSscore - //and output them by calling fn display_values in the about fn - // - //currently outputs info about all available scores, not quite - //looking to output info about the selected score and subscores - // - -fn print_value(&self, val: &Value, indent: usize) -> String { - let pad = " ".repeat(indent * 2); - let mut output = String::new(); - - match val { - Value::Bool(b) => output += &format!("{}{}\n", pad, b), - Value::U8(u) => output += &format!("{}{}\n", pad, u), - Value::U16(u) => output += &format!("{}{}\n", pad, u), - Value::U32(u) => output += &format!("{}{}\n", pad, u), - Value::U64(u) => output += &format!("{}{}\n", pad, u), - Value::I8(i) => output += &format!("{}{}\n", pad, i), - Value::I16(i) => output += &format!("{}{}\n", pad, i), - Value::I32(i) => output += &format!("{}{}\n", pad, i), - Value::I64(i) => output += &format!("{}{}\n", pad, i), - Value::F32(f) => output += &format!("{}{}\n", pad, f), - Value::F64(f) => output += &format!("{}{}\n", pad, f), - Value::Char(c) => output += &format!("{}{}\n", pad, c), - Value::String(s) => output += &format!("{}{}\n", pad, s), - Value::Unit => output += &format!("{}\n", pad), - Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes), - - Value::Option(opt) => match opt { - Some(inner) => { - output += &format!("{}Option:\n", pad); - output += &self.print_value(inner, indent + 1); - } - None => output += &format!("{}None\n", pad), - }, - - Value::Newtype(inner) => { - output += &format!("{}Newtype:\n", pad); - output += &self.print_value(inner, indent + 1); + + 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 } - - Value::Seq(seq) => { - if seq.is_empty() { - output += &format!("{}[]\n", pad); - } else { - output += &format!("{}[\n", pad); - for item in seq { - output += &self.print_value(item, indent + 1); - } - output += &format!("{}]\n", pad); - } - } - - Value::Map(map) => { - if map.is_empty() { - output += &format!("{}\n", pad); - } else { - output += &format!( - "\n{}+--------------------------+----------------------------+\n", - pad - ); - output += &format!( - "{}| {:<24} | {:<26} |\n", - pad, "Key", "Value" - ); - output += &format!( - "{}+--------------------------+----------------------------+\n", - pad - ); - - for (k, v) in map { - let key_str = match k { - Value::String(s) => s.clone(), - other => format!("{:?}", other), - }; - let val_str = match v { - Value::String(s) => s.clone(), - Value::Bool(b) => b.to_string(), - Value::Option(Some(inner)) => format!("{:?}", inner), - Value::Option(None) => "None".to_string(), - Value::Seq(seq) => format!("{:?}", seq), - _ => format!("{:?}", v), - }; - - output += &format!( - "{}| {:<24} | {:<26} |\n", - pad, key_str, val_str - ); - } - - output += &format!( - "{}+--------------------------+----------------------------+\n\n", - pad - ); - } - } - } - - output -} - - pub(crate) fn display_value(&self) -> String { - let mut output = String::new(); - output += "SCORE DETAILS"; - for score in &self.scores { - let val = score.serialize(); - output += &self.print_value(&val, 0); - } - output } pub(crate) fn scroll_down(&self) { -- 2.39.5 From f9d1935c7171716f4ff3445ff5bffc73a770ee20 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 11 Apr 2025 15:49:09 -0400 Subject: [PATCH 03/10] wip: trying to output in a better format --- harmony_tui/src/lib.rs | 43 +++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 33de969..fdeedda 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -1,6 +1,7 @@ mod widget; use log::{debug, error, info}; +use prettytable::{Table, cell, row}; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tui_logger::{TuiWidgetEvent, TuiWidgetState}; @@ -13,7 +14,8 @@ use harmony::{maestro::Maestro, score::Score, topology::Topology}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, - style::{Color, Style}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, widgets::{Block, Borders, Paragraph, Wrap}, }; @@ -177,18 +179,41 @@ impl HarmonyTUI { .state(&self.tui_state); frame.render_widget(tui_logger, logger_area); - let info_block = Block::default().borders(Borders::ALL).title("Score Details"); - - let scores = &self.score.get_selected_score(); - let mut info = String::new(); - scores.into_iter().for_each(|score| { - info += &score.display_value(); - }); - let info_paragraph = Paragraph::new(info) + let info_block = Block::default() + .borders(Borders::ALL) + .title("Score Details"); + + let mut text_output = Text::default(); + if let Some(score) = self.score.get_selected_score() { + //scores.into_iter().for_each(|score| { + // info += &score.display_value(); + //}); + text_output = self.format_score_for_output(score); + } + let info_paragraph = Paragraph::new(text_output) .block(info_block) .wrap(Wrap { trim: true }); frame.render_widget(info_paragraph, info_area); } + //TODO trying to format the output in a way that looks good in the debug output + //this has not changed anything so far + fn format_score_for_output(&self, score: Box>) -> Text<'_> { + let mut text = Text::default(); + let name_line = Line::from(vec![Span::styled( + score.name(), + Style::default().add_modifier(Modifier::BOLD), + )]); + text.lines.push(name_line); + + let val = score.serialize(); + let pretty = score.print_value(&val, 0); + + for line in pretty.lines() { + text.lines.push(Line::from(line.to_string())); + } + + text + } fn scores_list(maestro: &Maestro) -> Vec>> { let scores = maestro.scores(); -- 2.39.5 From ffe175df1b52fa272282d1325878e68d99de4c94 Mon Sep 17 00:00:00 2001 From: Willem Date: Mon, 14 Apr 2025 10:27:27 -0400 Subject: [PATCH 04/10] formatted score debug into table, removed score info box from tui --- harmony/src/domain/score.rs | 26 +++++++++---------- harmony_tui/src/lib.rs | 45 +++------------------------------ harmony_tui/src/widget/score.rs | 2 +- 3 files changed, 17 insertions(+), 56 deletions(-) diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 7f3d410..6fdab6a 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -4,7 +4,7 @@ use serde_value::Value; use super::{interpret::Interpret, topology::Topology}; pub trait Score: - std::fmt::Debug + DisplayValues + Send + Sync + CloneBoxScore + SerializeScore + std::fmt::Debug + ScoreToString + Send + Sync + CloneBoxScore + SerializeScore { fn create_interpret(&self) -> Box>; fn name(&self) -> String; @@ -40,25 +40,25 @@ where } } -pub trait DisplayValues { - fn display_value(&self) -> String; - fn print_value(&self, val: &Value, indent: usize) -> String; +pub trait ScoreToString { + fn print_score_details(&self) -> String; + fn format_value_as_string(&self, val: &Value, indent: usize) -> String; } -impl DisplayValues for S +impl ScoreToString for S where T: Topology, S: Score + 'static, { - fn display_value(&self) -> String { + fn print_score_details(&self) -> String { let mut output = String::new(); - output += &format!("{}\n", &self.name()); - output += &self.print_value(&self.serialize(), 0); + output += "\n"; + output += &self.format_value_as_string(&self.serialize(), 0); output += "\n"; output } - fn print_value(&self, val: &Value, indent: usize) -> String { + fn format_value_as_string(&self, val: &Value, indent: usize) -> String { let pad = " ".repeat(indent * 2); let mut output = String::new(); @@ -82,14 +82,14 @@ where Value::Option(opt) => match opt { Some(inner) => { output += &format!("{}Option:\n", pad); - output += &self.print_value(inner, indent + 1); + output += &self.format_value_as_string(inner, indent + 1); } None => output += &format!("{}None\n", pad), }, Value::Newtype(inner) => { output += &format!("{}Newtype:\n", pad); - output += &self.print_value(inner, indent + 1); + output += &self.format_value_as_string(inner, indent + 1); } Value::Seq(seq) => { @@ -98,7 +98,7 @@ where } else { output += &format!("{}[\n", pad); for item in seq { - output += &self.print_value(item, indent + 1); + output += &self.format_value_as_string(item, indent + 1); } output += &format!("{}]\n", pad); } @@ -112,7 +112,7 @@ where "{}+--------------------------+----------------------------+\n", pad ); - output += &format!("{}| {:<24} | {:<26} |\n", pad, "Key", "Value"); + output += &format!("{}| {:<24} | {:<26} |\n", pad, "score_name", self.name()); output += &format!( "{}+--------------------------+----------------------------+\n", pad diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index fdeedda..1d50d8d 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -1,7 +1,6 @@ mod widget; use log::{debug, error, info}; -use prettytable::{Table, cell, row}; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tui_logger::{TuiWidgetEvent, TuiWidgetState}; @@ -14,9 +13,8 @@ use harmony::{maestro::Maestro, score::Score, topology::Topology}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, - style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Paragraph, Wrap}, + style::{Color, Style}, + widgets::{Block, Borders}, }; pub mod tui { @@ -161,15 +159,13 @@ impl HarmonyTUI { frame.render_widget(&help_block, help_area); frame.render_widget(HelpWidget::new(), help_block.inner(help_area)); - let [list_area, right_area] = + let [list_area, logger_area] = Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]).areas(app_area); let block = Block::default().borders(Borders::RIGHT); frame.render_widget(&block, list_area); self.score.render(list_area, frame); - let [logger_area, info_area] = - Layout::vertical([Constraint::Min(30), Constraint::Percentage(100)]).areas(right_area); let tui_logger = tui_logger::TuiLoggerWidget::default() .style_error(Style::default().fg(Color::Red)) .style_warn(Style::default().fg(Color::LightRed)) @@ -179,42 +175,7 @@ impl HarmonyTUI { .state(&self.tui_state); frame.render_widget(tui_logger, logger_area); - let info_block = Block::default() - .borders(Borders::ALL) - .title("Score Details"); - - let mut text_output = Text::default(); - if let Some(score) = self.score.get_selected_score() { - //scores.into_iter().for_each(|score| { - // info += &score.display_value(); - //}); - text_output = self.format_score_for_output(score); - } - let info_paragraph = Paragraph::new(text_output) - .block(info_block) - .wrap(Wrap { trim: true }); - frame.render_widget(info_paragraph, info_area); } - //TODO trying to format the output in a way that looks good in the debug output - //this has not changed anything so far - fn format_score_for_output(&self, score: Box>) -> Text<'_> { - let mut text = Text::default(); - let name_line = Line::from(vec![Span::styled( - score.name(), - Style::default().add_modifier(Modifier::BOLD), - )]); - text.lines.push(name_line); - - let val = score.serialize(); - let pretty = score.print_value(&val, 0); - - for line in pretty.lines() { - text.lines.push(Line::from(line.to_string())); - } - - text - } - fn scores_list(maestro: &Maestro) -> Vec>> { let scores = maestro.scores(); let scores_read = scores.read().expect("Should be able to read scores"); diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index beae663..72fc5da 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -58,7 +58,7 @@ impl ScoreListWidget { score: score.clone_box(), }); info!("{:#?}\n\nConfirm Execution (Press y/n)", score); - info!("{:#?}", score.display_value()); + info!("{}", score.print_score_details()); } else { warn!("No Score selected, nothing to launch"); } -- 2.39.5 From 08dbfd827bc23a12775eac02b91619fdc431812d Mon Sep 17 00:00:00 2001 From: Willem Date: Wed, 16 Apr 2025 16:34:49 -0400 Subject: [PATCH 05/10] added test for layout of table in tui --- harmony/src/domain/score.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 6fdab6a..039d8d1 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -75,7 +75,7 @@ where Value::F32(f) => output += &format!("{}{}\n", pad, f), Value::F64(f) => output += &format!("{}{}\n", pad, f), Value::Char(c) => output += &format!("{}{}\n", pad, c), - Value::String(s) => output += &format!("{}{}\n", pad, s), + Value::String(s) => output += &format!("{}{:<26}\n", pad, s), Value::Unit => output += &format!("{}\n", pad), Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes), @@ -146,3 +146,21 @@ where output } } + +//TODO write test to check that the output is what it should be +// +#[cfg(test)] +mod tests { + use crate::modules::dns::DnsScore; + use crate::topology::{self, HAClusterTopology}; + use super::*; + + #[test] + fn test_format_values_as_string() { + + let dns_score = Box::new(DnsScore::new(vec![], None)); + let print_score_output = >::print_score_details(&dns_score); + let expected_empty_dns_score_table = "\n+--------------------------+----------------------------+\n| score_name | DnsScore |\n+--------------------------+----------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+----------------------------+\n\n\n"; + assert_eq!(print_score_output, expected_empty_dns_score_table); + } +} -- 2.39.5 From afbbde694acc3b881ff6640e94bdd3f9750f9431 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 17 Apr 2025 10:37:08 -0400 Subject: [PATCH 06/10] fix: Activate opnsense test only with opnsenseendtoend feature enabled --- opnsense-config/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/opnsense-config/src/lib.rs b/opnsense-config/src/lib.rs index d11ec41..f83b3e0 100644 --- a/opnsense-config/src/lib.rs +++ b/opnsense-config/src/lib.rs @@ -12,6 +12,7 @@ mod test { use crate::Config; use pretty_assertions::assert_eq; + #[cfg(opnsenseendtoend)] #[tokio::test] async fn test_public_sdk() { let mac = "11:22:33:44:55:66"; -- 2.39.5 From feea9780ab699f161f45efcf6f2206c441370e2f Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 17 Apr 2025 10:50:44 -0400 Subject: [PATCH 07/10] feat: example-lamp now uses harmony-tui --- Cargo.lock | 1 + examples/lamp/Cargo.toml | 2 +- examples/lamp/src/main.rs | 14 ++++++-------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c571489..692223a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,6 +823,7 @@ dependencies = [ "env_logger", "harmony", "harmony_macros", + "harmony_tui", "harmony_types", "log", "tokio", diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml index 1bdcf68..902548e 100644 --- a/examples/lamp/Cargo.toml +++ b/examples/lamp/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -#harmony_tui = { path = "../../harmony_tui" } +harmony_tui = { path = "../../harmony_tui" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 7277aa5..bdf0f38 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -1,8 +1,5 @@ use harmony::{ - data::Version, - maestro::Maestro, - modules::lamp::{LAMPConfig, LAMPScore}, - topology::{HAClusterTopology, Url}, + data::Version, inventory::Inventory, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, topology::{HAClusterTopology, Url} }; #[tokio::main] @@ -17,8 +14,9 @@ async fn main() { }, }; - Maestro::::load_from_env() - .interpret(Box::new(lamp_stack)) - .await - .unwrap(); + let inventory = Inventory::autoload(); + let topology = HAClusterTopology::autoload(); + let mut maestro = Maestro::new(inventory, topology); + maestro.register_all(vec![Box::new(lamp_stack)]); + harmony_tui::init(maestro).await.unwrap(); } -- 2.39.5 From d937ddca3aa6a91f4a0103614bec7fdad66fa203 Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 17 Apr 2025 15:11:55 -0400 Subject: [PATCH 08/10] fix: fixed rendering for map inside score, added wrap or truncate to keep string inside column, increased column width to accomodate urls --- harmony/src/domain/score.rs | 123 ++++++++++++++++++++++++-------- harmony_tui/src/lib.rs | 1 + harmony_tui/src/widget/score.rs | 2 +- 3 files changed, 96 insertions(+), 30 deletions(-) diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 039d8d1..caf74ff 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use serde::Serialize; use serde_value::Value; @@ -43,6 +45,8 @@ where pub trait ScoreToString { fn print_score_details(&self) -> String; fn format_value_as_string(&self, val: &Value, indent: usize) -> String; + fn format_map(&self, map: &BTreeMap, indent: usize) -> String; + fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec; } impl ScoreToString for S @@ -57,6 +61,78 @@ where output += "\n"; output } + fn format_map(&self, map: &BTreeMap, indent: usize) -> String { + let pad = " ".repeat(indent * 2); + let mut output = String::new(); + + output += &format!( + "{}+--------------------------+--------------------------------------------------+\n", + pad + ); + output += &format!("{}| {:<24} | {:<48} |\n", pad, "score_name", self.name()); + output += &format!( + "{}+--------------------------+--------------------------------------------------+\n", + pad + ); + + for (k, v) in map { + let key_str = match k { + Value::String(s) => s.clone(), + other => format!("{:?}", other), + }; + + let formatted_val = self.format_value_as_string(v, indent + 1); + let mut lines = formatted_val.lines().map(|line| line.trim_start()); + + let wrapped_lines: Vec<_> = lines + .flat_map(|line| self.wrap_or_truncate(line.trim_start(), 48)) + .collect(); + + if let Some(first) = wrapped_lines.first() { + output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first); + for line in &wrapped_lines[1..] { + output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line); + } + } + + // let first_line = lines.next().unwrap_or(""); + // output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first_line); + // + // for line in lines { + // output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line); + // } + } + + output += &format!( + "{}+--------------------------+--------------------------------------------------+\n\n", + pad + ); + + output + } + + fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec { + let mut lines = Vec::new(); + let mut current = s; + + while !current.is_empty() { + if current.len() <= width { + lines.push(current.to_string()); + break; + } + + // Try to wrap at whitespace if possible + let mut split_index = current[..width].rfind(' ').unwrap_or(width); + if split_index == 0 { + split_index = width; + } + + lines.push(current[..split_index].trim_end().to_string()); + current = current[split_index..].trim_start(); + } + + lines + } fn format_value_as_string(&self, val: &Value, indent: usize) -> String { let pad = " ".repeat(indent * 2); @@ -75,7 +151,7 @@ where Value::F32(f) => output += &format!("{}{}\n", pad, f), Value::F64(f) => output += &format!("{}{}\n", pad, f), Value::Char(c) => output += &format!("{}{}\n", pad, c), - Value::String(s) => output += &format!("{}{:<26}\n", pad, s), + Value::String(s) => output += &format!("{}{:<48}\n", pad, s), Value::Unit => output += &format!("{}\n", pad), Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes), @@ -107,38 +183,27 @@ where Value::Map(map) => { if map.is_empty() { output += &format!("{}\n", pad); + } else if indent == 0 { + output += &self.format_map(map, indent); } else { - output += &format!( - "{}+--------------------------+----------------------------+\n", - pad - ); - output += &format!("{}| {:<24} | {:<26} |\n", pad, "score_name", self.name()); - output += &format!( - "{}+--------------------------+----------------------------+\n", - pad - ); - for (k, v) in map { let key_str = match k { Value::String(s) => s.clone(), other => format!("{:?}", other), }; - let val_str = match v { - Value::String(s) => s.clone(), - Value::Bool(b) => b.to_string(), - Value::Option(Some(inner)) => format!("{:?}", inner), - Value::Option(None) => "None".to_string(), - Value::Seq(seq) => format!("{:?}", seq), - _ => format!("{:?}", v), - }; - output += &format!("{}| {:<24} | {:<26} |\n", pad, key_str, val_str); + let val_str = self + .format_value_as_string(v, indent + 1) + .trim() + .to_string(); + let val_lines: Vec<_> = val_str.lines().collect(); + + output += + &format!("{}{}: {}\n", pad, key_str, val_lines.first().unwrap_or(&"")); + for line in val_lines.iter().skip(1) { + output += &format!("{} {}\n", pad, line); + } } - - output += &format!( - "{}+--------------------------+----------------------------+\n\n", - pad - ); } } } @@ -151,15 +216,15 @@ where // #[cfg(test)] mod tests { + use super::*; use crate::modules::dns::DnsScore; use crate::topology::{self, HAClusterTopology}; - use super::*; - + #[test] fn test_format_values_as_string() { - let dns_score = Box::new(DnsScore::new(vec![], None)); - let print_score_output = >::print_score_details(&dns_score); + let print_score_output = + >::print_score_details(&dns_score); let expected_empty_dns_score_table = "\n+--------------------------+----------------------------+\n| score_name | DnsScore |\n+--------------------------+----------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+----------------------------+\n\n\n"; assert_eq!(print_score_output, expected_empty_dns_score_table); } diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 1d50d8d..d8c18e2 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -125,6 +125,7 @@ impl HarmonyTUI { std::fs::create_dir_all("log")?; tui_logger::set_log_file("log/harmony.log").unwrap(); + color_eyre::install()?; let mut terminal = ratatui::init(); log_panics::init(); diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index 72fc5da..e394b81 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -57,7 +57,7 @@ impl ScoreListWidget { state: ExecutionState::INITIATED, score: score.clone_box(), }); - info!("{:#?}\n\nConfirm Execution (Press y/n)", score); + info!("{}\n\nConfirm Execution (Press y/n)", score.name()); info!("{}", score.print_score_details()); } else { warn!("No Score selected, nothing to launch"); -- 2.39.5 From 27f6ed97f98c05561095dfc2d418ca5b06f47445 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 10:32:53 -0400 Subject: [PATCH 09/10] chore: Add heavy score example to tui to see what it looks like with long output. Good enough as a step forward! --- examples/lamp/src/main.rs | 6 ++- examples/tui/src/main.rs | 48 +++++++++++++++++++- harmony/src/domain/score.rs | 2 +- harmony/src/domain/topology/load_balancer.rs | 1 + harmony_tui/src/lib.rs | 5 +- harmony_tui/src/widget/score.rs | 2 +- 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index bdf0f38..feb8b4f 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -1,5 +1,9 @@ use harmony::{ - data::Version, inventory::Inventory, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, topology::{HAClusterTopology, Url} + data::Version, + inventory::Inventory, + maestro::Maestro, + modules::lamp::{LAMPConfig, LAMPScore}, + topology::{HAClusterTopology, Url}, }; #[tokio::main] diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index d912844..d29eae5 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -1,12 +1,20 @@ +use std::net::{SocketAddr, SocketAddrV4}; + use harmony::{ inventory::Inventory, maestro::Maestro, modules::{ dns::DnsScore, dummy::{ErrorScore, PanicScore, SuccessScore}, + load_balancer::LoadBalancerScore, + okd::load_balancer::OKDLoadBalancerScore, + }, + topology::{ + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, + LoadBalancerService, }, - topology::HAClusterTopology, }; +use harmony_macros::ipv4; #[tokio::main] async fn main() { @@ -14,11 +22,49 @@ async fn main() { let topology = HAClusterTopology::autoload(); let mut maestro = Maestro::new(inventory, topology); + let backend_server = BackendServer { + address: "192.168.0.0".to_string(), + port: 342, + }; + let lb_service = LoadBalancerService { + backend_servers: vec![ + backend_server.clone(), + backend_server.clone(), + backend_server.clone(), + ], + listening_port: SocketAddr::V4(SocketAddrV4::new(ipv4!("192.168.0.0"), 49387)), + health_check: Some(HealthCheck::HTTP( + "/some_long_ass_path_to_see_how_it_is_displayed_but_it_has_to_be_even_longer" + .to_string(), + HttpMethod::GET, + HttpStatusCode::Success2xx, + )), + }; + let okd_lbscore = LoadBalancerScore { + public_services: vec![ + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + ], + private_services: vec![ + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + ], + }; + maestro.register_all(vec![ Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), Box::new(DnsScore::new(vec![], None)), + Box::new(okd_lbscore), ]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index caf74ff..a48548c 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -225,7 +225,7 @@ mod tests { let dns_score = Box::new(DnsScore::new(vec![], None)); let print_score_output = >::print_score_details(&dns_score); - let expected_empty_dns_score_table = "\n+--------------------------+----------------------------+\n| score_name | DnsScore |\n+--------------------------+----------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+----------------------------+\n\n\n"; + let expected_empty_dns_score_table = "\n+--------------------------+--------------------------------------------------+\n| score_name | DnsScore |\n+--------------------------+--------------------------------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+--------------------------------------------------+\n\n\n"; assert_eq!(print_score_output, expected_empty_dns_score_table); } } diff --git a/harmony/src/domain/topology/load_balancer.rs b/harmony/src/domain/topology/load_balancer.rs index afb9092..6127019 100644 --- a/harmony/src/domain/topology/load_balancer.rs +++ b/harmony/src/domain/topology/load_balancer.rs @@ -46,6 +46,7 @@ pub struct LoadBalancerService { #[derive(Debug, PartialEq, Clone, Serialize)] pub struct BackendServer { + // TODO should not be a string, probably IPAddress pub address: String, pub port: u16, } diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index d8c18e2..7ae70bd 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -36,13 +36,13 @@ pub mod tui { /// modules::dummy::{ErrorScore, PanicScore, SuccessScore}, /// topology::HAClusterTopology, /// }; -/// +/// /// #[tokio::main] /// async fn main() { /// let inventory = Inventory::autoload(); /// let topology = HAClusterTopology::autoload(); /// let mut maestro = Maestro::new(inventory, topology); -/// +/// /// maestro.register_all(vec![ /// Box::new(SuccessScore {}), /// Box::new(ErrorScore {}), @@ -125,7 +125,6 @@ impl HarmonyTUI { std::fs::create_dir_all("log")?; tui_logger::set_log_file("log/harmony.log").unwrap(); - color_eyre::install()?; let mut terminal = ratatui::init(); log_panics::init(); diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index e394b81..3acb5c2 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -63,7 +63,7 @@ impl ScoreListWidget { 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() { -- 2.39.5 From 3e032b79f62d0c0679bec86c043f95f24130c00c Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 10:37:29 -0400 Subject: [PATCH 10/10] chore: Move large score to function --- examples/tui/src/main.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index d29eae5..5ed607a 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -22,6 +22,18 @@ async fn main() { let topology = HAClusterTopology::autoload(); let mut maestro = Maestro::new(inventory, topology); + + maestro.register_all(vec![ + Box::new(SuccessScore {}), + Box::new(ErrorScore {}), + Box::new(PanicScore {}), + Box::new(DnsScore::new(vec![], None)), + Box::new(build_large_score()), + ]); + harmony_tui::init(maestro).await.unwrap(); +} + +fn build_large_score() -> LoadBalancerScore { let backend_server = BackendServer { address: "192.168.0.0".to_string(), port: 342, @@ -40,7 +52,7 @@ async fn main() { HttpStatusCode::Success2xx, )), }; - let okd_lbscore = LoadBalancerScore { + LoadBalancerScore { public_services: vec![ lb_service.clone(), lb_service.clone(), @@ -57,14 +69,5 @@ async fn main() { lb_service.clone(), lb_service.clone(), ], - }; - - maestro.register_all(vec![ - Box::new(SuccessScore {}), - Box::new(ErrorScore {}), - Box::new(PanicScore {}), - Box::new(DnsScore::new(vec![], None)), - Box::new(okd_lbscore), - ]); - harmony_tui::init(maestro).await.unwrap(); + } } -- 2.39.5