From 460b51c503075d7a5121afee747ba3294dba6aeb Mon Sep 17 00:00:00 2001 From: Trevor Merritt Date: Thu, 24 Apr 2025 11:29:29 -0400 Subject: [PATCH] UI now has red/green for down/up hosts duration_to_string now in lib --- src/bin/pp.rs | 42 +------------ src/bin/rat.rs | 1 - src/lib.rs | 42 +++++++++++++ src/tui/mod.rs | 3 +- src/tui/ratatui_app.rs | 140 +++++++++++++++++++---------------------- src/tui/ratatui_ui.rs | 80 +++++++++++++++++++++++ 6 files changed, 188 insertions(+), 120 deletions(-) create mode 100644 src/tui/ratatui_ui.rs diff --git a/src/bin/pp.rs b/src/bin/pp.rs index e46afdb..2191e4b 100644 --- a/src/bin/pp.rs +++ b/src/bin/pp.rs @@ -11,7 +11,7 @@ use clap::Parser; use pp::ping_result::PingResult; use pp::ping_request::PingRequest; use pp::manager::Manager; -use pp::SECONDS_BETWEEN_DISPLAY; +use pp::{duration_to_string, SECONDS_BETWEEN_DISPLAY}; use pp::target_state::TargetState; use std::{env, error::Error, ffi::OsString, process}; use color_eyre::owo_colors::OwoColorize; @@ -19,46 +19,6 @@ use crossterm::style::Stylize; use log::debug; use pp::app_settings::AppSettings; -const SECONDS_IN_MINUTE: u32 = 60; -const SECONDS_IN_HOUR: u32 = SECONDS_IN_MINUTE * 60; -const SECONDS_IN_DAY: u32 = SECONDS_IN_HOUR * 24; - - -pub fn duration_to_string(to_convert: Duration) -> String { - let mut total_seconds = to_convert.as_secs() as u32; - let mut working_string = String::new(); - - if total_seconds > 86400 { - // days - let num_days = (total_seconds / SECONDS_IN_DAY) as u32; - working_string = format!("{} days", num_days); - total_seconds = total_seconds - (num_days * SECONDS_IN_DAY); - } - - if total_seconds > 3600 { - // hours - let num_hours = (total_seconds / SECONDS_IN_HOUR) as u32; - if num_hours > 0 { - working_string = format!("{} {} hours", working_string, num_hours); - total_seconds = total_seconds - (num_hours * SECONDS_IN_HOUR); - } - } - if total_seconds > 60 { - let num_minutes = (total_seconds / SECONDS_IN_MINUTE) as u32; - if num_minutes > 0 { - working_string = format!("{} {} minutes", working_string, num_minutes); - total_seconds = total_seconds - (num_minutes * SECONDS_IN_MINUTE); - } - // minutes - } - - - working_string = format!("{} {} seconds", working_string, total_seconds); - - working_string -} - - struct PPState {} impl PPState { diff --git a/src/bin/rat.rs b/src/bin/rat.rs index 0c8ce07..69017f9 100644 --- a/src/bin/rat.rs +++ b/src/bin/rat.rs @@ -5,7 +5,6 @@ fn main() -> color_eyre::Result<()> { // find out what file we are using to get our hosts let settings = AppSettings::parse(); - color_eyre::install()?; let terminal = ratatui::init(); let result = RatatuiApp::new(settings.ping_host_file).run(terminal); diff --git a/src/lib.rs b/src/lib.rs index 01e69c3..d6ec039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + pub mod manager; pub mod ping_request; pub mod ping_result; @@ -7,3 +9,43 @@ pub mod app_settings; pub const SECONDS_BETWEEN_DISPLAY: u32 = 1; pub const SECONDS_BETWEEN_PING: u32 = 2; + +const SECONDS_IN_MINUTE: u32 = 60; +const SECONDS_IN_HOUR: u32 = SECONDS_IN_MINUTE * 60; +const SECONDS_IN_DAY: u32 = SECONDS_IN_HOUR * 24; + + +pub fn duration_to_string(to_convert: Duration) -> String { + let mut total_seconds = to_convert.as_secs() as u32; + let mut working_string = String::new(); + + if total_seconds > 86400 { + // days + let num_days = (total_seconds / SECONDS_IN_DAY) as u32; + working_string = format!("{} days", num_days); + total_seconds = total_seconds - (num_days * SECONDS_IN_DAY); + } + + if total_seconds > 3600 { + // hours + let num_hours = (total_seconds / SECONDS_IN_HOUR) as u32; + if num_hours > 0 { + working_string = format!("{} {} hours", working_string, num_hours); + total_seconds = total_seconds - (num_hours * SECONDS_IN_HOUR); + } + } + if total_seconds > 60 { + let num_minutes = (total_seconds / SECONDS_IN_MINUTE) as u32; + if num_minutes > 0 { + working_string = format!("{} {} minutes", working_string, num_minutes); + total_seconds = total_seconds - (num_minutes * SECONDS_IN_MINUTE); + } + // minutes + } + + + working_string = format!("{} {} seconds", working_string, total_seconds); + + working_string +} + diff --git a/src/tui/mod.rs b/src/tui/mod.rs index dd1c881..397a6b9 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,2 +1,3 @@ pub mod target_state_widget; -pub mod ratatui_app; \ No newline at end of file +pub mod ratatui_app; +mod ratatui_ui; \ No newline at end of file diff --git a/src/tui/ratatui_app.rs b/src/tui/ratatui_app.rs index 30782d8..b4be85d 100644 --- a/src/tui/ratatui_app.rs +++ b/src/tui/ratatui_app.rs @@ -12,55 +12,24 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::{DefaultTerminal, Frame}; use ratatui::prelude::*; use ratatui::widgets::{Block, Paragraph}; +use crate::duration_to_string; use crate::manager::Manager; use crate::ping_result::PingResult; use crate::target_state::TargetState; +use crate::tui::ratatui_ui::RatatuiUi; - -const SECONDS_IN_MINUTE: u32 = 60; -const SECONDS_IN_HOUR: u32 = SECONDS_IN_MINUTE * 60; -const SECONDS_IN_DAY: u32 = SECONDS_IN_HOUR * 24; - - -pub fn duration_to_string(to_convert: Duration) -> String { - let mut total_seconds = to_convert.as_secs() as u32; - let mut working_string = String::new(); - - if total_seconds > 86400 { - // days - let num_days = (total_seconds / SECONDS_IN_DAY) as u32; - working_string = format!("{} days", num_days); - total_seconds = total_seconds - (num_days * SECONDS_IN_DAY); - } - - if total_seconds > 3600 { - // hours - let num_hours = (total_seconds / SECONDS_IN_HOUR) as u32; - if num_hours > 0 { - working_string = format!("{} {} hours", working_string, num_hours); - total_seconds = total_seconds - (num_hours * SECONDS_IN_HOUR); - } - } - if total_seconds > 60 { - let num_minutes = (total_seconds / SECONDS_IN_MINUTE) as u32; - if num_minutes > 0 { - working_string = format!("{} {} minutes", working_string, num_minutes); - total_seconds = total_seconds - (num_minutes * SECONDS_IN_MINUTE); - } - // minutes - } - - - working_string = format!("{} {} seconds", working_string, total_seconds); - - working_string +#[derive(Default)] +pub enum RatatuiScreens { + #[default] + Monitoring, + Exiting } - #[derive(Default)] pub struct RatatuiApp { running: bool, state: BTreeMap, + current_screen: RatatuiScreens } impl RatatuiApp { @@ -73,8 +42,18 @@ impl RatatuiApp { // check for any waiting ping results... self.consume_waiting_results(&receiver); - terminal.draw(|frame| self.render(frame)).expect("Unable to draw to screen"); - self.handle_crossterm_events()?; + match self.current_screen { + RatatuiScreens::Monitoring => { + terminal.draw(|frame| RatatuiUi::monitoring_mode(frame, &self.state)).expect("Unable to draw to screen"); + self.handle_monitoring_crossterm_events(); + } + RatatuiScreens::Exiting => { + terminal.draw(| frame | RatatuiUi::exiting_mode(frame)); + self.handle_exiting_crossterm_events(); + } + } + + // self.handle_crossterm_events()?; } Ok(()) } @@ -83,17 +62,17 @@ impl RatatuiApp { let mut local_state = self.state.clone(); if let Ok(new_message) = receiver.recv_timeout(Duration::from_millis(10)) { // find the right TargetState - for (name, mut state) in local_state.clone() { - if state.target == new_message.target { - let last_alive_change = if new_message.success == state.alive { - state.last_alive_change + for (name, mut current_state) in local_state.clone() { + if current_state.target == new_message.target { + let last_alive_change = if new_message.success == current_state.alive { + current_state.last_alive_change } else { SystemTime::now() }; let new_state = TargetState { - name: state.name.clone(), - target: state.target, + name: current_state.name.clone(), + target: current_state.target, alive: new_message.success, last_rtt: new_message.rtt, last_alive_change, @@ -114,37 +93,45 @@ impl RatatuiApp { } fn render(&mut self, frame: &mut Frame) { - let title = Line::from("PP Sample App") - .bold() - .blue() - .centered(); - let mut working = String::new(); - for (title, current) in &self.state { - let color_name = if current.alive { - current.name.clone().green() - } else { - current.name.clone().red() - }; - working = format!("{}\n{} ({}) - {} / {} / {}", - working, - color_name, - current.target, - current.alive, - current.last_rtt, - duration_to_string( - SystemTime::now() - .duration_since(current.last_alive_change) - .unwrap() - ) - ); + match self.current_screen { + RatatuiScreens::Monitoring => { RatatuiUi::monitoring_mode(frame, &self.state) } + RatatuiScreens::Exiting => { RatatuiUi::exiting_mode(frame)} } + } - frame.render_widget( - Paragraph::new(working) - .block(Block::bordered().title(title)) - .centered(), - frame.area(), - ); + fn handle_monitoring_crossterm_events(&mut self) -> Result<()> { + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => { + match (key.modifiers, key.code) { + (_, KeyCode::Esc) => { + self.current_screen = RatatuiScreens::Exiting; + } + _ => {} + } + }, + _ => {} + } + } + Ok(()) + } + + fn handle_exiting_crossterm_events(&mut self) -> Result<()> { + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => { + match key.code { + KeyCode::Enter => { self.running = false; } + KeyCode::Char('y') | KeyCode::Char('Y') => { self.running = false; + } + KeyCode::Char('n') | KeyCode::Char('N') => { self.current_screen = RatatuiScreens::Monitoring } + _ => {} + } + } + _ => {} + } + } + Ok(()) } fn handle_crossterm_events(&mut self) -> Result<()> { @@ -172,7 +159,6 @@ impl RatatuiApp { _ => {} } } - pub fn new(option: Option) -> Self { let targets = if let Some(file) = option { let mut working = BTreeMap::new(); diff --git a/src/tui/ratatui_ui.rs b/src/tui/ratatui_ui.rs new file mode 100644 index 0000000..46f34c3 --- /dev/null +++ b/src/tui/ratatui_ui.rs @@ -0,0 +1,80 @@ +use std::collections::BTreeMap; +use std::time::SystemTime; +use ratatui::Frame; +use ratatui::prelude::*; +use ratatui::widgets::{Block, Paragraph}; +use crate::duration_to_string; +use crate::target_state::TargetState; + +pub struct RatatuiUi {} + +impl RatatuiUi { + pub fn exiting_mode(frame: &mut Frame) { + let title = Line::from("Exit?") + .bold() + .red() + .centered(); + let mut body = "Do you want to exit? (Y/N)"; + frame.render_widget( + Paragraph::new(body) + .block(Block::bordered().title(title)) + .centered(), + frame.area() + ); + } + pub fn monitoring_mode(frame: &mut Frame, state: &BTreeMap) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(3) + ]) + .split(frame.area()); + + let title = Line::from("PP Sample App") + .bold() + .blue() + .centered(); + let mut working = vec![]; // Line::from("Empty"); + for (title, current) in state { + let mut name_field = format!("{} ({})", current.name.clone(), current.target.clone()); + + while name_field.len() < 40 { + name_field.push(' '); + } + + let name_style = if current.alive { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::Red) + }; + + working.push(Line::from(vec![ + Span::styled(name_field, name_style), + Span::styled(current.alive.to_string(), Style::default()), + Span::styled(current.last_rtt.to_string(), Style::default()), + Span::styled( + duration_to_string( + SystemTime::now() + .duration_since(current.last_alive_change) + .unwrap() + ), Style::default()) + ])); + } + + let footer_text = "Press to exit | Press + to Add a Host | Press - to Delete a host"; + + frame.render_widget( + Paragraph::new(working) + .block(Block::bordered().title(title)), + layout[0], + ); + + frame.render_widget( + Paragraph::new(footer_text) + .block(Block::bordered()) + .centered(), + layout[1] + ); + } +} \ No newline at end of file