1use project_dirs::{Directory, ProjectDirs};
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, path::PathBuf};
4
5fn default_true() -> bool {
6 true
7}
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11#[serde(rename_all = "kebab-case")]
12pub enum Fhs {
13 Local,
14
15 #[default]
16 Shared,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22pub enum Unix {
23 Pwd,
24 Home,
25 Binary,
26
27 #[serde(untagged)]
28 Custom {
29 path: PathBuf,
30 #[serde(default)]
31 prefix: Option<String>,
32 },
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case")]
37#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
38pub enum Windows {
39 Standard,
41 Local,
43 Shared,
45 System,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "kebab-case")]
51#[serde(tag = "strategy", content = "strategy_config")]
52#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53pub enum Strategy {
54 CurrentLocal,
56 CurrentUser,
58 CurrentSystem,
60 Fhs(#[serde(default)] Option<Fhs>),
62 Xdg,
64 Unix(Unix),
66 Windows(Windows),
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
71#[serde(rename_all = "kebab-case")]
72#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
73pub enum Filter {
74 FsPresent,
76
77 FsAbsent,
79
80 FsNotDir,
82
83 FsDenied,
85
86 FsNonValidDir,
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize)]
91#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
92pub struct SpecEntry {
93 #[serde(flatten)]
94 pub strategy: Strategy,
95 #[serde(default)]
96 pub directories: Vec<Directory>,
97 pub filter: Option<Filter>,
98
99 #[serde(default)]
100 pub mountpoint: Option<PathBuf>,
101}
102
103#[derive(Clone, Debug, Default, Serialize, Deserialize)]
104#[serde(rename_all = "kebab-case")]
105#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
106pub enum Spec {
107 #[default]
109 SystemDefault,
110
111 #[serde(untagged)]
113 Custom(HashMap<String, SpecEntry>),
114}
115
116#[derive(Clone, Debug, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
119pub struct CustomEnv {
121 #[serde(default)]
123 pub env: HashMap<String, Option<String>>,
124
125 #[serde(default = "default_true")]
127 pub fallback_to_system: bool,
128
129 #[serde(default)]
131 pub allow_variable_clearing: bool,
132}
133
134impl Default for CustomEnv {
135 fn default() -> Self {
136 CustomEnv {
137 env: HashMap::new(),
138 fallback_to_system: true,
139 allow_variable_clearing: false,
140 }
141 }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146pub struct Builder {
147 pub qualifier: String,
148 pub organization: String,
149 pub application: String,
150
151 #[serde(default)]
152 pub spec: Spec,
153
154 #[serde(default)]
157 pub custom_env: CustomEnv,
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
161#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
162pub struct BuilderResult {
163 pub application_name: String,
164 pub dirs: HashMap<String, ProjectDirs>,
165}
166
167impl Builder {
168 fn system_default(&self, project: &project_dirs::Project) -> HashMap<String, ProjectDirs> {
169 let dirs = project.project_dirs();
170 HashMap::from([
171 ("local".to_string(), dirs.local),
172 ("user".to_string(), dirs.user),
173 ("system".to_string(), dirs.system),
174 ])
175 }
176
177 pub fn process_spec_entry(
178 &self,
179 project: &project_dirs::Project,
180 entry: &SpecEntry,
181 ) -> ProjectDirs {
182 use project_dirs::dir_utils::{Filter as _, Mounted as _};
183 use project_dirs::strategy::fhs::Fhs as _;
184 use project_dirs::strategy::unix::Unix as _;
185 use project_dirs::strategy::windows::{Windows as _, WindowsEnv};
186 use project_dirs::strategy::xdg::{Xdg as _, XdgEnv};
187
188 let mut pd: ProjectDirs = match &entry.strategy {
189 Strategy::CurrentLocal => project.project_dirs().local,
190 Strategy::CurrentUser => project.project_dirs().user,
191 Strategy::CurrentSystem => project.project_dirs().system,
192 Strategy::Fhs(fhs) => match fhs {
193 Some(Fhs::Local) => project.fhs_local().into(),
194 Some(Fhs::Shared) | None => project.fhs().into(),
195 },
196 Strategy::Xdg => {
197 let mut env = if self.custom_env.fallback_to_system {
198 XdgEnv::new_system()
199 } else {
200 XdgEnv::default()
201 };
202
203 env.extend_with_env(
204 self.custom_env.env.iter().map(|x| (x.0, x.1.as_ref())),
205 self.custom_env.allow_variable_clearing,
206 );
207
208 if self.custom_env.fallback_to_system {
209 project
210 .xdg_with_env(env)
211 .map(ProjectDirs::from)
212 .unwrap_or(ProjectDirs::empty())
213 } else {
214 project.xdg_with_env_exclude_missing(env)
215 }
216 }
217 Strategy::Unix(unix) => match unix {
218 Unix::Pwd => project
219 .unix_pwd()
220 .map(Into::into)
221 .unwrap_or(ProjectDirs::empty()),
222 Unix::Home => project
223 .unix_home()
224 .map(Into::into)
225 .unwrap_or(ProjectDirs::empty()),
226 Unix::Binary => project
227 .unix_binary()
228 .map(Into::into)
229 .unwrap_or(ProjectDirs::empty()),
230 Unix::Custom { path, prefix } => match prefix {
231 Some(prefix) => project.unix_prefixed(path, prefix).into(),
232 None => project.unix(path).into(),
233 },
234 },
235 Strategy::Windows(windows) => {
236 #[cfg(target_os = "windows")]
237 let mut env = if self.custom_env.fallback_to_system {
238 WindowsEnv::new_system()
239 } else {
240 WindowsEnv::default()
241 };
242
243 #[cfg(not(target_os = "windows"))]
244 let mut env = WindowsEnv::default();
245
246 env.extend_with_env(
247 self.custom_env.env.iter().map(|x| (x.0, x.1.as_ref())),
248 self.custom_env.allow_variable_clearing,
249 );
250
251 match windows {
252 Windows::Standard => project.windows_user_with_env(env),
253 Windows::Local => project.windows_user_local_with_env(env),
254 Windows::Shared => project.windows_user_shared_with_env(env),
255 Windows::System => project.windows_system_with_env(env),
256 }
257 }
258 };
259
260 if let Some(filter) = &entry.filter {
261 pd = match filter {
262 Filter::FsPresent => pd.filter_existing_dirs(),
263 Filter::FsAbsent => pd.filter_absent(),
264 Filter::FsNotDir => pd.filter_non_dirs(),
265 Filter::FsDenied => pd.filter_denied(),
266 Filter::FsNonValidDir => pd.filter_non_valid(),
267 };
268 }
269
270 if let Some(mountpoint) = &entry.mountpoint {
271 pd = pd.mounted(mountpoint);
272 }
273
274 if !entry.directories.is_empty() {
275 pd = ProjectDirs::new(
276 pd.0.into_iter()
277 .filter(|d| entry.directories.contains(&d.0))
278 .collect(),
279 );
280 }
281
282 pd
283 }
284
285 pub fn build(&self) -> BuilderResult {
286 let project =
287 project_dirs::Project::new(&self.qualifier, &self.organization, &self.application);
288
289 let application_name = project.application_name().to_string();
290
291 BuilderResult {
292 application_name,
293 dirs: match &self.spec {
294 Spec::SystemDefault => self.system_default(&project),
295 Spec::Custom(items) => items.iter().fold(HashMap::new(), |mut acc, item| {
296 acc.insert(item.0.clone(), self.process_spec_entry(&project, item.1));
297 acc
298 }),
299 },
300 }
301 }
302}