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#[proc_macro]
9pub fn path(input: TokenStream) -> TokenStream {
10 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}