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                    "state" => {
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("state"); }
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/Application Support");
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_STATE_HOME").map(::std::path::PathBuf::from)
146                                .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/state")))
147                                .expect("Failed to resolve state directory");
148                            if has_app { p.push(APP_NAME); }
149                            p
150                        }
151                    }
152                    "cache" => {
153                        #[cfg(target_os = "windows")] {
154                            let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
155                            if has_app { p.push(APP_NAME); p.push("cache"); }
156                            p
157                        }
158                        #[cfg(target_os = "macos")] {
159                            let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Caches");
160                            if has_app { p.push(APP_NAME); }
161                            p
162                        }
163                        #[cfg(all(unix, not(target_os = "macos")))] {
164                            let mut p = ::std::env::var("XDG_CACHE_HOME").map(::std::path::PathBuf::from)
165                                .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".cache")))
166                                .expect("Failed to resolve cache directory");
167                            if has_app { p.push(APP_NAME); }
168                            p
169                        }
170                    }
171                    "temp" => {
172                        let mut p = ::std::env::temp_dir();
173                        if has_app { p.push(APP_NAME); }
174                        p
175                    }
176                    "downloads" | "documents" | "music" | "pictures" => {
177                        let dir_name = match key {
178                            "downloads" => "Downloads",
179                            "documents" => "Documents",
180                            "music" => "Music",
181                            "pictures" => "Pictures",
182                            _ => unreachable!(),
183                        };
184                        let mut p = {
185                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
186                            #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
187                        };
188                        p.push(dir_name);
189                        if has_app { p.push(APP_NAME); }
190                        p
191                    }
192                    "videos" => {
193                        let mut p = {
194                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
195                            #[cfg(not(target_os = "macos"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
196                        };
197                        #[cfg(target_os = "macos")] { p.push("Movies"); }
198                        #[cfg(not(target_os = "macos"))] { p.push("Videos"); }
199                        if has_app { p.push(APP_NAME); }
200                        p
201                    }
202                    _ => panic!("Unknown path prefix: ${}", key),
203                };
204
205                if !rest.is_empty() {
206                    base.push(rest);
207                }
208                base
209            }
210        } else {
211            ::std::path::PathBuf::from(path_str)
212        }
213    }}.into()
214}
215
216fn parse_literal_path(lit_str: &syn::LitStr, args: Option<TokenStream2>) -> TokenStream2 {
217    let path_raw = lit_str.value();
218
219    let make_rest_expr = |rest_str: &str| {
220        let has_brackets = rest_str.contains(&['{', '}'][..]);
221        if args.is_some() {
222            quote! { ::std::format!(#rest_str #args) }
223        } else if has_brackets {
224            quote! { ::std::format!(#rest_str) }
225        } else {
226            quote! { #rest_str }
227        }
228    };
229
230    if !path_raw.starts_with('$') && !path_raw.starts_with('~') {
231        let expr = make_rest_expr(&path_raw);
232        return quote! { ::std::path::PathBuf::from(#expr) };
233    }
234
235    let mut path_normalized = path_raw.replace("\\", "/");
236
237    if path_normalized == "~" {
238        path_normalized = "$home".to_string();
239    } else if path_normalized.starts_with("~/") {
240        path_normalized = ::std::format!("$home/{}", &path_normalized[2..]);
241    }
242
243    if path_normalized == "$" {
244        return quote! { ::std::env::current_exe().expect("Failed to get executable path") };
245    } else if path_normalized.starts_with("$/") {
246        let rest_expr = make_rest_expr(&path_normalized[2..]);
247        return quote! {{
248            let mut p = ::std::env::current_exe()
249                .expect("Failed to get executable path")
250                .parent()
251                .map(::std::path::PathBuf::from)
252                .expect("Failed to get executable directory");
253            let rest = #rest_expr;
254            if !rest.is_empty() {
255                p.push(rest);
256            }
257            p
258        }};
259    }
260
261    let token_end = path_normalized.find('/').unwrap_or(path_normalized.len());
262    let token = &path_normalized[1..token_end];
263    let rest = if token_end < path_normalized.len() {
264        &path_normalized[token_end + 1..]
265    } else {
266        ""
267    };
268
269    let (key, has_app) = if token.ends_with('$') {
270        (&token[..token.len() - 1], true)
271    } else {
272        (token, false)
273    };
274
275    let app_push = if has_app {
276        quote! {
277            #[cfg(target_os = "windows")] { p.push(APP_NAME); }
278            #[cfg(not(target_os = "windows"))] { p.push(::std::format!(".{}", APP_NAME)); }
279        }
280    } else {
281        quote! {}
282    };
283
284    let base_tokens = match key {
285        "home" => quote! {
286            #[cfg(target_os = "windows")] {
287                let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
288                #app_push
289                p
290            }
291            #[cfg(not(target_os = "windows"))] {
292                let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
293                #app_push
294                p
295            }
296        },
297        "ssh" => quote! {
298            #[cfg(target_os = "windows")] {
299                let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
300                p.push(".ssh");
301                #app_push
302                p
303            }
304            #[cfg(target_os = "macos")] {
305                let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
306                p.push(".ssh");
307                #app_push
308                p
309            }
310            #[cfg(all(unix, not(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        },
317        "config" => quote! {
318            #[cfg(target_os = "windows")] {
319                let mut p = ::std::env::var("APPDATA").map(::std::path::PathBuf::from).expect("APPDATA not found");
320                if #has_app { p.push(APP_NAME); p.push("config"); }
321                p
322            }
323            #[cfg(target_os = "macos")] {
324                let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
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("XDG_CONFIG_HOME").map(::std::path::PathBuf::from)
330                    .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".config")))
331                    .expect("Failed to resolve config directory");
332                if #has_app { p.push(APP_NAME); }
333                p
334            }
335        },
336        "share" => quote! {
337            #[cfg(target_os = "windows")] {
338                let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
339                if #has_app { p.push(APP_NAME); p.push("data"); }
340                p
341            }
342            #[cfg(target_os = "macos")] {
343                let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
344                if #has_app { p.push(APP_NAME); }
345                p
346            }
347            #[cfg(all(unix, not(target_os = "macos")))] {
348                let mut p = ::std::env::var("XDG_DATA_HOME").map(::std::path::PathBuf::from)
349                    .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/share")))
350                    .expect("Failed to resolve data directory");
351                if #has_app { p.push(APP_NAME); }
352                p
353            }
354        },
355        "state" => quote! {
356            #[cfg(target_os = "windows")] {
357                let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
358                if #has_app { p.push(APP_NAME); p.push("state"); }
359                p
360            }
361            #[cfg(target_os = "macos")] {
362                let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Application Support");
363                if #has_app { p.push(APP_NAME); }
364                p
365            }
366            #[cfg(all(unix, not(target_os = "macos")))] {
367                let mut p = ::std::env::var("XDG_STATE_HOME").map(::std::path::PathBuf::from)
368                    .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/state")))
369                    .expect("Failed to resolve state directory");
370                if #has_app { p.push(APP_NAME); }
371                p
372            }
373        },
374        "cache" => quote! {
375            #[cfg(target_os = "windows")] {
376                let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
377                if #has_app { p.push(APP_NAME); p.push("cache"); }
378                p
379            }
380            #[cfg(target_os = "macos")] {
381                let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Caches");
382                if #has_app { p.push(APP_NAME); }
383                p
384            }
385            #[cfg(all(unix, not(target_os = "macos")))] {
386                let mut p = ::std::env::var("XDG_CACHE_HOME").map(::std::path::PathBuf::from)
387                    .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".cache")))
388                    .expect("Failed to resolve cache directory");
389                if #has_app { p.push(APP_NAME); }
390                p
391            }
392        },
393        "temp" => quote! {{
394            let mut p = ::std::env::temp_dir();
395            if #has_app { p.push(APP_NAME); }
396            p
397        }},
398        "downloads" | "documents" | "music" | "pictures" => {
399            let dir_name = match key {
400                "downloads" => "Downloads",
401                "documents" => "Documents",
402                "music" => "Music",
403                "pictures" => "Pictures",
404                _ => unreachable!(),
405            };
406            quote! {{
407                let mut p = {
408                    #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
409                    #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
410                };
411                p.push(#dir_name);
412                if #has_app { p.push(APP_NAME); }
413                p
414            }}
415        }
416        "videos" => quote! {{
417            let mut p = {
418                #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
419                #[cfg(not(target_os = "macos"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
420            };
421            #[cfg(target_os = "macos")] { p.push("Movies"); }
422            #[cfg(not(target_os = "macos"))] { p.push("Videos"); }
423            if #has_app { p.push(APP_NAME); }
424            p
425        }},
426        _ => panic!("Unknown path prefix: ${}", key),
427    };
428
429    let rest_expr = make_rest_expr(rest);
430    quote! {{
431        let mut base = { #base_tokens };
432        let rest = #rest_expr;
433        if !rest.is_empty() {
434            base.push(rest);
435        }
436        base
437    }}
438}
439
440struct Format {
441    pub expr: syn::Expr,
442    pub args: Option<TokenStream2>,
443}
444
445impl syn::parse::Parse for Format {
446    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
447        let expr = input.parse()?;
448        let args = if input.peek(syn::token::Comma) {
449            Some(input.parse()?)
450        } else {
451            None
452        };
453        Ok(Self { expr, args })
454    }
455}