thisweek_core/
config.rs

1use crate::calendar::Calendar;
2use crate::calendar::CalendarLanguagePair;
3use crate::db_sqlite;
4use crate::language::Language;
5use crate::prelude::Error as AppError;
6use crate::prelude::Result as AppResult;
7use arc_swap::ArcSwap;
8use once_cell::sync::OnceCell;
9use serde::{Deserialize, Serialize};
10use std::path::Path;
11use std::sync::Arc;
12use std::{fs, path::PathBuf};
13
14// global ref to static value
15// static CONFIG: OnceCell<Config> = OnceCell::new();
16pub static CONFIG: OnceCell<ArcSwap<Config>> = OnceCell::new();
17
18pub fn set_config(new_cfg: Config) {
19    if CONFIG.get().is_none() {
20        CONFIG.set(ArcSwap::from_pointee(new_cfg.clone())).unwrap();
21    } else {
22        CONFIG.get().unwrap().store(Arc::new(new_cfg.clone()));
23    }
24}
25
26pub fn reload_config_file() {
27    println!("reloading config file...");
28    let config_path = get_config_path();
29    if let Ok(config) = load_from_filepath(config_path) {
30        set_config(config);
31        println!("success.");
32    } else {
33        println!("failed.");
34    }
35}
36
37pub fn read_config_file_or_save_default_config_file() -> Result<Config, AppError> {
38    let config_path = get_config_path();
39
40    if let Ok(config) = load_from_filepath(config_path) {
41        println!("config file available and ok");
42        println!("config: {config:?}");
43        Ok(config)
44    } else {
45        // no file or syntax err.
46        // create new config file with defaults
47        println!("no config file or syntax error!");
48        let default_config = Config::default();
49        println!("saving default config: {default_config:?}");
50        save_config(default_config.clone())?;
51        // return the default config
52        println!("save successful.");
53        Ok(default_config)
54    }
55}
56
57pub fn check_database_file_is_valid_or_create_database_file(db_path: &str) -> Result<(), AppError> {
58    if db_sqlite::is_correct_db(db_path) {
59        Ok(())
60    } else {
61        db_sqlite::create_db(db_path)
62    }
63}
64
65pub fn get_config() -> Config {
66    let gaurd = CONFIG.get_or_init(|| {
67        println!("Init CONFIG (global OnceCell first run init)");
68        // note: here we crash! we can not read nor we can save!
69        let new_cfg = read_config_file_or_save_default_config_file().unwrap();
70        // note: here we crash! we need to make sure the database path directory exists
71        if let Some(parent) = Path::new(&new_cfg.database).parent() {
72            fs::create_dir_all(parent)
73                .map_err(|_| AppError::DatabaseFileCreateError)
74                .unwrap();
75        }
76
77        // note: for now, only checking if the directory exists, because db can not create and gives
78        // error later!
79        // check if the database is valid, if not create one, if error, crash.
80        // check_database_file_is_valid_or_create_database_file(&new_cfg.database).unwrap();
81        ArcSwap::from_pointee(new_cfg)
82    });
83    let gaurd = gaurd.load();
84    gaurd.get_copy()
85}
86
87pub fn move_database<P: AsRef<str>>(filepath: P) -> Result<(), AppError> {
88    let filepath = filepath.as_ref();
89    let mut config = get_config();
90    let current_db_path = config.database;
91    let current_db_valid = db_sqlite::is_correct_db(&current_db_path);
92    let exists = Path::new(filepath).exists();
93    if !exists {
94        if !current_db_valid {
95            // create new db
96            println!("Attempting to creating new database file: {}", filepath);
97            db_sqlite::create_db(filepath)
98        } else {
99            // move current db
100            // ensure target directory exists
101            if let Some(parent) = Path::new(filepath).parent() {
102                fs::create_dir_all(parent).map_err(|_| AppError::DatabaseFileCopyError)?;
103            }
104            // copy database file
105            std::fs::copy(&current_db_path, filepath)
106                .map_err(|_| AppError::DatabaseFileCopyError)?;
107            // change and save config
108            config.database = filepath.to_string();
109            set_config(config.clone());
110            save_config(config)?;
111            // delete original database file
112            std::fs::remove_file(&current_db_path)
113                .map(|_| ())
114                .map_err(|_| AppError::DatabaseFileRemoveError)
115        }
116    } else {
117        Err(AppError::DatabaseFileNotEmptyError)
118    }
119}
120
121pub fn open_database<P: AsRef<str>>(filepath: P) -> Result<(), AppError> {
122    let filepath = filepath.as_ref();
123    let mut config = get_config();
124    let exists = Path::new(filepath).exists();
125    if !exists {
126        Err(AppError::DatabaseFileDontExistsError)
127    } else {
128        let db_valid = db_sqlite::is_correct_db(filepath);
129        if !db_valid {
130            Err(AppError::DatabaseFileInvalidError)
131        } else {
132            // switch database
133            // change and save config
134            config.database = filepath.to_string();
135            set_config(config.clone());
136            save_config(config)
137        }
138    }
139}
140
141/// check if the new filepath exists or not
142/// if exists, it should be a valid database and we only switch to it.
143/// if the path don't exists, we will move our database to that location.
144pub fn set_database_file(filepath: String) -> Result<(), AppError> {
145    let mut config = get_config();
146    let current_db_path = config.database;
147    let current_db_valid = db_sqlite::is_correct_db(&current_db_path);
148    let exists = Path::new(&filepath).exists();
149    let valid = db_sqlite::is_correct_db(&filepath);
150    if !exists {
151        if !current_db_valid {
152            // create new db
153            println!("Attempting to creating new database file: {}", filepath);
154            db_sqlite::create_db(&filepath)
155        } else {
156            // move current db
157            // ensure target directory exists
158            if let Some(parent) = Path::new(&filepath).parent() {
159                fs::create_dir_all(parent).map_err(|_| AppError::DatabaseFileCopyError)?;
160            }
161            // copy database file
162            std::fs::copy(&current_db_path, &filepath)
163                .map_err(|_| AppError::DatabaseFileCopyError)?;
164            // change and save config
165            config.database = filepath;
166            set_config(config.clone());
167            save_config(config)?;
168            // delete original database file
169            std::fs::remove_file(&current_db_path)
170                .map(|_| ())
171                .map_err(|_| AppError::DatabaseFileRemoveError)
172        }
173    } else if exists && valid {
174        // switch database
175        // change and save config
176        config.database = filepath;
177        set_config(config.clone());
178        save_config(config)
179    } else {
180        Err(AppError::DatabaseFileInvalidError)
181    }
182}
183
184pub fn set_main_cal_config(
185    main_calendar_type: String,
186    main_calendar_language: String,
187    main_calendar_start_weekday: String,
188    weekdates_display_direction: String,
189) -> Result<(), AppError> {
190    let mut config = get_config();
191    config.main_calendar_type = main_calendar_type;
192    config.main_calendar_language = main_calendar_language;
193    config.main_calendar_start_weekday = main_calendar_start_weekday;
194    config.weekdates_display_direction = weekdates_display_direction;
195    set_config(config.clone());
196    save_config(config)
197}
198
199pub fn set_secondary_cal_config(
200    secondary_calendar_type: Option<String>,
201    secondary_calendar_language: Option<String>,
202) -> Result<(), AppError> {
203    let mut config = get_config();
204    config.secondary_calendar_type = secondary_calendar_type;
205    config.secondary_calendar_language = secondary_calendar_language;
206    set_config(config.clone());
207    save_config(config)
208}
209
210pub fn set_items_display_direction_config(items_direction: String) -> Result<(), AppError> {
211    let mut config = get_config();
212    config.items_display_direction = items_direction;
213    set_config(config.clone());
214    save_config(config)
215}
216
217pub fn save_config(config: Config) -> Result<(), AppError> {
218    let toml_str = toml::to_string(&config).map_err(|e| {
219        println!("Failed to serialize config to TOML: {}", e);
220        AppError::ConfigTomlGenerateError
221    })?;
222
223    let config_path = get_config_path();
224    println!(
225        "Attempting to save config to: {}",
226        config_path.to_string_lossy()
227    );
228
229    // Ensure the parent directory exists
230    if let Some(parent) = config_path.parent() {
231        println!("Creating directory if needed: {}", parent.to_string_lossy());
232        fs::create_dir_all(parent).map_err(|e| {
233            println!("Failed to create directory: {}", e);
234            AppError::ConfigFileSaveError
235        })?;
236    }
237
238    // Write the config file
239    fs::write(&config_path, toml_str).map_err(|e| {
240        println!("Failed to write config file: {}", e);
241        println!("Path: {}", config_path.to_string_lossy());
242        AppError::ConfigFileSaveError
243    })
244}
245
246pub fn get_main_cal_lang_pair() -> CalendarLanguagePair {
247    let calendar: Calendar = get_config().main_calendar_type.into();
248    let language: Language = get_config().main_calendar_language.into();
249    CalendarLanguagePair { calendar, language }
250}
251
252pub fn get_second_cal_lang_pair() -> Option<CalendarLanguagePair> {
253    get_config().secondary_calendar_type.map(|cal| {
254        let language: Language = get_config()
255            .secondary_calendar_language
256            .unwrap_or_default()
257            .into();
258        let calendar: Calendar = cal.into();
259        CalendarLanguagePair { calendar, language }
260    })
261}
262
263fn default_config_data_path() -> AppResult<(PathBuf, PathBuf)> {
264    // Retrieve project-specific directories
265    if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "ThisWeek") {
266        // Config directory: ~/.config/ThisWeek on Linux, ~/Library/Application Support/ThisWeek on macOS, and %AppData%\ThisWeek on Windows
267        let config_path = proj_dirs.config_dir().join("config.toml");
268        // println!("Config directory: {}", config_dir.display());
269
270        // Data directory: similar to config_dir but used for storing database and other data files
271        let data_path = proj_dirs.data_dir().join("thisweek.db");
272        // println!("Data directory: {}", data_dir.display());
273        Ok((config_path, data_path))
274    } else {
275        eprintln!("Could not determine project directories!");
276        Err(AppError::DefaultAppPathError)
277    }
278}
279
280fn load_from_filepath(path: PathBuf) -> AppResult<Config> {
281    println!("reading config file {}...", path.to_string_lossy());
282    if let Ok(config) = fs::read_to_string(path) {
283        let config = toml::from_str(&config).map_err(AppError::ConfigSyntaxError)?;
284        Ok(config)
285    } else {
286        Err(AppError::ConfigNotFoundError)
287    }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct Config {
292    pub database: String,
293    pub main_calendar_type: String,
294    pub main_calendar_language: String,
295    pub main_calendar_start_weekday: String,
296    pub secondary_calendar_type: Option<String>,
297    pub secondary_calendar_language: Option<String>,
298    pub weekdates_display_direction: String,
299    pub items_display_direction: String,
300}
301
302impl Config {
303    pub fn get_copy(&self) -> Config {
304        Config {
305            database: self.database.clone(),
306            main_calendar_type: self.main_calendar_type.clone(),
307            main_calendar_language: self.main_calendar_language.clone(),
308            main_calendar_start_weekday: self.main_calendar_start_weekday.clone(),
309            secondary_calendar_type: self.secondary_calendar_type.clone(),
310            secondary_calendar_language: self.secondary_calendar_language.clone(),
311            weekdates_display_direction: self.weekdates_display_direction.clone(),
312            items_display_direction: self.items_display_direction.clone(),
313        }
314    }
315}
316
317impl Default for Config {
318    fn default() -> Self {
319        let data_path = default_config_data_path()
320            .unwrap()
321            .1
322            .to_string_lossy()
323            .into_owned();
324        Self {
325            database: data_path,
326            main_calendar_type: "Gregorian".into(),
327            main_calendar_language: "en".into(),
328            main_calendar_start_weekday: "MON".into(),
329            secondary_calendar_type: None,
330            secondary_calendar_language: None,
331            weekdates_display_direction: "ltr".into(),
332            items_display_direction: "auto".into(),
333        }
334    }
335}
336
337pub fn get_config_path() -> PathBuf {
338    default_config_data_path().unwrap().0
339}