Skip to main content

selene_core/
config.rs

1use std::{fs, io};
2
3use serde::{Deserialize, Serialize};
4use toml_edit::{DocumentMut, Item, Table};
5
6use crate::config_dir;
7
8pub mod common;
9
10#[derive(Debug, thiserror::Error)]
11pub enum ConfigError {
12    #[error("IoError: {0}")]
13    Io(#[from] std::io::Error),
14
15    #[error("No home directory was found for the current user")]
16    NoHomeDir,
17
18    #[error("TomlError: {0}")]
19    TomlEdit(#[from] toml_edit::TomlError),
20
21    #[error("TomlError: {0}")]
22    Toml(#[from] toml::de::Error),
23}
24
25pub trait Config: Sized + Serialize + for<'de> Deserialize<'de> + Default {
26    /// The name of the config file, excluding its extension
27    const CONFIG_FILE_NAME: &'static str;
28
29    /// Loads [`Self`] from the system config directory using [`Self::CONFIG_FILE_NAME`], creating a default config if one does not already exist
30    fn load() -> Result<Self, ConfigError> {
31        let mut file = config_dir().join(Self::CONFIG_FILE_NAME);
32        file.add_extension("toml");
33
34        let config = match std::fs::read_to_string(&file) {
35            Ok(toml) => toml::from_str(&toml)?,
36            Err(err) if matches![err.kind(), io::ErrorKind::NotFound] => {
37                let config = Self::default();
38                config.save()?;
39                config
40            }
41            Err(err) => return Err(err.into()),
42        };
43
44        Ok(config)
45    }
46
47    /// Saves [`Self`] to the system config directory using [`Self::CONFIG_FILE_NAME`]
48    ///
49    /// # Errors
50    ///
51    /// Errors when the program cannot create necessary directories or write to file
52    /// See [`std::fs::create_dir_all()`] and [`std::fs::write()`] for more information
53    fn save(&self) -> Result<(), ConfigError> {
54        let config_dir = config_dir();
55        let mut file = config_dir.join(Self::CONFIG_FILE_NAME);
56        file.add_extension("toml");
57
58        let config_doc = toml_edit::ser::to_document(self).unwrap();
59        let mut disk_doc: DocumentMut = if file.exists() {
60            fs::read_to_string(&file)?.parse()?
61        } else {
62            DocumentMut::new()
63        };
64
65        write_needed_config_values(disk_doc.as_table_mut(), config_doc.as_table());
66
67        fs::create_dir_all(config_dir)?;
68        fs::write(file, disk_doc.to_string())?;
69
70        Ok(())
71    }
72}
73
74fn write_needed_config_values(disk: &mut Table, current_table: &Table) {
75    for (key, value) in current_table {
76        match value {
77            Item::Table(table) => {
78                let disk_table = disk
79                    .entry(key)
80                    .or_insert(Item::Table(Table::new()))
81                    .as_table_mut()
82                    .unwrap();
83                write_needed_config_values(disk_table, table);
84            }
85            _ => {
86                disk.insert(key, value.clone());
87            }
88        }
89    }
90}