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}