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 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 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 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 ::std::env::current_exe().expect("Failed to get executable path")
50 } else if path_normalized.starts_with("$/") {
51 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 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 "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 "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 "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 "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 "temp" => {
163 let mut p = ::std::env::temp_dir();
164 if has_app { p.push(APP_NAME); }
165 p
166 }
167
168 "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 "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 "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 "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 "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 "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 "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}