1use std::{
2 ffi::OsStr,
3 path::{Path, PathBuf},
4};
5
6mod cli;
7
8mod config;
9pub(crate) use config::Config;
10
11mod env;
12
13mod error;
14pub(crate) use error::Error;
15
16pub(crate) mod prelude;
17use prelude::*;
18
19async fn delay_until_next_update(config: &Config) -> Result<jiff::Span> {
20 let interval = reqwest::get(format!("{}/api/interval", config.server_url()))
21 .await
22 .map_err(Error::IntervalRequest)?
23 .text()
24 .await
25 .map_err(Error::IntervalRequest)?
26 .parse::<u64>()
27 .map_err(|_| Error::InvalidInterval)?;
28
29 let current_timestamp = jiff::Timestamp::now();
30 let next_timestamp = jiff::Timestamp::from_millisecond(
31 ((current_timestamp.as_millisecond() as f64 / (interval * 1000) as f64) + 1.0).floor()
32 as i64
33 * (interval * 1000) as i64,
34 )
35 .expect("calculation should always succeed");
36 Ok(next_timestamp - current_timestamp)
37}
38
39async fn current_digest(config: &Config) -> Result<String> {
40 let digest_response = reqwest::get(format!(
41 "{}/api/pool/{}/digest",
42 config.server_url(),
43 config.pool_name()
44 ))
45 .await
46 .map_err(|e| Error::DigestRequest(e.to_string()))?;
47
48 if !digest_response.status().is_success() {
49 return Err(Error::DigestRequest("response was not 200".to_string()));
50 }
51
52 digest_response
53 .text()
54 .await
55 .map_err(|e| Error::DigestRequest(e.to_string()))
56}
57
58fn find_wallpaper_path(wallpapers_path: &Path, digest: &str) -> Result<Option<PathBuf>> {
59 Ok(std::fs::read_dir(wallpapers_path)
60 .map_err(|e| Error::WallpaperList {
61 io_error: e,
62 wallpapers_path: wallpapers_path.display().to_string(),
63 })?
64 .collect::<Result<Vec<std::fs::DirEntry>, _>>()
65 .map_err(|e| Error::WallpaperList {
66 io_error: e,
67 wallpapers_path: wallpapers_path.display().to_string(),
68 })?
69 .iter()
70 .map(|dir_entry| dir_entry.path())
71 .find(|wallpaper_path| {
72 wallpaper_path
73 .file_stem()
74 .map(|file_stem| file_stem == OsStr::new(digest))
75 .unwrap_or(false)
76 }))
77}
78
79async fn download_current_wallpaper(
80 wallpapers_path: &Path,
81 config: &Config,
82 digest: &str,
83) -> Result<PathBuf> {
84 info!("downloading current wallpaper");
85 let wallpaper_response = reqwest::get(format!(
86 "{}/api/pool/{}/wallpaper",
87 config.server_url(),
88 config.pool_name()
89 ))
90 .await
91 .map_err(|e| Error::WallpaperRequest(e.to_string()))?;
92
93 if !wallpaper_response.status().is_success() {
94 return Err(Error::WallpaperRequest("response was not 200".to_string()));
95 }
96
97 let content_type = wallpaper_response
98 .headers()
99 .get("Content-Type")
100 .expect("should always have a Content-Type")
101 .to_str()
102 .expect("content-type should always be a valid &str")
103 .to_string();
104 let extension = content_type
105 .split("/")
106 .nth(1)
107 .expect("content-type should always contain a slash");
108 let wallpaper_path = wallpapers_path.join(format!("{}.{}", digest, extension));
109
110 let image_content = wallpaper_response
111 .bytes()
112 .await
113 .map_err(|e| Error::WallpaperRequest(e.to_string()))?;
114
115 std::fs::write(&wallpaper_path, image_content).map_err(Error::WallpaperWrite)?;
116
117 Ok(wallpaper_path)
118}
119
120fn set_wallpaper(wallpaper_path: &Path) -> Result<()> {
121 let exit_status = match std::env::var("XDG_CURRENT_DESKTOP")
122 .unwrap_or_default()
123 .as_str()
124 {
125 "GNOME" => {
126 let mut exit_status = std::process::Command::new("gsettings")
127 .arg("set")
128 .arg("org.gnome.desktop.background")
129 .arg("picture-uri")
130 .arg(format!("file://{}", wallpaper_path.display()))
131 .spawn()
132 .map_err(Error::WallpaperSetCommand)?
133 .wait()
134 .map_err(Error::WallpaperSetCommand)?;
135 if exit_status.success() {
136 exit_status = std::process::Command::new("gsettings")
137 .arg("set")
138 .arg("org.gnome.desktop.background")
139 .arg("picture-uri-dark")
140 .arg(format!("file://{}", wallpaper_path.display()))
141 .spawn()
142 .map_err(Error::WallpaperSetCommand)?
143 .wait()
144 .map_err(Error::WallpaperSetCommand)?;
145 }
146 exit_status
147 }
148 _ => std::process::Command::new("swww")
149 .arg("img")
150 .arg("--transition-type")
151 .arg("fade")
152 .arg("--transition-bezier")
153 .arg("0,0,1,1")
154 .arg(wallpaper_path)
155 .spawn()
156 .map_err(Error::WallpaperSetCommand)?
157 .wait()
158 .map_err(Error::WallpaperSetCommand)?,
159 };
160
161 if !exit_status.success() {
162 return Err(Error::WallpaperSet {
164 exit_code: exit_status.code().unwrap_or_default(),
165 });
166 }
167
168 Ok(())
169}
170
171async fn update_wallpaper(
172 config: &Config,
173 wallpapers_path: &Path,
174 last_digest: &str,
175) -> Result<String> {
176 let current_digest = current_digest(config).await?;
177 if current_digest == last_digest {
178 info!("the wallpaper did not change; skipping");
179 } else {
180 let wallpaper_path = match find_wallpaper_path(wallpapers_path, ¤t_digest)? {
181 Some(wallpaper_path) => wallpaper_path,
182 None => download_current_wallpaper(wallpapers_path, config, ¤t_digest).await?,
183 };
184 info!("setting a new wallpaper");
185 set_wallpaper(&wallpaper_path)?;
186 }
187
188 Ok(current_digest)
189}
190
191pub async fn run() -> Result<()> {
192 env::load_dotenv()?;
193
194 let matches = cli::get_command().get_matches();
195 let custom_config_dir = matches.get_one::<PathBuf>("config-dir");
196
197 let data_home_path = directories::ProjectDirs::from("ch", "Mural Sync", "Mural Client")
198 .map(|project_dirs| {
199 project_dirs
200 .data_local_dir()
201 .to_path_buf()
202 .parent()
203 .expect("the directories crate always returns full paths")
204 .join("mural-client")
205 })
206 .ok_or(Error::DataHome)?;
207 let wallpapers_path = data_home_path.join("wallpapers");
208 let _ = std::fs::create_dir_all(&wallpapers_path);
209
210 let mut last_digest = String::new();
211 let mut delay = jiff::Span::new().seconds(5);
212
213 loop {
214 info!("updating wallpaper");
215 let config = Config::load(custom_config_dir)?;
216
217 last_digest = match update_wallpaper(&config, &wallpapers_path, &last_digest).await {
218 Ok(new_digest) => new_digest,
219 Err(e) => {
220 error!("updating wallpaper failed: {}", e);
221 last_digest
222 }
223 };
224
225 delay = match delay_until_next_update(&config).await {
226 Ok(new_delay) => new_delay,
227 Err(e) => {
228 error!("getting delay failed: {}", e);
229 delay
230 }
231 };
232 std::thread::sleep(std::time::Duration::from_millis(
233 delay
234 .total(jiff::Unit::Millisecond)
235 .expect("should only fail if the unit is bigger than hours") as u64
236 + 1,
237 ));
238 }
239}