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 'main_loop: 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 break 'main_loop;
290 } else if program.option.trim() == "n" || program.option.trim() == "N" {
291 program.option = String::with_capacity(3);
292 continue;
293 } else {
294 error!("Unexpected input! Ending program.");
295 break 'main_loop;
296 }
297 } else {
298 if !program.is_waiting {
300 green!("Waiting for Rocket League to start.\r");
301 io::stdout()
302 .flush()
303 .expect("could not flush the output stream");
304 thread::sleep(Duration::from_millis(500));
305 white!("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 black_bold!("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 print!("{}[2K\r", 27 as char);
316 red!("Waiting for Rocket League to start\r");
317 io::stdout()
318 .flush()
319 .expect("could not flush the output stream");
320 thread::sleep(Duration::from_millis(500));
321 }
322 }
323 }
324}
325
326fn record_hours(
332 process_name: &str,
333 stop_tracker: Arc<Mutex<bool>>,
334 currently_tracking: Arc<Mutex<bool>>,
335) {
336 let mut sw = Stopwatch::start_new();
337
338 blue_ln_bold!("\nRocket League is running\n");
339
340 *currently_tracking.try_lock().unwrap_or_else(|e| {
341 error!("error when attempting to access lock for currently_tracking: {e}");
342 panic!("could not access lock for currently_tracking");
343 }) = true;
344
345 live_stopwatch(process_name, stop_tracker.clone());
347
348 *currently_tracking.try_lock().unwrap_or_else(|e| {
349 error!("error when attempting to access lock for currently_tracking: {e}");
350 panic!("could not access lock for currently_tracking");
351 }) = false;
352
353 *stop_tracker.try_lock().unwrap_or_else(|e| {
354 error!("error when attempting to access lock for stop_tracking: {e}");
355 panic!("could not access lock for stop_tracking");
356 }) = false;
357
358 sw.stop();
360
361 info!("Record Hours: START\n");
362
363 let seconds: u64 = sw.elapsed_ms() as u64 / 1000;
364 let hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
365
366 let hours_result = File::open("C:\\RLHoursFolder\\hours.txt");
367 let date_result = File::open("C:\\RLHoursFolder\\date.txt");
368
369 write_to_date(date_result, &seconds).unwrap_or_else(|e| {
371 error!("error writing to date.txt: {e}");
372 process::exit(1);
373 });
374
375 let hours_buffer = calculate_past_two().unwrap_or_else(|e| {
377 warn!("failed to calculate past two: {e}");
378 0
379 });
380
381 if hours_buffer != 0 {
382 let hours_past_two = hours_buffer as f32 / 3600_f32;
383
384 write_to_hours(hours_result, &seconds, &hours, &hours_past_two, &sw).unwrap_or_else(|e| {
385 error!("error writing to hours.txt: {e}");
386 process::exit(1);
387 });
388 info!("Record Hours: FINISHED\n")
389 } else {
390 warn!("past two returned zero seconds")
391 }
392}
393
394fn live_stopwatch(process_name: &str, stop_tracker: Arc<Mutex<bool>>) {
395 let mut timer_early = SystemTime::now();
396
397 let mut seconds: u8 = 0;
398 let mut minutes: u8 = 0;
399 let mut hours: u16 = 0;
400
401 while check_for_process(process_name)
402 && !*stop_tracker.try_lock().unwrap_or_else(|e| {
403 error!("error when attempting to access lock for stop_tracking: {e}");
404 panic!("could not access lock for stop_tracking");
405 })
406 {
407 let timer_now = timer_early
408 .checked_add(Duration::from_millis(999))
409 .unwrap_or_else(|| {
410 error!("could not return system time");
411 SystemTime::now()
412 });
413
414 let delay = timer_now.duration_since(timer_early).unwrap_or_else(|e| {
415 warn!(
416 "system time is ahead of the timer. SystemTime difference: {:?}",
417 e.duration()
418 );
419 Duration::from_millis(1000)
420 });
421
422 if seconds == 59 {
424 seconds = 0;
425 minutes += 1;
426
427 if minutes == 60 {
429 minutes = 0;
430 hours += 1;
431 }
432 } else {
433 seconds += 1;
434 }
435 print!("{}[2K\r", 27 as char);
436
437 if hours < 10 && minutes < 10 && seconds < 10 {
439 cyan!("Time Elapsed: 0{}:0{}:0{}\r", hours, minutes, seconds);
440 } else if hours >= 10 {
441 if minutes < 10 && seconds < 10 {
442 cyan!("Time Elapsed: {}:0{}:0{}\r", hours, minutes, seconds);
443 } else if minutes < 10 && seconds >= 10 {
444 cyan!("Time Elapsed: {}:0{}:{}\r", hours, minutes, seconds);
445 } else if minutes >= 10 && seconds < 10 {
446 cyan!("Time Elapsed: {}:{}:0{}\r", hours, minutes, seconds);
447 } else {
448 cyan!("Time Elapsed: {}:{}:{}\r", hours, minutes, seconds);
449 }
450 } else if hours < 10 && minutes >= 10 && seconds < 10 {
451 cyan!("Time Elapsed: 0{}:{}:0{}\r", hours, minutes, seconds);
452 } else if hours < 10 && minutes < 10 && seconds >= 10 {
453 cyan!("Time Elapsed: 0{}:0{}:{}\r", hours, minutes, seconds);
454 } else {
455 cyan!("Time Elapsed: 0{}:{}:{}\r", hours, minutes, seconds);
456 }
457
458 io::stdout()
460 .flush()
461 .unwrap_or_else(|_| warn!("could not flush output stream"));
462
463 thread::sleep(delay);
464
465 timer_early += Duration::from_millis(999)
466 }
467}
468
469fn retrieve_time(contents: &str) -> Result<(u64, f32), Box<dyn Error>> {
472 let split_new_line: Vec<&str> = contents.split("\n").collect();
474
475 let split_whitspace_sec: Vec<&str> = split_new_line[1].split_whitespace().collect();
477 let split_whitespace_hrs: Vec<&str> = split_new_line[2].split_whitespace().collect();
478
479 let split_char_sec = split_whitspace_sec[2].chars();
481 let split_char_hrs = split_whitespace_hrs[2].chars();
482
483 let mut sec_vec: Vec<char> = vec![];
484 let mut hrs_vec: Vec<char> = vec![];
485
486 for num in split_char_sec {
488 if num.is_numeric() {
489 sec_vec.push(num);
490 }
491 }
492
493 for num in split_char_hrs {
495 if num.is_numeric() || num == '.' {
496 hrs_vec.push(num);
497 }
498 }
499
500 let seconds_str: String = sec_vec.iter().collect();
501 let hours_str: String = hrs_vec.iter().collect();
502
503 let old_seconds: u64 = seconds_str.parse()?;
504 let old_hours: f32 = hours_str.parse()?;
505
506 Ok((old_seconds, old_hours))
508}
509
510fn return_new_hours(
513 contents: &str,
514 seconds: &u64,
515 hours: &f32,
516 past_two: &f32,
517) -> Result<String, Box<dyn Error>> {
518 yellow_ln_bold!("Getting old hours...");
519 let (old_seconds, old_hours) = retrieve_time(contents)?;
521
522 let added_seconds = old_seconds + *seconds;
523 let added_hours = old_hours + *hours;
524
525 Ok(format!(
526 "Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n",
527 added_seconds, added_hours, past_two
528 ))
529}
530
531fn write_to_hours(
537 hours_result: IoResult<File>,
538 seconds: &u64,
539 hours: &f32,
540 hours_past_two: &f32,
541 sw: &Stopwatch,
542) -> Result<(), Box<dyn Error>> {
543 if let Ok(mut file) = hours_result {
545 let mut contents = String::new();
546
547 file.read_to_string(&mut contents)?;
549
550 let rl_hours_str = return_new_hours(&contents, seconds, hours, hours_past_two)?;
552
553 let mut truncated_file = File::create("C:\\RLHoursFolder\\hours.txt")?;
555
556 yellow_ln_bold!("Writing to hours.txt...");
557
558 truncated_file.write_all(rl_hours_str.as_bytes())?;
560
561 green_ln_bold!("Successful!\n");
562 Ok(())
563 } else {
564 let mut file = File::create("C:\\RLHoursFolder\\hours.txt")?;
566 let total_seconds = sw.elapsed_ms() / 1000;
567 let total_hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
568 let rl_hours_str = format!(
569 "Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n", total_seconds, total_hours, hours_past_two
570 );
571
572 yellow_ln_bold!("Writing to hours.txt...");
573
574 file.write_all(rl_hours_str.as_bytes())?;
576
577 green_ln_bold!("The hours file was successfully created");
578 Ok(())
579 }
580}
581
582fn write_to_date(date_result: IoResult<File>, seconds: &u64) -> IoResult<()> {
589 if date_result.is_ok() {
591 let mut append_date_result = File::options()
592 .append(true)
593 .open("C:\\RLHoursFolder\\date.txt")?;
594
595 let today = Local::now().date_naive();
597
598 let today_str = format!("{} {}s\n", today, seconds);
599
600 yellow_ln_bold!("Appending to date.txt...");
601
602 append_date_result.write_all(today_str.as_bytes())?;
604
605 green_ln_bold!("Successful!\n");
606 Ok(())
607 } else {
608 let mut file = File::create("C:\\RLHoursFolder\\date.txt")?;
610 let today = Local::now().date_naive();
611
612 let today_str = format!("{} {}s\n", today, seconds);
613
614 yellow_ln_bold!("Appending to date.txt...");
615
616 file.write_all(today_str.as_bytes())?;
618
619 green_ln_bold!("The date file was successfully created");
620 Ok(())
621 }
622}
623
624fn check_for_process(name: &str) -> bool {
626 let sys = System::new_all();
627 let mut result = false;
628
629 for process in sys.processes_by_exact_name(name.as_ref()) {
630 if process.name() == name {
631 result = true;
632 break;
633 }
634 }
635
636 result
637}