1use anyhow::Result;
2use std::fs;
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use self_update::backends::github::UpdateBuilder;
7use self_update::cargo_crate_version;
8
9const REPO_OWNER: &str = "subotic";
10const REPO_NAME: &str = "loom";
11const BIN_NAME: &str = "loom";
12
13const CHECK_INTERVAL_SECS: u64 = 3600; fn updater() -> UpdateBuilder {
20 let mut builder = self_update::backends::github::Update::configure();
21 builder
22 .repo_owner(REPO_OWNER)
23 .repo_name(REPO_NAME)
24 .bin_name(BIN_NAME)
25 .current_version(cargo_crate_version!());
26 builder
27}
28
29pub fn check_and_update(show_progress: bool) -> Result<Option<String>> {
38 let status = updater()
39 .show_download_progress(show_progress)
40 .no_confirm(true)
41 .build()?
42 .update()?;
43
44 if status.updated() {
45 Ok(Some(status.version().to_string()))
46 } else {
47 Ok(None)
48 }
49}
50
51pub fn check_version_throttled(force: bool) -> Result<Option<String>> {
64 if !force && !should_check()? {
65 return Ok(None);
66 }
67
68 let result = check_version();
69
70 let _ = record_check();
73
74 let (current, latest) = result?;
75 if latest != current {
76 Ok(Some(latest))
77 } else {
78 Ok(None)
79 }
80}
81
82pub fn check_version() -> Result<(String, String)> {
86 let current = cargo_crate_version!().to_string();
87 let latest = updater().build()?.get_latest_release()?;
88 Ok((current, latest.version))
89}
90
91pub fn is_disabled_by_env() -> bool {
98 std::env::var("LOOM_DISABLE_UPDATE")
99 .is_ok_and(|v| matches!(v.to_lowercase().as_str(), "1" | "true" | "yes"))
100}
101
102fn timestamp_path() -> Result<PathBuf> {
105 let home =
106 dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
107 Ok(home.join(".config").join("loom").join("last_update_check"))
108}
109
110fn should_check() -> Result<bool> {
111 let path = timestamp_path()?;
112 if !path.exists() {
113 return Ok(true);
114 }
115
116 let content = fs::read_to_string(&path)?;
117 let last_check: u64 = content.trim().parse().unwrap_or(0);
118 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
119
120 Ok(now.saturating_sub(last_check) >= CHECK_INTERVAL_SECS)
121}
122
123fn record_check() -> Result<()> {
124 let path = timestamp_path()?;
125 if let Some(parent) = path.parent() {
126 fs::create_dir_all(parent)?;
127 }
128 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
129 fs::write(&path, now.to_string())?;
130 Ok(())
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_is_disabled_by_env_truthy_values() {
139 for (value, expected) in [
140 ("1", true),
141 ("true", true),
142 ("TRUE", true),
143 ("yes", true),
144 ("Yes", true),
145 ("0", false),
146 ("false", false),
147 ("no", false),
148 ("", false),
149 ] {
150 unsafe {
152 std::env::set_var("LOOM_DISABLE_UPDATE", value);
153 }
154 assert_eq!(
155 is_disabled_by_env(),
156 expected,
157 "LOOM_DISABLE_UPDATE={value:?} should be {expected}"
158 );
159 }
160
161 unsafe {
163 std::env::remove_var("LOOM_DISABLE_UPDATE");
164 }
165 assert!(!is_disabled_by_env(), "unset should be false");
166 }
167
168 #[test]
169 fn test_timestamp_path() {
170 let path = timestamp_path().unwrap();
171 assert!(path.ends_with(".config/loom/last_update_check"));
172 }
173}