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 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}