rust_kanban/io/
data_handler.rs

1use crate::{
2    app::{
3        kanban::{Board, Boards},
4        AppConfig,
5    },
6    constants::{
7        CONFIG_DIR_NAME, CONFIG_FILE_NAME, SAVE_DIR_NAME, SAVE_FILE_NAME, SAVE_FILE_REGEX,
8        THEME_DIR_NAME, THEME_FILE_NAME,
9    },
10    inputs::key::Key,
11    io::io_handler::{get_config_dir, make_file_system_safe_name, prepare_config_dir},
12    ui::theme::Theme,
13};
14use log::{debug, error, info};
15use regex::Regex;
16use serde::{Deserialize, Serialize};
17use std::{cmp::Ordering, collections::HashMap, env, fs, path::PathBuf};
18
19pub fn get_config(ignore_overlapped_keybindings: bool) -> Result<AppConfig, String> {
20    let config_dir_status = get_config_dir();
21    let config_dir = if let Ok(config_dir) = config_dir_status {
22        config_dir
23    } else {
24        return Err(config_dir_status.unwrap_err());
25    };
26    let config_path = config_dir.join(CONFIG_FILE_NAME);
27    let config = match fs::read_to_string(config_path) {
28        Ok(config_json_string) => {
29            let serde_value = serde_json::from_str(&config_json_string);
30            if let Ok(app_config) = serde_value {
31                app_config
32            } else {
33                let parsed_config = AppConfig::from_json_string(&config_json_string);
34                if let Ok(parsed_config) = parsed_config {
35                    match write_config(&parsed_config) {
36                        Ok(_) => parsed_config,
37                        Err(e) => {
38                            error!("Error writing config file: {}", e);
39                            AppConfig::default()
40                        }
41                    }
42                } else {
43                    debug!(
44                        "Error parsing config from json: {}",
45                        parsed_config.unwrap_err()
46                    );
47                    write_default_config();
48                    AppConfig::default()
49                }
50            }
51        }
52        Err(_) => {
53            write_default_config();
54            AppConfig::default()
55        }
56    };
57    let config_keybindings = config.keybindings.clone();
58    if ignore_overlapped_keybindings {
59        return Ok(config);
60    }
61    let mut key_count_map: HashMap<Key, u16> = HashMap::new();
62    for (_, value) in config_keybindings.iter() {
63        for key in value.iter() {
64            let key_count = key_count_map.entry(*key).or_insert(0);
65            *key_count += 1;
66        }
67    }
68    let mut overlapped_keys: Vec<Key> = Vec::new();
69    for (key, count) in key_count_map.iter() {
70        if *count > 1 {
71            overlapped_keys.push(*key);
72        }
73    }
74    if !overlapped_keys.is_empty() {
75        let mut overlapped_keys_str = String::new();
76        for key in overlapped_keys.iter() {
77            overlapped_keys_str.push_str(&format!("{:?}, ", key));
78        }
79        return Err(format!(
80            "Overlapped keybindings found: {}",
81            overlapped_keys_str
82        ));
83    }
84    Ok(config)
85}
86
87pub fn write_config(config: &AppConfig) -> Result<(), String> {
88    let config_str = serde_json::to_string_pretty(&config).unwrap();
89    prepare_config_dir()?;
90    let config_dir = get_config_dir()?;
91    let write_result = fs::write(config_dir.join(CONFIG_FILE_NAME), config_str);
92    match write_result {
93        Ok(_) => Ok(()),
94        Err(e) => {
95            debug!("Error writing config file: {}", e);
96            Err("Error writing config file".to_string())
97        }
98    }
99}
100
101pub fn reset_config() {
102    let config = AppConfig::default();
103    let write_config_status = write_config(&config);
104    if write_config_status.is_err() {
105        error!(
106            "Error writing config file: {}",
107            write_config_status.unwrap_err()
108        );
109    }
110}
111
112pub fn save_kanban_state_locally(boards: Vec<Board>, config: &AppConfig) -> Result<(), String> {
113    let files = fs::read_dir(&config.save_directory);
114    if files.is_err() {
115        return Err("Error reading save directory".to_string());
116    }
117    let files = files.unwrap();
118    let mut version = 1;
119    for file in files {
120        if file.is_err() {
121            continue;
122        }
123        let file = file.unwrap();
124        let file_name = file.file_name().into_string().unwrap();
125        if file_name.contains(SAVE_FILE_NAME)
126            && file_name.contains(chrono::Local::now().format("%d-%m-%Y").to_string().as_str())
127        {
128            let file_version = file_name.split('_').last();
129            if let Some(file_version) = file_version {
130                let file_version = file_version.replace('v', "");
131                let file_version = file_version.replace(".json", "");
132                let file_version = file_version.parse::<u32>();
133                if let Ok(file_version) = file_version {
134                    match file_version.cmp(&version) {
135                        Ordering::Greater => {
136                            version = file_version;
137                            version += 1;
138                        }
139                        Ordering::Equal => {
140                            version += 1;
141                        }
142                        Ordering::Less => {}
143                    }
144                } else {
145                    debug!(
146                        "Error parsing version number: {}",
147                        file_version.unwrap_err()
148                    );
149                    continue;
150                }
151            }
152        }
153    }
154    let file_name = format!(
155        "{}_{}_v{}.json",
156        SAVE_FILE_NAME,
157        chrono::Local::now().format("%d-%m-%Y"),
158        version
159    );
160    match export_kanban_to_json(&boards, config, file_name) {
161        Ok(_) => Ok(()),
162        Err(e) => Err(e),
163    }
164}
165
166pub fn get_local_kanban_state(
167    file_name: String,
168    preview_mode: bool,
169    config: &AppConfig,
170) -> Result<Boards, String> {
171    let file_path = config.save_directory.join(file_name);
172    if !preview_mode {
173        info!("Loading local save file: {:?}", file_path);
174    }
175    let file = fs::File::open(file_path);
176    if file.is_err() {
177        debug!("Error opening save file: {}", file.err().unwrap());
178        return Err("Error opening save file".to_string());
179    }
180    let file = file.unwrap();
181    let serde_object = serde_json::from_reader(file);
182    if serde_object.is_err() {
183        debug!("Error parsing save file: {}", serde_object.err().unwrap());
184        return Err("Error parsing save file".to_string());
185    }
186    let serde_object: serde_json::Value = serde_object.unwrap();
187    let boards = serde_object.get("boards");
188    if boards.is_none() {
189        debug!("Error parsing save file, no boards found");
190        return Err("Error parsing save file".to_string());
191    }
192    let boards = boards.unwrap();
193    let boards = boards.as_array();
194    if boards.is_none() {
195        debug!("Error parsing save file, boards is not an array");
196        return Err("Error parsing save file".to_string());
197    }
198    let boards = boards.unwrap();
199    let mut parsed_boards = Vec::new();
200    for board in boards {
201        let parsed_board = Board::from_json(board)?;
202        parsed_boards.push(parsed_board);
203    }
204    Ok(Boards::from(parsed_boards))
205}
206
207pub fn get_available_local_save_files(config: &AppConfig) -> Option<Vec<String>> {
208    let read_dir_status = fs::read_dir(&config.save_directory);
209    match read_dir_status {
210        Ok(files) => {
211            let mut save_files = Vec::new();
212            for file in files {
213                let file = file.unwrap();
214                let file_name = file.file_name().into_string().unwrap();
215                save_files.push(file_name);
216            }
217            let re = Regex::new(SAVE_FILE_REGEX).unwrap();
218
219            save_files.retain(|file| re.is_match(file));
220            save_files.sort_by(|a, b| {
221                let a_date = a.split('_').nth(1).unwrap();
222                let b_date = b.split('_').nth(1).unwrap();
223                let a_version = a.split('_').nth(2).unwrap();
224                let b_version = b.split('_').nth(2).unwrap();
225                let a_date = chrono::NaiveDate::parse_from_str(a_date, "%d-%m-%Y").unwrap();
226                let b_date = chrono::NaiveDate::parse_from_str(b_date, "%d-%m-%Y").unwrap();
227                let a_version = a_version
228                    .split('v')
229                    .nth(1)
230                    .unwrap()
231                    .replace(".json", "")
232                    .parse::<u32>()
233                    .unwrap();
234                let b_version = b_version
235                    .split('v')
236                    .nth(1)
237                    .unwrap()
238                    .replace(".json", "")
239                    .parse::<u32>()
240                    .unwrap();
241                if a_date > b_date {
242                    std::cmp::Ordering::Greater
243                } else if a_date < b_date {
244                    std::cmp::Ordering::Less
245                } else if a_version > b_version {
246                    std::cmp::Ordering::Greater
247                } else if a_version < b_version {
248                    std::cmp::Ordering::Less
249                } else {
250                    std::cmp::Ordering::Equal
251                }
252            });
253            Some(save_files)
254        }
255        Err(_) => {
256            let default_save_path = env::temp_dir().join(SAVE_DIR_NAME);
257            let dir_creation_status = fs::create_dir_all(&default_save_path);
258            match dir_creation_status {
259                Ok(_) => {
260                    info!(
261                        "Could not find save directory, created default save directory at: {:?}",
262                        default_save_path
263                    );
264                }
265                Err(e) => {
266                    error!("Could not find save directory and could not create default save directory at: {:?}, error: {}", default_save_path, e);
267                }
268            }
269            None
270        }
271    }
272}
273
274pub fn export_kanban_to_json(
275    boards: &[Board],
276    config: &AppConfig,
277    file_name: String,
278) -> Result<String, String> {
279    let version = env!("CARGO_PKG_VERSION");
280    let date = format!(
281        "{} ({})",
282        chrono::Local::now().format(config.date_time_format.to_parser_string()),
283        config.date_time_format.to_human_readable_string()
284    );
285    let export_struct = ExportStruct {
286        boards: boards.to_vec(),
287        export_date: date,
288        kanban_version: version.to_string(),
289    };
290    let file_path = config.save_directory.join(file_name);
291    let write_status = fs::write(
292        file_path.clone(),
293        serde_json::to_string_pretty(&export_struct).unwrap(),
294    );
295    match write_status {
296        Ok(_) => Ok(file_path.to_str().unwrap().to_string()),
297        Err(e) => Err(e.to_string()),
298    }
299}
300
301pub fn get_default_save_directory() -> PathBuf {
302    let mut default_save_path = env::temp_dir();
303    default_save_path.push(SAVE_DIR_NAME);
304    default_save_path
305}
306
307fn get_theme_dir() -> Result<PathBuf, String> {
308    let home_dir = home::home_dir();
309    if home_dir.is_none() {
310        return Err(String::from("Error getting home directory"));
311    }
312    let mut theme_dir = home_dir.unwrap();
313    if cfg!(windows) {
314        theme_dir.push("AppData");
315        theme_dir.push("Roaming");
316    } else {
317        theme_dir.push(".config");
318    }
319    theme_dir.push(CONFIG_DIR_NAME);
320    theme_dir.push(THEME_DIR_NAME);
321    Ok(theme_dir)
322}
323
324pub fn get_saved_themes() -> Option<Vec<Theme>> {
325    let theme_dir = get_theme_dir();
326    if theme_dir.is_err() {
327        return None;
328    }
329    let theme_dir = theme_dir.unwrap();
330    let read_dir_status = fs::read_dir(&theme_dir);
331    let file_prefix = format!("{}_", THEME_FILE_NAME);
332    let regex_str = format!("^{}.*\\.json$", file_prefix);
333    let re = Regex::new(&regex_str).unwrap();
334    match read_dir_status {
335        Ok(files) => {
336            let mut themes = Vec::new();
337            for file in files {
338                let file = file.unwrap();
339                let file_name = file.file_name().into_string().unwrap();
340                if re.is_match(&file_name) {
341                    let file_path = theme_dir.join(file_name);
342                    let read_status = fs::read_to_string(file_path);
343                    if read_status.is_err() {
344                        continue;
345                    }
346                    let read_status = read_status.unwrap();
347                    let theme: Theme = serde_json::from_str(&read_status).unwrap();
348                    themes.push(theme);
349                }
350            }
351            Some(themes)
352        }
353        Err(_) => None,
354    }
355}
356
357pub fn save_theme(theme: Theme) -> Result<String, String> {
358    let theme_dir = get_theme_dir()?;
359    let create_dir_status = fs::create_dir_all(&theme_dir);
360    if let Err(e) = create_dir_status {
361        return Err(e.to_string());
362    }
363    let theme_name = format!(
364        "{}_{}.json",
365        THEME_FILE_NAME,
366        make_file_system_safe_name(&theme.name)
367    );
368    let theme_path = theme_dir.join(theme_name);
369    let write_status = fs::write(
370        theme_path.clone(),
371        serde_json::to_string_pretty(&theme).unwrap(),
372    );
373    if let Err(write_status) = write_status {
374        return Err(write_status.to_string());
375    }
376    Ok(theme_path.to_str().unwrap().to_string())
377}
378
379fn write_default_config() {
380    let config = AppConfig::default();
381    let write_config_status = write_config(&config);
382    if write_config_status.is_err() {
383        error!("{}", write_config_status.unwrap_err());
384    }
385}
386
387#[derive(Serialize, Deserialize, Debug)]
388pub struct ExportStruct {
389    pub boards: Vec<Board>,
390    pub export_date: String,
391    pub kanban_version: String,
392}