use anyhow::Result;
use chrono::{DateTime, Duration, Utc};
use dirs::data_local_dir;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize)]
struct Cache {
checked_at: DateTime<Utc>,
latest_version: String,
}
fn cache_path() -> PathBuf {
data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("tui_breath")
.join("last_check.json")
}
fn cached_version(path: &Path) -> Option<String> {
let data = std::fs::read_to_string(path).ok()?;
let cache: Cache = serde_json::from_str(&data).ok()?;
if Utc::now() - cache.checked_at < Duration::hours(24) {
Some(cache.latest_version)
} else {
None
}
}
fn fetch_latest() -> Result<String> {
let body: serde_json::Value = ureq::get("https://crates.io/api/v1/crates/tui_breath")
.set(
"User-Agent",
&format!(
"tui_breath/{} (update-check)",
env!("CARGO_PKG_VERSION")
),
)
.call()?
.into_json()?;
Ok(body["crate"]["newest_version"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("missing field"))?
.to_string())
}
fn write_cache(path: &Path, version: &str) {
if let Ok(data) = serde_json::to_string_pretty(&Cache {
checked_at: Utc::now(),
latest_version: version.to_string(),
}) {
let _ = std::fs::write(path, data);
}
}
fn is_newer(v: &str) -> bool {
fn parse(s: &str) -> (u32, u32, u32) {
let mut parts = s.splitn(3, '.').map(|p| p.parse().unwrap_or(0));
(
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
)
}
parse(v) > parse(env!("CARGO_PKG_VERSION"))
}
pub async fn check_for_update() -> Option<String> {
let path = cache_path();
let version = if let Some(cached) = cached_version(&path) {
cached
} else {
let p = path.clone();
tokio::task::spawn_blocking(move || {
fetch_latest().ok().map(|v| {
write_cache(&p, &v);
v
})
})
.await
.ok()
.flatten()?
};
if is_newer(&version) {
Some(version)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_newer() {
let current = env!("CARGO_PKG_VERSION");
assert!(!is_newer(current));
assert!(is_newer("999.0.0"));
assert!(!is_newer("0.0.0"));
}
#[test]
fn test_is_newer_malformed() {
assert!(!is_newer("invalid"));
assert!(!is_newer(""));
}
}