use crossterm::event::{KeyCode, KeyEvent};
use crate::tui::action::{Action, EditTarget, FollowParams};
use crate::tui::panes::PaneOutcome;
pub struct FollowState {
pub interval_secs: u64,
pub count: usize,
pub running: bool,
pub log: Vec<seer_core::dns::FollowIteration>,
pub gen: u64,
}
impl Default for FollowState {
fn default() -> Self {
Self {
interval_secs: 30,
count: 20,
running: false,
log: vec![],
gen: 0,
}
}
}
impl FollowState {
pub fn push(&mut self, it: seer_core::dns::FollowIteration) {
self.log.insert(0, it);
}
pub fn reset_for_new_domain(&mut self) {
self.gen += 1;
self.running = false;
self.log.clear();
}
pub fn handle_key(&mut self, key: KeyEvent, domain: Option<&str>) -> Option<PaneOutcome> {
match key.code {
KeyCode::Char('s') | KeyCode::Enter => {
let Some(d) = domain else {
return Some(PaneOutcome::None);
};
self.log.clear();
self.running = true;
self.gen += 1;
Some(PaneOutcome::Action(Action::StartFollow(FollowParams {
domain: d.to_string(),
iterations: self.count,
interval_secs: self.interval_secs,
gen: self.gen,
})))
}
KeyCode::Char('i') => Some(PaneOutcome::EditField(EditTarget::FollowInterval)),
KeyCode::Char('n') => Some(PaneOutcome::EditField(EditTarget::FollowCount)),
KeyCode::Char('x') => {
self.running = false;
Some(PaneOutcome::None)
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::action::Action;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn default_has_expected_values() {
let s = FollowState::default();
assert_eq!(s.interval_secs, 30);
assert_eq!(s.count, 20);
assert!(!s.running);
assert!(s.log.is_empty());
}
#[test]
fn s_with_domain_sets_running_and_returns_start_follow() {
let mut s = FollowState::default();
let outcome = s.handle_key(key(KeyCode::Char('s')), Some("x.com"));
assert!(s.running);
assert!(matches!(
outcome,
Some(PaneOutcome::Action(Action::StartFollow(ref p)))
if p.domain == "x.com" && p.iterations == 20 && p.interval_secs == 30 && p.gen == 1
));
}
#[test]
fn enter_with_domain_also_starts() {
let mut s = FollowState::default();
let outcome = s.handle_key(key(KeyCode::Enter), Some("y.com"));
assert!(s.running);
assert!(matches!(
outcome,
Some(PaneOutcome::Action(Action::StartFollow(_)))
));
}
#[test]
fn s_without_domain_consumes_but_noop() {
let mut s = FollowState::default();
let outcome = s.handle_key(key(KeyCode::Char('s')), None);
assert!(!s.running);
assert!(matches!(outcome, Some(PaneOutcome::None)));
}
#[test]
fn i_returns_edit_interval() {
let mut s = FollowState::default();
let outcome = s.handle_key(key(KeyCode::Char('i')), None);
assert!(matches!(
outcome,
Some(PaneOutcome::EditField(EditTarget::FollowInterval))
));
}
#[test]
fn n_returns_edit_count() {
let mut s = FollowState::default();
let outcome = s.handle_key(key(KeyCode::Char('n')), None);
assert!(matches!(
outcome,
Some(PaneOutcome::EditField(EditTarget::FollowCount))
));
}
#[test]
fn x_stops_running() {
let mut s = FollowState::default();
s.running = true;
let outcome = s.handle_key(key(KeyCode::Char('x')), None);
assert!(!s.running);
assert!(matches!(outcome, Some(PaneOutcome::None)));
}
#[test]
fn esc_returns_none() {
let mut s = FollowState::default();
let outcome = s.handle_key(key(KeyCode::Esc), None);
assert!(outcome.is_none(), "Esc must not be swallowed");
}
#[test]
fn push_prepends_newest_first() {
use chrono::Utc;
let mut s = FollowState::default();
let it1 = seer_core::dns::FollowIteration {
iteration: 1,
total_iterations: 5,
timestamp: Utc::now(),
records: vec![],
changed: false,
added: vec![],
removed: vec![],
error: None,
};
let it2 = seer_core::dns::FollowIteration {
iteration: 2,
total_iterations: 5,
timestamp: Utc::now(),
records: vec![],
changed: true,
added: vec!["1.2.3.4".into()],
removed: vec![],
error: None,
};
s.push(it1);
s.push(it2);
assert_eq!(s.log[0].iteration, 2, "newest (iter 2) should be first");
assert_eq!(s.log[1].iteration, 1);
}
}