tycode_core/settings/
manager.rs1use crate::settings::config::Settings;
2use anyhow::{Context, Result};
3use serde::{de::DeserializeOwned, Serialize};
4use std::fs;
5use std::ops::DerefMut;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex};
8
9#[derive(Clone)]
14pub struct SettingsManager {
15 settings_dir: PathBuf,
16 settings_path: PathBuf,
17 current_profile: Option<String>,
18 inner: Arc<Mutex<Settings>>,
21}
22
23impl SettingsManager {
24 pub fn from_settings_dir(settings_dir: PathBuf, profile_name: Option<&str>) -> Result<Self> {
26 fs::create_dir_all(&settings_dir)
28 .with_context(|| format!("Failed to create settings directory: {:?}", settings_dir))?;
29
30 let settings_path = if let Some(name) = profile_name {
31 settings_dir.join(format!("settings_{}.toml", name))
32 } else {
33 settings_dir.join("settings.toml")
34 };
35
36 let current_profile = profile_name.map(|s| s.to_string());
37
38 let loaded = Self::load_from_file_with_backup(&settings_path)?;
39
40 Ok(Self {
41 settings_dir,
42 settings_path,
43 current_profile,
44 inner: Arc::new(Mutex::new(loaded)),
45 })
46 }
47
48 pub fn from_path(path: PathBuf) -> Result<Self> {
50 let settings_dir = path
51 .parent()
52 .ok_or_else(|| anyhow::anyhow!("Settings path has no parent directory"))?
53 .to_path_buf();
54
55 let current_profile = Self::infer_profile_from_path(&path);
56
57 let loaded = Self::load_from_file_with_backup(&path)?;
58
59 Ok(Self {
60 settings_dir,
61 settings_path: path,
62 current_profile,
63 inner: Arc::new(Mutex::new(loaded)),
64 })
65 }
66
67 fn infer_profile_from_path(path: &Path) -> Option<String> {
68 let file_name = match path.file_name().and_then(|s| s.to_str()) {
69 Some(name) => name,
70 None => return None,
71 };
72
73 if file_name == "settings.toml" {
74 return None;
75 }
76
77 if !file_name.ends_with(".toml") {
78 return None;
79 }
80
81 let len = file_name.len();
82 let without_ext = &file_name[..len - 5];
83
84 if !without_ext.starts_with("settings_") {
85 return None;
86 }
87
88 let potential_name = &without_ext[9..];
89
90 if potential_name.is_empty() {
91 None
92 } else {
93 Some(potential_name.to_string())
94 }
95 }
96
97 fn load_from_file_with_backup(path: &Path) -> Result<Settings> {
99 if !path.exists() {
100 let default_settings = Settings::default();
101 if let Some(parent) = path.parent() {
102 fs::create_dir_all(parent)
103 .with_context(|| format!("Failed to create directory: {parent:?}"))?;
104 }
105 let contents = toml::to_string_pretty(&default_settings)
106 .context("Failed to serialize default settings")?;
107 fs::write(path, contents)
108 .with_context(|| format!("Failed to write default settings to {path:?}"))?;
109 return Ok(default_settings);
110 }
111
112 let contents = fs::read_to_string(path)
113 .with_context(|| format!("Failed to read settings from {path:?}"))?;
114
115 match toml::from_str::<Settings>(&contents) {
116 Ok(settings) => Ok(settings),
117 Err(_) => {
118 let backup_path = path.with_extension("toml.backup");
120 fs::rename(path, &backup_path).with_context(|| {
121 format!("Failed to backup corrupted settings to {backup_path:?}")
122 })?;
123
124 let default_settings = Settings::default();
126 if let Some(parent) = path.parent() {
127 fs::create_dir_all(parent)
128 .with_context(|| format!("Failed to create directory: {parent:?}"))?;
129 }
130 let contents = toml::to_string_pretty(&default_settings)
131 .context("Failed to serialize default settings")?;
132 fs::write(path, contents)
133 .with_context(|| format!("Failed to write default settings to {path:?}"))?;
134
135 Ok(default_settings)
136 }
137 }
138 }
139
140 pub fn settings(&self) -> Settings {
142 self.inner.lock().unwrap().clone()
143 }
144
145 pub fn update_setting<F>(&self, updater: F)
147 where
148 F: FnOnce(&mut Settings),
149 {
150 let mut guard = self.inner.lock().unwrap();
151 updater(guard.deref_mut());
152 }
153
154 pub fn save_settings(&self, settings: Settings) -> Result<()> {
156 if let Some(parent) = self.settings_path.parent() {
158 fs::create_dir_all(parent)
159 .with_context(|| format!("Failed to create directory: {parent:?}"))?;
160 }
161
162 let contents = toml::to_string_pretty(&settings).context("Failed to serialize settings")?;
163
164 fs::write(&self.settings_path, contents)
165 .with_context(|| format!("Failed to write settings to {:?}", self.settings_path))?;
166 *self.inner.lock().unwrap() = settings;
167
168 Ok(())
169 }
170
171 pub fn save(&self) -> Result<()> {
173 self.save_settings(self.settings())
174 }
175
176 pub fn current_profile(&self) -> Option<&str> {
178 self.current_profile.as_deref()
179 }
180
181 pub fn switch_profile(&mut self, name: &str) -> Result<()> {
183 let new_path = if name == "default" {
184 self.settings_dir.join("settings.toml")
185 } else {
186 self.settings_dir.join(format!("settings_{}.toml", name))
187 };
188 fs::create_dir_all(&self.settings_dir)
189 .with_context(|| format!("Failed to create directory: {:?}", self.settings_dir))?;
190 let new_settings = Self::load_from_file_with_backup(&new_path)?;
191 self.settings_path = new_path;
192 self.current_profile = if name == "default" {
193 None
194 } else {
195 Some(name.to_string())
196 };
197 *self.inner.lock().unwrap() = new_settings;
198 Ok(())
199 }
200
201 pub fn save_as_profile(&self, name: &str) -> Result<()> {
203 fs::create_dir_all(&self.settings_dir)
204 .with_context(|| format!("Failed to create directory: {:?}", self.settings_dir))?;
205 let target_path = self.settings_dir.join(format!("settings_{}.toml", name));
206 let contents =
207 toml::to_string_pretty(&self.settings()).context("Failed to serialize settings")?;
208 fs::write(&target_path, contents)
209 .with_context(|| format!("Failed to write settings to {target_path:?}"))?;
210 Ok(())
211 }
212
213 pub fn list_profiles(&self) -> Result<Vec<String>> {
215 let mut profiles = Vec::new();
216
217 let default_path = self.settings_dir.join("settings.toml");
218 if default_path.exists() {
219 profiles.push("default".to_string());
220 }
221
222 let entries = fs::read_dir(&self.settings_dir).with_context(|| {
223 format!("Failed to read settings directory: {:?}", self.settings_dir)
224 })?;
225
226 for entry in entries {
227 let entry = entry?;
228 let path = entry.path();
229
230 if !path.is_file() {
231 continue;
232 }
233
234 if let Some(profile_name) = Self::infer_profile_from_path(&path) {
235 profiles.push(profile_name);
236 }
237 }
238
239 profiles.sort();
240 Ok(profiles)
241 }
242
243 pub fn path(&self) -> &Path {
245 &self.settings_path
246 }
247
248 pub fn get_module_config<T: Default + DeserializeOwned>(&self, namespace: &str) -> T {
250 self.inner
251 .lock()
252 .unwrap()
253 .modules
254 .get(namespace)
255 .and_then(|v| {
256 serde_json::from_value(v.clone())
257 .map_err(|e| tracing::warn!("Failed to parse module config '{namespace}': {e}"))
258 .ok()
259 })
260 .unwrap_or_default()
261 }
262
263 pub fn set_module_config<T: Serialize>(&self, namespace: &str, value: T) {
264 if let Ok(json_value) = serde_json::to_value(value) {
265 self.inner
266 .lock()
267 .unwrap()
268 .modules
269 .insert(namespace.to_string(), json_value);
270 }
271 }
272}