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