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    // prepare the raw string expression: either a formatted literal or a passed expression
18    let raw_expr = if let syn::Expr::Lit(syn::ExprLit {
19        lit: syn::Lit::Str(lit_str),
20        ..
21    }) = &expr
22    {
23        if let Some(args) = args {
24            quote! { ::std::format!(#lit_str #args) }
25        } else {
26            quote! { ::std::format!(#lit_str) }
27        }
28    } else {
29        quote! { (#expr).to_string() }
30    };
31
32    // unified cross-platform path resolution engine
33    quote! {{
34        let path_raw = #raw_expr;
35        let path_str: &str = path_raw.as_ref();
36
37        if path_str.starts_with('$') || path_str.starts_with('~') {
38            let mut path_normalized = path_str.replace("\\", "/");
39
40            // handle tilde home directory aliases right away
41            if path_normalized == "~" {
42                path_normalized = "$home".to_string();
43            } else if path_normalized.starts_with("~/") {
44                path_normalized = ::std::format!("$home/{}", &path_normalized[2..]);
45            }
46
47            if path_normalized == "$" {
48                // return current executable path
49                ::std::env::current_exe().expect("Failed to get executable path")
50            } else if path_normalized.starts_with("$/") {
51                // return parent directory of the executable
52                let rest = &path_normalized[2..];
53                let mut p = ::std::env::current_exe()
54                    .expect("Failed to get executable path")
55                    .parent()
56                    .map(::std::path::PathBuf::from)
57                    .expect("Failed to get executable directory");
58                if !rest.is_empty() {
59                    p.push(rest);
60                }
61                p
62            } else {
63                // parse the prefix token (e.g., "$config$/settings.toml" -> "config$")
64                let token_end = path_normalized.find('/').unwrap_or(path_normalized.len());
65                let token = &path_normalized[1..token_end];
66                let rest = if token_end < path_normalized.len() { &path_normalized[token_end + 1..] } else { "" };
67
68                let (key, has_app) = if token.ends_with('$') {
69                    (&token[..token.len() - 1], true)
70                } else {
71                    (token, false)
72                };
73
74                let mut base = match key {
75                    // Linux: /home/user | /home/user/.ovsy
76                    // macOS: /Users/user | /Users/user/.ovsy
77                    // Windows: C:\Users\user | C:\Users\user\ovsy
78                    "home" => {
79                        #[cfg(target_os = "windows")] {
80                            let mut p = ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found");
81                            if has_app { p.push(APP_NAME); }
82                            p
83                        }
84                        #[cfg(not(target_os = "windows"))] {
85                            let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found");
86                            if has_app { p.push(::std::format!(".{}", APP_NAME)); }
87                            p
88                        }
89                    }
90
91                    // Linux: ~/.config | ~/.config/ovsy
92                    // macOS: ~/Library/Application Support | ~/Library/Application Support/ovsy
93                    // Windows: %APPDATA% | %APPDATA%\ovsy\config
94                    "config" => {
95                        #[cfg(target_os = "windows")] {
96                            let mut p = ::std::env::var("APPDATA").map(::std::path::PathBuf::from).expect("APPDATA not found");
97                            if has_app { p.push(APP_NAME); p.push("config"); }
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").join("Library/Application Support");
102                            if has_app { p.push(APP_NAME); }
103                            p
104                        }
105                        #[cfg(all(unix, not(target_os = "macos")))] {
106                            let mut p = ::std::env::var("XDG_CONFIG_HOME").map(::std::path::PathBuf::from)
107                                .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".config")))
108                                .expect("Failed to resolve config directory");
109                            if has_app { p.push(APP_NAME); }
110                            p
111                        }
112                    }
113
114                    // Linux: ~/.local/share | ~/.local/share/ovsy
115                    // macOS: ~/Library/Application Support | ~/Library/Application Support/ovsy
116                    // Windows: %LOCALAPPDATA% | %LOCALAPPDATA%\ovsy\data
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(APP_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(APP_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(APP_NAME); }
133                            p
134                        }
135                    }
136
137                    // Linux: ~/.cache | ~/.cache/ovsy
138                    // macOS: ~/Library/Caches | ~/Library/Caches/ovsy
139                    // Windows: %LOCALAPPDATA% | %LOCALAPPDATA%\ovsy\cache
140                    "cache" => {
141                        #[cfg(target_os = "windows")] {
142                            let mut p = ::std::env::var("LOCALAPPDATA").map(::std::path::PathBuf::from).expect("LOCALAPPDATA not found");
143                            if has_app { p.push(APP_NAME); p.push("cache"); }
144                            p
145                        }
146                        #[cfg(target_os = "macos")] {
147                            let mut p = ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found").join("Library/Caches");
148                            if has_app { p.push(APP_NAME); }
149                            p
150                        }
151                        #[cfg(all(unix, not(target_os = "macos")))] {
152                            let mut p = ::std::env::var("XDG_CACHE_HOME").map(::std::path::PathBuf::from)
153                                .or_else(|_| ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".cache")))
154                                .expect("Failed to resolve cache directory");
155                            if has_app { p.push(APP_NAME); }
156                            p
157                        }
158                    }
159
160                    // Linux/macOS: /tmp | /tmp/ovsy
161                    // Windows: %LOCALAPPDATA%\Temp | %LOCALAPPDATA%\Temp\ovsy
162                    "temp" => {
163                        let mut p = ::std::env::temp_dir();
164                        if has_app { p.push(APP_NAME); }
165                        p
166                    }
167
168                    // Linux: /opt | /opt/ovsy
169                    // macOS: /Applications | /Applications/ovsy
170                    // Windows: %PROGRAMFILES% | %PROGRAMFILES%\ovsy
171                    "global" => {
172                        #[cfg(target_os = "windows")] {
173                            let global_p = ::std::env::var("PROGRAMFILES").map(::std::path::PathBuf::from).expect("PROGRAMFILES not found");
174                            let local_p = ::std::env::var("LOCALAPPDATA").map(|l| ::std::path::PathBuf::from(l).join("Programs")).expect("LOCALAPPDATA not found");
175                            if has_app {
176                                let mut p = global_p.join(APP_NAME);
177                                if p.exists() { p } else { local_p.join(APP_NAME) }
178                            } else { global_p }
179                        }
180                        #[cfg(target_os = "macos")] {
181                            let global_p = ::std::path::PathBuf::from("/Applications");
182                            let local_p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join("Applications")).expect("HOME not found");
183                            if has_app {
184                                let mut p = global_p.join(APP_NAME);
185                                if p.exists() || global_p.join(::std::format!("{}.app", APP_NAME)).exists() { p } else { local_p.join(APP_NAME) }
186                            } else { global_p }
187                        }
188                        #[cfg(all(unix, not(target_os = "macos")))] {
189                            let global_p = ::std::path::PathBuf::from("/opt");
190                            let local_p = ::std::env::var("HOME").map(|h| ::std::path::PathBuf::from(h).join(".local/opt")).expect("HOME not found");
191                            if has_app {
192                                let mut p = global_p.join(APP_NAME);
193                                if p.exists() { p } else { local_p.join(APP_NAME) }
194                            } else { global_p }
195                        }
196                    }
197
198                    // Linux: ~/.local/opt | ~/.local/opt/ovsy
199                    // macOS: ~/Applications | ~/Applications/ovsy
200                    // Windows: %LOCALAPPDATA%\Programs | %LOCALAPPDATA%\Programs\ovsy
201                    "local" => {
202                        #[cfg(target_os = "windows")] {
203                            let mut p = ::std::env::var("LOCALAPPDATA")
204                                .map(|l| ::std::path::PathBuf::from(l).join("Programs"))
205                                .expect("LOCALAPPDATA not found");
206                            if has_app { p.push(APP_NAME); }
207                            p
208                        }
209                        #[cfg(target_os = "macos")] {
210                            let mut p = ::std::env::var("HOME")
211                                .map(|h| ::std::path::PathBuf::from(h).join("Applications"))
212                                .expect("HOME not found");
213                            if has_app { p.push(APP_NAME); }
214                            p
215                        }
216                        #[cfg(all(unix, not(target_os = "macos")))] {
217                            let mut p = ::std::env::var("HOME")
218                                .map(|h| ::std::path::PathBuf::from(h).join(".local/opt"))
219                                .expect("HOME not found");
220                            if has_app { p.push(APP_NAME); }
221                            p
222                        }
223                    }
224
225                    // Linux/macOS/Windows: ~/Downloads | ~/Downloads/ovsy
226                    "downloads" => {
227                        let mut p = {
228                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
229                            #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
230                        };
231                        p.push("Downloads");
232                        if has_app { p.push(APP_NAME); }
233                        p
234                    }
235
236                    // Linux/macOS/Windows: ~/Documents | ~/Documents/ovsy
237                    "documents" => {
238                        let mut p = {
239                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
240                            #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
241                        };
242                        p.push("Documents");
243                        if has_app { p.push(APP_NAME); }
244                        p
245                    }
246
247                    // Linux/macOS/Windows: ~/Music | ~/Music/ovsy
248                    "music" => {
249                        let mut p = {
250                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
251                            #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
252                        };
253                        p.push("Music");
254                        if has_app { p.push(APP_NAME); }
255                        p
256                    }
257
258                    // Linux/macOS/Windows: ~/Pictures | ~/Pictures/ovsy
259                    "pictures" => {
260                        let mut p = {
261                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
262                            #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
263                        };
264                        p.push("Pictures");
265                        if has_app { p.push(APP_NAME); }
266                        p
267                    }
268
269                    // Linux/Windows: ~/Videos | ~/Videos/ovsy
270                    // macOS: ~/Movies | ~/Movies/ovsy
271                    "videos" => {
272                        let mut p = {
273                            #[cfg(target_os = "windows")] { ::std::env::var("USERPROFILE").map(::std::path::PathBuf::from).expect("USERPROFILE not found") }
274                            #[cfg(not(target_os = "windows"))] { ::std::env::var("HOME").map(::std::path::PathBuf::from).expect("HOME not found") }
275                        };
276                        #[cfg(target_os = "macos")] { p.push("Movies"); }
277                        #[cfg(not(target_os = "macos"))] { p.push("Videos"); }
278                        if has_app { p.push(APP_NAME); }
279                        p
280                    }
281
282                    _ => panic!("Unknown path prefix: ${}", key),
283                };
284
285                if !rest.is_empty() {
286                    base.push(rest);
287                }
288                base
289            }
290        } else {
291            ::std::path::PathBuf::from(path_str)
292        }
293    }}
294    .into()
295}
296
297struct Format {
298    pub expr: syn::Expr,
299    pub args: Option<TokenStream2>,
300}
301
302impl syn::parse::Parse for Format {
303    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
304        let expr = input.parse()?;
305        let args = if input.peek(syn::token::Comma) {
306            Some(input.parse()?)
307        } else {
308            None
309        };
310        Ok(Self { expr, args })
311    }
312}