mod file_change;
mod input;
mod refresh;
mod search;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::app::App;
use crate::file_events::VcsLockState;
use crate::limits::DiffThresholds;
use crate::message::{Message, UpdateResult, FALLBACK_REFRESH_SECS};
use crate::vcs::Vcs;
pub struct Timers {
pub last_refresh: Instant,
pub last_fetch: Instant,
pub fetch_in_progress: bool,
pub pending_vcs_event: Option<Instant>,
pub last_vcs_check: Instant,
pub jj_present: bool,
pub last_refresh_completed: Option<Instant>,
pub last_known_revision: Option<String>,
pub transient_retry_at: Option<Instant>,
pub transient_retry_attempt: u32,
}
impl Timers {
pub fn new(jj_present: bool) -> Self {
Self {
last_refresh: Instant::now(),
last_fetch: Instant::now(),
fetch_in_progress: false,
pending_vcs_event: None,
last_vcs_check: Instant::now(),
jj_present,
last_refresh_completed: None,
last_known_revision: None,
transient_retry_at: None,
transient_retry_attempt: 0,
}
}
}
impl Default for Timers {
fn default() -> Self {
Self::new(false)
}
}
pub enum RefreshState {
Idle,
InProgress {
started_at: Instant,
cancel_flag: Arc<AtomicBool>,
},
InProgressPending {
started_at: Instant,
cancel_flag: Arc<AtomicBool>,
},
}
impl RefreshState {
pub fn is_idle(&self) -> bool {
matches!(self, RefreshState::Idle)
}
pub fn started_at(&self) -> Option<Instant> {
match self {
RefreshState::Idle => None,
RefreshState::InProgress { started_at, .. } => Some(*started_at),
RefreshState::InProgressPending { started_at, .. } => Some(*started_at),
}
}
pub fn has_pending(&self) -> bool {
matches!(self, RefreshState::InProgressPending { .. })
}
pub fn mark_pending(&mut self) {
if let RefreshState::InProgress {
started_at,
cancel_flag,
} = self
{
*self = RefreshState::InProgressPending {
started_at: *started_at,
cancel_flag: cancel_flag.clone(),
};
}
}
pub fn cancel_and_mark_pending(&mut self) {
match self {
RefreshState::InProgress {
started_at,
cancel_flag,
} => {
cancel_flag.store(true, Ordering::Relaxed);
*self = RefreshState::InProgressPending {
started_at: *started_at,
cancel_flag: cancel_flag.clone(),
};
}
RefreshState::InProgressPending { cancel_flag, .. } => {
cancel_flag.store(true, Ordering::Relaxed);
}
RefreshState::Idle => {}
}
}
pub fn start(&mut self) -> Arc<AtomicBool> {
match self {
RefreshState::InProgress { cancel_flag, .. }
| RefreshState::InProgressPending { cancel_flag, .. } => {
cancel_flag.store(true, Ordering::Relaxed);
}
RefreshState::Idle => {}
}
let cancel_flag = Arc::new(AtomicBool::new(false));
*self = RefreshState::InProgress {
started_at: Instant::now(),
cancel_flag: cancel_flag.clone(),
};
cancel_flag
}
pub fn start_single_file(&mut self) {
*self = RefreshState::InProgress {
started_at: Instant::now(),
cancel_flag: Arc::new(AtomicBool::new(false)),
};
}
pub fn complete(&mut self) -> bool {
let had_pending = self.has_pending();
*self = RefreshState::Idle;
had_pending
}
}
pub struct UpdateConfig {
pub fetch_interval: Duration,
pub refresh_fallback_interval: Duration,
pub refresh_watchdog_timeout: Duration,
pub auto_fetch: bool,
pub diff_thresholds: DiffThresholds,
pub needs_fallback_refresh: bool,
pub repo_path: PathBuf,
}
impl Default for UpdateConfig {
fn default() -> Self {
Self {
fetch_interval: Duration::from_secs(30),
refresh_fallback_interval: Duration::from_secs(FALLBACK_REFRESH_SECS),
refresh_watchdog_timeout: Duration::from_secs(10),
auto_fetch: true,
diff_thresholds: DiffThresholds::default(),
needs_fallback_refresh: false,
repo_path: PathBuf::new(),
}
}
}
pub fn update(
msg: Message,
app: &mut App,
refresh_state: &mut RefreshState,
vcs_lock: &mut VcsLockState,
timers: &mut Timers,
config: &UpdateConfig,
vcs: &dyn Vcs,
) -> UpdateResult {
match msg {
Message::Input(action) => input::handle_input(action, app, refresh_state),
Message::SearchInput(event) => search::handle_search_input(event, app),
Message::RefreshCompleted(outcome) => {
let outcome = *outcome;
refresh::handle_refresh(outcome, app, refresh_state, timers, config, vcs)
}
Message::FileChanged(events) => {
file_change::handle_file_change(events, app, refresh_state, vcs_lock, timers, vcs)
}
Message::FetchCompleted(result) => {
refresh::handle_fetch(result, app, refresh_state, timers)
}
Message::Tick => {
let mut result = refresh::handle_tick(refresh_state, timers, config);
if app.check_and_execute_pending_copy() {
result.needs_redraw = true;
}
result
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::Ordering;
#[test]
fn test_refresh_state_lifecycle() {
let mut state = RefreshState::Idle;
assert!(state.is_idle());
let cancel_flag = state.start();
assert!(!state.is_idle());
assert!(state.started_at().is_some());
state.mark_pending();
assert!(state.has_pending());
let had_pending = state.complete();
assert!(had_pending);
assert!(state.is_idle());
assert!(!cancel_flag.load(Ordering::Relaxed));
}
#[test]
fn test_start_cancels_existing_in_progress_refresh() {
let mut state = RefreshState::Idle;
let old_flag = state.start();
assert!(!old_flag.load(Ordering::Relaxed));
let new_flag = state.start();
assert!(old_flag.load(Ordering::Relaxed), "old cancel flag should be set");
assert!(!new_flag.load(Ordering::Relaxed), "new cancel flag should be fresh");
}
#[test]
fn test_start_cancels_existing_in_progress_pending_refresh() {
let mut state = RefreshState::Idle;
let old_flag = state.start();
state.mark_pending();
assert!(state.has_pending());
let new_flag = state.start();
assert!(old_flag.load(Ordering::Relaxed), "old cancel flag should be set");
assert!(!new_flag.load(Ordering::Relaxed), "new cancel flag should be fresh");
assert!(!state.has_pending());
}
#[test]
fn test_start_single_file_transitions_from_idle() {
let mut state = RefreshState::Idle;
state.start_single_file();
assert!(!state.is_idle());
assert!(state.started_at().is_some());
assert!(!state.has_pending());
}
#[test]
fn test_start_single_file_replaces_without_cancelling() {
let mut state = RefreshState::Idle;
let old_flag = state.start();
assert!(!old_flag.load(Ordering::Relaxed));
state.start_single_file();
assert!(!old_flag.load(Ordering::Relaxed), "start_single_file should not cancel previous");
assert!(!state.is_idle());
}
#[test]
fn test_cancel_and_mark_pending_from_idle_is_noop() {
let mut state = RefreshState::Idle;
state.cancel_and_mark_pending();
assert!(state.is_idle());
}
#[test]
fn test_cancel_and_mark_pending_from_in_progress_pending() {
let mut state = RefreshState::Idle;
let flag = state.start();
state.mark_pending();
assert!(state.has_pending());
state.cancel_and_mark_pending();
assert!(flag.load(Ordering::Relaxed), "cancel flag should be set");
assert!(state.has_pending(), "should remain InProgressPending");
}
}