use std::any::Any;
use std::time::{Duration, Instant};
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::Frame;
use super::chrome::ModalChrome;
use super::types::{Modal, ModalKind, ModalOutcome, ModalRenderCtx};
use crate::app::update_check;
use crate::app::App;
use crate::ui::{about, ModalButton, ModalResponse};
const INSTALLED_VERSION: &str = env!("CARGO_PKG_VERSION");
const TAGLINE_INTERVAL: Duration = Duration::from_secs(4);
const CHECK_UPDATES_BUTTON: usize = 0;
pub struct AboutModal {
chrome: ModalChrome,
buttons: Vec<ModalButton>,
opened_at: Instant,
tagline_start: usize,
}
impl AboutModal {
pub(crate) fn new() -> Self {
let tagline_start = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as usize % about::TAGLINES.len())
.unwrap_or(0);
Self {
chrome: ModalChrome::new(ModalKind::Normal, true),
buttons: vec![
ModalButton::new("Check for updates"),
ModalButton::new("View on GitHub"),
],
opened_at: Instant::now(),
tagline_start,
}
}
fn tagline_index(&self) -> usize {
let flips = self.opened_at.elapsed().as_secs() / TAGLINE_INTERVAL.as_secs();
self.tagline_start + flips as usize
}
fn resolve(&mut self, response: ModalResponse) -> ModalOutcome {
match response {
ModalResponse::Continue => ModalOutcome::Continue,
ModalResponse::Cancelled => ModalOutcome::Close,
ModalResponse::ButtonPressed(CHECK_UPDATES_BUTTON) => {
ModalOutcome::ContinueAnd(Box::new(|app| app.open_update_modal()))
}
ModalResponse::ButtonPressed(_) => ModalOutcome::ContinueAnd(Box::new(|app| {
app.spawn_open_worker(update_check::GITHUB_URL.to_owned());
})),
}
}
}
impl Modal for AboutModal {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &ModalRenderCtx<'_>) {
let body = about::body_lines(ctx.theme, self.tagline_index(), INSTALLED_VERSION);
self.chrome
.render(frame, area, ctx, "About edamame", &body, &self.buttons);
}
fn handle_key(
&mut self,
key: KeyEvent,
_app: &mut App,
_doc_height: usize,
_doc_width: usize,
) -> ModalOutcome {
let response = self.chrome.on_key(&key, self.buttons.len());
self.resolve(response)
}
fn handle_wheel(&mut self, delta: i32) {
self.chrome.on_wheel(delta);
}
fn handle_click(&mut self, col: u16, row: u16, _app: &mut App) -> ModalOutcome {
let response = self.chrome.on_click(col, row);
self.resolve(response)
}
fn kind(&self) -> ModalKind {
self.chrome.kind()
}
fn dismissable(&self) -> bool {
self.chrome.dismissable()
}
fn next_deadline(&self) -> Option<Instant> {
let periods = self.opened_at.elapsed().as_nanos() / TAGLINE_INTERVAL.as_nanos();
let periods = u32::try_from(periods).unwrap_or(u32::MAX);
Some(self.opened_at + TAGLINE_INTERVAL * periods.saturating_add(1))
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::*;
use crate::app::test_utils::make_app;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn open_about_pushes_to_stack_once() {
let mut app = make_app();
app.open_about_modal();
assert!(app.modal_stack.contains::<AboutModal>());
app.open_about_modal();
assert_eq!(app.modal_stack.count::<AboutModal>(), 1);
}
#[test]
fn opening_about_does_not_touch_the_network() {
let mut app = make_app();
app.open_about_modal();
assert!(!app.release_check_in_flight);
assert!(app.latest_release.is_none());
}
#[test]
fn the_check_button_stacks_the_update_modal_over_about() {
let mut app = make_app();
app.open_about_modal();
let mut modal = AboutModal::new();
let outcome = modal.handle_key(key(KeyCode::Enter), &mut app, 24, 80);
let ModalOutcome::ContinueAnd(action) = outcome else {
panic!("expected the About page to stay open");
};
action(&mut app);
assert!(app.modal_stack.contains::<crate::app::modal::UpdateModal>());
assert!(
app.modal_stack.contains::<AboutModal>(),
"About stays underneath"
);
}
#[test]
fn enter_on_github_button_keeps_modal_open() {
let mut app = make_app();
let mut modal = AboutModal::new();
modal.handle_key(key(KeyCode::Tab), &mut app, 24, 80);
let outcome = modal.handle_key(key(KeyCode::Enter), &mut app, 24, 80);
assert!(matches!(outcome, ModalOutcome::ContinueAnd(_)));
}
#[test]
fn the_tagline_keeps_asking_for_a_redraw() {
let modal = AboutModal::new();
let now = Instant::now();
let d = modal.next_deadline().expect("tagline deadline");
assert!(d > now && d <= now + TAGLINE_INTERVAL);
}
#[test]
fn esc_dismisses() {
let mut app = make_app();
let mut modal = AboutModal::new();
let outcome = modal.handle_key(key(KeyCode::Esc), &mut app, 24, 80);
assert!(matches!(outcome, ModalOutcome::Close));
}
}