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(®ex_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}