1 Commits

Author SHA1 Message Date
tmerritt c6dff58d8a Adds colour to the text display
optimized main loop
splits code into structs to better structure
tui shows hosts from the app state
single display shows the stats for a passed TargetState
multi display is woefully incomplete
2025-04-22 16:04:29 -04:00
24 changed files with 478 additions and 1128 deletions
Generated
+11 -213
View File
@@ -32,21 +32,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 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]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.12.1" version = "0.12.1"
@@ -133,12 +118,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]] [[package]]
name = "cassowary" name = "cassowary"
version = "0.3.0" version = "0.3.0"
@@ -169,25 +148,11 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "clap" name = "clap"
version = "4.5.37" version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -195,9 +160,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.37" version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -270,12 +235,6 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.28.1" version = "0.28.1"
@@ -447,30 +406,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 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]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@@ -525,9 +460,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.10" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488"
dependencies = [ dependencies = [
"jiff-static", "jiff-static",
"log", "log",
@@ -538,25 +473,15 @@ dependencies = [
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.10" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -627,15 +552,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@@ -709,10 +625,9 @@ dependencies = [
[[package]] [[package]]
name = "pp" name = "pp"
version = "0.2.0" version = "0.1.1"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"chrono",
"clap", "clap",
"color-eyre", "color-eyre",
"crossterm", "crossterm",
@@ -724,9 +639,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1056,64 +971,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -1136,65 +993,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
+4 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pp" name = "pp"
version = "0.2.0" version = "0.1.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -15,7 +15,8 @@ color-eyre = "0.6.3"
crossterm = "0.28.1" crossterm = "0.28.1"
ratatui = "0.29.0" ratatui = "0.29.0"
# Time Display [[bin]]
chrono = "0.4" name = "duration_to_string"
[[bin]] [[bin]]
name = "pp" name = "pp"
-1
View File
@@ -1,4 +1,3 @@
address,name
10.3.100.1,Belleville Router 10.3.100.1,Belleville Router
10.11.31.3,Belleville VPN 11-31 10.11.31.3,Belleville VPN 11-31
10.12.32.1,Belleville VPN 12-32 10.12.32.1,Belleville VPN 12-32
View File
-1
View File
@@ -1,4 +1,3 @@
address,name
10.3.100.1,Belleville Router 10.3.100.1,Belleville Router
10.11.31.3,Belleville VPN 11-31 10.11.31.3,Belleville VPN 11-31
10.12.32.1,Belleville VPN 12-32 10.12.32.1,Belleville VPN 12-32
+6 -8
View File
@@ -1,16 +1,14 @@
address,name 10.3.100.1,Belleville Router
10.3.100.1,Belleville Gateway
10.11.31.3,Belleville VPN 11-31 10.11.31.3,Belleville VPN 11-31
10.12.32.1,Belleville VPN 12-32 10.12.32.1,Belleville VPN 12-32
192.186.110.6,Belleville Cogeco
129.222.197.36,Belleville Starlink
10.2.100.1,Lindsay Gateway
24.143.184.98,Lindsay Cogeco
192.168.1.50,Lindsay Sign
10.11.21.1,Lindsay VPN 11-21 10.11.21.1,Lindsay VPN 11-21
10.12.22.1,Lindsay VPN 11-22 10.12.22.1,Lindsay VPN 11-22
8.8.8.8,Google DNS 8.8.8.8,Google DNS
1.1.1.1,1111 DNS 1.1.1.1,1111 DNS
192.168.0.1,Peterborough Gateway
24.51.235.162,Peterborough Cogeco 24.51.235.162,Peterborough Cogeco
99.214.0.92,Peterborough Rogers 99.214.0.92,Peterborough Rogers
216.234.202.122,Lindsay Starlink
24.143.184.98,Lindsay Cogeco
192.186.110.6,Belleville Cogeco
129.222.197.36,Belleville Starlink
192.168.0.50,Sign
-10
View File
@@ -1,10 +0,0 @@
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<PathBuf>,
}
+41
View File
@@ -0,0 +1,41 @@
use std::time::Duration;
pub fn duration_to_string(to_convert: Duration) -> String {
let mut total_seconds = to_convert.as_secs();
let mut working_string = String::new();
if total_seconds > 86400 {
// days
let num_days = total_seconds / 86400;
working_string = format!("{} days", num_days);
total_seconds = total_seconds - (num_days * 86400);
}
if total_seconds > 3600 {
// hours
let num_hours = total_seconds / 3600;
if num_hours > 0 {
working_string = format!("{} {} hours", working_string, num_hours);
total_seconds = total_seconds - (num_hours * 3600);
}
}
if total_seconds > 60 {
let num_minutes = total_seconds / 60;
if num_minutes > 0 {
working_string = format!("{} {} minutes", working_string, num_minutes);
total_seconds = total_seconds - (num_minutes * 60);
}
// minutes
}
working_string = format!("{} {} seconds.", working_string, total_seconds);
working_string
}
fn main() {
println!("1m 12s => {}", duration_to_string(Duration::from_secs(72)));
println!("1h 1m 12s => {}", duration_to_string(Duration::from_secs(3672)));
println!("1d 1h 1m 12s => {}", duration_to_string(Duration::from_secs(90072)));
println!("30d 1h 1m 12s => {}", duration_to_string(Duration::from_secs(2595672)));
}
+50 -207
View File
@@ -17,224 +17,67 @@ use std::{env, error::Error, ffi::OsString, process};
use color_eyre::owo_colors::OwoColorize; use color_eyre::owo_colors::OwoColorize;
use crossterm::style::Stylize; use crossterm::style::Stylize;
use log::debug; use log::debug;
use pp::app_settings::AppSettings; use pp::ppstate::PPState;
const SECONDS_IN_MINUTE: u32 = 60; /// Simple program to greet a person
const SECONDS_IN_HOUR: u32 = SECONDS_IN_MINUTE * 60; #[derive(Parser, Debug)]
const SECONDS_IN_DAY: u32 = SECONDS_IN_HOUR * 24; #[command(version, about, long_about = None)]
pub struct AppSettings {
pub fn duration_to_string(to_convert: Duration) -> String { /// File of list of hosts
let mut total_seconds = to_convert.as_secs() as u32; #[arg(short, long)]
let mut working_string = String::new(); ping_host_file: Option<PathBuf>,
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
} }
fn main() {
// Get App Settings
let settings = AppSettings::parse();
struct PPState {} print!("Creating new State...");
let mut state = PPState::new_from_file(settings.ping_host_file.unwrap_or(PathBuf::from("INVALID FILE")));
impl PPState { // channel to send requests to ping
pub fn get_default_targets() -> BTreeMap<String, TargetState> { let (ping_response_sender, ping_response_listener) = mpsc::channel::<PingResult>();
let mut working = BTreeMap::new();
working.insert("Localhost".to_string(),
TargetState {
name: "Localhost".to_string(),
target: Ipv4Addr::new(127, 0, 0, 1),
..TargetState::default()
},
);
working.insert("Home Gateway".to_string(), println!("Setting up requests for {} hosts.", state.ips_from_state().len());
TargetState { Manager::spawn_manager_thread(PPState::targets_states_from_state(&state), ping_response_sender.clone());
name: "Home Gateway".to_string(),
target: Ipv4Addr::new(172, 24, 0, 1),
..TargetState::default()
},
);
working.insert("1111 DNS".to_string(), let mut display_loop_start = SystemTime::now();
TargetState { let mut duration_since_last_loop = SystemTime::now().duration_since(display_loop_start).unwrap();
name: "1111 DNS".to_string(), loop {
target: Ipv4Addr::new(1, 1, 1, 1), state.check_for_ping(&ping_response_listener);
..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("Site IP 1".to_string(), let now = SystemTime::now();
TargetState {
name: "Site IP 1".to_string(),
target: Ipv4Addr::new(216, 121, 247, 231),
..TargetState::default()
},
);
working.insert("Site IP 2".to_string(),
TargetState {
name: "Site IP 2".to_string(),
target: Ipv4Addr::new(216, 234, 202, 122),
..TargetState::default()
},
);
working.insert("Site IP 3".to_string(),
TargetState {
name: "Site IP 3".to_string(),
target: Ipv4Addr::new(24, 143, 184, 98),
..TargetState::default()
},
);
working
}
pub fn build_targets_from_file(filename: Option<PathBuf>) -> BTreeMap<String, TargetState> {
// PPState::get_default_targets(); duration_since_last_loop = now
if let Some(file) = filename { .duration_since(display_loop_start)
let mut working = BTreeMap::new(); .expect("unable to figure out how long ago we displayed stuff");
if !&file.exists() { if duration_since_last_loop.as_secs() > SECONDS_BETWEEN_DISPLAY as u64 {
debug!("Cant load hosts from {:?}. Using default host list.", file.clone().as_os_str()); println!("DISPLAY LOOP");
// use println!("Host \t\t\t\t\t | Alive \t | RTT \t\t");
PPState::get_default_targets() for current_result in state.targets_states_from_state() {
} else { let time_since_last_change = now
debug!("LOADING HOSTS FROM {:?}", file.to_str()); .duration_since(current_result.last_alive_change)
let file = File::open(file); .unwrap_or(Duration::from_secs(0));
let mut rdr = csv::Reader::from_reader(file.unwrap()); let mut target_string = format!("{} ({})", current_result.name, current_result.target);
for result in rdr.records() { while target_string.len() < 34 {
let record = result.unwrap(); target_string.push(' ');
working.insert(record[1].to_string(), // target_string = format!("{} ", target_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
target_string = if current_result.alive {
target_string.green().to_string()
} else {
target_string.red().to_string()
};
println!("{} \t | {} \t | {}\t | Changed {}s ago",
target_string,
current_result.alive,
current_result.last_rtt, time_since_last_change.as_secs()
);
} }
} else { display_loop_start = now;
PPState::get_default_targets()
} }
} }
} }
fn ips_from_state(to_read_from: BTreeMap<String, TargetState>) -> Vec<Ipv4Addr> {
let mut working: Vec<Ipv4Addr> = vec![];
for current in to_read_from {
working.push(current.1.target);
}
working
}
/// Simple program to greet a person
fn main() {
// Get App Settings
let settings = AppSettings::parse();
print!("Prep to load targets...");
let file_to_check = match settings.ping_host_file {
None => {
PathBuf::from("./hosts.txt")
}
Some(actual) => {
actual
}
};
let mut targets = PPState::build_targets_from_file(Some(file_to_check));
// channel to send requests to ping
let (ping_response_sender, ping_response_listener) = mpsc::channel::<PingResult>();
println!("Setting up requests for {} hosts.", targets.len());
Manager::spawn_manager_thread(ips_from_state(targets.clone()), ping_response_sender.clone());
let mut display_loop_start = SystemTime::now();
let mut duration_since_last_loop = SystemTime::now().duration_since(display_loop_start).unwrap();
loop {
let now = SystemTime::now();
if let Ok(response) = ping_response_listener.recv_timeout(Duration::from_millis(100)) {
let local_targets = targets.clone();
for (_, (name, current_state)) in local_targets.iter().enumerate() {
if current_state.target == response.target {
let last_alive_change = if response.success == current_state.alive {
current_state.last_alive_change
} else {
SystemTime::now()
};
let new_state = TargetState {
name: current_state.name.clone(),
target: current_state.target,
alive: response.success,
last_rtt: response.rtt,
last_alive_change,
};
targets.insert(name.clone(), new_state);
}
}
}
duration_since_last_loop = now
.duration_since(display_loop_start)
.expect("unable to figure out how long ago we displayed stuff");
if duration_since_last_loop.as_secs() > SECONDS_BETWEEN_DISPLAY as u64 {
println!("DISPLAY LOOP");
println!("Host \t\t\t\t\t | Alive \t | RTT \t\t");
for (name, current_result) in targets.clone() {
let time_since_last_change = now
.duration_since(current_result.last_alive_change)
.unwrap_or(Duration::from_secs(0));
let mut target_string = format!("{} ({})", name, current_result.target);
while target_string.len() < 34 {
target_string.push(' ');
// target_string = format!("{} ", target_string);
}
target_string = if current_result.alive {
target_string.green().to_string()
} else {
target_string.red().to_string()
};
println!("{} \t | {} \t | {}\t | Changed {} ago",
target_string,
current_result.alive,
current_result.last_rtt,
duration_to_string(time_since_last_change)
);
}
display_loop_start = now;
}
}
}
+136 -12
View File
@@ -1,15 +1,139 @@
use clap::Parser; use pp::tui::target_display::TargetDisplay;
use ratatui::widgets::TableState; use std::net::Ipv4Addr;
use pp::app_settings::AppSettings; use std::sync::mpsc;
use pp::tui::ratatui_app::RatatuiApp; use std::sync::mpsc::Receiver;
fn main() -> color_eyre::Result<()> { use std::sync::mpsc::Sender;
// find out what file we are using to get our hosts use std::time::Duration;
let settings = AppSettings::parse();
color_eyre::install()?; use color_eyre::Result;
let terminal = ratatui::init(); use crossterm::event;
let result = RatatuiApp::new(settings.ping_host_file).run(terminal); use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
ratatui::restore(); use pp::ping_result::PingResult;
result use pp::target_state::TargetState;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::{DefaultTerminal, Frame};
use std::collections::BTreeMap;
use std::time::SystemTime;
#[derive(Default)]
pub struct App {
running: bool,
counter: u32,
known_hosts: Vec<Ipv4Addr>,
targets: BTreeMap<String, TargetState>,
result_tx: Option<Sender<PingResult>>,
result_rx: Option<Receiver<PingResult>>,
log_events: Vec<String>,
} }
impl App {
pub fn new() -> Self {
let mut working = Self::default();
working.known_hosts.push(Ipv4Addr::new(127, 0, 0, 1));
working.known_hosts.push(Ipv4Addr::new(8, 8, 8, 8));
working.known_hosts.push(Ipv4Addr::new(1, 1, 1, 1));
working
.targets
.insert("Test Host 1".to_string(), TargetState {
name: "Test Host 1".to_string(),
target: Ipv4Addr::new(127, 0, 0, 1),
..Default::default()
});
working
.targets
.insert("Test Host 2".to_string(), TargetState {
target: Ipv4Addr::new(1, 1, 1, 1),
name: "Test Host 2".to_string(),
..Default::default()
});
let (sender, receiver) = mpsc::channel::<PingResult>();
working.result_tx = Some(sender);
working.result_rx = Some(receiver);
working
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.running = true;
while self.running {
// check if we have any waiting ping responses
if let Ok(response) = self
.result_rx
.as_mut()
.unwrap()
.recv_timeout(Duration::from_millis(50))
{
let local_targets = self.targets.clone();
for (name, current_state) in local_targets {
if current_state.target == response.target {
let last_alive_change = if response.success == current_state.alive {
current_state.last_alive_change
} else {
SystemTime::now()
};
let new_state = TargetState {
name: name.clone(),
target: current_state.target,
alive: response.success,
last_rtt: response.rtt,
last_alive_change,
};
self.targets.insert(name.clone(), new_state);
//self.set_target_state(current_state.name.clone(), new_state);
}
}
}
terminal.draw(|frame| self.render(frame))?;
self.handle_crossterm_events()?;
}
Ok(())
}
fn render(&mut self, frame: &mut Frame) {
let title = Line::from("PP Status").bold().centered().blue();
let mut body_string = String::new();
for (name, target) in &self.targets {
frame.render_widget(TargetDisplay::new(target.clone()), frame.area());
body_string = format!("{}\n{} = {}", body_string, target.name, target.target);
}
frame.render_widget(Paragraph::new(body_string), frame.area());
}
fn handle_crossterm_events(&mut self) -> Result<()> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_event(key),
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
_ => {}
}
Ok(())
}
fn on_key_event(&mut self, key: KeyEvent) {
match (key.modifiers, key.code) {
(_, KeyCode::Esc | KeyCode::Char('q'))
| (KeyModifiers::CONTROL, KeyCode::Char('c') | KeyCode::Char('C')) => self.quit(),
_ => {}
}
}
fn quit(&mut self) {
self.running = false;
}
}
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = App::new().run(terminal);
ratatui::restore();
println!("Exited.");
result
}
+1 -43
View File
@@ -1,51 +1,9 @@
use std::time::Duration;
pub mod manager; pub mod manager;
pub mod ping_request; pub mod ping_request;
pub mod ping_result; pub mod ping_result;
pub mod ppstate;
pub mod target_state; pub mod target_state;
pub mod tui; pub mod tui;
pub mod app_settings;
pub const SECONDS_BETWEEN_DISPLAY: u32 = 1; pub const SECONDS_BETWEEN_DISPLAY: u32 = 1;
pub const SECONDS_BETWEEN_PING: u32 = 2; 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
}
+38 -19
View File
@@ -1,11 +1,12 @@
use crate::SECONDS_BETWEEN_PING;
use crate::ping_request::PingRequest;
use crate::ping_result::PingResult;
use crate::target_state::TargetState;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::process::Command; use std::process::Command;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::thread; use std::thread;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use crate::ping_request::PingRequest;
use crate::ping_result::PingResult;
use crate::SECONDS_BETWEEN_PING;
pub struct Manager; pub struct Manager;
@@ -13,12 +14,17 @@ const WIN_PING_SUCCESS: i32 = 1;
const LIN_PING_SUCCESS: i32 = 0; const LIN_PING_SUCCESS: i32 = 0;
impl Manager { impl Manager {
pub fn spawn_manager_thread(targets: Vec<Ipv4Addr>, sender: Sender<PingResult>) { pub fn spawn_manager_thread(targets: Vec<TargetState>, sender: Sender<PingResult>) {
let local_targets = targets.clone(); let local_targets = targets.clone();
thread::spawn(move || { thread::spawn(move || {
loop { loop {
for target in &local_targets { for target in &local_targets {
Manager::spawn_single_ping(PingRequest { target: *target }, sender.clone()); Manager::spawn_single_ping(
PingRequest {
target: target.target,
},
sender.clone(),
);
} }
thread::sleep(Duration::from_secs(SECONDS_BETWEEN_PING as u64)); thread::sleep(Duration::from_secs(SECONDS_BETWEEN_PING as u64));
} }
@@ -26,40 +32,53 @@ impl Manager {
} }
// Spawns a thread that pings a target and sends the response back to the main thread // Spawns a thread that pings a target and sends the response back to the main thread
// via the sender // via the sender
fn spawn_single_ping(request: PingRequest, sender: Sender<PingResult>) { fn spawn_single_ping(request: PingRequest, sender: Sender<PingResult>) {
let local_request = request.clone(); let local_request = request.clone();
thread::spawn(move || { thread::spawn(move || {
let mut result = 0; let mut result = 0;
let mut success = true; let mut success = true;
let start_time = SystemTime::now(); let start_time = SystemTime::now();
#[cfg(any(target_os="windows"))] { #[cfg(any(target_os = "windows"))]
result = Command::new("c:/windows/system32/ping.exe").arg(request.target.to_string()) {
result = Command::new("c:/windows/system32/ping.exe")
.arg(request.target.to_string())
.arg("-n 1") .arg("-n 1")
.arg(format!("-w {}", crate::SECONDS_BETWEEN_PING)) .arg(format!("-w {}", crate::SECONDS_BETWEEN_PING))
.arg("-4") .arg("-4")
.output().unwrap().status.code().unwrap_or(255); .output()
success = result == WIN_PING_SUCCESS as i32;; .unwrap()
.status
.code()
.unwrap_or(255);
success = result == WIN_PING_SUCCESS as i32;
} }
#[cfg(any(target_os="linux"))] { #[cfg(any(target_os = "linux"))]
result = Command::new("/usr/bin/ping").arg(request.target.to_string()) {
result = Command::new("/usr/bin/ping")
.arg(request.target.to_string())
.arg("-c 1") .arg("-c 1")
.arg("-4") .arg("-4")
.arg(format!("-w {}", SECONDS_BETWEEN_PING)) .arg(format!("-w {}", SECONDS_BETWEEN_PING))
.arg("-W 1") .arg("-W 1")
.output().unwrap().status.code().unwrap_or(255); .output()
.unwrap()
.status
.code()
.unwrap_or(255);
success = result == LIN_PING_SUCCESS as i32; success = result == LIN_PING_SUCCESS as i32;
} }
let ping_duration = SystemTime::now() let ping_duration = SystemTime::now()
.duration_since(start_time) .duration_since(start_time)
.unwrap_or(Duration::from_secs(600)); .unwrap_or(Duration::from_secs(600));
let success = ping_duration.as_millis() >= SECONDS_BETWEEN_PING as u128 && success; let success = ping_duration.as_millis() >= SECONDS_BETWEEN_PING as u128 && success;
sender.send(PingResult { sender
target: local_request.target, .send(PingResult {
success, target: local_request.target,
rtt: ping_duration.as_millis() as u32 success,
}).expect("Unable to send response"); rtt: ping_duration.as_millis() as u32,
})
.expect("Unable to send response");
// println!("Attempt for for {}", local_request.target); // println!("Attempt for for {}", local_request.target);
}); });
+145
View File
@@ -0,0 +1,145 @@
use crate::ping_result::PingResult;
use crate::target_state::TargetState;
use log::debug;
use std::collections::BTreeMap;
use std::fs::File;
use std::net::Ipv4Addr;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::mpsc::Receiver;
use std::time::{Duration, SystemTime};
#[derive(Default)]
pub struct PPState {
targets: BTreeMap<String, TargetState>,
}
impl PPState {
pub fn check_for_ping(&mut self, channel: &Receiver<PingResult>) {
if let Ok(response) = channel.recv_timeout(Duration::from_millis(100)) {
let local_targets = self.targets_states_from_state().clone();
for current_state in local_targets {
if current_state.target == response.target {
let last_alive_change = if response.success == current_state.alive {
current_state.last_alive_change
} else {
SystemTime::now()
};
let new_state = TargetState {
name: current_state.name.clone(),
target: current_state.target,
alive: response.success,
last_rtt: response.rtt,
last_alive_change,
};
self.set_target_state(current_state.name.clone(), new_state);
}
}
}
}
pub fn new_from_file(file_to_load_from: PathBuf) -> Self {
let mut working = Self::new();
working.targets = PPState::build_targets_from_file(Some(file_to_load_from));
working
}
pub fn new() -> Self {
Self::default()
}
pub fn get_default_targets() -> BTreeMap<String, TargetState> {
let mut working = BTreeMap::new();
working.insert("Localhost".to_string(), TargetState {
name: "Localhost".to_string(),
target: Ipv4Addr::new(127, 0, 0, 1),
..TargetState::default()
});
working.insert("Home Gateway".to_string(), TargetState {
name: "Home Gateway".to_string(),
target: Ipv4Addr::new(172, 24, 0, 1),
..TargetState::default()
});
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("Site IP 1".to_string(), TargetState {
name: "Site IP 1".to_string(),
target: Ipv4Addr::new(216, 121, 247, 231),
..TargetState::default()
});
working.insert("Site IP 2".to_string(), TargetState {
name: "Site IP 2".to_string(),
target: Ipv4Addr::new(216, 234, 202, 122),
..TargetState::default()
});
working.insert("Site IP 3".to_string(), TargetState {
name: "Site IP 3".to_string(),
target: Ipv4Addr::new(24, 143, 184, 98),
..TargetState::default()
});
working
}
pub fn build_targets_from_file(filename: Option<PathBuf>) -> BTreeMap<String, TargetState> {
PPState::get_default_targets();
if let Some(file) = filename {
let mut working = BTreeMap::new();
if !&file.exists() {
debug!(
"Cant load hosts from {:?}. Using default host list.",
file.clone().as_os_str()
);
// use
PPState::get_default_targets()
} else {
debug!("LOADING HOSTS FROM {:?}", file.to_str());
let file = File::open(file);
let mut rdr = csv::Reader::from_reader(file.unwrap());
for result in rdr.records() {
let record = result.unwrap();
working.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
}
} else {
PPState::get_default_targets()
}
}
pub fn ips_from_state(&self) -> Vec<Ipv4Addr> {
let mut working: Vec<Ipv4Addr> = vec![];
for (name, current) in &self.targets {
working.push(current.target);
}
working
}
pub fn targets_states_from_state(&self) -> Vec<TargetState> {
let mut working: Vec<TargetState> = vec![];
for (_, target) in &self.targets {
working.push(target.clone());
}
working
}
pub fn set_target_state(&mut self, key: String, value: TargetState) {
self.targets.insert(key, value);
}
}
+2 -8
View File
@@ -1,8 +1,2 @@
pub mod mode_editing; pub mod target_display;
pub mod mode_exiting; mod multi_target_display;
pub mod mode_monitoring;
pub mod ratatui_app;
pub mod target_state_widget;
mod ratatui_screens;
mod mode_deleting;
mod mode_adding;
-36
View File
@@ -1,36 +0,0 @@
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(())
}
}
-46
View File
@@ -1,46 +0,0 @@
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(())
}
}
-62
View File
@@ -1,62 +0,0 @@
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<String, TargetState>) {
let title = Line::from("Editing Hosts").bold().blue().centered();
let body_text = "This is the body text for editing hosts\n'q' to exit <esc> to return to monitoring";
frame.render_widget(
Paragraph::new(body_text)
.centered()
.block(Block::new().title(title).blue()),
frame.area(),
);
}
}
-43
View File
@@ -1,43 +0,0 @@
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(())
}
}
-175
View File
@@ -1,175 +0,0 @@
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 <ESC> or q to exit";
// let footer_text = "Press <ESC> 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
}
+15
View File
@@ -0,0 +1,15 @@
use ratatui::buffer::{Buffer, Cell};
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::{Widget};
pub struct MultiTargetDisplay {
}
impl Widget for MultiTargetDisplay {
fn render(self, area: Rect, buf: &mut Buffer)
{
buf.content.push(Cell::from("Widgets go here"));
}
}
-220
View File
@@ -1,220 +0,0 @@
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<String, TargetState>,
current_screen: RatatuiScreens,
log_entries: Vec<String>,
filename: Option<String>,
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::<PingResult>();
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<PingResult>) {
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<Local> = 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<Ipv4Addr> {
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<String, TargetState> {
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<PathBuf>) -> 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<String> {
let mut return_value = vec![];
for current in self.log_entries.clone().into_iter().rev() {
return_value.push(current);
}
return_value
}
}
-10
View File
@@ -1,10 +0,0 @@
#[derive(Default)]
pub enum RatatuiScreens {
#[default]
Monitoring,
Exiting,
Editing,
Deleting,
Adding
}
+29
View File
@@ -0,0 +1,29 @@
use crate::target_state::TargetState;
use ratatui::prelude::Buffer;
use ratatui::prelude::Rect;
use ratatui::style::Style;
use ratatui::widgets::Widget;
#[derive(Default)]
pub struct TargetDisplay {
target: TargetState,
}
impl TargetDisplay {
pub fn new(state: TargetState) -> Self {
let mut working = TargetDisplay::default();
working.target = state;
working
}
}
impl Widget for TargetDisplay {
fn render(self, area: Rect, buf: &mut Buffer) {
let row = format!(
"{} -> {} / {} / {}",
self.target.name, self.target.target, self.target.alive, self.target.last_rtt
);
buf.set_string(area.x, area.y, row, Style::default());
}
}
-11
View File
@@ -1,11 +0,0 @@
use ratatui::buffer::Cell;
use ratatui::prelude::*;
pub struct TargetStateWidget;
impl Widget for TargetStateWidget {
fn render(self, area: Rect, buf: &mut Buffer)
{
}
}