1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6
7#[proc_macro]
9pub fn path(input: TokenStream) -> TokenStream {
10 if input.is_empty() {
12 return quote! { ::std::path::PathBuf::new() }.into();
13 }
14
15 let Format { expr, args } = syn::parse_macro_input!(input as Format);
16
17 if args.is_none() {
18 if let syn::Expr::Lit(syn::ExprLit {
19 lit: syn::Lit::Str(lit_str),
20 ..
21 }) = &expr
22 {
23 if let Some(static_path_tokens) = try_parse_static_path(&lit_str.value()) {
24 return static_path_tokens.into();
25 }
26 }
27 }
28
29 let raw_expr = if let syn::Expr::Lit(syn::ExprLit {
30 lit: syn::Lit::Str(lit_str),
31 ..
32 }) = &expr
33 {
34 if let Some(args) = args {
35 quote! { ::std::format!(#lit_str #args) }
36 } else {
37 quote! { ::std::format!(#lit_str) }
38 }
39 } else {
40 quote! { (#expr).to_string() }
41 };
42
43 quote! {{
44 let path_raw = #raw_expr;
45 let path_str: &str = path_raw.as_ref();
46
47 if path_str.starts_with('$') || path_str.starts_with('~') {
48 let mut path_normalized = path_str.replace("\\", "/");
49
50 if path_normalized == "~" {
51 path_normalized = "$home".to_string();
52 } else if path_normalized.starts_with("~/") {
53 path_normalized = ::std::format!("$home/{}", &path_normalized[2..]);
54 }
55
56 if path_normalized == "$" {
57 ::std::env::current_exe().expect("Failed to get executable path")
58 } else if path_normalized.starts_with("$/") {
59 let rest = &path_normalized[2..];
60 let mut p = ::std::env::current_exe()
61 .expect("Failed to get executable path")
62 .parent()
63 .map(::std::path::PathBuf::from)
64 .expect("Failed to get executable directory");
65 if !rest.is_empty() {
66 p.push(rest);
67 }
68 p
69 } else {
70 let token_end = path_normalized.find('/').unwrap_or(path_normalized.len());
71 let token = &path_normalized[1..token_end];
72 let rest = if token_end < path_normalized.len() { &path_normalized[token_end + 1..] } else { "" };
73
74 let (key, has_app) = if token.ends_with('$') {
75 (&token[..token.len() - 1], true)
76 } else {
77 (token, false)
78 };
79
80 let mut base = match key {
81 "home" => {
82 #[cfg(target_os = "windows")] {
83 let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
84 if has_app { p.push(APP_NAME); }
85 p
86 }
87 #[cfg(not(target_os = "windows"))] {
88 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
89 if has_app { p.push(::std::format!(".{}", APP_NAME)); }
90 p
91 }
92 }
93 "ssh" => {
94 #[cfg(target_os = "windows")] {
95 let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
96 p.push(".ssh");
97 if has_app { p.push(APP_NAME); }
98 p
99 }
100 #[cfg(target_os = "macos")] {
101 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
102 p.push(".ssh");
103 if has_app { p.push(APP_NAME); }
104 p
105 }
106 #[cfg(all(unix, not(target_os = "macos")))] {
107 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
108 p.push(".ssh");
109 if has_app { p.push(::std::format!(".{}", APP_NAME)); }
110 p
111 }
112 }
113 "config" => {
114 #[cfg(target_os = "windows")] {
115 let mut p = ::std::env::var("APPDATA").map(::std::path::PathBuf::from).expect("APPDATA not found");
116 if has_app { p.push(APP_NAME); p.push("config"); }
117 p
118 }
119 #[cfg(target_os = "macos")] {
120 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
121 if has_app { p.push(APP_NAME); }
122 p
123 }
124 #[cfg(all(unix, not(target_os = "macos")))] {
125 let mut p = ::std::env::var("XDG_CONFIG_HOME").map(::std::path::PathBuf::from)
126 .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".config")))
127 .expect("Failed to resolve config directory");
128 if has_app { p.push(APP_NAME); }
129 p
130 }
131 }
132 "share" => {
133 #[cfg(target_os = "windows")] {
134 let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
135 if has_app { p.push(APP_NAME); p.push("data"); }
136 p
137 }
138 #[cfg(target_os = "macos")] {
139 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
140 if has_app { p.push(APP_NAME); }
141 p
142 }
143 #[cfg(all(unix, not(target_os = "macos")))] {
144 let mut p = ::std::env::var("XDG_DATA_HOME").map(::std::path::PathBuf::from)
145 .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/share")))
146 .expect("Failed to resolve data directory");
147 if has_app { p.push(APP_NAME); }
148 p
149 }
150 }
151 "cache" => {
152 #[cfg(target_os = "windows")] {
153 let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
154 if has_app { p.push(APP_NAME); p.push("cache"); }
155 p
156 }
157 #[cfg(target_os = "macos")] {
158 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Caches");
159 if has_app { p.push(APP_NAME); }
160 p
161 }
162 #[cfg(all(unix, not(target_os = "macos")))] {
163 let mut p = ::std::env::var("XDG_CACHE_HOME").map(::std::path::PathBuf::from)
164 .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".cache")))
165 .expect("Failed to resolve cache directory");
166 if has_app { p.push(APP_NAME); }
167 p
168 }
169 }
170 "temp" => {
171 let mut p = ::std::env::temp_dir();
172 if has_app { p.push(APP_NAME); }
173 p
174 }
175 "global" => {
176 #[cfg(target_os = "windows")] {
177 let global_p = ::std::env::var("PROGRAMFILES").map(::std::path::PathBuf::from).expect("PROGRAMFILES not found");
178 let local_p = ::std::env::var("LOCALAPPDATA").map(|l| ::std::path::PathBuf::from(l).join("Programs")).expect("LOCALAPPDATA not found");
179 if has_app {
180 let mut p = global_p.join(APP_NAME);
181 if p.exists() { p } else { local_p.join(APP_NAME) }
182 } else { global_p }
183 }
184 #[cfg(target_os = "macos")] {
185 let global_p = ::std::path::PathBuf::from("/Applications");
186 let local_p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join("Applications")).expect("HOME not found");
187 if has_app {
188 let mut p = global_p.join(APP_NAME);
189 if p.exists() || global_p.join(::std::format!("{}.app", APP_NAME)).exists() { p } else { local_p.join(APP_NAME) }
190 } else { global_p }
191 }
192 #[cfg(all(unix, not(target_os = "macos")))] {
193 let global_p = ::std::path::PathBuf::from("/opt");
194 let local_p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/opt")).expect("HOME not found");
195 if has_app {
196 let mut p = global_p.join(APP_NAME);
197 if p.exists() { p } else { local_p.join(APP_NAME) }
198 } else { global_p }
199 }
200 }
201 "local" => {
202 #[cfg(target_os = "windows")] {
203 let mut p = ::std::env::var("LOCALAPPDATA").map(|l| ::std::path::PathBuf::from(l).join("Programs")).expect("LOCALAPPDATA not found");
204 if has_app { p.push(APP_NAME); }
205 p
206 }
207 #[cfg(target_os = "macos")] {
208 let mut p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join("Applications")).expect("HOME not found");
209 if has_app { p.push(APP_NAME); }
210 p
211 }
212 #[cfg(all(unix, not(target_os = "macos")))] {
213 let mut p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/opt")).expect("HOME not found");
214 if has_app { p.push(APP_NAME); }
215 p
216 }
217 }
218 "downloads" | "documents" | "music" | "pictures" => {
219 let dir_name = match key {
220 "downloads" => "Downloads",
221 "documents" => "Documents",
222 "music" => "Music",
223 "pictures" => "Pictures",
224 _ => unreachable!(),
225 };
226 let mut p = {
227 #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
228 #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
229 };
230 p.push(dir_name);
231 if has_app { p.push(APP_NAME); }
232 p
233 }
234 "videos" => {
235 let mut p = {
236 #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
237 #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
238 };
239 #[cfg(target_os = "macos")] { p.push("Movies"); }
240 #[cfg(not(target_os = "macos"))] { p.push("Videos"); }
241 if has_app { p.push(APP_NAME); }
242 p
243 }
244 _ => panic!("Unknown path prefix: ${}", key),
245 };
246
247 if !rest.is_empty() {
248 base.push(rest);
249 }
250 base
251 }
252 } else {
253 ::std::path::PathBuf::from(path_str)
254 }
255 }}.into()
256}
257
258fn try_parse_static_path(path_str: &str) -> Option<TokenStream2> {
259 if !path_str.starts_with('$') && !path_str.starts_with('~') {
260 return Some(quote! { ::std::path::PathBuf::from(#path_str) });
261 }
262
263 let mut path_normalized = path_str.replace("\\", "/");
264
265 if path_normalized == "~" {
266 path_normalized = "$home".to_string();
267 } else if path_normalized.starts_with("~/") {
268 path_normalized = ::std::format!("$home/{}", &path_normalized[2..]);
269 }
270
271 if path_normalized == "$" {
272 return Some(quote! { ::std::env::current_exe().expect("Failed to get executable path") });
273 } else if path_normalized.starts_with("$/") {
274 let rest = path_normalized[2..].to_string();
275 return Some(quote! {{
276 let mut p = ::std::env::current_exe()
277 .expect("Failed to get executable path")
278 .parent()
279 .map(::std::path::PathBuf::from)
280 .expect("Failed to get executable directory");
281 if !#rest.is_empty() {
282 p.push(#rest);
283 }
284 p
285 }});
286 }
287
288 let token_end = path_normalized.find('/').unwrap_or(path_normalized.len());
289 let token = &path_normalized[1..token_end];
290 let rest = if token_end < path_normalized.len() {
291 &path_normalized[token_end + 1..]
292 } else {
293 ""
294 };
295
296 let (key, has_app) = if token.ends_with('$') {
297 (&token[..token.len() - 1], true)
298 } else {
299 (token, false)
300 };
301
302 let base_tokens = match key {
303 "home" => quote! {
304 #[cfg(target_os = "windows")] {
305 let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
306 if #has_app { p.push(APP_NAME); }
307 p
308 }
309 #[cfg(not(target_os = "windows"))] {
310 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
311 if #has_app { p.push(::std::format!(".{}", APP_NAME)); }
312 p
313 }
314 },
315 "ssh" => quote! {
316 #[cfg(target_os = "windows")] {
317 let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
318 p.push(".ssh");
319 if #has_app { p.push(APP_NAME); }
320 p
321 }
322 #[cfg(target_os = "macos")] {
323 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
324 p.push(".ssh");
325 if #has_app { p.push(APP_NAME); }
326 p
327 }
328 #[cfg(all(unix, not(target_os = "macos")))] {
329 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
330 p.push(".ssh");
331 if #has_app { p.push(::std::format!(".{}", APP_NAME)); }
332 p
333 }
334 },
335 "config" => quote! {
336 #[cfg(target_os = "windows")] {
337 let mut p = ::std::env::var("APPDATA").map(::std::path::PathBuf::from).expect("APPDATA not found");
338 if #has_app { p.push(APP_NAME); p.push("config"); }
339 p
340 }
341 #[cfg(target_os = "macos")] {
342 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
343 if #has_app { p.push(APP_NAME); }
344 p
345 }
346 #[cfg(all(unix, not(target_os = "macos")))] {
347 let mut p = ::std::env::var("XDG_CONFIG_HOME").map(::std::path::PathBuf::from)
348 .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".config")))
349 .expect("Failed to resolve config directory");
350 if #has_app { p.push(APP_NAME); }
351 p
352 }
353 },
354 "share" => quote! {
355 #[cfg(target_os = "windows")] {
356 let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
357 if #has_app { p.push(APP_NAME); p.push("data"); }
358 p
359 }
360 #[cfg(target_os = "macos")] {
361 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
362 if #has_app { p.push(APP_NAME); }
363 p
364 }
365 #[cfg(all(unix, not(target_os = "macos")))] {
366 let mut p = ::std::env::var("XDG_DATA_HOME").map(::std::path::PathBuf::from)
367 .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/share")))
368 .expect("Failed to resolve data directory");
369 if #has_app { p.push(APP_NAME); }
370 p
371 }
372 },
373 "cache" => quote! {
374 #[cfg(target_os = "windows")] {
375 let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
376 if #has_app { p.push(APP_NAME); p.push("cache"); }
377 p
378 }
379 #[cfg(target_os = "macos")] {
380 let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Caches");
381 if #has_app { p.push(APP_NAME); }
382 p
383 }
384 #[cfg(all(unix, not(target_os = "macos")))] {
385 let mut p = ::std::env::var("XDG_CACHE_HOME").map(::std::path::PathBuf::from)
386 .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".cache")))
387 .expect("Failed to resolve cache directory");
388 if #has_app { p.push(APP_NAME); }
389 p
390 }
391 },
392 "temp" => quote! {{
393 let mut p = ::std::env::temp_dir();
394 if #has_app { p.push(APP_NAME); }
395 p
396 }},
397 "global" => quote! {
398 #[cfg(target_os = "windows")] {
399 let global_p = ::std::env::var("PROGRAMFILES").map(::std::path::PathBuf::from).expect("PROGRAMFILES not found");
400 let local_p = ::std::env::var("LOCALAPPDATA").map(|l| ::std::path::PathBuf::from(l).join("Programs")).expect("LOCALAPPDATA not found");
401 if #has_app {
402 let mut p = global_p.join(APP_NAME);
403 if p.exists() { p } else { local_p.join(APP_NAME) }
404 } else { global_p }
405 }
406 #[cfg(target_os = "macos")] {
407 let global_p = ::std::path::PathBuf::from("/Applications");
408 let local_p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join("Applications")).expect("HOME not found");
409 if #has_app {
410 let mut p = global_p.join(APP_NAME);
411 if p.exists() || global_p.join(::std::format!("{}.app", APP_NAME)).exists() { p } else { local_p.join(APP_NAME) }
412 } else { global_p }
413 }
414 #[cfg(all(unix, not(target_os = "macos")))] {
415 let global_p = ::std::path::PathBuf::from("/opt");
416 let local_p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/opt")).expect("HOME not found");
417 if #has_app {
418 let mut p = global_p.join(APP_NAME);
419 if p.exists() { p } else { local_p.join(APP_NAME) }
420 } else { global_p }
421 }
422 },
423 "local" => quote! {
424 #[cfg(target_os = "windows")] {
425 let mut p = ::std::env::var("LOCALAPPDATA").map(|l| ::std::path::PathBuf::from(l).join("Programs")).expect("LOCALAPPDATA not found");
426 if #has_app { p.push(APP_NAME); }
427 p
428 }
429 #[cfg(target_os = "macos")] {
430 let mut p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join("Applications")).expect("HOME not found");
431 if #has_app { p.push(APP_NAME); }
432 p
433 }
434 #[cfg(all(unix, not(target_os = "macos")))] {
435 let mut p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/opt")).expect("HOME not found");
436 if #has_app { p.push(APP_NAME); }
437 p
438 }
439 },
440 "downloads" | "documents" | "music" | "pictures" => {
441 let dir_name = match key {
442 "downloads" => "Downloads",
443 "documents" => "Documents",
444 "music" => "Music",
445 "pictures" => "Pictures",
446 _ => unreachable!(),
447 };
448 quote! {{
449 let mut p = {
450 #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
451 #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
452 };
453 p.push(#dir_name);
454 if #has_app { p.push(APP_NAME); }
455 p
456 }}
457 }
458 "videos" => quote! {{
459 let mut p = {
460 #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
461 #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
462 };
463 #[cfg(target_os = "macos")] { p.push("Movies"); }
464 #[cfg(not(target_os = "macos"))] { p.push("Videos"); }
465 if #has_app { p.push(APP_NAME); }
466 p
467 }},
468 _ => panic!("Unknown path prefix: ${}", key),
469 };
470
471 let rest_str = rest.to_string();
472 Some(quote! {{
473 let mut base = { #base_tokens };
474 if !#rest_str.is_empty() {
475 base.push(#rest_str);
476 }
477 base
478 }})
479}
480
481struct Format {
482 pub expr: syn::Expr,
483 pub args: Option<TokenStream2>,
484}
485
486impl syn::parse::Parse for Format {
487 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
488 let expr = input.parse()?;
489 let args = if input.peek(syn::token::Comma) {
490 Some(input.parse()?)
491 } else {
492 None
493 };
494 Ok(Self { expr, args })
495 }
496}