tmc_langs/config/
tmc_config.rs1use 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#[derive(Debug, Serialize, Deserialize)]
18#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
19pub struct TmcConfig {
20 #[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 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 pub fn load_from(client_name: &str, path: PathBuf) -> Result<TmcConfig, LangsError> {
40 let config = if path.exists() {
42 let data = file_util::read_file_to_string(&path)?;
44 match deserialize::toml_from_str::<Self>(&data) {
45 Ok(mut config) => {
47 config.location = path;
49 config }
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 log::info!("initializing a new config file at {}", path.display());
63 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 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 pub fn get_projects_dir(&self) -> &Path {
100 &self.projects_dir
101 }
102
103 pub fn set_projects_dir(&mut self, mut target: PathBuf) -> Result<PathBuf, LangsError> {
106 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 pub fn get(&self, key: &str) -> Option<&Value> {
116 self.table.get(key)
117 }
118
119 pub fn insert(&mut self, key: String, value: Value) -> Option<Value> {
122 self.table.insert(key, value)
123 }
124
125 pub fn remove(&mut self, key: &str) -> Option<Value> {
128 self.table.remove(key)
129 }
130
131 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 pub fn reset(client_name: &str) -> Result<(), LangsError> {
151 let path = Self::get_location(client_name)?;
152 Self::init_at(client_name, path)?; Ok(())
154 }
155
156 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
172fn get_client_stub(client: &str) -> &str {
174 match client {
175 "vscode_plugin" => "vscode",
176 s => s,
177 }
178}