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