jsonette/settings.rs
1/*
2 * Copyright (c) 2026 DevEtte.
3 *
4 * This project is dual-licensed under both the MIT License and the
5 * Apache License, Version 2.0 (the "License"). You may not use this
6 * file except in compliance with one of these licenses.
7 *
8 * You may obtain a copy of the Licenses at:
9 * - MIT: https://opensource.org
10 * - Apache 2.0: http://apache.org
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19//! Thread-safe global settings singleton and configuration cache manager.
20
21use crate::types::AppSettings;
22use std::fs;
23use std::path::PathBuf;
24use std::sync::{OnceLock, RwLock};
25
26/// Global static container holding the active application preferences in-memory.
27/// Wrapped in `OnceLock` for safe initialization and `RwLock` for thread-safe read/write access.
28static SETTINGS: OnceLock<RwLock<AppSettings>> = OnceLock::new();
29
30/// Singleton manager for system settings matching the StarUML architecture model.
31#[derive(Debug, Clone, Copy)]
32pub struct Settings {
33 /// The active configuration preferences.
34 pub settings: AppSettings,
35}
36
37impl Settings {
38 /// Returns the global singleton instance of the `Settings` struct.
39 /// Resolves preferences lazily from disk on the first invocation.
40 ///
41 /// # Returns
42 ///
43 /// A `Settings` struct containing a copy of the active application settings.
44 pub fn get_settings() -> Self {
45 let lock = Self::get_settings_lock();
46 let read_guard = lock.read().expect("Failed to acquire settings read lock");
47 Self {
48 settings: *read_guard,
49 }
50 }
51
52 /// Returns a reference to the global thread-safe RwLock container.
53 /// Initializes lazily on the first request by trying to load settings from disk.
54 /// Falls back to defaults if parsing or loading fails.
55 ///
56 /// # Returns
57 ///
58 /// A reference to the static RwLock containing the configuration.
59 pub fn get_settings_lock() -> &'static RwLock<AppSettings> {
60 SETTINGS.get_or_init(|| {
61 let settings = Self::load_settings_from_disk().unwrap_or_else(|_| {
62 let defaults = AppSettings::default();
63 let _ = Self::save_settings_to_disk(&defaults);
64 defaults
65 });
66 RwLock::new(settings)
67 })
68 }
69
70 /// Retrieves the home directory path in a platform-independent way.
71 /// On Unix/macOS, reads the `HOME` environment variable.
72 /// On Windows, reads `USERPROFILE`, falling back to `HOMEDRIVE` + `HOMEPATH`.
73 ///
74 /// # Returns
75 ///
76 /// An optional `PathBuf` pointing to the user's home folder.
77 #[allow(dead_code)]
78 fn get_home_dir() -> Option<PathBuf> {
79 #[cfg(target_os = "windows")]
80 {
81 std::env::var("USERPROFILE")
82 .ok()
83 .or_else(|| {
84 let drive = std::env::var("HOMEDRIVE").ok()?;
85 let path = std::env::var("HOMEPATH").ok()?;
86 Some(format!("{}{}", drive, path))
87 })
88 .map(PathBuf::from)
89 }
90 #[cfg(not(target_os = "windows"))]
91 {
92 std::env::var("HOME").ok().map(PathBuf::from)
93 }
94 }
95
96 /// Returns the absolute path to the settings file on disk.
97 /// Resolves to the user's home folder in normal execution, or to a process-isolated file
98 /// in the system temporary directory during tests to avoid concurrent test collision.
99 ///
100 /// # Returns
101 ///
102 /// An optional `PathBuf` pointing to the settings file.
103 fn get_settings_path() -> Option<PathBuf> {
104 let is_test = cfg!(test)
105 || std::env::current_exe()
106 .map(|p| {
107 let s = p.to_string_lossy();
108 s.contains("/deps/") || s.contains("\\deps\\")
109 })
110 .unwrap_or(false);
111
112 if is_test {
113 let mut path = std::env::temp_dir();
114 path.push(format!(
115 "jsonette_test_settings_{}.json",
116 std::process::id()
117 ));
118 Some(path)
119 } else {
120 #[cfg(target_os = "windows")]
121 {
122 let mut path = std::env::var("LOCALAPPDATA")
123 .ok()
124 .map(PathBuf::from)
125 .or_else(|| {
126 let mut home = Self::get_home_dir()?;
127 home.push("AppData");
128 home.push("Local");
129 Some(home)
130 })?;
131 path.push("jsonette");
132 path.push("settings.json");
133 Some(path)
134 }
135 #[cfg(not(target_os = "windows"))]
136 {
137 let mut path = std::env::var("XDG_CONFIG_HOME")
138 .ok()
139 .map(PathBuf::from)
140 .or_else(|| {
141 let mut home = Self::get_home_dir()?;
142 home.push(".config");
143 Some(home)
144 })?;
145 path.push("jsonette");
146 path.push("settings.json");
147 Some(path)
148 }
149 }
150 }
151
152 /// Reads the configuration file from disk and parses it.
153 /// If the file does not exist, writes the default options to disk first and returns them.
154 ///
155 /// # Returns
156 ///
157 /// A `Result` containing the loaded `AppSettings` or an error string.
158 fn load_settings_from_disk() -> Result<AppSettings, String> {
159 let path = Self::get_settings_path()
160 .ok_or_else(|| "Unable to locate home directory".to_string())?;
161 if !path.exists() {
162 let defaults = AppSettings::default();
163 Self::save_settings_to_disk(&defaults)?;
164 return Ok(defaults);
165 }
166 let content = fs::read_to_string(&path)
167 .map_err(|e| format!("Failed to read settings file: {}", e))?;
168 let settings: AppSettings = serde_json::from_str(&content)
169 .map_err(|e| format!("Failed to parse settings JSON: {}", e))?;
170 Ok(settings)
171 }
172
173 /// Serializes and writes the provided configuration options to disk.
174 ///
175 /// # Arguments
176 ///
177 /// * `settings` - A reference to the `AppSettings` structure to write to disk.
178 ///
179 /// # Returns
180 ///
181 /// A `Result` indicating success or containing an error string.
182 fn save_settings_to_disk(settings: &AppSettings) -> Result<(), String> {
183 let path = Self::get_settings_path()
184 .ok_or_else(|| "Unable to locate home directory".to_string())?;
185 if let Some(parent) = path.parent() {
186 fs::create_dir_all(parent)
187 .map_err(|e| format!("Failed to create settings directory: {}", e))?;
188 }
189 let content = serde_json::to_string_pretty(settings)
190 .map_err(|e| format!("Failed to serialize settings: {}", e))?;
191 fs::write(&path, content).map_err(|e| format!("Failed to write settings file: {}", e))?;
192 Ok(())
193 }
194}
195
196/// Retrieves a copy of the current global configuration settings.
197/// Resolves preferences lazily from disk on the first invocation.
198///
199/// # Returns
200///
201/// A copied `AppSettings` struct reflecting the active preferences.
202pub fn get_settings() -> AppSettings {
203 Settings::get_settings().settings
204}
205
206/// Updates the in-memory preferences and persists the changes to disk.
207///
208/// # Arguments
209///
210/// * `settings` - The new `AppSettings` configuration to apply globally.
211///
212/// # Returns
213///
214/// A `Result` indicating success or containing an error string.
215pub fn update_settings(settings: AppSettings) -> Result<(), String> {
216 let lock = Settings::get_settings_lock();
217 {
218 let mut write_guard = lock.write().expect("Failed to acquire settings write lock");
219 *write_guard = settings;
220 }
221 Settings::save_settings_to_disk(&settings)
222}
223
224/// Updates the in-memory preferences without persisting them to disk.
225pub fn set_in_memory_settings(settings: AppSettings) {
226 let lock = Settings::get_settings_lock();
227 let mut write_guard = lock.write().expect("Failed to acquire settings write lock");
228 *write_guard = settings;
229}