config_finder/lib.rs
1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![warn(
4 missing_docs,
5 clippy::missing_errors_doc,
6 clippy::missing_panics_doc,
7 rustdoc::missing_crate_level_docs
8)]
9#![allow(clippy::bool_comparison)]
10
11use std::borrow::Cow;
12use std::ffi::{OsStr, OsString};
13use std::iter::FusedIterator;
14use std::path::{Path, PathBuf};
15use std::slice::Iter;
16
17/// Common ground for the three way to look for configuration paths.
18#[derive(Debug, Clone, Default)]
19pub struct ConfigDirs {
20 /// Paths *without* the added app directory/file
21 paths: Vec<PathBuf>,
22
23 /// If `true`, the current working directory has already been added
24 added_cwd: bool,
25 /// If `true`, `$XDG_CONFIG_HOME` (defaulting to `~/.config/`) has already been added
26 ///
27 /// On Windows this is the AppData/Roaming directory.
28 added_platform: bool,
29 /// If `true`, `/etc` has already been added
30 #[cfg(unix)]
31 added_etc: bool,
32}
33
34impl ConfigDirs {
35 /// Empty list of paths to search configs in.
36 ///
37 /// ```
38 /// use config_finder::ConfigDirs;
39 ///
40 /// assert!(ConfigDirs::empty().paths().is_empty());
41 /// ```
42 pub const fn empty() -> Self {
43 Self {
44 paths: Vec::new(),
45 added_cwd: false,
46 added_platform: false,
47 #[cfg(unix)]
48 added_etc: false,
49 }
50 }
51
52 /// Iterator yielding possible config files or directories.
53 ///
54 /// # Behaviour
55 ///
56 /// Will search for `app/base.ext` and `app/base.local.ext`. If the extension is empty, it will
57 /// search for `app/base` and `app/base.local` instead.
58 ///
59 /// Giving an empty `app` or `ext`ension is valid, see examples below.
60 ///
61 /// # Example
62 ///
63 /// ```
64 /// # fn main() { wrapped(); }
65 /// # fn wrapped() -> Option<()> {
66 /// use std::path::Path;
67 ///
68 /// use config_finder::ConfigDirs;
69 ///
70 /// let mut cd = ConfigDirs::empty();
71 /// let mut app_files = cd.add_path("start")
72 /// .add_path("second")
73 /// .add_path("end")
74 /// .search("my-app", "main", "kdl");
75 ///
76 /// let wl = app_files.next()?;
77 /// assert_eq!(wl.path(), Path::new("start/.config/my-app/main.kdl"));
78 /// assert_eq!(wl.local_path(), Path::new("start/.config/my-app/main.local.kdl"));
79 ///
80 /// let wl = app_files.next_back()?;
81 /// assert_eq!(wl.path(), Path::new("end/.config/my-app/main.kdl"));
82 /// assert_eq!(wl.local_path(), Path::new("end/.config/my-app/main.local.kdl"));
83 ///
84 /// let wl = app_files.next()?;
85 /// assert_eq!(wl.path(), Path::new("second/.config/my-app/main.kdl"));
86 /// assert_eq!(wl.local_path(), Path::new("second/.config/my-app/main.local.kdl"));
87 ///
88 /// assert_eq!(app_files.next(), None);
89 /// # Some(()) }
90 /// ```
91 ///
92 /// Without an app subdirectory:
93 ///
94 /// ```
95 /// # fn main() { wrapped(); }
96 /// # fn wrapped() -> Option<()> {
97 /// use std::path::Path;
98 ///
99 /// use config_finder::ConfigDirs;
100 ///
101 /// let mut cd = ConfigDirs::empty();
102 /// cd.add_path("start");
103 /// let mut app_files =
104 /// cd.add_path("start").search("", "my-app", "kdl");
105 ///
106 /// let wl = app_files.next()?;
107 /// assert_eq!(wl.path(), Path::new("start/.config/my-app.kdl"));
108 /// assert_eq!(wl.local_path(), Path::new("start/.config/my-app.local.kdl"));
109 ///
110 /// assert_eq!(app_files.next(), None);
111 /// # Some(()) }
112 /// ```
113 ///
114 /// Without an extension:
115 ///
116 /// ```
117 /// # fn main() { wrapped(); }
118 /// # fn wrapped() -> Option<()> {
119 /// use std::path::Path;
120 ///
121 /// use config_finder::ConfigDirs;
122 ///
123 /// let mut cd = ConfigDirs::empty();
124 /// let mut app_files =
125 /// cd.add_path("start").search("my-app", "main", "");
126 ///
127 /// let wl = app_files.next()?;
128 /// assert_eq!(wl.path(), Path::new("start/.config/my-app/main"));
129 /// assert_eq!(wl.local_path(), Path::new("start/.config/my-app/main.local"));
130 ///
131 /// assert_eq!(app_files.next(), None);
132 /// # Some(()) }
133 /// ```
134 #[inline]
135 pub fn search(&self, app: impl AsRef<Path>, base: impl AsRef<OsStr>, ext: impl AsRef<OsStr>) -> ConfigCandidates {
136 ConfigCandidates::new(&self.paths, app, base, ext)
137 }
138}
139
140/// Accessors
141impl ConfigDirs {
142 /// Look at the config paths already added.
143 ///
144 /// ```
145 /// use std::path::PathBuf;
146 ///
147 /// use config_finder::ConfigDirs;
148 ///
149 /// let mut cd = ConfigDirs::empty();
150 /// assert!(cd.paths().is_empty());
151 /// cd.add_path("my/config/path");
152 /// assert_eq!(cd.paths(), &[PathBuf::from("my/config/path/.config")]);
153 /// ```
154 pub fn paths(&self) -> &[PathBuf] {
155 &self.paths
156 }
157}
158
159/// Adding paths to the list
160impl ConfigDirs {
161 /// Adds `path` to the list of directories to check, if not previously added.
162 ///
163 /// This path should **not** contain the config directory (or file) passed during
164 /// construction.
165 ///
166 /// # Behaviour
167 ///
168 /// This function will add `.config` to the given path if it does not end with that
169 /// already. This means you can just pass the workspace for your application (e.g. the root of
170 /// a git repository) and this type will look for `workspace/.config/<app>`.
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use std::path::PathBuf;
176 ///
177 /// use config_finder::ConfigDirs;
178 ///
179 /// let mut cd = ConfigDirs::empty();
180 /// assert!(cd.paths().is_empty());
181 /// cd.add_path("my/config/path")
182 /// .add_path("my/other/path/.config"); // .config already present at the end
183 /// assert_eq!(cd.paths(), &[
184 /// PathBuf::from("my/config/path/.config"),
185 /// PathBuf::from("my/other/path/.config"), // it has not been added again
186 /// ]);
187 /// ```
188 #[inline]
189 pub fn add_path<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
190 self._add_path(path, true)
191 }
192
193 /// Adds all the paths starting from `start` and going up until a parent is out of `container`.
194 ///
195 /// This *includes* `container`.
196 ///
197 /// If `start` does not [starts with][Path::starts_with] `container`, this will do nothing since
198 /// `start` is already out of the containing path.
199 ///
200 /// # Behaviour
201 ///
202 /// See [`Self::add_path()`]. This behaviour will be applied to each path added by this method.
203 ///
204 /// # Examples
205 ///
206 /// ```
207 /// use std::path::PathBuf;
208 ///
209 /// use config_finder::ConfigDirs;
210 ///
211 /// let mut cd = ConfigDirs::empty();
212 /// assert!(cd.paths().is_empty());
213 /// cd.add_all_paths_until("look/my/config/path", "look/my");
214 /// assert_eq!(cd.paths(), &[
215 /// PathBuf::from("look/my/config/path/.config"),
216 /// PathBuf::from("look/my/config/.config"),
217 /// PathBuf::from("look/my/.config"),
218 /// ]);
219 /// ```
220 ///
221 /// `"other"` is not a root of `"my/config/path"`:
222 ///
223 /// ```
224 /// use config_finder::ConfigDirs;
225 ///
226 /// let mut cd = ConfigDirs::empty();
227 /// assert!(cd.paths().is_empty());
228 /// cd.add_all_paths_until("my/config/path", "other");
229 /// assert!(cd.paths().is_empty());
230 /// ```
231 #[inline]
232 pub fn add_all_paths_until<P1: AsRef<Path>, P2: AsRef<Path>>(&mut self, start: P1, container: P2) -> &mut Self {
233 fn helper(this: &mut ConfigDirs, start: &Path, container: &Path) {
234 start
235 .ancestors()
236 .take_while(|p| p.starts_with(container))
237 .for_each(|p| {
238 this._add_path(p, true);
239 });
240 }
241
242 helper(self, start.as_ref(), container.as_ref());
243 self
244 }
245
246 /// Adds the platform's config directory to the list of paths to check.
247 ///
248 /// |Platform | Value | Example |
249 /// | ------- | ------------------------------------- | -------------------------------- |
250 /// | Unix(1) | `$XDG_CONFIG_HOME` or `$HOME/.config` | `/home/alice/.config` |
251 /// | Windows | `{FOLDERID_RoamingAppData}` | `C:\Users\Alice\AppData\Roaming` |
252 ///
253 /// (1): *Unix* stand for both Linux and macOS here. Since this crate is primarily intended for
254 /// CLI applications & tools, having the macOS files hidden in `$HOME/Library/Application
255 /// Support` is not practical.
256 ///
257 /// # Behaviour
258 ///
259 /// This method will **not** add `.config`, unlike [`Self::add_path()`].
260 ///
261 /// ## Examples
262 ///
263 /// ```
264 /// use std::path::PathBuf;
265 ///
266 /// use config_finder::ConfigDirs;
267 ///
268 /// if cfg!(windows) {
269 /// let mut cd = ConfigDirs::empty();
270 /// cd.add_platform_config_dir()
271 /// .add_platform_config_dir(); // Adding twice does not affect the final list
272 /// assert_eq!(cd.paths().len(), 1);
273 /// assert!(cd.paths()[0].ends_with("AppData/Roaming"));
274 /// } else {
275 /// std::env::set_var("HOME", "/home/testuser");
276 ///
277 /// // With `XDG_CONFIG_HOME` unset
278 /// std::env::remove_var("XDG_CONFIG_HOME");
279 /// let mut cd = ConfigDirs::empty();
280 /// cd.add_platform_config_dir();
281 /// assert_eq!(cd.paths(), &[PathBuf::from("/home/testuser/.config")]);
282 ///
283 /// // With `XDG_CONFIG_HOME` set
284 /// std::env::set_var("XDG_CONFIG_HOME", "/home/.shared_configs");
285 /// let mut cd = ConfigDirs::empty();
286 /// cd.add_platform_config_dir();
287 /// assert_eq!(cd.paths(), &[PathBuf::from("/home/.shared_configs")]); // No `.config` added
288 /// }
289 /// ```
290 pub fn add_platform_config_dir(&mut self) -> &mut Self {
291 if self.added_platform {
292 return self;
293 }
294
295 // We don't set `self.added_platform` unconditionnally because the environment can change
296 // between the failing call and the next one (which may succeed and then set to true)
297
298 #[cfg(windows)]
299 if let Some(path) = dirs_sys::known_folder_roaming_app_data() {
300 self._add_path(path, false);
301 self.added_platform = true;
302 }
303
304 #[cfg(not(windows))]
305 if let Some(path) = std::env::var_os("XDG_CONFIG_HOME").and_then(dirs_sys::is_absolute_path) {
306 self._add_path(path, false);
307 self.added_platform = true;
308 } else if let Some(path) = dirs_sys::home_dir().filter(|p| p.is_absolute()) {
309 self._add_path(path, true);
310 self.added_platform = true;
311 }
312
313 self
314 }
315
316 /// Adds the current directory to the list of paths to search in.
317 ///
318 /// # Errors
319 ///
320 /// Returns an error if [`std::env::current_dir()`] fails.
321 ///
322 /// # Behaviour
323 ///
324 /// See [`Self::add_path()`].
325 ///
326 /// # Examples
327 ///
328 /// ```
329 /// use config_finder::ConfigDirs;
330 ///
331 /// let current_dir = std::env::current_dir().unwrap().join(".config");
332 ///
333 /// let mut cd = ConfigDirs::empty();
334 /// cd.add_current_dir();
335 /// assert_eq!(cd.paths(), &[current_dir]);
336 /// ```
337 #[inline]
338 pub fn add_current_dir(&mut self) -> std::io::Result<&mut Self> {
339 if self.added_cwd == false {
340 self._add_path(std::env::current_dir()?, true);
341 self.added_cwd = true;
342 }
343 Ok(self)
344 }
345}
346
347/// Unix-only methods
348#[cfg(unix)]
349impl ConfigDirs {
350 /// Adds `/etc` to the list of paths to checks if not previously added.
351 ///
352 /// # Behaviour
353 ///
354 /// This method will **not** add `.config`, unlike [`Self::add_path()`].
355 ///
356 /// # Examples
357 ///
358 /// ```
359 /// use std::path::PathBuf;
360 ///
361 /// use config_finder::ConfigDirs;
362 ///
363 /// let mut cd = ConfigDirs::empty();
364 /// cd.add_root_etc();
365 /// assert_eq!(cd.paths(), &[PathBuf::from("/etc")]);
366 /// ```
367 #[inline]
368 pub fn add_root_etc(&mut self) -> &mut Self {
369 if self.added_etc == false {
370 self._add_path("/etc", false);
371 self.added_etc = true;
372 }
373 self
374 }
375}
376
377/// Private methods
378impl ConfigDirs {
379 /// Helper that will add the `.config` at the end if asked AND if the given path does *not* end
380 /// with `.config` already.
381 #[inline]
382 pub(crate) fn _add_path<P>(&mut self, path: P, check_for_dot_config: bool) -> &mut Self
383 where
384 P: AsRef<Path>,
385 {
386 fn helper(this: &mut ConfigDirs, pr: &Path, check_for_dot_config: bool) {
387 let path = if check_for_dot_config == false || pr.ends_with(".config") {
388 Cow::Borrowed(pr)
389 } else {
390 Cow::Owned(pr.join(".config"))
391 };
392
393 if this.paths.iter().all(|p| p != &path) {
394 this.paths.push(path.into_owned());
395 }
396 }
397
398 helper(self, path.as_ref(), check_for_dot_config);
399 self
400 }
401}
402
403/// Iterator for [`ConfigDirs::search()`].
404pub struct ConfigCandidates<'c> {
405 conf: WithLocal,
406 paths: Iter<'c, PathBuf>,
407}
408
409impl<'c> ConfigCandidates<'c> {
410 pub(crate) fn new(
411 paths: &'c [PathBuf],
412 app: impl AsRef<Path>,
413 base: impl AsRef<OsStr>,
414 ext: impl AsRef<OsStr>,
415 ) -> Self {
416 Self {
417 conf: WithLocal::new(app.as_ref().join(base.as_ref()), ext),
418 paths: paths.iter(),
419 }
420 }
421}
422
423impl Iterator for ConfigCandidates<'_> {
424 type Item = WithLocal;
425
426 #[inline]
427 fn next(&mut self) -> Option<Self::Item> {
428 let dir = self.paths.next()?;
429 Some(self.conf.joined_to(dir))
430 }
431
432 #[inline]
433 fn last(self) -> Option<Self::Item>
434 where
435 Self: Sized,
436 {
437 let dir = self.paths.last()?;
438 Some(self.conf.joined_to(dir))
439 }
440
441 #[inline]
442 fn nth(&mut self, n: usize) -> Option<Self::Item> {
443 let dir = self.paths.nth(n)?;
444 Some(self.conf.joined_to(dir))
445 }
446
447 #[inline]
448 fn size_hint(&self) -> (usize, Option<usize>) {
449 self.paths.size_hint()
450 }
451
452 #[inline]
453 fn count(self) -> usize
454 where
455 Self: Sized,
456 {
457 self.paths.count()
458 }
459}
460
461impl DoubleEndedIterator for ConfigCandidates<'_> {
462 #[inline]
463 fn next_back(&mut self) -> Option<Self::Item> {
464 let dir = self.paths.next_back()?;
465 Some(self.conf.joined_to(dir))
466 }
467}
468
469impl ExactSizeIterator for ConfigCandidates<'_> {}
470
471impl FusedIterator for ConfigCandidates<'_> {}
472
473/// Stores both the normal and local form a configuration path.
474///
475/// The local form has `.local` inserted just before the extension: `cli-app.kdl` has the local form
476/// `cli-app.local.kdl`.
477///
478/// While this is mostly intended for file, nothing precludes an application from using it for
479/// directories.
480///
481/// ```
482/// use std::path::{Path, PathBuf};
483///
484/// use config_finder::WithLocal;
485///
486/// // `.local` is inserted before the extension for the `.local_path()` form
487/// let wl = WithLocal::new("cli-app", "kdl");
488/// assert_eq!(wl.path(), Path::new("cli-app.kdl"));
489/// assert_eq!(wl.local_path(), Path::new("cli-app.local.kdl"));
490///
491/// // Even if the extension is empty (can notably be used for directories)
492/// let wl = WithLocal::new("cli-app", "");
493/// assert_eq!(wl.path(), Path::new("cli-app"));
494/// assert_eq!(wl.local_path(), Path::new("cli-app.local"));
495///
496/// // An empty base is valid too
497/// let wl = WithLocal::new("", "kdl");
498/// assert_eq!(wl.path(), Path::new(".kdl"));
499/// assert_eq!(wl.local_path(), Path::new(".local.kdl"));
500///
501/// // If you need to store a form (local or not),
502/// let wl = WithLocal::new("zellij", "kdl");
503/// assert_eq!(wl.into_paths(), (PathBuf::from("zellij.kdl"), PathBuf::from("zellij.local.kdl")));
504/// ```
505#[derive(Debug, Clone, PartialEq, Eq, Hash)]
506pub struct WithLocal {
507 /// The normal path
508 path: PathBuf,
509 /// The local form of the path.
510 local_path: PathBuf,
511}
512
513impl WithLocal {
514 /// Computes both the normal and local forms of the path.
515 ///
516 /// If the `ext`ension is non-empty, inserts a dot (`.`) between the `base` and the `ext`ension.
517 #[inline]
518 pub fn new(base: impl Into<OsString>, ext: impl AsRef<OsStr>) -> Self {
519 fn helper(mut path: OsString, ext: &OsStr) -> WithLocal {
520 let mut local_path = path.clone();
521 local_path.push(".local");
522
523 if ext.is_empty() == false {
524 path.push(".");
525 path.push(ext);
526
527 local_path.push(".");
528 local_path.push(ext);
529 }
530
531 WithLocal {
532 path: path.into(),
533 local_path: local_path.into(),
534 }
535 }
536
537 helper(base.into(), ext.as_ref())
538 }
539
540 /// Path without the added `.local` just before the extension.
541 #[inline]
542 pub fn path(&self) -> &Path {
543 &self.path
544 }
545
546 /// Path with the added `.local` just before the extension.
547 #[inline]
548 pub fn local_path(&self) -> &Path {
549 &self.local_path
550 }
551
552 /// Destructure into the inner `(path, local_path)` without allocating.
553 #[inline]
554 pub fn into_paths(self) -> (PathBuf, PathBuf) {
555 (self.path, self.local_path)
556 }
557}
558
559impl WithLocal {
560 // Helper function for the iterator
561 fn joined_to(&self, base: &Path) -> Self {
562 Self {
563 path: base.join(&self.path),
564 local_path: base.join(&self.local_path),
565 }
566 }
567}