1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4};
5
6pub const BACKUP_EXT: &str = "dotrbak";
7pub const SYMLINK_FOLDER: &str = "deployed";
8
9pub fn resolve_path(path: &str, cwd: &Path) -> anyhow::Result<PathBuf> {
14 if path.starts_with('/') {
15 Ok(PathBuf::from(path))
16 } else if path.starts_with("~") {
17 let home_dir = std::env::var("HOME")
18 .map(PathBuf::from)
19 .map_err(|_| anyhow::anyhow!("HOME environment variable not set"))?;
20 let p = path.splitn(2, '/').collect::<Vec<&str>>();
21 Ok(home_dir.join(p[1..].join("/")))
22 } else {
23 let p = cwd.join(path);
24 Ok(std::path::absolute(&p)?)
25 }
26}
27
28pub fn normalize_home_path(path: &str) -> String {
32 if path.starts_with('~') {
33 return path.to_string();
34 }
35
36 if let Ok(home_dir) = std::env::var("HOME").map(PathBuf::from) {
37 let home_str = home_dir.to_string_lossy();
38
39 if path == home_str.as_ref() {
40 return "~".to_string();
41 }
42
43 let home_with_slash = format!("{}/", home_str);
44 if path.starts_with(&home_with_slash) {
45 let relative = &path[home_str.len()..];
46 return format!("~{}", relative);
47 }
48 }
49
50 path.to_string()
51}
52
53pub const COLOR_WARNING: &str = "\x1b[33m"; pub const COLOR_ERROR: &str = "\x1b[31m"; pub const COLOR_INFO: &str = "\x1b[34m"; pub const COLOR_FATAL: &str = "\x1b[35m"; pub const RESET_COLOR: &str = "\x1b[0m"; pub enum LogLevel {
60 Warning,
61 Error,
62 Info,
63 Fatal,
64}
65
66impl LogLevel {
67 pub fn as_str(&self) -> &str {
68 match self {
69 LogLevel::Warning => "WARNING",
70 LogLevel::Error => "ERROR",
71 LogLevel::Info => "INFO",
72 LogLevel::Fatal => "FATAL",
73 }
74 }
75
76 pub fn to_colorful_str(&self) -> String {
77 match self {
78 LogLevel::Warning => format!("{}[{}]{}", COLOR_WARNING, self.as_str(), RESET_COLOR),
79 LogLevel::Error => format!("{}[{}]{}", COLOR_ERROR, self.as_str(), RESET_COLOR),
80 LogLevel::Info => format!("{}[{}]{}", COLOR_INFO, self.as_str(), RESET_COLOR),
81 LogLevel::Fatal => format!("{}[{}]{}", COLOR_FATAL, self.as_str(), RESET_COLOR),
82 }
83 }
84}
85
86pub fn cprintln(message: &str, level: &LogLevel) {
87 match level {
88 LogLevel::Error | LogLevel::Fatal => {
89 eprintln!("{} {}", level.to_colorful_str(), message);
90 }
91 LogLevel::Warning | LogLevel::Info => {
92 println!("{} {}", level.to_colorful_str(), message);
93 }
94 }
95}
96
97pub fn get_string_from_value(v: Option<&toml::Value>, field_name: &str) -> anyhow::Result<String> {
98 Ok(
99 v.ok_or_else(|| anyhow::anyhow!("'{}' is required", field_name))?
100 .as_str()
101 .ok_or_else(|| anyhow::anyhow!("'{}' must be a string", field_name))?
102 .to_string(),
103 )
104}
105
106pub fn get_string_hashmap_from_value(
107 v: Option<&toml::Value>,
108) -> anyhow::Result<HashMap<String, String>> {
109 match v {
110 Some(value) => value
111 .as_table()
112 .ok_or_else(|| anyhow::anyhow!("field must be a table"))?
113 .iter()
114 .map(|(key, value)| {
115 let s = value
116 .as_str()
117 .ok_or_else(|| anyhow::anyhow!("table values must be strings"))?;
118 Ok((key.clone(), s.to_string()))
119 })
120 .collect::<Result<HashMap<_, _>, _>>(),
121 None => Ok(HashMap::new()),
122 }
123}
124
125pub fn get_vec_string_from_value(v: Option<&toml::Value>) -> anyhow::Result<Vec<String>> {
126 match v {
127 Some(block) => block
128 .as_array()
129 .ok_or_else(|| anyhow::anyhow!("field must be an array"))?
130 .iter()
131 .map(|v| {
132 v.as_str()
133 .ok_or_else(|| anyhow::anyhow!("array elements must be strings"))
134 .map(|s| s.to_string())
135 })
136 .collect::<Result<Vec<_>, _>>(),
137 None => Ok(Vec::new()),
138 }
139}
140
141pub fn is_empty_table(t: &toml::Table) -> bool {
142 t.is_empty()
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use std::path::PathBuf;
149
150 #[test]
151 fn test_resolve_path_absolute() {
152 let cwd = PathBuf::from("/some/cwd");
153 let path = "/absolute/path";
154 let resolved = resolve_path(path, &cwd).expect("Failed to resolve path");
155 assert_eq!(resolved, PathBuf::from("/absolute/path"));
156 }
157
158 #[test]
159 fn test_resolve_path_with_tilde() {
160 let cwd = PathBuf::from("/some/cwd");
161 let home = std::env::home_dir().expect("Failed to get home directory");
162
163 let path = "~/Documents";
165 let resolved = resolve_path(path, &cwd).unwrap();
166 assert_eq!(resolved, home.join("Documents"));
167
168 let path = "~";
170 let resolved = resolve_path(path, &cwd).unwrap();
171 assert_eq!(resolved, home);
172 }
173
174 #[test]
175 fn test_resolve_path_relative() {
176 let cwd = PathBuf::from("/some/cwd");
177 let path = "relative/path";
178 let resolved = resolve_path(path, &cwd).unwrap();
179
180 assert!(resolved.is_absolute());
182 assert!(resolved.ends_with("relative/path"));
183 }
184
185 #[test]
186 fn test_resolve_path_dot_relative() {
187 let cwd = PathBuf::from("/some/cwd");
188 let path = "./file.txt";
189 let resolved = resolve_path(path, &cwd).unwrap();
190
191 assert!(resolved.is_absolute());
192 assert!(resolved.ends_with("file.txt"));
193 }
194
195 #[test]
196 fn test_resolve_path_parent_relative() {
197 let cwd = PathBuf::from("/some/cwd/subdir");
198 let path = "../file.txt";
199 let resolved = resolve_path(path, &cwd).unwrap();
200
201 assert!(resolved.is_absolute());
202 }
203
204 #[test]
205 fn test_normalize_home_path_already_normalized() {
206 let path = "~/.config/nvim";
207 let normalized = normalize_home_path(path);
208 assert_eq!(normalized, "~/.config/nvim");
209 }
210
211 #[test]
212 fn test_normalize_home_path_in_home_directory() {
213 let home = std::env::home_dir().expect("Failed to get home directory");
214 let home_str = home.to_string_lossy();
215
216 let path = format!("{}/.config/nvim", home_str);
218 let normalized = normalize_home_path(&path);
219 assert_eq!(normalized, "~/.config/nvim");
220 }
221
222 #[test]
223 fn test_normalize_home_path_home_root() {
224 let home = std::env::home_dir().expect("Failed to get home directory");
225 let home_str = home.to_string_lossy().to_string();
226
227 let normalized = normalize_home_path(&home_str);
229 assert_eq!(normalized, "~");
230 }
231
232 #[test]
233 fn test_normalize_home_path_outside_home() {
234 let path = "/etc/config";
235 let normalized = normalize_home_path(path);
236 assert_eq!(normalized, "/etc/config");
237
238 let path = "/tmp/test";
239 let normalized = normalize_home_path(path);
240 assert_eq!(normalized, "/tmp/test");
241 }
242
243 #[test]
244 fn test_normalize_home_path_with_trailing_slash() {
245 let home = std::env::home_dir().expect("Failed to get home directory");
246 let home_str = home.to_string_lossy();
247
248 let path = format!("{}/.config/", home_str);
249 let normalized = normalize_home_path(&path);
250 assert_eq!(normalized, "~/.config/");
251 }
252
253 #[test]
254 fn test_normalize_home_path_deep_nested() {
255 let home = std::env::home_dir().expect("Failed to get home directory");
256 let home_str = home.to_string_lossy();
257
258 let path = format!("{}/a/b/c/d/e/f", home_str);
259 let normalized = normalize_home_path(&path);
260 assert_eq!(normalized, "~/a/b/c/d/e/f");
261 }
262
263 #[test]
264 fn test_backup_ext_constant() {
265 assert_eq!(BACKUP_EXT, "dotrbak");
266 }
267
268 #[test]
269 fn test_resolve_path_empty_relative() {
270 let cwd = PathBuf::from("/some/cwd");
271 let path = "";
272 let resolved = resolve_path(path, &cwd).unwrap();
273
274 assert!(resolved.is_absolute());
275 }
276
277 #[test]
278 fn test_normalize_home_path_similar_prefix() {
279 let home = std::env::home_dir().expect("Failed to get home directory");
281 let home_str = home.to_string_lossy();
282
283 let fake_path = format!("{}_fake/config", home_str);
285 let normalized = normalize_home_path(&fake_path);
286 assert_eq!(normalized, fake_path);
288 }
289
290 #[test]
291 fn test_resolve_and_normalize_round_trip() {
292 let cwd = PathBuf::from("/some/cwd");
293 let home = std::env::home_dir().expect("Failed to get home directory");
294
295 let original = "~/.bashrc";
297
298 let resolved = resolve_path(original, &cwd).unwrap();
300 assert_eq!(resolved, home.join(".bashrc"));
301
302 let normalized = normalize_home_path(resolved.to_string_lossy().as_ref());
304 assert_eq!(normalized, original);
305 }
306
307 #[test]
308 fn test_normalize_home_path_with_spaces() {
309 let home = std::env::home_dir().expect("Failed to get home directory");
310 let home_str = home.to_string_lossy();
311
312 let path = format!("{}/My Documents/file.txt", home_str);
313 let normalized = normalize_home_path(&path);
314 assert_eq!(normalized, "~/My Documents/file.txt");
315 }
316
317 #[test]
318 fn test_normalize_home_path_with_dots() {
319 let home = std::env::home_dir().expect("Failed to get home directory");
320 let home_str = home.to_string_lossy();
321
322 let path = format!("{}/.config/.hidden/..dotfile", home_str);
323 let normalized = normalize_home_path(&path);
324 assert_eq!(normalized, "~/.config/.hidden/..dotfile");
325 }
326}