use chrono::{DateTime, Utc};
use crate::cosmos::meta::{Cursor, CursorKey};
pub struct App {
pub rows: Vec<(CursorKey, Cursor)>,
pub last_poll_at: Option<DateTime<Utc>>,
pub last_poll_error: Option<String>,
pub selected_index: usize,
pub help_visible: bool,
}
impl App {
pub fn new() -> Self {
Self {
rows: Vec::new(),
last_poll_at: None,
last_poll_error: None,
selected_index: 0,
help_visible: false,
}
}
pub fn handle_poll_result(&mut self, result: Result<Vec<(CursorKey, Cursor)>, String>) {
self.last_poll_at = Some(Utc::now());
match result {
Ok(rows) => {
self.rows = rows;
self.last_poll_error = None;
self.ensure_valid_selection();
}
Err(e) => {
self.last_poll_error = Some(e);
}
}
}
pub fn move_selection(&mut self, delta: i32) {
if self.rows.is_empty() {
return;
}
let len = self.rows.len() as i32;
let next = (self.selected_index as i32 + delta).rem_euclid(len);
self.selected_index = next as usize;
}
pub fn toggle_help(&mut self) {
self.help_visible = !self.help_visible;
}
pub fn selected_row(&self) -> Option<&(CursorKey, Cursor)> {
self.rows.get(self.selected_index)
}
fn ensure_valid_selection(&mut self) {
if self.rows.is_empty() {
self.selected_index = 0;
} else {
self.selected_index = self.selected_index.min(self.rows.len() - 1);
}
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cosmos::meta::CursorKey;
fn key(_deployment: &str, source: &str, subsource: &str) -> CursorKey {
CursorKey {
source_name: source.to_string(),
subsource: subsource.to_string(),
}
}
fn two_rows() -> Vec<(CursorKey, Cursor)> {
vec![
(key("prod", "jira-cloud", "DO"), Cursor::default()),
(key("prod", "jira-cloud", "INT"), Cursor::default()),
]
}
#[test]
fn initial_state_is_empty() {
let app = App::new();
assert!(app.rows.is_empty());
assert!(app.last_poll_at.is_none());
assert!(app.last_poll_error.is_none());
assert_eq!(app.selected_index, 0);
assert!(!app.help_visible);
}
#[test]
fn handle_poll_result_success_replaces_rows() {
let mut app = App::new();
app.handle_poll_result(Ok(two_rows()));
assert_eq!(app.rows.len(), 2);
assert!(app.last_poll_at.is_some());
assert!(app.last_poll_error.is_none());
}
#[test]
fn handle_poll_result_error_preserves_stale_rows() {
let mut app = App::new();
app.handle_poll_result(Ok(two_rows()));
app.handle_poll_result(Err("network timeout".to_string()));
assert_eq!(app.rows.len(), 2);
assert!(app.last_poll_error.is_some());
}
#[test]
fn move_selection_wraps_around() {
let mut app = App::new();
app.handle_poll_result(Ok(two_rows()));
assert_eq!(app.selected_index, 0);
app.move_selection(1);
assert_eq!(app.selected_index, 1);
app.move_selection(1);
assert_eq!(app.selected_index, 0);
app.move_selection(-1);
assert_eq!(app.selected_index, 1);
}
#[test]
fn move_selection_noop_on_empty() {
let mut app = App::new();
app.move_selection(1);
assert_eq!(app.selected_index, 0);
}
#[test]
fn toggle_help_flips() {
let mut app = App::new();
assert!(!app.help_visible);
app.toggle_help();
assert!(app.help_visible);
app.toggle_help();
assert!(!app.help_visible);
}
#[test]
fn selected_row_returns_correct_entry() {
let mut app = App::new();
app.handle_poll_result(Ok(two_rows()));
app.move_selection(1);
let (k, _) = app.selected_row().unwrap();
assert_eq!(k.subsource, "INT");
}
#[test]
fn selected_row_none_when_empty() {
let app = App::new();
assert!(app.selected_row().is_none());
}
#[test]
fn selection_clamped_after_poll_shrinks_list() {
let mut app = App::new();
app.handle_poll_result(Ok(two_rows()));
app.selected_index = 1;
app.handle_poll_result(Ok(vec![(key("prod", "jira", "DO"), Cursor::default())]));
assert_eq!(app.selected_index, 0);
}
}