1#![warn(clippy::cargo)]
2#![allow(clippy::multiple_crate_versions)] pub mod utils;
5use std::{
6 fs,
7 path::{Path, PathBuf},
8 process::Command,
9};
10
11use log::{debug, error, info, warn};
12
13pub fn exec(cmd: &str) -> std::io::Result<()> {
15 let mut parts = cmd.split_whitespace();
16 let true_cmd = parts.next().expect("Command cannot be empty");
17 let args: Vec<&str> = parts.collect();
18 debug!("Executing `{cmd}`");
19 Command::new(true_cmd).args(args).status()?;
20 Ok(())
21}
22
23pub fn read_first_line(path: &Path) -> std::io::Result<String> {
25 debug!("Reading first line of `{}`", path.display());
26 let content = fs::read_to_string(path)?;
27 for line in content.lines() {
28 let stripped = line.trim();
29 if !stripped.is_empty() && !stripped.starts_with("#!") {
30 return Ok(stripped.to_string());
31 }
32 }
33 Err(std::io::Error::other("File is empty"))
34}
35
36pub fn extract_name_from_cmd(cmd: &str) -> String {
46 cmd.split_whitespace()
47 .find(|x| !x.is_empty())
48 .expect("Command is empty!")
49 .split(['/', '\\'])
50 .rev()
51 .find(|x| !x.is_empty())
52 .expect("cannot extract name from invalid command")
53 .split('.')
54 .find(|x| !x.is_empty())
55 .expect("cannot extract name from invalid command")
56 .into()
57}
58
59pub fn find_writable_path(name: impl AsRef<str>) -> PathBuf {
64 let name = name.as_ref();
65 debug!("Finding writable path for `{name}`");
66 let base_path = &utils::CONFIG_PATH;
67 let ext = utils::FILE_EXT;
68
69 let initial_path = base_path.join(format!("{name}{ext}"));
70 if !initial_path.exists() {
71 return initial_path;
72 }
73
74 for i in 1..1000 {
75 let path = base_path.join(format!("{name}{i}{ext}"));
76 if !path.exists() {
77 debug!(
78 "Found writable path `{}`",
79 path.file_name().unwrap_or_default().to_string_lossy()
80 );
81 return path;
82 }
83 }
84 panic!("TOO MANY ITEMS OF SAME NAME!");
85}
86
87pub fn add_item(cmd: &str, name: Option<&str>, stdout: Option<&str>, stderr: Option<&str>) {
89 if cfg!(target_os = "linux") && (stdout.is_some() || stderr.is_some()) {
90 warn!("--stdout and --stderr are not supported for linux startup scripts");
91 }
92
93 let path = if let Some(name) = name {
94 find_writable_path(name)
95 } else {
96 find_writable_path(extract_name_from_cmd(cmd))
97 };
98
99 fs::write(&path, utils::format(cmd, None, stdout, stderr))
100 .expect("Failed to write config file");
101
102 info!("Added `{}` to `{}`", cmd, path.display());
103
104 #[cfg(target_os = "linux")]
106 {
107 exec("systemctl daemon-reload --user").expect("daemon reloading error");
108 exec(
109 format!(
110 "systemctl enable {} --user",
111 path.file_name().unwrap().to_string_lossy()
112 )
113 .as_str(),
114 )
115 .expect("daemon enabling error");
116 }
117}
118
119pub fn get_items_list() -> Vec<(String, String)> {
126 let config_path = utils::CONFIG_PATH.as_os_str();
127 debug!(
128 "Finding config files in `{}` with extension `{}`",
129 config_path.to_string_lossy(),
130 utils::FILE_EXT
131 );
132 let mut res = vec![];
133
134 for entry in fs::read_dir(config_path)
135 .expect("Failed to read config directory")
136 .flatten()
137 {
138 let path = entry.path();
139 if path
140 .extension()
141 .is_some_and(|ext| ext == utils::FILE_EXT.trim_start_matches('.'))
142 && let Ok(first_line) = read_first_line(&path)
143 {
144 let id = path.file_stem().unwrap().to_string_lossy().into_owned();
145 if first_line.starts_with(utils::COMMENT_PREFIX) {
146 let command = first_line.trim_start_matches(utils::COMMENT_PREFIX).trim();
147 res.push((id, command.to_string()));
148 }
149 }
150 }
151 res
152}
153
154pub fn remove_items(ids: Vec<String>) {
156 for id in ids {
157 let path = utils::CONFIG_PATH.join(format!("{}{}", id, utils::FILE_EXT));
158 if path.exists() {
159 #[cfg(target_os = "linux")]
161 {
162 exec(
163 format!(
164 "systemctl disable {} --user",
165 path.file_name().unwrap().to_string_lossy()
166 )
167 .as_str(),
168 )
169 .expect("daemon disabling error");
170 }
171 fs::remove_file(&path)
172 .unwrap_or_else(|e| panic!("Failed to remove file `{}`: {}", path.display(), e));
173 info!("Removed id `{id}`");
174 } else {
175 error!("Config file id `{id}` not found");
176 }
177 }
178}
179
180pub fn open_config_folder() {
182 Command::new(utils::OPEN_COMMAND)
183 .arg(utils::CONFIG_PATH.as_os_str())
184 .spawn()
185 .expect("Failed to open config folder")
186 .wait()
187 .expect("Failed to open config folder");
188}
189
190#[cfg(test)]
191mod tests {
192 #[cfg(windows)]
193 use super::*;
194
195 #[test]
196 #[cfg(windows)]
197 fn test_find_writable_path() {
198 let path = find_writable_path("test");
199 assert_eq!(path, utils::CONFIG_PATH.join("test.cmd"));
200 add_item("test", None, None, None);
201 let path = find_writable_path("test");
202 assert_eq!(path, utils::CONFIG_PATH.join("test1.cmd"));
203 remove_items(vec!["test".to_string()]);
204 }
205}