use crate::{log_print, log_verbose};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration};
const RELEASES_PAGE_URL: &str = "https://github.com/Quantus-Network/quantus-cli/releases";
const CACHE_TTL: Duration = Duration::from_secs(60 * 60 * 4);
const REFRESH_GRACE: Duration = Duration::from_secs(3);
const DISABLE_ENV: &str = "QUANTUS_NO_UPDATE_CHECK";
#[derive(Debug, Serialize, Deserialize)]
struct UpdateCache {
last_checked: u64,
latest_version: String,
}
fn cache_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".quantus").join("update_check.json"))
}
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn read_cache() -> Option<UpdateCache> {
let path = cache_path()?;
let contents = match std::fs::read_to_string(&path) {
Ok(contents) => contents,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
log_verbose!("update check: failed to read cache {}: {e}", path.display());
return None;
},
};
match serde_json::from_str(&contents) {
Ok(cache) => Some(cache),
Err(e) => {
log_verbose!("update check: failed to parse cache {}: {e}", path.display());
None
},
}
}
fn write_cache(cache: &UpdateCache) {
let Some(path) = cache_path() else {
log_verbose!("update check: could not determine cache path; skipping cache write");
return;
};
if let Some(parent) = path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
log_verbose!("update check: failed to create cache dir {}: {e}", parent.display());
return;
}
}
match serde_json::to_string_pretty(cache) {
Ok(contents) =>
if let Err(e) = std::fs::write(&path, contents) {
log_verbose!("update check: failed to write cache {}: {e}", path.display());
},
Err(e) => log_verbose!("update check: failed to serialize cache: {e}"),
}
}
pub fn notify_if_update_available() {
if std::env::var_os(DISABLE_ENV).is_some() {
return;
}
let Some(cache) = read_cache() else {
log_verbose!("update check: no cached result yet; nothing to show");
return;
};
if now_secs().saturating_sub(cache.last_checked) >= CACHE_TTL.as_secs() {
log_verbose!("update check: cached result is stale; refreshing in background");
return;
}
let current = env!("CARGO_PKG_VERSION");
if self_update::version::bump_is_greater(current, &cache.latest_version).unwrap_or(false) {
log_print!("");
log_print!(
"{} A new version of Quantus CLI is available: {} → {}",
"⬆️".bright_yellow(),
current.dimmed(),
cache.latest_version.bright_green().bold()
);
log_print!(" Download it from {}", RELEASES_PAGE_URL.bright_cyan());
}
}
pub async fn refresh_cache_in_background() {
if std::env::var_os(DISABLE_ENV).is_some() {
return;
}
if let Some(cache) = read_cache() {
if now_secs().saturating_sub(cache.last_checked) < CACHE_TTL.as_secs() {
log_verbose!("update check: cache is fresh; skipping refresh");
return;
}
}
match tokio::task::spawn_blocking(crate::cli::update::latest_stable_version).await {
Ok(Ok(latest)) => {
write_cache(&UpdateCache { last_checked: now_secs(), latest_version: latest });
log_verbose!("update check: refreshed cached latest version");
},
Ok(Err(e)) => log_verbose!("update check: could not refresh latest version: {e}"),
Err(e) => log_verbose!("update check: refresh task failed: {e}"),
}
}
pub async fn finish_update_check(handle: Option<tokio::task::JoinHandle<()>>) {
if let Some(handle) = handle {
let _ = tokio::time::timeout(REFRESH_GRACE, handle).await;
}
notify_if_update_available();
}