Skip to main content

macron_path/
lib.rs

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