1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//! APIs for reading [`pacaptr`](crate) configurations from the filesystem.
//!
//! I decided not to trash user's `$HOME` without their permission, so:
//! - If the user hasn't yet specified any path to look at, we will look for the
//!   config file in the default path.
//! - If the config file is not present anyway, a default one will be loaded
//!   with [`Default::default`], and no files will be written.
//! - Any config item can be overridden by the corresponding `PACAPTR_*`
//!   environment variable. For example, `PACAPTR_NEEDED=false` is prioritized
//!   over `needed = true` in `pacaptr.toml`.

use std::{env, path::PathBuf};

use figment::{
    providers::{Env, Format, Toml},
    Figment, Provider,
};
use serde::{Deserialize, Serialize};
use tap::prelude::*;

/// The crate name.
const CRATE_NAME: &str = clap::crate_name!();

/// The environment variable prefix for config item literals.
const CONFIG_ITEM_ENV_PREFIX: &str = "PACAPTR_";

/// The environment variable name for custom config file path.
const CONFIG_FILE_ENV: &str = "PACAPTR_CONFIG";

/// Configurations that may vary when running the package manager.
#[must_use]
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct Config {
    /// Perform a dry run.
    #[serde(default)]
    pub dry_run: bool,

    /// Prevent reinstalling previously installed packages.
    #[serde(default)]
    pub needed: bool,

    /// Answer yes to every question.
    #[serde(default)]
    pub no_confirm: bool,

    /// Remove cache after installation.
    #[serde(default)]
    pub no_cache: bool,

    /// The default package manager to be invoked.
    pub default_pm: Option<String>,
}

impl Config {
    /// Performs a left-biased join of two `Config`s.
    pub fn join(&self, other: Self) -> Self {
        Self {
            dry_run: self.dry_run || other.dry_run,
            needed: self.needed || other.dry_run,
            no_confirm: self.no_confirm || other.no_confirm,
            no_cache: self.no_cache || other.no_cache,
            default_pm: self.default_pm.clone().or(other.default_pm),
        }
    }

    /// The default config file path is defined with the following precedence:
    ///
    /// - `$XDG_CONFIG_HOME/pacaptr/pacaptr.toml`, if `$XDG_CONFIG_HOME` is set;
    /// - `$HOME/.config/pacaptr/pacaptr.toml`.
    ///
    /// This aligns with `fish`'s behavior.
    /// See: <https://github.com/fish-shell/fish-shell/issues/3170#issuecomment-228311857>
    fn default_path() -> Option<PathBuf> {
        env::var_os("XDG_CONFIG_HOME")
            .map(PathBuf::from)
            .filter(|p| p.is_absolute())
            .or_else(|| dirs_next::home_dir().map(|p| p.join(".config")))
            .tap_some_mut(|p| {
                p.extend([CRATE_NAME, &format!("{CRATE_NAME}.toml")]);
            })
    }

    /// Gets the custom config file path specified by the `PACAPTR_CONFIG`
    /// environment variable.
    fn custom_path() -> Option<PathBuf> {
        env::var_os(CONFIG_FILE_ENV).map(PathBuf::from)
    }

    /// Returns the config [`Provider`] from the custom or default config file
    /// path.
    #[must_use]
    pub fn file_provider() -> impl Provider {
        Self::custom_path()
            .or_else(Self::default_path)
            .map_or_else(Figment::new, |f| Figment::from(Toml::file(f)))
    }

    /// Returns the environment config [`Provider`].
    #[must_use]
    pub fn env_provider() -> impl Provider {
        Env::prefixed(CONFIG_ITEM_ENV_PREFIX)
    }
}