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