Skip to main content

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.
225///
226/// # Returns
227///
228/// Nothing.
229pub fn set_in_memory_settings(settings: AppSettings) {
230    let lock = Settings::get_settings_lock();
231    let mut write_guard = lock.write().expect("Failed to acquire settings write lock");
232    *write_guard = settings;
233}