1use crate::{AutoLaunch, LinuxLaunchMode, Result};
2use std::{fs, io::Write, path::PathBuf};
3
4impl AutoLaunch {
6 pub fn new(
16 app_name: &str,
17 app_path: &str,
18 launch_mode: LinuxLaunchMode,
19 args: &[impl AsRef<str>],
20 ) -> AutoLaunch {
21 AutoLaunch {
22 app_name: app_name.into(),
23 app_path: app_path.into(),
24 launch_mode,
25 args: args.iter().map(|s| s.as_ref().to_string()).collect(),
26 }
27 }
28
29 pub fn enable(&self) -> Result<()> {
38 match self.launch_mode {
39 LinuxLaunchMode::XdgAutostart => self.enable_xdg_autostart(),
40 LinuxLaunchMode::Systemd => self.enable_systemd(),
41 }
42 }
43
44 fn enable_xdg_autostart(&self) -> Result<()> {
46 let data = build_xdg_autostart_data(&self.app_name, &self.app_path, &self.args);
47
48 let dir = get_xdg_autostart_dir()?;
49 if !dir.exists() {
50 fs::create_dir_all(&dir).or_else(|e| {
51 if e.kind() == std::io::ErrorKind::AlreadyExists {
52 Ok(())
53 } else {
54 Err(e)
55 }
56 })?;
57 }
58 let file_path = self.get_xdg_desktop_file()?;
59 let mut file = fs::OpenOptions::new()
60 .write(true)
61 .create(true)
62 .truncate(true)
63 .open(file_path)?;
64 file.write_all(data.as_bytes())?;
65 Ok(())
66 }
67
68 fn enable_systemd(&self) -> Result<()> {
70 let data = build_systemd_service_data(&self.app_name, &self.app_path, &self.args);
72
73 let dir = get_systemd_user_dir()?;
75 if !dir.exists() {
76 fs::create_dir_all(&dir).or_else(|e| {
77 if e.kind() == std::io::ErrorKind::AlreadyExists {
78 Ok(())
79 } else {
80 Err(e)
81 }
82 })?;
83 }
84
85 let service_file = self.get_systemd_service_file()?;
87 let mut file = fs::OpenOptions::new()
88 .write(true)
89 .create(true)
90 .truncate(true)
91 .open(&service_file)?;
92 file.write_all(data.as_bytes())?;
93
94 self.systemctl_enable()?;
96
97 Ok(())
98 }
99
100 fn systemctl_enable(&self) -> Result<()> {
102 let service_name = format!("{}.service", self.app_name);
103 let output = std::process::Command::new("systemctl")
104 .args(&["--user", "enable", &service_name])
105 .output()?;
106
107 if !output.status.success() {
108 return Err(std::io::Error::new(
109 std::io::ErrorKind::Other,
110 format!(
111 "Failed to enable systemd service: {}",
112 String::from_utf8_lossy(&output.stderr)
113 ),
114 )
115 .into());
116 }
117
118 Ok(())
119 }
120
121 pub fn disable(&self) -> Result<()> {
128 match self.launch_mode {
129 LinuxLaunchMode::XdgAutostart => self.disable_xdg_autostart(),
130 LinuxLaunchMode::Systemd => self.disable_systemd(),
131 }
132 }
133
134 fn disable_xdg_autostart(&self) -> Result<()> {
136 let file = self.get_xdg_desktop_file()?;
137 if file.exists() {
138 fs::remove_file(file)?;
139 }
140 Ok(())
141 }
142
143 fn disable_systemd(&self) -> Result<()> {
145 self.systemctl_disable()?;
147
148 let service_file = self.get_systemd_service_file()?;
150 if service_file.exists() {
151 fs::remove_file(service_file)?;
152 }
153
154 let _ = std::process::Command::new("systemctl")
156 .args(&["--user", "daemon-reload"])
157 .output();
158
159 Ok(())
160 }
161
162 fn systemctl_disable(&self) -> Result<()> {
164 let service_name = format!("{}.service", self.app_name);
165 let output = std::process::Command::new("systemctl")
166 .args(&["--user", "disable", &service_name])
167 .output()?;
168
169 if !output.status.success() {
171 let stderr = String::from_utf8_lossy(&output.stderr);
172 if !stderr.contains("No such file or directory") && !stderr.contains("not loaded") {
173 return Err(std::io::Error::new(
174 std::io::ErrorKind::Other,
175 format!("Failed to disable systemd service: {}", stderr),
176 )
177 .into());
178 }
179 }
180
181 Ok(())
182 }
183
184 pub fn is_enabled(&self) -> Result<bool> {
186 match self.launch_mode {
187 LinuxLaunchMode::XdgAutostart => Ok(self.get_xdg_desktop_file()?.exists()),
188 LinuxLaunchMode::Systemd => self.is_systemd_enabled(),
189 }
190 }
191
192 fn is_systemd_enabled(&self) -> Result<bool> {
194 let service_name = format!("{}.service", self.app_name);
195 let output = std::process::Command::new("systemctl")
196 .args(&["--user", "is-enabled", &service_name])
197 .output()?;
198
199 Ok(output.status.success())
204 }
205
206 fn get_xdg_desktop_file(&self) -> Result<PathBuf> {
208 Ok(get_xdg_autostart_dir()?.join(format!("{}.desktop", self.app_name)))
209 }
210
211 fn get_systemd_service_file(&self) -> Result<PathBuf> {
213 Ok(get_systemd_user_dir()?.join(format!("{}.service", self.app_name)))
214 }
215}
216
217fn build_xdg_autostart_data(app_name: &str, app_path: &str, args: &[String]) -> String {
218 format!(
219 "[Desktop Entry]\n\
220 Type=Application\n\
221 Version=1.0\n\
222 Name={}\n\
223 Comment={} startup script\n\
224 Exec={} {}\n\
225 StartupNotify=false\n\
226 Terminal=false",
227 app_name,
228 app_name,
229 app_path,
230 args.join(" ")
231 )
232}
233
234fn build_systemd_service_data(app_name: &str, app_path: &str, args: &[String]) -> String {
235 let args_str = if args.is_empty() {
236 String::new()
237 } else {
238 format!(" {}", args.join(" "))
239 };
240
241 format!(
242 "[Unit]\n\
243 Description={}\n\
244 After=default.target\n\
245 \n\
246 [Service]\n\
247 Type=simple\n\
248 ExecStart={}{}\n\
249 Restart=on-failure\n\
250 RestartSec=10\n\
251 \n\
252 [Install]\n\
253 WantedBy=default.target",
254 app_name, app_path, args_str
255 )
256}
257
258fn get_xdg_autostart_dir() -> Result<PathBuf> {
260 let home_dir = dirs::home_dir().ok_or_else(|| {
261 std::io::Error::new(std::io::ErrorKind::NotFound, "Failed to find home directory")
262 })?;
263 Ok(home_dir.join(".config").join("autostart"))
264}
265
266fn get_systemd_user_dir() -> Result<PathBuf> {
268 let home_dir = dirs::home_dir().ok_or_else(|| {
269 std::io::Error::new(std::io::ErrorKind::NotFound, "Failed to find home directory")
270 })?;
271 Ok(home_dir.join(".config").join("systemd").join("user"))
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_build_xdg_autostart_data() {
280 let data = build_xdg_autostart_data(
281 "TestApp",
282 "/opt/test-app",
283 &vec!["--flag".into(), "value".into()],
284 );
285
286 assert!(data.contains("Type=Application"));
287 assert!(data.contains("Name=TestApp"));
288 assert!(data.contains("Comment=TestApp startup script"));
289 assert!(data.contains("Exec=/opt/test-app --flag value"));
290 assert!(data.contains("StartupNotify=false"));
291 assert!(data.contains("Terminal=false"));
292 }
293
294 #[test]
295 fn test_build_systemd_service_data() {
296 let data = build_systemd_service_data(
297 "TestApp",
298 "/opt/test-app",
299 &vec!["--flag".into()],
300 );
301
302 assert!(data.contains("Description=TestApp"));
303 assert!(data.contains("After=default.target"));
304 assert!(data.contains("ExecStart=/opt/test-app --flag"));
305 assert!(data.contains("Restart=on-failure"));
306 assert!(data.contains("WantedBy=default.target"));
307 }
308}