Skip to main content

sericom_core/configs/
mod.rs

1//! This module handles the structuring, valid options, and parsing of user config
2//! files. User config files must be `config.toml` and are parsed with [`serde`] and
3//! respectively serde's [`toml`] crate.
4
5mod appearance;
6mod defaults;
7pub mod errors;
8pub use appearance::*;
9pub use defaults::*;
10
11use crate::{
12    configs::errors::{ConfigError, TomlError},
13    create_recursive,
14};
15use serde::Deserialize;
16use std::{io::Read, ops::Range, path::PathBuf, sync::OnceLock};
17
18/// Global value of the user's config.
19///
20/// Currently it is immutable after initialized, therefore any changes to the
21/// underlying [`Config`] must be made before calling [`initialize_config()`].
22///
23/// To get a reference to the global config during runtime, call [`get_config()`].
24pub static CONFIG: OnceLock<Config> = OnceLock::new();
25
26/// Represents the entire `config.toml` configuration file.
27///
28/// See [`Appearance`] and [`Defaults`]
29#[derive(Default, Debug, Deserialize, PartialEq)]
30pub struct Config {
31    #[serde(default)]
32    pub appearance: Appearance,
33    #[serde(default)]
34    pub defaults: Defaults,
35}
36
37impl Config {
38    fn apply_overrides(&mut self, overrides: ConfigOverride) {
39        if let Some(color) = overrides.color {
40            self.appearance.fg = color;
41        }
42        if let Some(dir) = overrides.out_dir {
43            self.defaults.out_dir = dir;
44        }
45        if let Some(script) = overrides.exit_script {
46            self.defaults.exit_script = Some(script);
47        }
48    }
49}
50
51/// This function constructs a global `static CONFIG` for the rest of the program's
52/// duration to provide a reference to the config for the remainder of the program.
53///
54/// It checks for the user's config file and if it doesn't exist, it will use
55/// [`Config::default()`]. If the user's config does exist but does not set values
56/// for every field, the global `static CONFIG` will be initialized with the user's
57/// values and fill in the unspecified fields with their default values.
58///
59/// Takes [`ConfigOverride`] to set any overriding values before initialization.
60///
61/// Returns a [`ConfigError::AlreadyInitialized`] error if called after it has
62/// already been called ([`CONFIG`] has already been set).
63pub fn initialize_config(overrides: ConfigOverride) -> miette::Result<(), ConfigError> {
64    let mut config: Config = if let Ok(config_file) = get_config_file() {
65        let mut file = std::fs::File::open(config_file).expect("File should exist");
66        let mut contents = String::new();
67        file.read_to_string(&mut contents)?;
68        toml::from_str(&contents).map_err(|e| {
69            TomlError::new(
70                e.span().unwrap_or(Range { start: 0, end: 0 }),
71                contents,
72                e.message().to_string(),
73            )
74        })?
75    } else {
76        Config::default()
77    };
78
79    config.apply_overrides(overrides);
80
81    CONFIG
82        .set(config)
83        .map_err(|_| ConfigError::AlreadyInitialized)?;
84    Ok(())
85}
86
87/// When called, [`get_config()`] returns a reference to the global [`CONFIG`]
88/// that was initialized at the start of the program.
89///
90/// See [`Config`].
91///
92/// ## Panics
93/// Will panic if [`CONFIG`] as not been initialized before calling with [`initialize_config()`].
94pub fn get_config() -> &'static Config {
95    CONFIG.get().expect("Config not initialized")
96}
97
98#[derive(Debug)]
99/// Available configuration options that can be overridden
100pub struct ConfigOverride {
101    /// Overrides [`Appearance::fg`]
102    pub color: Option<SeriColor>,
103    /// Overrides [`Defaults::out_dir`]
104    pub out_dir: Option<PathBuf>,
105    /// Overrides [`Defaults::exit_script`]
106    pub exit_script: Option<PathBuf>,
107}
108
109fn get_conf_dir() -> std::path::PathBuf {
110    let mut user_home_dir = std::env::home_dir().expect("Failed to get home directory");
111
112    if cfg!(windows) {
113        user_home_dir.push(".config\\sericom");
114    } else {
115        user_home_dir.push(".config/sericom");
116    }
117
118    let user_conf_dir = user_home_dir;
119    create_recursive!(user_conf_dir.as_path());
120
121    user_conf_dir
122}
123
124fn get_config_file() -> miette::Result<std::path::PathBuf, ConfigError> {
125    let mut conf_dir = get_conf_dir();
126    conf_dir.push("config.toml");
127    let conf_file = conf_dir;
128
129    if conf_file.exists() && conf_file.is_file() {
130        Ok(conf_file)
131    } else {
132        Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Could not find config file.").into())
133    }
134}
135
136#[test]
137fn parse_test_config() -> miette::Result<()> {
138    use miette::IntoDiagnostic;
139    let file: Config = toml::from_str(
140        r#"
141            [appearance]
142            fg = "dark-grey"
143            bg = "red"
144
145            [defaults]
146            out-dir = "$HOME/.config"
147            exit-script = "~/.local/bin/format-cisco"
148            "#,
149    )
150    .into_diagnostic()?;
151
152    let parsed_conf = Config {
153        appearance: Appearance {
154            fg: SeriColor::DarkGrey,
155            bg: SeriColor::Red,
156        },
157        defaults: Defaults {
158            out_dir: PathBuf::from("/home/thomas/.config"),
159            exit_script: Some(PathBuf::from("/home/thomas/.local/bin/format-cisco")),
160            debug_dir: PathBuf::from("/home/thomas/Code/Work/sericom/sericom-core"),
161            // file_exit_script: None,
162        },
163    };
164
165    assert_eq!(file, parsed_conf);
166    Ok(())
167}
168
169#[test]
170fn check_conf_dir_is_dir() {
171    let dir = get_conf_dir();
172    assert!(std::fs::metadata(dir).unwrap().is_dir())
173}
174
175#[test]
176fn valid_conf_dir() {
177    let dir = get_conf_dir();
178    if cfg!(target_family = "windows") {
179        assert_eq!(dir.to_str().unwrap(), "C:\\Users\\Thomas\\.config\\sericom")
180    } else {
181        assert_eq!(dir.to_str().unwrap(), "/home/thomas/.config/sericom")
182    }
183}
184
185#[test]
186fn get_expanded_path() {
187    use crate::path_utils::ExpandPaths;
188
189    let p = PathBuf::from("$HOME/.config/sericom/config.toml")
190        .get_expanded_path()
191        .unwrap();
192
193    let p2 = PathBuf::from("~/.config/sericom/config.toml")
194        .get_expanded_path()
195        .unwrap();
196
197    let p3 = PathBuf::from("$XDG_CONFIG_HOME/some/path")
198        .get_expanded_path()
199        .unwrap();
200
201    assert_eq!(p, PathBuf::from("/home/thomas/.config/sericom/config.toml"));
202    assert_eq!(
203        p2,
204        PathBuf::from("/home/thomas/.config/sericom/config.toml")
205    );
206    assert_eq!(p3, PathBuf::from("/home/thomas/.config/some/path"))
207}
208
209// #[test]
210// fn initialize_conf() -> miette::Result<()> {
211//     initialize_config(ConfigOverride { color: None, out_dir: None, file_exit_script: None, })?;
212//     // assert_eq!(config, &Config::default())
213//     Ok(())
214// }