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 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
87pub 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
115pub fn lerp_between(
117 color_a: (u8, u8, u8),
118 color_b: (u8, u8, u8),
119 normalized_time: f32,
120) -> (u8, u8, u8) {
121 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
129pub 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
147pub 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
255pub 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}