use anyhow::Result;
use crate::actions::{switch_off, switch_profile};
use crate::lock::with_state_lock;
use crate::profile::{AppConfig, Profile};
use crate::usage::UsageStore;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SwitchAction {
To(String),
Off,
}
pub(crate) const DEFAULT_THRESHOLD: f64 = 95.0;
pub(crate) fn threshold_for(profile: &Profile) -> f64 {
profile.fallback_threshold.unwrap_or(DEFAULT_THRESHOLD)
}
fn is_exhausted(profile: &Profile) -> bool {
let Some(window) = profile.usage.as_ref().and_then(|u| u.five_hour.as_ref()) else {
return false;
};
window.utilization >= threshold_for(profile)
}
#[derive(Debug, Clone)]
pub(crate) struct ChainMember {
pub(crate) name: String,
pub(crate) threshold: f64,
}
#[derive(Debug, Clone)]
pub(crate) struct ChainSnapshot {
pub(crate) active: String,
pub(crate) chain: Vec<ChainMember>,
pub(crate) wrap_off: bool,
}
pub(crate) fn snapshot_chain(config: &AppConfig) -> Option<ChainSnapshot> {
let active = config.state.active_profile.as_deref()?.to_string();
let chain = &config.state.fallback_chain;
if !chain.iter().any(|n| n == &active) {
return None;
}
let chain = chain
.iter()
.map(|name| ChainMember {
name: name.clone(),
threshold: config
.find(name)
.map(threshold_for)
.unwrap_or(DEFAULT_THRESHOLD),
})
.collect();
Some(ChainSnapshot {
active,
chain,
wrap_off: config.state.wrap_off,
})
}
fn is_exhausted_from_store(name: &str, threshold: f64, store: &UsageStore) -> bool {
let util = match store.lock() {
Ok(s) => s
.get(name)
.and_then(|u| u.five_hour.as_ref())
.map(|w| w.utilization),
Err(_) => return false,
};
let Some(util) = util else {
return false;
};
util >= threshold
}
pub(crate) fn next_target(config: &AppConfig) -> Option<SwitchAction> {
let active = config.state.active_profile.as_deref()?;
let chain = &config.state.fallback_chain;
let active_idx = chain.iter().position(|n| n == active)?;
let len = chain.len();
let walk = |accept: &dyn Fn(&Profile) -> bool| -> Option<String> {
for offset in 1..=len {
let candidate = &chain[(active_idx + offset) % len];
if candidate == active {
continue;
}
let Some(profile) = config.find(candidate) else {
continue;
};
if accept(profile) {
return Some(candidate.clone());
}
}
None
};
if let Some(name) = walk(&|p| !is_exhausted(p)) {
return Some(SwitchAction::To(name));
}
let active_is_sink = config
.find(active)
.is_some_and(|p| threshold_for(p) >= 100.0);
if active_is_sink {
return None;
}
if let Some(name) = walk(&|p| threshold_for(p) >= 100.0) {
return Some(SwitchAction::To(name));
}
if config.state.wrap_off && config.find(active).is_some_and(is_exhausted) {
return Some(SwitchAction::Off);
}
None
}
pub(crate) fn next_auto_switch_target(
snapshot: &ChainSnapshot,
store: &UsageStore,
) -> Option<SwitchAction> {
let active_idx = snapshot
.chain
.iter()
.position(|m| m.name == snapshot.active)?;
let len = snapshot.chain.len();
let active = &snapshot.chain[active_idx];
if !is_exhausted_from_store(&active.name, active.threshold, store) {
return None;
}
let walk = |accept: &dyn Fn(&ChainMember) -> bool| -> Option<String> {
for offset in 1..=len {
let candidate = &snapshot.chain[(active_idx + offset) % len];
if candidate.name == active.name {
continue;
}
if accept(candidate) {
return Some(candidate.name.clone());
}
}
None
};
if let Some(name) = walk(&|m| !is_exhausted_from_store(&m.name, m.threshold, store)) {
return Some(SwitchAction::To(name));
}
let active_is_sink = active.threshold >= 100.0;
if active_is_sink {
return None;
}
if let Some(name) = walk(&|m| m.threshold >= 100.0) {
return Some(SwitchAction::To(name));
}
if snapshot.wrap_off {
return Some(SwitchAction::Off);
}
None
}
pub(crate) fn auto_switch_if_needed(config: &mut AppConfig) -> Result<Option<SwitchAction>> {
with_state_lock(|| {
let active_name = config.state.active_profile.clone();
let Some(active_name) = active_name else {
return Ok(None);
};
if !config
.state
.fallback_chain
.iter()
.any(|n| n == &active_name)
{
return Ok(None);
}
let Some(active) = config.find(&active_name) else {
return Ok(None);
};
if !is_exhausted(active) {
return Ok(None);
}
let Some(action) = next_target(config) else {
return Ok(None);
};
match &action {
SwitchAction::To(target) => switch_profile(config, target)?,
SwitchAction::Off => switch_off(config)?,
}
Ok(Some(action))
})
}
#[cfg(test)]
#[path = "../tests/inline/fallback.rs"]
mod tests;