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