use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime};
use owo_colors::{OwoColorize, Stream::Stderr};
const TAP_FORMULA: &str = "deeplabua/tap/deepshrink";
const THROTTLE: Duration = Duration::from_secs(24 * 60 * 60);
pub fn upgrade_hint(quiet: bool, json: bool) -> Option<String> {
if quiet
|| json
|| std::env::var_os("DEEPSHRINK_NO_UPDATE_CHECK").is_some()
|| std::env::var_os("CI").is_some()
|| !std::io::stderr().is_terminal()
|| !is_homebrew_install()
{
return None;
}
let cache = cache_path()?;
if is_stale(&cache) {
spawn_refresh(&cache);
}
let (installed, current) = parse_versions(&std::fs::read_to_string(&cache).ok()?)?;
Some(styled_hint(&installed, ¤t))
}
fn is_homebrew_install() -> bool {
std::env::current_exe()
.and_then(|p| p.canonicalize())
.map(|p| is_cellar_path(&p.to_string_lossy()))
.unwrap_or(false)
}
fn is_cellar_path(path: &str) -> bool {
path.contains("/Cellar/deepshrink/")
}
fn cache_path() -> Option<PathBuf> {
let base = match std::env::var_os("XDG_CACHE_HOME").filter(|v| !v.is_empty()) {
Some(x) => PathBuf::from(x),
None => PathBuf::from(std::env::var_os("HOME").filter(|v| !v.is_empty())?).join(".cache"),
};
Some(base.join("deepshrink").join("brew-outdated.json"))
}
fn is_stale(cache: &Path) -> bool {
match std::fs::metadata(cache).and_then(|m| m.modified()) {
Ok(mtime) => SystemTime::now()
.duration_since(mtime)
.map(|age| age >= THROTTLE)
.unwrap_or(true),
Err(_) => true,
}
}
fn spawn_refresh(cache: &Path) {
let Some(dir) = cache.parent() else {
return;
};
if std::fs::create_dir_all(dir).is_err() {
return;
}
let tmp = dir.join(format!("brew-outdated.{}.tmp", std::process::id()));
let script = format!(
"HOMEBREW_NO_AUTO_UPDATE=1 brew outdated --json=v2 {formula} > {tmp} 2>/dev/null \
&& mv -f {tmp} {cache} || rm -f {tmp}",
formula = TAP_FORMULA,
tmp = shell_single_quote(&tmp),
cache = shell_single_quote(cache),
);
let _ = Command::new("sh")
.arg("-c")
.arg(script)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
fn shell_single_quote(p: &Path) -> String {
format!("'{}'", p.to_string_lossy().replace('\'', r"'\''"))
}
fn parse_versions(brew_json: &str) -> Option<(String, String)> {
let v: serde_json::Value = serde_json::from_str(brew_json).ok()?;
let formula = v.get("formulae")?.as_array()?.iter().find(|f| {
f.get("name")
.and_then(|n| n.as_str())
.is_some_and(|n| n.ends_with("deepshrink"))
})?;
let current = formula.get("current_version")?.as_str()?;
let installed = formula
.get("installed_versions")?
.as_array()?
.last()?
.as_str()?;
if installed == current {
return None;
}
Some((installed.to_string(), current.to_string()))
}
fn styled_hint(installed: &str, current: &str) -> String {
let cmd = format!("brew upgrade {TAP_FORMULA}");
format!(
"{} {installed} → {}\nUpdate: {}",
"A new version of deepshrink is available:"
.if_supports_color(Stderr, |t| t.dimmed().to_string()),
current.if_supports_color(Stderr, |t| t.green().bold().to_string()),
cmd.if_supports_color(Stderr, |t| t.cyan().bold().to_string()),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cellar_path_detection() {
assert!(is_cellar_path(
"/opt/homebrew/Cellar/deepshrink/0.3.0/bin/deepshrink"
));
assert!(is_cellar_path(
"/usr/local/Cellar/deepshrink/0.3.0/bin/deepshrink"
));
assert!(!is_cellar_path("/usr/local/bin/deepshrink"));
assert!(!is_cellar_path("/home/u/.cargo/bin/deepshrink"));
}
#[test]
fn parse_versions_when_outdated() {
let json = r#"{"formulae":[{"name":"deeplabua/tap/deepshrink",
"installed_versions":["0.2.1"],"current_version":"0.3.0"}],"casks":[]}"#;
assert_eq!(
parse_versions(json),
Some(("0.2.1".to_string(), "0.3.0".to_string()))
);
}
#[test]
fn no_versions_when_up_to_date() {
assert_eq!(parse_versions(r#"{"formulae":[],"casks":[]}"#), None);
let json = r#"{"formulae":[{"name":"deepshrink",
"installed_versions":["0.3.0"],"current_version":"0.3.0"}]}"#;
assert_eq!(parse_versions(json), None);
}
#[test]
fn no_versions_on_garbage() {
assert_eq!(parse_versions("not json"), None);
assert_eq!(parse_versions("{}"), None);
assert_eq!(parse_versions(r#"{"formulae":[]}"#), None);
}
#[test]
fn styled_hint_carries_versions_and_command() {
let hint = styled_hint("0.2.1", "0.3.0");
assert!(hint.contains("0.2.1"), "got: {hint}");
assert!(hint.contains("0.3.0"));
assert!(hint.contains("brew upgrade deeplabua/tap/deepshrink"));
}
}