1use chrono::Local;
82use colour::{black_bold, blue_ln_bold, cyan, green, green_ln_bold, red, white, yellow_ln_bold};
83use log::{error, info, 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::{Arc, Mutex},
97 thread,
98 time::{Duration, SystemTime},
99};
100use stopwatch::Stopwatch;
101use sysinfo::System;
102use tokio::runtime::Runtime;
103
104use crate::calculate_past_two::calculate_past_two;
105
106pub mod calculate_past_two;
107#[cfg(test)]
108mod tests;
109pub mod update;
110pub mod website_files;
111pub mod winit_tray_icon;
112
113pub type IoResult<T> = Result<T, io::Error>;
115
116struct ProgramRunVars {
118 process_name: String,
119 is_waiting: bool,
120 option: String,
121 currently_tracking: Arc<Mutex<bool>>,
122 stop_tracker: Arc<Mutex<bool>>,
123}
124
125impl ProgramRunVars {
126 fn new(stop_tracker: Arc<Mutex<bool>>, currently_tracking: Arc<Mutex<bool>>) -> Self {
127 Self {
128 process_name: String::from("RocketLeague.exe"),
129 is_waiting: false,
130 option: String::with_capacity(3),
131 stop_tracker,
132 currently_tracking,
133 }
134 }
135}
136
137#[derive(Debug, Clone)]
139pub struct PastTwoError;
140
141impl Display for PastTwoError {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(
144 f,
145 "next closest date to the date two weeks ago could not be found."
146 )
147 }
148}
149
150impl Error for PastTwoError {}
151
152pub fn initialize_logging() -> Result<Handle, Box<dyn Error>> {
156 let stdout = ConsoleAppender::builder().build();
158 let general_logs = FileAppender::builder()
159 .build("C:/RLHoursFolder/logs/general_$TIME{%Y-%m-%d_%H-%M-%S}.log")?;
160 let wti_errors = FileAppender::builder()
161 .build("C:/RLHoursFolder/logs/tray-icon_$TIME{%Y-%m-%d_%H-%M-%S}.log")?;
162 let requests = FileAppender::builder()
163 .encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
164 .build("C:/RLHoursFolder/logs/requests.log")?;
165
166 let rl_hours_tracker_logger = Logger::builder()
168 .additive(false)
169 .appenders(vec!["general_logs"])
170 .build("rl_hours_tracker", LevelFilter::Info);
171 let rl_hours_tracker_update_logger = Logger::builder()
172 .additive(false)
173 .appenders(vec!["requests", "general_logs"])
174 .build("rl_hours_tracker::update", LevelFilter::Trace);
175 let rl_hours_tracker_cpt_logger = Logger::builder()
176 .additive(false)
177 .appenders(vec!["general_logs"])
178 .build("rl_hours_tracker::calculate_past_two", LevelFilter::Info);
179 let rl_hours_tracker_wti_logger = Logger::builder()
180 .additive(false)
181 .appenders(vec!["general_logs"])
182 .build("rl_hours_tracker::winit_tray_icon", LevelFilter::Error);
183
184 let loggers = vec![
186 rl_hours_tracker_logger,
187 rl_hours_tracker_update_logger,
188 rl_hours_tracker_cpt_logger,
189 rl_hours_tracker_wti_logger,
190 ];
191 let appenders = vec![
192 Appender::builder().build("stdout", Box::new(stdout)),
193 Appender::builder().build("general_logs", Box::new(general_logs)),
194 Appender::builder().build("requests", Box::new(requests)),
195 Appender::builder().build("wti_errors", Box::new(wti_errors)),
196 ];
197
198 let config = Config::builder()
199 .appenders(appenders)
200 .loggers(loggers)
201 .build(Root::builder().appender("stdout").build(LevelFilter::Warn))?;
202
203 let handle = log4rs::init_config(config)?;
205
206 Ok(handle)
207}
208
209pub fn run_self_update() -> Result<(), Box<dyn Error>> {
211 let rt = Runtime::new()?;
212
213 rt.block_on(update::check_for_update())?;
214
215 Ok(())
216}
217
218pub fn run(stop_tracker: Arc<Mutex<bool>>, currently_tracking: Arc<Mutex<bool>>) {
220 let mut program = ProgramRunVars::new(stop_tracker, currently_tracking);
221
222 run_main_loop(&mut program);
224}
225
226pub fn create_directory() -> Vec<IoResult<()>> {
234 let folder = fs::create_dir("C:\\RLHoursFolder");
236 let website_folder = fs::create_dir("C:\\RLHoursFolder\\website");
237 let website_pages = fs::create_dir("C:\\RLHoursFolder\\website\\pages");
238 let website_css = fs::create_dir("C:\\RLHoursFolder\\website\\css");
239 let website_js = fs::create_dir("C:\\RLHoursFolder\\website\\js");
240 let website_images = fs::create_dir("C:\\RLHoursFolder\\website\\images");
241
242 let folder_vec: Vec<IoResult<()>> = vec![
244 folder,
245 website_folder,
246 website_pages,
247 website_css,
248 website_js,
249 website_images,
250 ];
251
252 let result: Vec<IoResult<()>> = folder_vec.into_iter().filter(|f| f.is_err()).collect();
254
255 result
256}
257
258fn run_main_loop(program: &mut ProgramRunVars) {
261 loop {
262 if check_for_process(&program.process_name) {
264 record_hours(
265 &program.process_name,
266 program.stop_tracker.clone(),
267 program.currently_tracking.clone(),
268 );
269
270 website_files::generate_website_files(true)
272 .unwrap_or_else(|e| warn!("failed to generate website files: {e}"));
273
274 program.is_waiting = false;
275
276 print!("End program (");
277 green!("y");
278 print!(" / ");
279 red!("n");
280 print!("): ");
281 std::io::stdout()
282 .flush()
283 .unwrap_or_else(|_| println!("End program (y/n)?\n"));
284 io::stdin()
285 .read_line(&mut program.option)
286 .unwrap_or_default();
287
288 if program.option.trim() == "y" || program.option.trim() == "Y" {
289 print!("{}[2K\r", 27 as char);
290 std::io::stdout()
291 .flush()
292 .expect("could not flush the output stream");
293 yellow_ln_bold!("Goodbye!");
294 process::exit(0);
295 } else if program.option.trim() == "n" || program.option.trim() == "N" {
296 program.option = String::with_capacity(3);
297 continue;
298 } else {
299 error!("Unexpected input! Ending program.");
300 process::exit(0)
301 }
302 } else {
303 if !program.is_waiting {
305 green!("Waiting for Rocket League to start.\r");
306 io::stdout()
307 .flush()
308 .expect("could not flush the output stream");
309 thread::sleep(Duration::from_millis(500));
310 white!("Waiting for Rocket League to start..\r");
311 io::stdout()
312 .flush()
313 .expect("could not flush the output stream");
314 thread::sleep(Duration::from_millis(500));
315 black_bold!("Waiting for Rocket League to start...\r");
316 io::stdout()
317 .flush()
318 .expect("could not flush the output stream");
319 thread::sleep(Duration::from_millis(500));
320 print!("{}[2K\r", 27 as char);
321 red!("Waiting for Rocket League to start\r");
322 io::stdout()
323 .flush()
324 .expect("could not flush the output stream");
325 thread::sleep(Duration::from_millis(500));
326 }
327 }
328 }
329}
330
331fn record_hours(
337 process_name: &str,
338 stop_tracker: Arc<Mutex<bool>>,
339 currently_tracking: Arc<Mutex<bool>>,
340) {
341 let mut sw = Stopwatch::start_new();
342
343 blue_ln_bold!("\nRocket League is running\n");
344
345 *currently_tracking.try_lock().unwrap_or_else(|e| {
346 error!("error when attempting to access lock for currently_tracking: {e}");
347 panic!("could not access lock for currently_tracking");
348 }) = true;
349
350 live_stopwatch(process_name, stop_tracker.clone());
352
353 *currently_tracking.try_lock().unwrap_or_else(|e| {
354 error!("error when attempting to access lock for currently_tracking: {e}");
355 panic!("could not access lock for currently_tracking");
356 }) = false;
357
358 *stop_tracker.try_lock().unwrap_or_else(|e| {
359 error!("error when attempting to access lock for stop_tracking: {e}");
360 panic!("could not access lock for stop_tracking");
361 }) = false;
362
363 sw.stop();
365
366 info!("Record Hours: START\n");
367
368 let seconds: u64 = sw.elapsed_ms() as u64 / 1000;
369 let hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
370
371 let hours_result = File::open("C:\\RLHoursFolder\\hours.txt");
372 let date_result = File::open("C:\\RLHoursFolder\\date.txt");
373
374 write_to_date(date_result, &seconds).unwrap_or_else(|e| {
376 error!("error writing to date.txt: {e}");
377 process::exit(1);
378 });
379
380 let hours_buffer = calculate_past_two().unwrap_or_else(|e| {
382 warn!("failed to calculate past two: {e}");
383 0
384 });
385
386 if hours_buffer != 0 {
387 let hours_past_two = hours_buffer as f32 / 3600_f32;
388
389 write_to_hours(hours_result, &seconds, &hours, &hours_past_two, &sw).unwrap_or_else(|e| {
390 error!("error writing to hours.txt: {e}");
391 process::exit(1);
392 });
393 info!("Record Hours: FINISHED\n")
394 } else {
395 warn!("past two returned zero seconds")
396 }
397}
398
399fn live_stopwatch(process_name: &str, stop_tracker: Arc<Mutex<bool>>) {
400 let mut timer_early = SystemTime::now();
401
402 let mut seconds: u8 = 0;
403 let mut minutes: u8 = 0;
404 let mut hours: u16 = 0;
405
406 while check_for_process(process_name)
407 && !*stop_tracker.try_lock().unwrap_or_else(|e| {
408 error!("error when attempting to access lock for stop_tracking: {e}");
409 panic!("could not access lock for stop_tracking");
410 })
411 {
412 let timer_now = timer_early
413 .checked_add(Duration::from_millis(999))
414 .unwrap_or_else(|| {
415 error!("could not return system time");
416 SystemTime::now()
417 });
418
419 let delay = timer_now.duration_since(timer_early).unwrap_or_else(|e| {
420 warn!(
421 "system time is ahead of the timer. SystemTime difference: {:?}",
422 e.duration()
423 );
424 Duration::from_millis(1000)
425 });
426
427 if seconds == 59 {
429 seconds = 0;
430 minutes += 1;
431
432 if minutes == 60 {
434 minutes = 0;
435 hours += 1;
436 }
437 } else {
438 seconds += 1;
439 }
440 print!("{}[2K\r", 27 as char);
441
442 if hours < 10 && minutes < 10 && seconds < 10 {
444 cyan!("Time Elapsed: 0{}:0{}:0{}\r", hours, minutes, seconds);
445 } else if hours >= 10 {
446 if minutes < 10 && seconds < 10 {
447 cyan!("Time Elapsed: {}:0{}:0{}\r", hours, minutes, seconds);
448 } else if minutes < 10 && seconds >= 10 {
449 cyan!("Time Elapsed: {}:0{}:{}\r", hours, minutes, seconds);
450 } else if minutes >= 10 && seconds < 10 {
451 cyan!("Time Elapsed: {}:{}:0{}\r", hours, minutes, seconds);
452 } else {
453 cyan!("Time Elapsed: {}:{}:{}\r", hours, minutes, seconds);
454 }
455 } else if hours < 10 && minutes >= 10 && seconds < 10 {
456 cyan!("Time Elapsed: 0{}:{}:0{}\r", hours, minutes, seconds);
457 } else if hours < 10 && minutes < 10 && seconds >= 10 {
458 cyan!("Time Elapsed: 0{}:0{}:{}\r", hours, minutes, seconds);
459 } else {
460 cyan!("Time Elapsed: 0{}:{}:{}\r", hours, minutes, seconds);
461 }
462
463 io::stdout()
465 .flush()
466 .unwrap_or_else(|_| warn!("could not flush output stream"));
467
468 thread::sleep(delay);
469
470 timer_early += Duration::from_millis(999)
471 }
472}
473
474fn retrieve_time(contents: &str) -> Result<(u64, f32), Box<dyn Error>> {
477 let split_new_line: Vec<&str> = contents.split("\n").collect();
479
480 let split_whitspace_sec: Vec<&str> = split_new_line[1].split_whitespace().collect();
482 let split_whitespace_hrs: Vec<&str> = split_new_line[2].split_whitespace().collect();
483
484 let split_char_sec = split_whitspace_sec[2].chars();
486 let split_char_hrs = split_whitespace_hrs[2].chars();
487
488 let mut sec_vec: Vec<char> = vec![];
489 let mut hrs_vec: Vec<char> = vec![];
490
491 for num in split_char_sec {
493 if num.is_numeric() {
494 sec_vec.push(num);
495 }
496 }
497
498 for num in split_char_hrs {
500 if num.is_numeric() || num == '.' {
501 hrs_vec.push(num);
502 }
503 }
504
505 let seconds_str: String = sec_vec.iter().collect();
506 let hours_str: String = hrs_vec.iter().collect();
507
508 let old_seconds: u64 = seconds_str.parse()?;
509 let old_hours: f32 = hours_str.parse()?;
510
511 Ok((old_seconds, old_hours))
513}
514
515fn return_new_hours(
518 contents: &str,
519 seconds: &u64,
520 hours: &f32,
521 past_two: &f32,
522) -> Result<String, Box<dyn Error>> {
523 yellow_ln_bold!("Getting old hours...");
524 let (old_seconds, old_hours) = retrieve_time(contents)?;
526
527 let added_seconds = old_seconds + *seconds;
528 let added_hours = old_hours + *hours;
529
530 Ok(format!(
531 "Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n",
532 added_seconds, added_hours, past_two
533 ))
534}
535
536fn write_to_hours(
542 hours_result: IoResult<File>,
543 seconds: &u64,
544 hours: &f32,
545 hours_past_two: &f32,
546 sw: &Stopwatch,
547) -> Result<(), Box<dyn Error>> {
548 if let Ok(mut file) = hours_result {
550 let mut contents = String::new();
551
552 file.read_to_string(&mut contents)?;
554
555 let rl_hours_str = return_new_hours(&contents, seconds, hours, hours_past_two)?;
557
558 let mut truncated_file = File::create("C:\\RLHoursFolder\\hours.txt")?;
560
561 yellow_ln_bold!("Writing to hours.txt...");
562
563 truncated_file.write_all(rl_hours_str.as_bytes())?;
565
566 green_ln_bold!("Successful!\n");
567 Ok(())
568 } else {
569 let mut file = File::create("C:\\RLHoursFolder\\hours.txt")?;
571 let total_seconds = sw.elapsed_ms() / 1000;
572 let total_hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
573 let rl_hours_str = format!(
574 "Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n", total_seconds, total_hours, hours_past_two
575 );
576
577 yellow_ln_bold!("Writing to hours.txt...");
578
579 file.write_all(rl_hours_str.as_bytes())?;
581
582 green_ln_bold!("The hours file was successfully created");
583 Ok(())
584 }
585}
586
587fn write_to_date(date_result: IoResult<File>, seconds: &u64) -> IoResult<()> {
594 if date_result.is_ok() {
596 let mut append_date_result = File::options()
597 .append(true)
598 .open("C:\\RLHoursFolder\\date.txt")?;
599
600 let today = Local::now().date_naive();
602
603 let today_str = format!("{} {}s\n", today, seconds);
604
605 yellow_ln_bold!("Appending to date.txt...");
606
607 append_date_result.write_all(today_str.as_bytes())?;
609
610 green_ln_bold!("Successful!\n");
611 Ok(())
612 } else {
613 let mut file = File::create("C:\\RLHoursFolder\\date.txt")?;
615 let today = Local::now().date_naive();
616
617 let today_str = format!("{} {}s\n", today, seconds);
618
619 yellow_ln_bold!("Appending to date.txt...");
620
621 file.write_all(today_str.as_bytes())?;
623
624 green_ln_bold!("The date file was successfully created");
625 Ok(())
626 }
627}
628
629fn check_for_process(name: &str) -> bool {
631 let sys = System::new_all();
632 let mut result = false;
633
634 for process in sys.processes_by_exact_name(name.as_ref()) {
635 if process.name() == name {
636 result = true;
637 break;
638 }
639 }
640
641 result
642}