1use chrono::Local;
82use colour::{black_bold, blue_ln_bold, cyan, green, green_ln_bold, red, white, yellow_ln_bold};
83use log::{error, info, trace, warn, LevelFilter};
84use log4rs::{
85 append::{console::ConsoleAppender, file::FileAppender},
86 config::{Appender, Logger, Root},
87 encode::pattern::PatternEncoder,
88 Config, Handle,
89};
90use std::{
91 error::Error,
92 fmt::Display,
93 fs::{self, File},
94 io::{self, Read, Write},
95 process,
96 sync::{
97 atomic::{AtomicBool, Ordering},
98 Arc, Mutex,
99 },
100 thread,
101 time::{Duration, SystemTime},
102};
103use stopwatch::Stopwatch;
104use sysinfo::System;
105use tokio::runtime::Runtime;
106use winit::event_loop::EventLoopProxy;
107
108use crate::{calculate_past_two::calculate_past_two, winit_tray_icon::UserEvent};
109
110pub mod calculate_past_two;
111#[cfg(test)]
112mod tests;
113pub mod update;
114pub mod website_files;
115pub mod winit_tray_icon;
116
117pub type IoResult<T> = Result<T, io::Error>;
119
120struct ProgramRunVars {
122 proxy: EventLoopProxy<UserEvent>,
123 process_name: String,
124 is_waiting: bool,
125 option: String,
126 currently_tracking: Arc<Mutex<AtomicBool>>,
127 stop_tracker: Arc<Mutex<AtomicBool>>,
128}
129
130impl ProgramRunVars {
131 fn new(
132 proxy: EventLoopProxy<UserEvent>,
133 stop_tracker: Arc<Mutex<AtomicBool>>,
134 currently_tracking: Arc<Mutex<AtomicBool>>,
135 ) -> Self {
136 Self {
137 process_name: String::from("RocketLeague.exe"),
138 is_waiting: false,
139 option: String::with_capacity(1),
140 proxy,
141 stop_tracker,
142 currently_tracking,
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct PastTwoError;
150
151impl Display for PastTwoError {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write!(
154 f,
155 "next closest date to the date two weeks ago could not be found."
156 )
157 }
158}
159
160impl Error for PastTwoError {}
161
162pub fn initialize_logging() -> Result<Handle, Box<dyn Error>> {
166 let stdout = ConsoleAppender::builder().build();
168 let general_logs = FileAppender::builder()
169 .build("C:/RLHoursFolder/logs/general_$TIME{%Y-%m-%d_%H-%M-%S}.log")?;
170 let wti_logs = FileAppender::builder()
171 .build("C:/RLHoursFolder/logs/tray-icon_$TIME{%Y-%m-%d_%H-%M-%S}.log")?;
172 let requests = FileAppender::builder()
173 .encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
174 .build("C:/RLHoursFolder/logs/requests.log")?;
175
176 let rl_hours_tracker_logger = Logger::builder()
178 .additive(false)
179 .appenders(vec!["general_logs"])
180 .build("rl_hours_tracker", LevelFilter::Trace);
181 let rl_hours_tracker_update_logger = Logger::builder()
182 .additive(false)
183 .appenders(vec!["requests", "general_logs"])
184 .build("rl_hours_tracker::update", LevelFilter::Trace);
185 let rl_hours_tracker_cpt_logger = Logger::builder()
186 .additive(false)
187 .appenders(vec!["general_logs"])
188 .build("rl_hours_tracker::calculate_past_two", LevelFilter::Info);
189 let rl_hours_tracker_wti_logger = Logger::builder()
190 .additive(false)
191 .appenders(vec!["wti_logs"])
192 .build("rl_hours_tracker::winit_tray_icon", LevelFilter::Info);
193
194 let loggers = vec![
196 rl_hours_tracker_logger,
197 rl_hours_tracker_update_logger,
198 rl_hours_tracker_cpt_logger,
199 rl_hours_tracker_wti_logger,
200 ];
201 let appenders = vec![
202 Appender::builder().build("stdout", Box::new(stdout)),
203 Appender::builder().build("general_logs", Box::new(general_logs)),
204 Appender::builder().build("requests", Box::new(requests)),
205 Appender::builder().build("wti_logs", Box::new(wti_logs)),
206 ];
207
208 let config = Config::builder()
209 .appenders(appenders)
210 .loggers(loggers)
211 .build(Root::builder().appender("stdout").build(LevelFilter::Warn))?;
212
213 let handle = log4rs::init_config(config)?;
215
216 Ok(handle)
217}
218
219pub fn run_self_update() -> Result<(), Box<dyn Error>> {
221 let rt = Runtime::new()?;
222
223 rt.block_on(update::check_for_update())?;
224
225 Ok(())
226}
227
228pub fn run(
230 proxy: EventLoopProxy<UserEvent>,
231 stop_tracker: Arc<Mutex<AtomicBool>>,
232 currently_tracking: Arc<Mutex<AtomicBool>>,
233) {
234 let mut program = ProgramRunVars::new(proxy, stop_tracker, currently_tracking);
235
236 run_main_loop(&mut program);
238}
239
240pub fn create_directory() -> Vec<IoResult<()>> {
248 let folder = fs::create_dir("C:\\RLHoursFolder");
250 let website_folder = fs::create_dir("C:\\RLHoursFolder\\website");
251 let website_pages = fs::create_dir("C:\\RLHoursFolder\\website\\pages");
252 let website_css = fs::create_dir("C:\\RLHoursFolder\\website\\css");
253 let website_js = fs::create_dir("C:\\RLHoursFolder\\website\\js");
254 let website_images = fs::create_dir("C:\\RLHoursFolder\\website\\images");
255
256 let folder_vec: Vec<IoResult<()>> = vec![
258 folder,
259 website_folder,
260 website_pages,
261 website_css,
262 website_js,
263 website_images,
264 ];
265
266 let result: Vec<IoResult<()>> = folder_vec.into_iter().filter(|f| f.is_err()).collect();
268
269 result
270}
271
272fn run_main_loop(program: &mut ProgramRunVars) {
275 loop {
276 if check_for_process(&program.process_name) {
278 record_hours(
279 &program.process_name,
280 program.stop_tracker.clone(),
281 program.currently_tracking.clone(),
282 );
283
284 website_files::generate_website_files(true)
286 .unwrap_or_else(|e| warn!("failed to generate website files: {e}"));
287
288 program.is_waiting = false;
289
290 print!("End program (");
291 green!("y");
292 print!(" / ");
293 red!("n");
294 print!("): ");
295 std::io::stdout()
296 .flush()
297 .unwrap_or_else(|_| println!("End program (y/n)?\n"));
298 io::stdin()
299 .read_line(&mut program.option)
300 .unwrap_or_default();
301
302 if program.option.trim() == "y" || program.option.trim() == "Y" {
303 print!("{}[2K\r", 27 as char);
304 std::io::stdout()
305 .flush()
306 .expect("could not flush the output stream");
307 yellow_ln_bold!("Goodbye!");
308 program
309 .proxy
310 .send_event(UserEvent::QuitApp(AtomicBool::new(true)))
311 .unwrap_or_else(|_| error!("event loop already closed"));
312 break;
313 } else if program.option.trim() == "n" || program.option.trim() == "N" {
314 program.option = String::with_capacity(1);
315 continue;
316 } else {
317 error!("Unexpected input! Ending program.");
318 program
319 .proxy
320 .send_event(UserEvent::QuitApp(AtomicBool::new(true)))
321 .unwrap_or_else(|_| error!("event loop already closed"));
322 break;
323 }
324 } else {
325 if !program.is_waiting {
327 green!("Waiting for Rocket League to start.\r");
328 io::stdout()
329 .flush()
330 .expect("could not flush the output stream");
331 thread::sleep(Duration::from_millis(500));
332 white!("Waiting for Rocket League to start..\r");
333 io::stdout()
334 .flush()
335 .expect("could not flush the output stream");
336 thread::sleep(Duration::from_millis(500));
337 black_bold!("Waiting for Rocket League to start...\r");
338 io::stdout()
339 .flush()
340 .expect("could not flush the output stream");
341 thread::sleep(Duration::from_millis(500));
342 print!("{}[2K\r", 27 as char);
343 red!("Waiting for Rocket League to start\r");
344 io::stdout()
345 .flush()
346 .expect("could not flush the output stream");
347 thread::sleep(Duration::from_millis(500));
348 }
349 }
350 }
351}
352
353fn record_hours(
359 process_name: &str,
360 stop_tracker: Arc<Mutex<AtomicBool>>,
361 currently_tracking: Arc<Mutex<AtomicBool>>,
362) {
363 let mut sw = Stopwatch::start_new();
364
365 blue_ln_bold!("\nRocket League is running\n");
366
367 currently_tracking
368 .try_lock()
369 .unwrap_or_else(|e| {
370 error!("error when attempting to access lock for currently_tracking: {e}");
371 panic!("could not access lock for currently_tracking");
372 })
373 .store(true, Ordering::SeqCst);
374
375 trace!(
376 "<< fn record_hours >> currently_tracking set to {} before live_stopwatch",
377 currently_tracking
378 .try_lock()
379 .unwrap()
380 .load(Ordering::Relaxed)
381 );
382 trace!(
383 "<< fn record_hours >> stop_tracker set to {} before live_stopwatch",
384 stop_tracker.try_lock().unwrap().load(Ordering::Relaxed)
385 );
386
387 live_stopwatch(process_name, stop_tracker.clone());
389
390 trace!(
391 "<< fn record_hours >> stop_tracker set to {} after live_stopwatch",
392 stop_tracker.try_lock().unwrap().load(Ordering::Relaxed)
393 );
394
395 currently_tracking
396 .try_lock()
397 .unwrap_or_else(|e| {
398 error!("error when attempting to access lock for currently_tracking: {e}");
399 panic!("could not access lock for currently_tracking");
400 })
401 .store(false, Ordering::SeqCst);
402
403 trace!(
404 "<< fn record_hours >> currently_tracking set to {} after live_stopwatch",
405 currently_tracking
406 .try_lock()
407 .unwrap()
408 .load(Ordering::Relaxed)
409 );
410
411 stop_tracker
412 .try_lock()
413 .unwrap_or_else(|e| {
414 error!("error when attempting to access lock for stop_tracking: {e}");
415 panic!("could not access lock for stop_tracking");
416 })
417 .store(false, Ordering::SeqCst);
418
419 sw.stop();
421
422 info!("Record Hours: START\n");
423
424 let seconds: u64 = sw.elapsed_ms() as u64 / 1000;
425 let hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
426
427 trace!("<< fn record_hours >> seconds: {seconds}, hours: {hours:.1}");
428
429 let hours_result = File::open("C:\\RLHoursFolder\\hours.txt");
430 let date_result = File::open("C:\\RLHoursFolder\\date.txt");
431
432 write_to_date(date_result, &seconds).unwrap_or_else(|e| {
434 error!("error writing to date.txt: {e}");
435 process::exit(1);
436 });
437
438 let hours_buffer = calculate_past_two().unwrap_or_else(|e| {
440 warn!("failed to calculate past two: {e}");
441 0
442 });
443
444 trace!("<< fn record_hours >> hours_buffer is set to {hours_buffer}");
445
446 if hours_buffer != 0 {
447 let hours_past_two = hours_buffer as f32 / 3600_f32;
448
449 write_to_hours(hours_result, &seconds, &hours, &hours_past_two, &sw).unwrap_or_else(|e| {
450 error!("error writing to hours.txt: {e}");
451 process::exit(1);
452 });
453 info!("Record Hours: FINISHED\n")
454 } else {
455 warn!("past two returned zero seconds")
456 }
457}
458
459fn live_stopwatch(process_name: &str, stop_tracker: Arc<Mutex<AtomicBool>>) {
460 let mut timer_early = SystemTime::now();
461
462 let mut seconds: u8 = 0;
463 let mut minutes: u8 = 0;
464 let mut hours: u16 = 0;
465
466 while check_for_process(process_name)
467 && !stop_tracker
468 .try_lock()
469 .unwrap_or_else(|e| {
470 error!("error when attempting to access lock for stop_tracking: {e}");
471 panic!("could not access lock for stop_tracking");
472 })
473 .load(Ordering::SeqCst)
474 {
475 let timer_now = timer_early
476 .checked_add(Duration::from_millis(999))
477 .unwrap_or_else(|| {
478 error!("could not return system time");
479 SystemTime::now()
480 });
481
482 let delay = timer_now.duration_since(timer_early).unwrap_or_else(|e| {
483 warn!(
484 "system time is ahead of the timer. SystemTime difference: {:?}",
485 e.duration()
486 );
487 Duration::from_millis(1000)
488 });
489
490 if seconds == 59 {
492 seconds = 0;
493 minutes += 1;
494
495 if minutes == 60 {
497 minutes = 0;
498 hours += 1;
499 }
500 } else {
501 seconds += 1;
502 }
503 print!("{}[2K\r", 27 as char);
504
505 if hours < 10 && minutes < 10 && seconds < 10 {
507 cyan!("Time Elapsed: 0{}:0{}:0{}\r", hours, minutes, seconds);
508 } else if hours >= 10 {
509 if minutes < 10 && seconds < 10 {
510 cyan!("Time Elapsed: {}:0{}:0{}\r", hours, minutes, seconds);
511 } else if minutes < 10 && seconds >= 10 {
512 cyan!("Time Elapsed: {}:0{}:{}\r", hours, minutes, seconds);
513 } else if minutes >= 10 && seconds < 10 {
514 cyan!("Time Elapsed: {}:{}:0{}\r", hours, minutes, seconds);
515 } else {
516 cyan!("Time Elapsed: {}:{}:{}\r", hours, minutes, seconds);
517 }
518 } else if hours < 10 && minutes >= 10 && seconds < 10 {
519 cyan!("Time Elapsed: 0{}:{}:0{}\r", hours, minutes, seconds);
520 } else if hours < 10 && minutes < 10 && seconds >= 10 {
521 cyan!("Time Elapsed: 0{}:0{}:{}\r", hours, minutes, seconds);
522 } else {
523 cyan!("Time Elapsed: 0{}:{}:{}\r", hours, minutes, seconds);
524 }
525
526 io::stdout()
528 .flush()
529 .unwrap_or_else(|_| warn!("could not flush output stream"));
530
531 thread::sleep(delay);
532
533 timer_early += Duration::from_millis(999)
534 }
535
536 trace!("<< fn live_stopwatch >> hours: {hours}, minutes: {minutes}, seconds: {seconds}");
537}
538
539fn retrieve_time(contents: &str) -> Result<(u64, f32), Box<dyn Error>> {
542 let split_new_line: Vec<&str> = contents.split("\n").collect();
544
545 let split_whitspace_sec: Vec<&str> = split_new_line[1].split_whitespace().collect();
547 let split_whitespace_hrs: Vec<&str> = split_new_line[2].split_whitespace().collect();
548
549 let split_char_sec = split_whitspace_sec[2].chars();
551 let split_char_hrs = split_whitespace_hrs[2].chars();
552
553 let mut sec_vec: Vec<char> = vec![];
554 let mut hrs_vec: Vec<char> = vec![];
555
556 for num in split_char_sec {
558 if num.is_numeric() {
559 sec_vec.push(num);
560 }
561 }
562
563 trace!("<< fn retrieve_time >> seconds vector: {sec_vec:?}");
564
565 for num in split_char_hrs {
567 if num.is_numeric() || num == '.' {
568 hrs_vec.push(num);
569 }
570 }
571
572 trace!("<< fn retrieve_time >> hours vector: {hrs_vec:?}");
573
574 let seconds_str: String = sec_vec.iter().collect();
575 let hours_str: String = hrs_vec.iter().collect();
576
577 let old_seconds: u64 = seconds_str.parse()?;
578 let old_hours: f32 = hours_str.parse()?;
579
580 trace!("<< fn retrieve_time >> old seconds: {old_seconds}, old_hours: {old_hours:.1}");
581
582 Ok((old_seconds, old_hours))
584}
585
586fn return_new_hours(
589 contents: &str,
590 seconds: &u64,
591 hours: &f32,
592 past_two: &f32,
593) -> Result<String, Box<dyn Error>> {
594 yellow_ln_bold!("Getting old hours...");
595 let (old_seconds, old_hours) = retrieve_time(contents)?;
597
598 let added_seconds = old_seconds + *seconds;
599 let added_hours = old_hours + *hours;
600
601 trace!("<< fn return_new_hours >> added_seconds: {added_seconds}, added_hours: {added_hours:.1}");
602
603 Ok(format!(
604 "Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n",
605 added_seconds, added_hours, past_two
606 ))
607}
608
609fn write_to_hours(
615 hours_result: IoResult<File>,
616 seconds: &u64,
617 hours: &f32,
618 hours_past_two: &f32,
619 sw: &Stopwatch,
620) -> Result<(), Box<dyn Error>> {
621 if let Ok(mut file) = hours_result {
623 let mut contents = String::new();
624
625 file.read_to_string(&mut contents)?;
627
628 let rl_hours_str = return_new_hours(&contents, seconds, hours, hours_past_two)?;
630
631 let mut truncated_file = File::create("C:\\RLHoursFolder\\hours.txt")?;
633
634 yellow_ln_bold!("Writing to hours.txt...");
635
636 truncated_file.write_all(rl_hours_str.as_bytes())?;
638
639 green_ln_bold!("Successful!\n");
640 Ok(())
641 } else {
642 let mut file = File::create("C:\\RLHoursFolder\\hours.txt")?;
644 let total_seconds = sw.elapsed_ms() / 1000;
645 let total_hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
646 let rl_hours_str = format!(
647 "Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n", total_seconds, total_hours, hours_past_two
648 );
649
650 yellow_ln_bold!("Writing to hours.txt...");
651
652 file.write_all(rl_hours_str.as_bytes())?;
654
655 green_ln_bold!("The hours file was successfully created");
656 Ok(())
657 }
658}
659
660fn write_to_date(date_result: IoResult<File>, seconds: &u64) -> IoResult<()> {
667 if date_result.is_ok() {
669 let mut append_date_result = File::options()
670 .append(true)
671 .open("C:\\RLHoursFolder\\date.txt")?;
672
673 let today = Local::now().date_naive();
675
676 let today_str = format!("{} {}s\n", today, seconds);
677
678 yellow_ln_bold!("Appending to date.txt...");
679
680 append_date_result.write_all(today_str.as_bytes())?;
682
683 green_ln_bold!("Successful!\n");
684 Ok(())
685 } else {
686 let mut file = File::create("C:\\RLHoursFolder\\date.txt")?;
688 let today = Local::now().date_naive();
689
690 let today_str = format!("{} {}s\n", today, seconds);
691
692 yellow_ln_bold!("Appending to date.txt...");
693
694 file.write_all(today_str.as_bytes())?;
696
697 green_ln_bold!("The date file was successfully created");
698 Ok(())
699 }
700}
701
702fn check_for_process(name: &str) -> bool {
704 let sys = System::new_all();
705 let mut result = false;
706
707 for process in sys.processes_by_exact_name(name.as_ref()) {
708 if process.name() == name {
709 result = true;
710 break;
711 }
712 }
713
714 result
715}