diff --git a/Cargo.lock b/Cargo.lock index 6e99111..4241010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -118,6 +133,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "cassowary" version = "0.3.0" @@ -148,6 +169,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.37" @@ -235,6 +270,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossterm" version = "0.28.1" @@ -406,6 +447,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -482,6 +547,16 @@ dependencies = [ "syn", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -552,6 +627,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.32.2" @@ -625,9 +709,10 @@ dependencies = [ [[package]] name = "pp" -version = "0.1.1" +version = "0.2.0-PREVIEW" dependencies = [ "ansi_term", + "chrono", "clap", "color-eyre", "crossterm", @@ -971,6 +1056,64 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -993,6 +1136,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 9b1c29b..c1c1e08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pp" -version = "0.1.1" +version = "0.2.0-PREVIEW" edition = "2024" [dependencies] @@ -15,5 +15,7 @@ color-eyre = "0.6.3" crossterm = "0.28.1" ratatui = "0.29.0" +# Time Display +chrono = "0.4" [[bin]] name = "pp" diff --git a/hosts.txt b/hosts.txt deleted file mode 100644 index e69de29..0000000 diff --git a/peterborough.txt b/peterborough.txt index 8a5c0a7..8253f40 100644 --- a/peterborough.txt +++ b/peterborough.txt @@ -1,3 +1,4 @@ +address,name 10.3.100.1,Belleville Gateway 10.11.31.3,Belleville VPN 11-31 10.12.32.1,Belleville VPN 12-32 diff --git a/src/app_settings.rs b/src/app_settings.rs new file mode 100644 index 0000000..fb55ffe --- /dev/null +++ b/src/app_settings.rs @@ -0,0 +1,10 @@ +use std::path::PathBuf; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct AppSettings { + /// File of list of hosts + #[arg(short, long)] + pub ping_host_file: Option, +} \ No newline at end of file diff --git a/src/bin/pp.rs b/src/bin/pp.rs index 30c213d..1970104 100644 --- a/src/bin/pp.rs +++ b/src/bin/pp.rs @@ -17,6 +17,8 @@ use std::{env, error::Error, ffi::OsString, process}; use color_eyre::owo_colors::OwoColorize; 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; @@ -117,7 +119,7 @@ impl PPState { } pub fn build_targets_from_file(filename: Option) -> BTreeMap { - PPState::get_default_targets(); + // PPState::get_default_targets(); if let Some(file) = filename { let mut working = BTreeMap::new(); if !&file.exists() { @@ -155,13 +157,7 @@ impl PPState { } /// Simple program to greet a person - #[derive(Parser, Debug)] - #[command(version, about, long_about = None)] - pub struct AppSettings { - /// File of list of hosts - #[arg(short, long)] - ping_host_file: Option, - } + fn main() { // Get App Settings diff --git a/src/bin/rat.rs b/src/bin/rat.rs new file mode 100644 index 0000000..7b3f576 --- /dev/null +++ b/src/bin/rat.rs @@ -0,0 +1,15 @@ +use clap::Parser; +use ratatui::widgets::TableState; +use pp::app_settings::AppSettings; +use pp::tui::ratatui_app::RatatuiApp; +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); + ratatui::restore(); + result +} + diff --git a/src/lib.rs b/src/lib.rs index 05567e8..d6ec039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,51 @@ +use std::time::Duration; + pub mod manager; pub mod ping_request; pub mod ping_result; pub mod target_state; -mod tui; +pub mod tui; +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 e69de29..1cb6103 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -0,0 +1,8 @@ +pub mod mode_editing; +pub mod mode_exiting; +pub mod mode_monitoring; +pub mod ratatui_app; +pub mod target_state_widget; +mod ratatui_screens; +mod mode_deleting; +mod mode_adding; diff --git a/src/tui/mode_adding.rs b/src/tui/mode_adding.rs new file mode 100644 index 0000000..70de156 --- /dev/null +++ b/src/tui/mode_adding.rs @@ -0,0 +1,36 @@ +use std::time::Duration; +use ratatui::Frame; +use crate::tui::ratatui_app::RatatuiApp; +use color_eyre::Result; +use crossterm::event; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::widgets::{Block, Paragraph}; +use crate::tui::ratatui_screens::RatatuiScreens::Monitoring; + +pub struct RatatuiAddingMode {} + +impl RatatuiAddingMode { + pub fn render(frame: &mut Frame, state: &mut RatatuiApp) { + frame.render_widget( + Paragraph::new("Not Yet Written.") + .block(Block::bordered()) + .centered(), + frame.area() + ); + + } + + pub fn handle_crossterm_events(app: &mut RatatuiApp) -> Result<()> { + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Esc => app.set_screen(Monitoring), + _ => {} + } + _ => {} + } + + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/tui/mode_deleting.rs b/src/tui/mode_deleting.rs new file mode 100644 index 0000000..6f84c4c --- /dev/null +++ b/src/tui/mode_deleting.rs @@ -0,0 +1,46 @@ +use std::time::Duration; +use crossterm::event; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::Frame; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::{Block, Paragraph}; +use crate::tui::ratatui_app::RatatuiApp; +use crate::tui::ratatui_screens::RatatuiScreens::Monitoring; +use color_eyre::Result; + +pub struct RatatuiDeletingMode {} + +impl RatatuiDeletingMode { + pub fn render(frame: &mut Frame, state: &mut RatatuiApp) { + let title = Line::from("Delete Host").bold().red().centered(); + let mut body = format!("Do you really want to delete {} (Y/N)", state.selected_host); + + frame.render_widget( + Paragraph::new(body) + .block(Block::bordered().title(title)) + .centered(), + frame.area() + ); + } + + pub fn handle_crossterm_events(app: &mut RatatuiApp) -> Result<()> { + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => { + println!("TiME TO DELETE SELECTED"); + } + KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => { + app.set_screen(Monitoring) + } + _ => {} + } + _ => {} + } + } + + Ok(()) + } +} + diff --git a/src/tui/mode_editing.rs b/src/tui/mode_editing.rs new file mode 100644 index 0000000..4261aa3 --- /dev/null +++ b/src/tui/mode_editing.rs @@ -0,0 +1,62 @@ +use std::collections::BTreeMap; +use std::time::Duration; +use crossterm::event; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::Frame; +use ratatui::prelude::Line; +use ratatui::style::Stylize; +use ratatui::widgets::{Block, Paragraph}; +use crate::target_state::TargetState; +use crate::tui::ratatui_app::RatatuiApp; +use color_eyre::Result; +use crate::tui::ratatui_screens::RatatuiScreens::Monitoring; + +pub struct RatatuiEditingMode {} + +impl RatatuiEditingMode { + pub fn handle_crossterm_events(app: &mut RatatuiApp) -> Result<()> { + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Up => { + println!("UP") + } + KeyCode::Down => { + println!("Down") + } + KeyCode::Char('+') => { + println!("ADD HOST") + } + KeyCode::Char('-') => { + println!("Delete Host") + } + KeyCode::Char('q') => { + app.set_running(false); + } + KeyCode::Esc => { + app.set_screen(Monitoring); + } + KeyCode::Char('s') => { + println!("Save to existing file."); + } + _ => {} + }, + _ => {} + } + } + Ok(()) + } + + pub fn render(frame: &mut Frame, state: &mut BTreeMap) { + let title = Line::from("Editing Hosts").bold().blue().centered(); + + let body_text = "This is the body text for editing hosts\n'q' to exit to return to monitoring"; + + frame.render_widget( + Paragraph::new(body_text) + .centered() + .block(Block::new().title(title).blue()), + frame.area(), + ); + } +} diff --git a/src/tui/mode_exiting.rs b/src/tui/mode_exiting.rs new file mode 100644 index 0000000..298506b --- /dev/null +++ b/src/tui/mode_exiting.rs @@ -0,0 +1,43 @@ +use std::time::Duration; +use crossterm::event; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::Frame; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::{Block, Paragraph}; +use crate::tui::ratatui_app::RatatuiApp; +use color_eyre::Result; +use crate::tui::ratatui_screens::RatatuiScreens::Monitoring; + +pub struct RatatuiExitingMode {} + +impl RatatuiExitingMode { + pub fn render(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 handle_crossterm_events(app: &mut RatatuiApp) -> Result<()>{ + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => { + app.set_running(false); + } + KeyCode::Char('n') | KeyCode::Char('N') => { + app.set_screen(Monitoring); + } + _ => {} + }, + _ => {} + } + } + Ok(()) + } +} diff --git a/src/tui/mode_monitoring.rs b/src/tui/mode_monitoring.rs new file mode 100644 index 0000000..cf9de00 --- /dev/null +++ b/src/tui/mode_monitoring.rs @@ -0,0 +1,174 @@ +use crate::duration_to_string; +use color_eyre::Result; +use crossterm::event; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::prelude::*; +use ratatui::widgets::{Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table, TableState}; +use ratatui::Frame; +use std::time::{Duration, SystemTime}; +use ratatui::layout::Direction::Vertical; +use crate::tui::ratatui_app::RatatuiApp; +use crate::tui::ratatui_screens::RatatuiScreens::{Deleting, Editing, Exiting}; + +pub struct RatatuiMonitoringMode {} + +impl RatatuiMonitoringMode { + pub fn render(frame: &mut Frame, state: &mut RatatuiApp) { + let layouts = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(10), + Constraint::Length(3), + ]) + .split(frame.area()); + + let body_layout = layouts[0]; + let logs_layout = layouts[1]; + let footer_layout = layouts[2]; + // + // let title = Line::from(format!("PP v{}", env!("CARGO_PKG_VERSION"))) + // .bold() + // .blue() + // .centered(); + // let table_size = layouts[0].area(); + // let columns = Layout::default() + // .direction(Vertical) + // .constraints([Constraint::Min(30), + // Constraint::Min(6), + // Constraint::Min(4), + // Constraint::Min(30)]); + + let headers = ["Host", "Alive", "RTT", "Last Change"] + .iter() + .map(|h| Cell::from(*h)); + let header = Row::new(headers) + .style(Style::default().fg(Color::Yellow)) + .bottom_margin(1); + let mut rows = vec![]; + + for (_, current) in state.state.iter() { + 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) + }; + + let to_push = vec![ + Cell::from(name_field).style(name_style), + Cell::from(current.alive.to_string()), + Cell::from(current.last_rtt.to_string()), + Cell::from( + format!("{} ago.", + duration_to_string( + SystemTime::now() + .duration_since(current.last_alive_change) + .unwrap() + ) + ) + ) + ]; + + rows.push(Row::new(to_push)); + } + + let table = Table::new(rows, vec![Constraint::Min(30), Constraint::Min(6), Constraint::Min(5), Constraint::Min(30)]) + .header(header) + .block(Block::default() + .title("Hosts") + .borders(Borders::ALL)) + .widths(&[ + Constraint::Min(30), + Constraint::Min(6), + Constraint::Min(4), + Constraint::Min(30) + ]) + .highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + // frame.render_widget(table, layouts[0]); + frame.render_stateful_widget(table, layouts[0], &mut make_state(state.selected_host)); + + let footer_text = "Press or q to exit | Press d to delete host | Press a to add host"; + + let mut list_items = vec![]; + for entry in state.get_log_entries(10) { + list_items.push(ListItem::new(entry)); + } + + let list = List::new(list_items) + .block(Block::default().title("Logs").borders(Borders::ALL)) + .highlight_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + let mut list_state = ListState::default(); + list_state.select(Some(0)); + + frame.render_stateful_widget(list, logs_layout, &mut list_state); + + frame.render_widget( + Paragraph::new(footer_text) + .block(Block::bordered()) + .centered(), + footer_layout, + ); + } + + pub fn handle_crossterm_events(app: &mut RatatuiApp) -> 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::Down) => { + if app.selected_host + 1 == app.state.len() { + app.selected_host = 0; + } else { + app.selected_host += 1; + } + } + (_, KeyCode::Up) => { + if app.selected_host == 0 { + app.selected_host = app.state.len() - 1; + } else { + app.selected_host -= 1; + } + } + (_, KeyCode::Esc) | (_, KeyCode::Char('q')) | (_, KeyCode::Char('Q')) => { + app.set_screen(Exiting); + } + (_, KeyCode::Char('e')) | (_, KeyCode::Char('E')) => { + app.set_screen(Editing); + } + (_, KeyCode::Char('d')) | (_, KeyCode::Char('D')) => { + app.set_screen(Deleting); + } + _ => {} + } + } + _ => {} + } + } + Ok(()) + } +} + +pub fn make_state(selected: usize) -> TableState { + let mut state = TableState::default(); + state.select(Some(selected)); + state +} \ No newline at end of file diff --git a/src/tui/ratatui_app.rs b/src/tui/ratatui_app.rs new file mode 100644 index 0000000..3b70932 --- /dev/null +++ b/src/tui/ratatui_app.rs @@ -0,0 +1,220 @@ +use crate::tui::ratatui_screens::RatatuiScreens; +use crate::manager::Manager; +use crate::ping_result::PingResult; +use crate::target_state::TargetState; +use chrono::{DateTime, Local}; +use color_eyre::Result; +use ratatui::prelude::*; +use ratatui::{DefaultTerminal, Frame}; +use std::collections::BTreeMap; +use std::fs::File; +use std::net::Ipv4Addr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::time::{Duration, SystemTime}; +use crate::tui::mode_adding::RatatuiAddingMode; +use crate::tui::mode_deleting::RatatuiDeletingMode; +use crate::tui::mode_editing::RatatuiEditingMode; +use crate::tui::mode_exiting::RatatuiExitingMode; +use crate::tui::mode_monitoring::RatatuiMonitoringMode; + + + +#[derive(Default)] +pub struct RatatuiApp { + running: bool, + pub state: BTreeMap, + current_screen: RatatuiScreens, + log_entries: Vec, + filename: Option, + pub selected_host: usize +} + +/// Private Methods +impl RatatuiApp { + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + self.running = true; + // start the 'manager' thread that spawns its ping threads as needed + let (sender, receiver) = mpsc::channel::(); + Manager::spawn_manager_thread(self.targets_as_vec(), sender); + while self.running { + // check for any waiting ping results... + self.consume_waiting_results(&receiver); + + match self.current_screen { + RatatuiScreens::Deleting => { + terminal + .draw(|frame| RatatuiDeletingMode::render(frame, &mut self))?; + RatatuiDeletingMode::handle_crossterm_events(&mut self)? + + } + RatatuiScreens::Monitoring => { + terminal + .draw(|frame| RatatuiMonitoringMode::render(frame, &mut self))?; + RatatuiMonitoringMode::handle_crossterm_events(&mut self)?; + } + RatatuiScreens::Exiting => { + terminal.draw(|frame| RatatuiExitingMode::render(frame))?; + RatatuiExitingMode::handle_crossterm_events(&mut self)?; + } + RatatuiScreens::Editing => { + terminal.draw(|frame| RatatuiEditingMode::render(frame, &mut self.state))?; + RatatuiEditingMode::handle_crossterm_events(&mut self)?; + } + RatatuiScreens::Adding => { + terminal.draw(|frame| RatatuiAddingMode::render(frame, &mut self))?; + RatatuiAddingMode::handle_crossterm_events(&mut self)? + } + } + } + Ok(()) + } + + fn consume_waiting_results(&mut self, receiver: &Receiver) { + 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 current_state) in local_state.clone() { + if current_state.target == new_message.target { + let did_change = new_message.success != current_state.alive; + + let last_alive_change = if did_change { + SystemTime::now() + } else { + current_state.last_alive_change + }; + + let new_state = TargetState { + name: current_state.name.clone(), + target: current_state.target, + alive: new_message.success, + last_rtt: new_message.rtt, + last_alive_change, + }; + local_state.insert(name.clone(), new_state.clone()); + let success_message = if new_state.alive { + "Success" + } else { + "Failure" + }; + + let current_time: DateTime = SystemTime::now().into(); + + if did_change { + self.log_entries.push(format!( + "{:?} {} for {}", + current_time.format("%Y-%m-%d %H:%M:%S: ").to_string(), + success_message, + new_state.name.clone() + )); + } + } + } + } + self.state = local_state; + } + + fn targets_as_vec(&self) -> Vec { + let mut working = vec![]; + for (_, current) in &self.state { + working.push(current.target); + } + working + } + + fn render(&mut self, frame: &mut Frame) { + match self.current_screen { + RatatuiScreens::Monitoring => RatatuiMonitoringMode::render(frame, self), + RatatuiScreens::Exiting => RatatuiExitingMode::render(frame), + RatatuiScreens::Editing => RatatuiEditingMode::render(frame, &mut self.state), + RatatuiScreens::Deleting => RatatuiDeletingMode::render(frame,self), + RatatuiScreens::Adding => RatatuiAddingMode::render(frame, self) + } + } + + fn quit(&mut self) { + self.running = false; + } + + fn get_default_targets() -> BTreeMap { + let mut working = BTreeMap::new(); + + working.insert("1111 DNS".to_string(), TargetState { + name: "1111 DNS".to_string(), + target: Ipv4Addr::new(1, 1, 1, 1), + ..TargetState::default() + }); + working.insert("Google DNS".to_string(), TargetState { + name: "Google DNS".to_string(), + target: Ipv4Addr::new(8, 8, 8, 8), + ..TargetState::default() + }); + working.insert("Test Site 1".to_string(), TargetState { + name: "Test Site 1".to_string(), + target: Ipv4Addr::new(216, 234, 202, 122), + ..TargetState::default() + }); + working + } +} + + +/// Public Methods +impl RatatuiApp { + pub fn new(option: Option) -> Self { + + let mut working = Self::default(); + let targets = if let Some(file) = option { + let mut working_btree = BTreeMap::new(); + if !&file.exists() { + RatatuiApp::get_default_targets(); + working.filename = Some("hosts.txt".to_string()); + } else { + let real_file = File::open(file.clone()); + working.filename = Some(file.as_os_str().to_string_lossy().parse().unwrap()); + let mut rdr = csv::Reader::from_reader(real_file.unwrap()); + for result in rdr.records() { + let record = result.unwrap(); + working_btree.insert(record[1].to_string(), TargetState { + name: record[1].to_string(), + target: Ipv4Addr::from_str(&record[0]).unwrap(), + alive: false, + last_alive_change: SystemTime::now(), + last_rtt: 0, + }); + } + } + working_btree + } else { + RatatuiApp::get_default_targets() + }; + working.state = targets; + working + } + + pub fn set_screen(&mut self, new_screen: RatatuiScreens) { + self.current_screen = new_screen + } + + pub fn set_running(&mut self, new_state: bool) { + self.running = new_state + } + + pub fn clear_log(&mut self) { + self.log_entries.clear(); + } + pub fn log_event(&mut self, to_log: String) { + self.log_entries.push(to_log); + } + + pub fn get_log_entries(&self, how_many: u32) -> Vec { + let mut return_value = vec![]; + for current in self.log_entries.clone().into_iter().rev() { + return_value.push(current); + } + return_value + + } +} diff --git a/src/tui/ratatui_screens.rs b/src/tui/ratatui_screens.rs new file mode 100644 index 0000000..b1a6889 --- /dev/null +++ b/src/tui/ratatui_screens.rs @@ -0,0 +1,10 @@ + +#[derive(Default)] +pub enum RatatuiScreens { + #[default] + Monitoring, + Exiting, + Editing, + Deleting, + Adding +} \ No newline at end of file diff --git a/src/tui/target_state_widget.rs b/src/tui/target_state_widget.rs new file mode 100644 index 0000000..fcf2458 --- /dev/null +++ b/src/tui/target_state_widget.rs @@ -0,0 +1,11 @@ +use ratatui::buffer::Cell; +use ratatui::prelude::*; + +pub struct TargetStateWidget; + +impl Widget for TargetStateWidget { + fn render(self, area: Rect, buf: &mut Buffer) + { + + } +}