tiny_update_notifier/
lib.rs1#![allow(clippy::multiple_crate_versions)] use directories::ProjectDirs;
3use notify_rust::Notification;
21
22use std::{
23 fs,
24 io::{self, Error, ErrorKind},
25 time::Duration,
26};
27
28pub enum Source {
30 CratesIO,
34 GitHub,
38}
39
40#[allow(non_snake_case)]
53pub fn check_cratesIO(version: &'static str, name: &'static str) {
54 spawn(Source::CratesIO, version, name, "");
55}
56
57pub fn check_github(version: &'static str, name: &'static str, repo_url: &'static str) {
71 spawn(Source::GitHub, version, name, repo_url);
72}
73
74fn spawn(source: Source, version: &'static str, name: &'static str, repo_url: &'static str) {
75 std::thread::spawn(move || {
76 Notifier::new(source, version, name, repo_url).run();
77 });
78}
79
80pub struct Notifier {
99 version: &'static str,
100 name: &'static str,
101 repo_url: &'static str,
102 source: Source,
103 interval: Duration,
104}
105
106impl Notifier {
107 #[must_use]
126 pub const fn new(
127 source: Source,
128 version: &'static str,
129 name: &'static str,
130 repo_url: &'static str,
131 ) -> Self {
132 Self {
133 version,
134 name,
135 repo_url,
136 source,
137 interval: Duration::from_secs(60 * 60 * 24), }
139 }
140
141 #[must_use]
144 pub const fn interval(mut self, interval: Duration) -> Self {
145 self.interval = interval;
146 self
147 }
148
149 pub fn run(&mut self) {
151 match Self::should_check_update(self) {
152 Err(e) => {
153 Self::notification(self, &format!("Error: should_check_update() Failed: \n{e}"));
154 }
155 Ok(true) => Self::check_version(self),
156 Ok(false) => (),
157 };
158 }
159
160 fn check_version(&mut self) {
161 if let Ok(new_version) = Self::get_latest_version(self) {
162 if new_version != self.version {
163 let link = if self.repo_url.is_empty() {
164 String::new()
165 } else {
166 format!(
167 "\n{repo_url}/releases/tag/{new_version}",
168 repo_url = self.repo_url,
169 )
170 };
171
172 Self::notification(
173 self,
174 &format!(
175 "A new release of {pkg_name} is available: \n\
176 v{current_version} -> v{new_version}{link}",
177 pkg_name = self.name,
178 current_version = self.version
179 ),
180 );
181 }
182
183 Self::write_last_checked(self).unwrap_or_else(|e| {
184 Self::notification(self, &format!("Error: write_last_checked() failed: \n{e}"));
185 });
186 }
187 }
188
189 fn notification(&mut self, body: &str) {
190 Notification::new()
191 .summary(self.name)
192 .body(body)
193 .icon("/usr/share/icons/hicolor/256x256/apps/gnome-software.png")
194 .timeout(5000)
195 .show()
196 .ok();
197 }
198
199 fn get_latest_version(&mut self) -> io::Result<String> {
200 let output = std::process::Command::new("curl")
201 .arg("--silent")
202 .arg(self.get_api_link()?)
203 .output();
204
205 match output {
206 Ok(output) => {
207 let stdout = String::from_utf8_lossy(&output.stdout);
208 let data: serde_json::Value = serde_json::from_str(&stdout)?;
209 let version = self.extract_version_from_json(&data);
210 Ok(version)
211 }
212 Err(e) => {
213 Self::notification(self, &format!("Error: get_latest_version() failed: \n{e}"));
214 Err(e)
215 }
216 }
217 }
218
219 fn get_api_link(&self) -> io::Result<String> {
220 match self.source {
221 Source::CratesIO => Ok(format!("https://crates.io/api/v1/crates/{}", self.name)),
222 Source::GitHub => {
223 let repo_url = self.repo_url;
224 let data = repo_url.split('/').collect::<Vec<&str>>();
225 if data.len() < 5 {
226 return Err(Error::new(
227 ErrorKind::InvalidInput,
228 "Invalid GitHub repo url",
229 ));
230 };
231
232 Ok(format!(
233 "https://api.github.com/repos/{owner}/{repo}/releases/latest",
234 owner = data[3],
235 repo = data[4]
236 ))
237 }
238 }
239 }
240
241 fn extract_version_from_json(&self, data: &serde_json::Value) -> String {
242 match self.source {
243 Source::CratesIO => data["crate"]["max_stable_version"]
244 .to_string()
245 .trim_matches('"')
246 .to_string(),
247 Source::GitHub => data["tag_name"]
248 .to_string()
249 .trim_matches('"')
250 .trim_start_matches('v')
251 .to_string(),
252 }
253 }
254
255 fn should_check_update(&mut self) -> io::Result<bool> {
256 let binding = Self::get_cache_dir(self)?;
257 let cache_dir = binding.cache_dir();
258 if !cache_dir.exists() {
259 fs::create_dir_all(cache_dir)?;
260 }
261 let path = cache_dir.join(format!("{}-last-update-check", self.name));
262 if path.exists() {
263 let metadata = fs::metadata(path)?;
264 let last_modified_diff = metadata.modified()?.elapsed().unwrap_or_default();
265 Ok(last_modified_diff > self.interval)
266 } else {
267 Ok(true)
268 }
269 }
270
271 fn write_last_checked(&mut self) -> io::Result<()> {
272 let path = Self::get_cache_dir(self)?
273 .cache_dir()
274 .join(format!("{}-last-update-check", self.name));
275 fs::write(path, "")
276 }
277
278 fn get_cache_dir(&mut self) -> io::Result<ProjectDirs> {
279 let project_dir = ProjectDirs::from("", "", self.name);
280 project_dir
281 .ok_or_else(|| io::Error::new(ErrorKind::Other, "Could not get project directory"))
282 }
283}