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