tmc_langs/config/
tmc_config.rs

1//! Handles the CLI's configuration file.
2
3use crate::error::LangsError;
4use serde::{Deserialize, Serialize};
5use std::{
6    env,
7    io::Write,
8    path::{Path, PathBuf},
9};
10use tmc_langs_util::{
11    FileError, deserialize,
12    file_util::{self, Lock, LockOptions},
13};
14use toml::{Value, value::Table};
15
16/// The main configuration file. A separate one is used for each client.
17#[derive(Debug, Serialize, Deserialize)]
18#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
19pub struct TmcConfig {
20    // this is not serialized or deserialized, but set while loading
21    #[serde(skip)]
22    pub location: PathBuf,
23    #[serde(alias = "projects-dir")]
24    pub projects_dir: PathBuf,
25    #[serde(flatten)]
26    #[cfg_attr(feature = "ts-rs", ts(skip))]
27    pub table: Table,
28}
29
30impl TmcConfig {
31    /// Reads or initialises the config for the given client.
32    pub fn load(client_name: &str) -> Result<TmcConfig, LangsError> {
33        let path = Self::get_location(client_name)?;
34        log::debug!("Loading config at {}", path.display());
35        Self::load_from(client_name, path)
36    }
37
38    /// Reads or initialises for the client from the given path.
39    pub fn load_from(client_name: &str, path: PathBuf) -> Result<TmcConfig, LangsError> {
40        // try to open config file
41        let config = if path.exists() {
42            // found config file
43            let data = file_util::read_file_to_string(&path)?;
44            match deserialize::toml_from_str::<Self>(&data) {
45                // successfully read file, try to deserialize
46                Ok(mut config) => {
47                    // set the path which was set to default during deserialization
48                    config.location = path;
49                    config // successfully read and deserialized the config
50                }
51                Err(e) => {
52                    log::error!(
53                        "Failed to deserialize config at {} due to {}, resetting",
54                        path.display(),
55                        e
56                    );
57                    Self::init_at(client_name, path)?
58                }
59            }
60        } else {
61            // failed to open config file, create new one
62            log::info!("initializing a new config file at {}", path.display());
63            // todo: check the cause to make sure this makes sense, might be necessary to propagate some error kinds
64            Self::init_at(client_name, path)?
65        };
66
67        if !config.projects_dir.exists() {
68            file_util::create_dir_all(&config.projects_dir)?;
69        }
70        Ok(config)
71    }
72
73    // initializes the default configuration file at the given path
74    fn init_at(client_name: &str, path: PathBuf) -> Result<TmcConfig, LangsError> {
75        if let Some(parent) = path.parent() {
76            file_util::create_dir_all(parent)?;
77        }
78
79        let mut lock = Lock::file(&path, LockOptions::WriteTruncate)?;
80        let mut guard = lock.lock()?;
81        let default_project_dir = get_projects_dir_root()?.join(get_client_stub(client_name));
82        file_util::create_dir_all(&default_project_dir)?;
83
84        let config = TmcConfig {
85            location: path,
86            projects_dir: default_project_dir,
87            table: Table::new(),
88        };
89
90        let toml = toml::to_string_pretty(&config).expect("this should never fail");
91        guard
92            .get_file_mut()
93            .write_all(toml.as_bytes())
94            .map_err(|e| FileError::FileWrite(config.location.to_path_buf(), e))?;
95        Ok(config)
96    }
97
98    /// Returns the projects dir.
99    pub fn get_projects_dir(&self) -> &Path {
100        &self.projects_dir
101    }
102
103    /// Sets the projects dir.
104    /// Returns the old projects dir.
105    pub fn set_projects_dir(&mut self, mut target: PathBuf) -> Result<PathBuf, LangsError> {
106        // check if the directory is empty or not
107        if file_util::read_dir(&target)?.next().is_some() {
108            return Err(LangsError::NonEmptyDir(target));
109        }
110        std::mem::swap(&mut self.projects_dir, &mut target);
111        Ok(target)
112    }
113
114    /// Fetches a value with the given key.
115    pub fn get(&self, key: &str) -> Option<&Value> {
116        self.table.get(key)
117    }
118
119    /// Inserts a value with the given key and value.
120    /// Returns the old value, if any.
121    pub fn insert(&mut self, key: String, value: Value) -> Option<Value> {
122        self.table.insert(key, value)
123    }
124
125    /// Removes the value with the given key.
126    /// Returns the removed value, if any.
127    pub fn remove(&mut self, key: &str) -> Option<Value> {
128        self.table.remove(key)
129    }
130
131    /// Saves the config struct to the given path.
132    pub fn save(&mut self) -> Result<(), LangsError> {
133        let path = &self.location;
134        log::info!("Saving config at {}", path.display());
135
136        log::debug!("Saving config to temporary path");
137        let parent = path
138            .parent()
139            .ok_or_else(|| LangsError::NoParentDir(path.to_path_buf()))?;
140        let temp_file = file_util::named_temp_file_in(parent)?;
141        let toml = toml::to_string_pretty(&self)?;
142        file_util::write_to_file(toml, temp_file.path())?;
143
144        log::debug!("Moving new config over old one");
145        temp_file.persist(path)?;
146        Ok(())
147    }
148
149    /// Reinitialises the config file.
150    pub fn reset(client_name: &str) -> Result<(), LangsError> {
151        let path = Self::get_location(client_name)?;
152        Self::init_at(client_name, path)?; // init locks the file
153        Ok(())
154    }
155
156    // path to the configuration file
157    pub fn get_location(client_name: &str) -> Result<PathBuf, LangsError> {
158        super::get_tmc_dir(client_name).map(|dir| dir.join("config.toml"))
159    }
160}
161
162fn get_projects_dir_root() -> Result<PathBuf, LangsError> {
163    let data_dir = match env::var("TMC_LANGS_DEFAULT_PROJECTS_DIR") {
164        Ok(v) => PathBuf::from(v),
165        Err(_) => dirs::data_local_dir()
166            .ok_or(LangsError::NoLocalDataDir)?
167            .join("tmc"),
168    };
169    Ok(data_dir)
170}
171
172// some clients use a different name for the directory
173fn get_client_stub(client: &str) -> &str {
174    match client {
175        "vscode_plugin" => "vscode",
176        s => s,
177    }
178}