rust_kanban/
util.rs

1use crate::{
2    app::{App, AppReturn, DateTimeFormat},
3    constants::{ENCRYPTION_KEY_FILE_NAME, FIELD_NOT_SET},
4    inputs::{events::Events, InputEvent},
5    io::{
6        data_handler::reset_config,
7        io_handler::{
8            delete_a_save_from_database, generate_new_encryption_key,
9            get_all_save_ids_and_creation_dates_for_user, get_config_dir, login_for_user,
10            save_user_encryption_key,
11        },
12        IoEvent,
13    },
14    ui::ui_main,
15};
16use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime};
17use crossterm::{event::EnableMouseCapture, execute};
18use eyre::Result;
19use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal};
20use std::{borrow::Cow, io::stdout, sync::Arc, time::Duration};
21use tokio::time::Instant;
22
23pub async fn start_ui(app: &Arc<tokio::sync::Mutex<App<'_>>>) -> Result<()> {
24    crossterm::terminal::enable_raw_mode()?;
25    {
26        let app = app.lock().await;
27        if app.config.enable_mouse_support {
28            execute!(stdout(), EnableMouseCapture)?;
29        }
30    }
31    let my_stdout = stdout();
32    let backend = CrosstermBackend::new(my_stdout);
33    let mut terminal = Terminal::new(backend)?;
34    terminal.clear()?;
35    terminal.hide_cursor()?;
36
37    let mut events = {
38        let tick_rate = app.lock().await.config.tickrate;
39        Events::new(Duration::from_millis(tick_rate as u64))
40    };
41
42    {
43        let mut app = app.lock().await;
44        app.dispatch(IoEvent::Initialize).await;
45    }
46
47    loop {
48        let mut app = app.lock().await;
49        let render_start_time = Instant::now();
50        terminal.draw(|rect| ui_main::draw(rect, &mut app))?;
51        if app.state.ui_render_time.len() < 10 {
52            app.state
53                .ui_render_time
54                .push(render_start_time.elapsed().as_micros());
55        } else {
56            app.state.ui_render_time.remove(0);
57            app.state
58                .ui_render_time
59                .push(render_start_time.elapsed().as_micros());
60        }
61        // app.state.ui_render_time = Some(render_start_time.elapsed().as_micros());
62        let result = match events.next().await {
63            InputEvent::KeyBoardInput(key) => app.do_action(key).await,
64            InputEvent::MouseAction(mouse_action) => app.handle_mouse(mouse_action).await,
65            InputEvent::Tick => {
66                if app.state.previous_mouse_coordinates != app.state.current_mouse_coordinates {
67                    app.state.previous_mouse_coordinates = app.state.current_mouse_coordinates;
68                }
69                AppReturn::Continue
70            }
71        };
72        if result == AppReturn::Exit {
73            events.close();
74            break;
75        }
76    }
77
78    execute!(stdout(), crossterm::event::DisableMouseCapture)?;
79    terminal.clear()?;
80    terminal.set_cursor_position((0, 0))?;
81    terminal.show_cursor()?;
82    crossterm::terminal::disable_raw_mode()?;
83
84    Ok(())
85}
86
87/// Takes wrapped text and the current cursor position (1D) and the available space to return the x and y position of the cursor (2D)
88/// Will be replaced by a better algorithm/implementation in the future
89pub fn calculate_cursor_position(
90    text: Vec<Cow<str>>,
91    current_cursor_position: usize,
92    view_box: Rect,
93) -> (u16, u16) {
94    let wrapped_text_iter = text.iter();
95    let mut cursor_pos = current_cursor_position;
96
97    let mut x_pos = view_box.x + 1 + cursor_pos as u16;
98    let mut y_pos = view_box.y + 1;
99    for (i, line) in wrapped_text_iter.enumerate() {
100        x_pos = view_box.x + 1 + cursor_pos as u16;
101        y_pos = view_box.y + 1 + i as u16;
102        if cursor_pos <= line.chars().count() {
103            let x_pos = if x_pos > i as u16 {
104                x_pos - i as u16
105            } else {
106                x_pos
107            };
108            return (x_pos, y_pos);
109        }
110        cursor_pos -= line.chars().count();
111    }
112    (x_pos, y_pos)
113}
114
115/// function to lerp between rgb values of two colors
116pub fn lerp_between(
117    color_a: (u8, u8, u8),
118    color_b: (u8, u8, u8),
119    normalized_time: f32,
120) -> (u8, u8, u8) {
121    // clamp the normalized time between 0 and 1
122    let normalized_time = normalized_time.clamp(0.0, 1.0);
123    let r = (color_a.0 as f32 * (1.0 - normalized_time) + color_b.0 as f32 * normalized_time) as u8;
124    let g = (color_a.1 as f32 * (1.0 - normalized_time) + color_b.1 as f32 * normalized_time) as u8;
125    let b = (color_a.2 as f32 * (1.0 - normalized_time) + color_b.2 as f32 * normalized_time) as u8;
126    (r, g, b)
127}
128
129// parses a string to rgb values from a hex string
130pub fn parse_hex_to_rgb(hex_string: &str) -> Option<(u8, u8, u8)> {
131    if hex_string.len() != 7 {
132        return None;
133    }
134    if !hex_string.starts_with('#') {
135        return None;
136    }
137    let hex_string = hex_string.trim_start_matches('#');
138    let r = u8::from_str_radix(&hex_string[0..2], 16);
139    let g = u8::from_str_radix(&hex_string[2..4], 16);
140    let b = u8::from_str_radix(&hex_string[4..6], 16);
141    match (r, g, b) {
142        (Ok(r), Ok(g), Ok(b)) => Some((r, g, b)),
143        _ => None,
144    }
145}
146
147// TODO: Find a way to get the terminal background color
148pub fn get_term_bg_color() -> (u8, u8, u8) {
149    (0, 0, 0)
150}
151
152pub fn date_format_finder(date_string: &str) -> Result<DateTimeFormat, String> {
153    let all_formats_with_time = DateTimeFormat::all_formats_with_time();
154    for date_format in DateTimeFormat::get_all_date_formats() {
155        if all_formats_with_time.contains(&date_format) {
156            match NaiveDateTime::parse_from_str(date_string, date_format.to_parser_string()) {
157                Ok(_) => return Ok(date_format),
158                Err(_) => {
159                    continue;
160                }
161            }
162        } else {
163            match NaiveDate::parse_from_str(date_string, date_format.to_parser_string()) {
164                Ok(_) => return Ok(date_format),
165                Err(_) => {
166                    continue;
167                }
168            }
169        }
170    }
171    Err("Invalid date format".to_string())
172}
173
174pub fn date_format_converter(
175    date_string: &str,
176    date_format: DateTimeFormat,
177) -> Result<String, String> {
178    if date_string == FIELD_NOT_SET || date_string.is_empty() {
179        return Ok(date_string.to_string());
180    }
181    let given_date_format = date_format_finder(date_string)?;
182    if given_date_format == date_format {
183        return Ok(date_string.to_string());
184    }
185    let all_formats_with_time = DateTimeFormat::all_formats_with_time();
186    let all_formats_without_time = DateTimeFormat::all_formats_without_time();
187    if all_formats_with_time.contains(&given_date_format)
188        && all_formats_without_time.contains(&date_format)
189    {
190        let naive_date_time =
191            NaiveDateTime::parse_from_str(date_string, given_date_format.to_parser_string());
192        if let Ok(naive_date_time) = naive_date_time {
193            let naive_date = NaiveDate::from_ymd_opt(
194                naive_date_time.year(),
195                naive_date_time.month(),
196                naive_date_time.day(),
197            );
198            if let Some(naive_date) = naive_date {
199                return Ok(naive_date
200                    .format(date_format.to_parser_string())
201                    .to_string());
202            } else {
203                Err("Invalid date format".to_string())
204            }
205        } else {
206            Err("Invalid date format".to_string())
207        }
208    } else if all_formats_without_time.contains(&given_date_format)
209        && all_formats_with_time.contains(&date_format)
210    {
211        let naive_date =
212            NaiveDate::parse_from_str(date_string, given_date_format.to_parser_string());
213        if let Ok(naive_date) = naive_date {
214            let default_time = NaiveTime::from_hms_opt(0, 0, 0);
215            if let Some(default_time) = default_time {
216                let naive_date_time = NaiveDateTime::new(naive_date, default_time);
217                return Ok(naive_date_time
218                    .format(date_format.to_parser_string())
219                    .to_string());
220            } else {
221                Err("Invalid date format".to_string())
222            }
223        } else {
224            Err("Invalid date format".to_string())
225        }
226    } else if all_formats_with_time.contains(&given_date_format)
227        && all_formats_with_time.contains(&date_format)
228    {
229        let naive_date_time =
230            NaiveDateTime::parse_from_str(date_string, given_date_format.to_parser_string());
231        if let Ok(naive_date_time) = naive_date_time {
232            return Ok(naive_date_time
233                .format(date_format.to_parser_string())
234                .to_string());
235        } else {
236            Err("Invalid date format".to_string())
237        }
238    } else if all_formats_without_time.contains(&given_date_format)
239        && all_formats_without_time.contains(&date_format)
240    {
241        let naive_date =
242            NaiveDate::parse_from_str(date_string, given_date_format.to_parser_string());
243        if let Ok(naive_date) = naive_date {
244            return Ok(naive_date
245                .format(date_format.to_parser_string())
246                .to_string());
247        } else {
248            Err("Invalid date format".to_string())
249        }
250    } else {
251        Err("Invalid date format".to_string())
252    }
253}
254
255/// only to be used as a cli argument function
256pub async fn gen_new_key_main(email_id: String, password: String) -> Result<()> {
257    let mut previous_key_lost = false;
258    let mut key_default_path = get_config_dir().unwrap();
259    key_default_path.push(ENCRYPTION_KEY_FILE_NAME);
260    if key_default_path.exists() {
261        print_info(
262            "An encryption key already exists, are you sure you want to generate a new one? (y/n)",
263        );
264        println!("> ");
265        let mut input = String::new();
266        std::io::stdin().read_line(&mut input).unwrap();
267        let input = input.trim().to_lowercase();
268        if input == "y" || input == "yes" {
269            print_info("Preparing to generate new encryption key...");
270        } else {
271            print_info("Aborting...");
272            return Ok(());
273        }
274    } else {
275        print_warn(
276            "Previous encryption key not found, preparing to generate new encryption key...",
277        );
278        previous_key_lost = true;
279    }
280    print_info("Trying to login...");
281    let (access_token, user_id, _refresh_token) =
282        match login_for_user(&email_id, &password, false).await {
283            Ok((access_token, user_id, refresh_token)) => (access_token, user_id, refresh_token),
284            Err(err) => {
285                print_debug(&format!("Error logging in: {:?}", err));
286                print_error("Error logging in, please check your credentials and try again");
287                print_error("Aborting...");
288                return Ok(());
289            }
290        };
291    let save_ids =
292        get_all_save_ids_and_creation_dates_for_user(user_id.to_owned(), &access_token, true)
293            .await?;
294    if save_ids.is_empty() {
295        print_warn("No Cloud save files found");
296        print_info("Generating new encryption key...");
297        let key = generate_new_encryption_key();
298        match save_user_encryption_key(&key) {
299            Ok(save_location) => {
300                print_info("Encryption key generated and saved");
301                print_info(
302                    "Please keep this key safe as it will be required to access your save files",
303                );
304                print_info(&format!("New Key generated_at: {}", save_location));
305            }
306            Err(err) => {
307                print_error("Error saving encryption key");
308                print_debug(&format!("Error: {:?}", err));
309                return Ok(());
310            }
311        }
312    } else {
313        print_info(&format!("{} save files found", save_ids.len()));
314        if previous_key_lost {
315            print_warn(
316                "It seems like the previous encryption key was lost as it could not be found",
317            );
318        }
319        print_info("Cloud save files found:");
320        print_info("-------------------------");
321        for (i, save) in save_ids.iter().enumerate() {
322            print_info(&format!(
323                "{}) Cloud_save_{} - Created at (UTC) {}",
324                i + 1,
325                save.0,
326                save.1
327            ));
328        }
329        print_info("-------------------------");
330        print_warn("Input 'Y' to delete all the save files and generate a new encryption key");
331        print_info("or");
332        print_info(
333            format!(
334                "Input 'N' to find the encryption key yourself and move it to {}",
335                key_default_path.display()
336            )
337            .as_str(),
338        );
339        println!("> ");
340        let mut input = String::new();
341        std::io::stdin().read_line(&mut input).unwrap();
342        println!();
343        let input = input.trim().to_lowercase();
344        if input == "y" || input == "yes" {
345            for save_id in save_ids {
346                print_info(&format!("Deleting save file: {}", save_id.0));
347                let delete_status =
348                    delete_a_save_from_database(&access_token, true, save_id.2 as u64, None).await;
349                if delete_status.is_err() {
350                    print_error("Error deleting save file");
351                    print_debug(&format!("Error: {:?}", delete_status.err()));
352                    print_error("Aborting...");
353                    return Ok(());
354                }
355            }
356            print_info("All save files deleted");
357            print_info("Preparing to generate new encryption key...");
358            let key = generate_new_encryption_key();
359            match save_user_encryption_key(&key) {
360                Ok(save_location) => {
361                    print_info("Encryption key generated and saved");
362                    print_info(
363                        "Please keep this key safe as it will be required to access your save files",
364                    );
365                    print_info(&format!("New Key generated_at: {}", save_location));
366                }
367                Err(err) => {
368                    print_error("Error saving encryption key");
369                    print_debug(&format!("Error: {:?}", err));
370                    return Ok(());
371                }
372            }
373        } else {
374            print_info("Aborting...");
375            return Ok(());
376        }
377    }
378    Ok(())
379}
380
381pub fn reset_app_main() {
382    print_info("🚀 Resetting config");
383    reset_config();
384    print_info("👍 Config reset");
385}
386
387pub fn print_error(error: &str) {
388    bunt::println!("{$red}[ERROR]{/$} - {}", error);
389}
390
391pub fn print_info(info: &str) {
392    bunt::println!("{$cyan}[INFO]{/$}  - {}", info);
393}
394
395pub fn print_debug(debug: &str) {
396    if cfg!(debug_assertions) {
397        bunt::println!("{$green}[DEBUG]{/$} - {}", debug);
398    }
399}
400
401pub fn print_warn(warn: &str) {
402    bunt::println!("{$yellow}[WARN]{/$}  - {}", warn);
403}
404
405pub fn spaces(size: u8) -> &'static str {
406    const SPACES: &str = "                                                                                                                                                                                                                                                                ";
407    &SPACES[..size as usize]
408}
409
410pub fn num_digits(i: usize) -> u8 {
411    f64::log10(i as f64) as u8 + 1
412}
413
414pub fn replace_tabs(s: &str, tab_len: u8) -> Cow<'_, str> {
415    let tab = spaces(tab_len);
416    let mut buf = String::new();
417    for (i, c) in s.char_indices() {
418        if buf.is_empty() {
419            if c == '\t' {
420                buf.reserve(s.len());
421                buf.push_str(&s[..i]);
422                buf.push_str(tab);
423            }
424        } else if c == '\t' {
425            buf.push_str(tab);
426        } else {
427            buf.push(c);
428        }
429    }
430    if buf.is_empty() {
431        Cow::Borrowed(s)
432    } else {
433        Cow::Owned(buf)
434    }
435}