use chrono::Local;
use colour::{black_bold, blue_ln_bold, cyan, green, green_ln_bold, red, white, yellow_ln_bold};
use log::{error, info, trace, warn, LevelFilter};
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Logger, Root},
encode::pattern::PatternEncoder,
Config, Handle,
};
use std::{
error::Error,
fmt::Display,
fs::{self, File},
io::{self, Read, Write},
process,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread,
time::{Duration, SystemTime},
};
use stopwatch::Stopwatch;
use sysinfo::System;
use tokio::runtime::Runtime;
use winit::event_loop::EventLoopProxy;
use crate::{calculate_past_two::calculate_past_two, winit_tray_icon::UserEvent};
pub mod calculate_past_two;
#[cfg(test)]
mod tests;
pub mod update;
pub mod website_files;
pub mod winit_tray_icon;
pub type IoResult<T> = Result<T, io::Error>;
struct ProgramRunVars {
proxy: EventLoopProxy<UserEvent>,
process_name: String,
is_waiting: bool,
option: String,
currently_tracking: Arc<Mutex<AtomicBool>>,
stop_tracker: Arc<Mutex<AtomicBool>>,
}
impl ProgramRunVars {
fn new(
proxy: EventLoopProxy<UserEvent>,
stop_tracker: Arc<Mutex<AtomicBool>>,
currently_tracking: Arc<Mutex<AtomicBool>>,
) -> Self {
Self {
process_name: String::from("RocketLeague.exe"),
is_waiting: false,
option: String::with_capacity(1),
proxy,
stop_tracker,
currently_tracking,
}
}
}
#[derive(Debug, Clone)]
pub struct PastTwoError;
impl Display for PastTwoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"next closest date to the date two weeks ago could not be found."
)
}
}
impl Error for PastTwoError {}
pub fn initialize_logging() -> Result<Handle, Box<dyn Error>> {
let stdout = ConsoleAppender::builder().build();
let general_logs = FileAppender::builder()
.build("C:/RLHoursFolder/logs/general_$TIME{%Y-%m-%d_%H-%M-%S}.log")?;
let wti_logs = FileAppender::builder()
.build("C:/RLHoursFolder/logs/tray-icon_$TIME{%Y-%m-%d_%H-%M-%S}.log")?;
let requests = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
.build("C:/RLHoursFolder/logs/requests.log")?;
let rl_hours_tracker_logger = Logger::builder()
.additive(false)
.appenders(vec!["general_logs"])
.build("rl_hours_tracker", LevelFilter::Trace);
let rl_hours_tracker_update_logger = Logger::builder()
.additive(false)
.appenders(vec!["requests", "general_logs"])
.build("rl_hours_tracker::update", LevelFilter::Trace);
let rl_hours_tracker_cpt_logger = Logger::builder()
.additive(false)
.appenders(vec!["general_logs"])
.build("rl_hours_tracker::calculate_past_two", LevelFilter::Info);
let rl_hours_tracker_wti_logger = Logger::builder()
.additive(false)
.appenders(vec!["wti_logs"])
.build("rl_hours_tracker::winit_tray_icon", LevelFilter::Info);
let loggers = vec![
rl_hours_tracker_logger,
rl_hours_tracker_update_logger,
rl_hours_tracker_cpt_logger,
rl_hours_tracker_wti_logger,
];
let appenders = vec![
Appender::builder().build("stdout", Box::new(stdout)),
Appender::builder().build("general_logs", Box::new(general_logs)),
Appender::builder().build("requests", Box::new(requests)),
Appender::builder().build("wti_logs", Box::new(wti_logs)),
];
let config = Config::builder()
.appenders(appenders)
.loggers(loggers)
.build(Root::builder().appender("stdout").build(LevelFilter::Warn))?;
let handle = log4rs::init_config(config)?;
Ok(handle)
}
pub fn run_self_update() -> Result<(), Box<dyn Error>> {
let rt = Runtime::new()?;
rt.block_on(update::check_for_update())?;
Ok(())
}
pub fn run(
proxy: EventLoopProxy<UserEvent>,
stop_tracker: Arc<Mutex<AtomicBool>>,
currently_tracking: Arc<Mutex<AtomicBool>>,
) {
let mut program = ProgramRunVars::new(proxy, stop_tracker, currently_tracking);
run_main_loop(&mut program);
}
pub fn create_directory() -> Vec<IoResult<()>> {
let folder = fs::create_dir("C:\\RLHoursFolder");
let website_folder = fs::create_dir("C:\\RLHoursFolder\\website");
let website_pages = fs::create_dir("C:\\RLHoursFolder\\website\\pages");
let website_css = fs::create_dir("C:\\RLHoursFolder\\website\\css");
let website_js = fs::create_dir("C:\\RLHoursFolder\\website\\js");
let website_images = fs::create_dir("C:\\RLHoursFolder\\website\\images");
let folder_vec: Vec<IoResult<()>> = vec![
folder,
website_folder,
website_pages,
website_css,
website_js,
website_images,
];
let result: Vec<IoResult<()>> = folder_vec.into_iter().filter(|f| f.is_err()).collect();
result
}
fn run_main_loop(program: &mut ProgramRunVars) {
loop {
if check_for_process(&program.process_name) {
record_hours(
&program.process_name,
program.stop_tracker.clone(),
program.currently_tracking.clone(),
);
website_files::generate_website_files(true)
.unwrap_or_else(|e| warn!("failed to generate website files: {e}"));
program.is_waiting = false;
print!("End program (");
green!("y");
print!(" / ");
red!("n");
print!("): ");
std::io::stdout()
.flush()
.unwrap_or_else(|_| println!("End program (y/n)?\n"));
io::stdin()
.read_line(&mut program.option)
.unwrap_or_default();
if program.option.trim() == "y" || program.option.trim() == "Y" {
print!("{}[2K\r", 27 as char);
std::io::stdout()
.flush()
.expect("could not flush the output stream");
yellow_ln_bold!("Goodbye!");
program
.proxy
.send_event(UserEvent::QuitApp(AtomicBool::new(true)))
.unwrap_or_else(|_| error!("event loop already closed"));
break;
} else if program.option.trim() == "n" || program.option.trim() == "N" {
program.option = String::with_capacity(1);
continue;
} else {
error!("Unexpected input! Ending program.");
program
.proxy
.send_event(UserEvent::QuitApp(AtomicBool::new(true)))
.unwrap_or_else(|_| error!("event loop already closed"));
break;
}
} else {
if !program.is_waiting {
green!("Waiting for Rocket League to start.\r");
io::stdout()
.flush()
.expect("could not flush the output stream");
thread::sleep(Duration::from_millis(500));
white!("Waiting for Rocket League to start..\r");
io::stdout()
.flush()
.expect("could not flush the output stream");
thread::sleep(Duration::from_millis(500));
black_bold!("Waiting for Rocket League to start...\r");
io::stdout()
.flush()
.expect("could not flush the output stream");
thread::sleep(Duration::from_millis(500));
print!("{}[2K\r", 27 as char);
red!("Waiting for Rocket League to start\r");
io::stdout()
.flush()
.expect("could not flush the output stream");
thread::sleep(Duration::from_millis(500));
}
}
}
}
fn record_hours(
process_name: &str,
stop_tracker: Arc<Mutex<AtomicBool>>,
currently_tracking: Arc<Mutex<AtomicBool>>,
) {
let mut sw = Stopwatch::start_new();
blue_ln_bold!("\nRocket League is running\n");
currently_tracking
.try_lock()
.unwrap_or_else(|e| {
error!("error when attempting to access lock for currently_tracking: {e}");
panic!("could not access lock for currently_tracking");
})
.store(true, Ordering::SeqCst);
trace!(
"<< fn record_hours >> currently_tracking set to {} before live_stopwatch",
currently_tracking
.try_lock()
.unwrap()
.load(Ordering::Relaxed)
);
trace!(
"<< fn record_hours >> stop_tracker set to {} before live_stopwatch",
stop_tracker.try_lock().unwrap().load(Ordering::Relaxed)
);
live_stopwatch(process_name, stop_tracker.clone());
trace!(
"<< fn record_hours >> stop_tracker set to {} after live_stopwatch",
stop_tracker.try_lock().unwrap().load(Ordering::Relaxed)
);
currently_tracking
.try_lock()
.unwrap_or_else(|e| {
error!("error when attempting to access lock for currently_tracking: {e}");
panic!("could not access lock for currently_tracking");
})
.store(false, Ordering::SeqCst);
trace!(
"<< fn record_hours >> currently_tracking set to {} after live_stopwatch",
currently_tracking
.try_lock()
.unwrap()
.load(Ordering::Relaxed)
);
stop_tracker
.try_lock()
.unwrap_or_else(|e| {
error!("error when attempting to access lock for stop_tracking: {e}");
panic!("could not access lock for stop_tracking");
})
.store(false, Ordering::SeqCst);
sw.stop();
info!("Record Hours: START\n");
let seconds: u64 = sw.elapsed_ms() as u64 / 1000;
let hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
trace!("<< fn record_hours >> seconds: {seconds}, hours: {hours:.1}");
let hours_result = File::open("C:\\RLHoursFolder\\hours.txt");
let date_result = File::open("C:\\RLHoursFolder\\date.txt");
write_to_date(date_result, &seconds).unwrap_or_else(|e| {
error!("error writing to date.txt: {e}");
process::exit(1);
});
let hours_buffer = calculate_past_two().unwrap_or_else(|e| {
warn!("failed to calculate past two: {e}");
0
});
trace!("<< fn record_hours >> hours_buffer is set to {hours_buffer}");
if hours_buffer != 0 {
let hours_past_two = hours_buffer as f32 / 3600_f32;
write_to_hours(hours_result, &seconds, &hours, &hours_past_two, &sw).unwrap_or_else(|e| {
error!("error writing to hours.txt: {e}");
process::exit(1);
});
info!("Record Hours: FINISHED\n")
} else {
warn!("past two returned zero seconds")
}
}
fn live_stopwatch(process_name: &str, stop_tracker: Arc<Mutex<AtomicBool>>) {
let mut timer_early = SystemTime::now();
let mut seconds: u8 = 0;
let mut minutes: u8 = 0;
let mut hours: u16 = 0;
while check_for_process(process_name)
&& !stop_tracker
.try_lock()
.unwrap_or_else(|e| {
error!("error when attempting to access lock for stop_tracking: {e}");
panic!("could not access lock for stop_tracking");
})
.load(Ordering::SeqCst)
{
let timer_now = timer_early
.checked_add(Duration::from_millis(999))
.unwrap_or_else(|| {
error!("could not return system time");
SystemTime::now()
});
let delay = timer_now.duration_since(timer_early).unwrap_or_else(|e| {
warn!(
"system time is ahead of the timer. SystemTime difference: {:?}",
e.duration()
);
Duration::from_millis(1000)
});
if seconds == 59 {
seconds = 0;
minutes += 1;
if minutes == 60 {
minutes = 0;
hours += 1;
}
} else {
seconds += 1;
}
print!("{}[2K\r", 27 as char);
if hours < 10 && minutes < 10 && seconds < 10 {
cyan!("Time Elapsed: 0{}:0{}:0{}\r", hours, minutes, seconds);
} else if hours >= 10 {
if minutes < 10 && seconds < 10 {
cyan!("Time Elapsed: {}:0{}:0{}\r", hours, minutes, seconds);
} else if minutes < 10 && seconds >= 10 {
cyan!("Time Elapsed: {}:0{}:{}\r", hours, minutes, seconds);
} else if minutes >= 10 && seconds < 10 {
cyan!("Time Elapsed: {}:{}:0{}\r", hours, minutes, seconds);
} else {
cyan!("Time Elapsed: {}:{}:{}\r", hours, minutes, seconds);
}
} else if hours < 10 && minutes >= 10 && seconds < 10 {
cyan!("Time Elapsed: 0{}:{}:0{}\r", hours, minutes, seconds);
} else if hours < 10 && minutes < 10 && seconds >= 10 {
cyan!("Time Elapsed: 0{}:0{}:{}\r", hours, minutes, seconds);
} else {
cyan!("Time Elapsed: 0{}:{}:{}\r", hours, minutes, seconds);
}
io::stdout()
.flush()
.unwrap_or_else(|_| warn!("could not flush output stream"));
thread::sleep(delay);
timer_early += Duration::from_millis(999)
}
trace!("<< fn live_stopwatch >> hours: {hours}, minutes: {minutes}, seconds: {seconds}");
}
fn retrieve_time(contents: &str) -> Result<(u64, f32), Box<dyn Error>> {
let split_new_line: Vec<&str> = contents.split("\n").collect();
let split_whitspace_sec: Vec<&str> = split_new_line[1].split_whitespace().collect();
let split_whitespace_hrs: Vec<&str> = split_new_line[2].split_whitespace().collect();
let split_char_sec = split_whitspace_sec[2].chars();
let split_char_hrs = split_whitespace_hrs[2].chars();
let mut sec_vec: Vec<char> = vec![];
let mut hrs_vec: Vec<char> = vec![];
for num in split_char_sec {
if num.is_numeric() {
sec_vec.push(num);
}
}
trace!("<< fn retrieve_time >> seconds vector: {sec_vec:?}");
for num in split_char_hrs {
if num.is_numeric() || num == '.' {
hrs_vec.push(num);
}
}
trace!("<< fn retrieve_time >> hours vector: {hrs_vec:?}");
let seconds_str: String = sec_vec.iter().collect();
let hours_str: String = hrs_vec.iter().collect();
let old_seconds: u64 = seconds_str.parse()?;
let old_hours: f32 = hours_str.parse()?;
trace!("<< fn retrieve_time >> old seconds: {old_seconds}, old_hours: {old_hours:.1}");
Ok((old_seconds, old_hours))
}
fn return_new_hours(
contents: &str,
seconds: &u64,
hours: &f32,
past_two: &f32,
) -> Result<String, Box<dyn Error>> {
yellow_ln_bold!("Getting old hours...");
let (old_seconds, old_hours) = retrieve_time(contents)?;
let added_seconds = old_seconds + *seconds;
let added_hours = old_hours + *hours;
trace!("<< fn return_new_hours >> added_seconds: {added_seconds}, added_hours: {added_hours:.1}");
Ok(format!(
"Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n",
added_seconds, added_hours, past_two
))
}
fn write_to_hours(
hours_result: IoResult<File>,
seconds: &u64,
hours: &f32,
hours_past_two: &f32,
sw: &Stopwatch,
) -> Result<(), Box<dyn Error>> {
if let Ok(mut file) = hours_result {
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let rl_hours_str = return_new_hours(&contents, seconds, hours, hours_past_two)?;
let mut truncated_file = File::create("C:\\RLHoursFolder\\hours.txt")?;
yellow_ln_bold!("Writing to hours.txt...");
truncated_file.write_all(rl_hours_str.as_bytes())?;
green_ln_bold!("Successful!\n");
Ok(())
} else {
let mut file = File::create("C:\\RLHoursFolder\\hours.txt")?;
let total_seconds = sw.elapsed_ms() / 1000;
let total_hours: f32 = (sw.elapsed_ms() as f32 / 1000_f32) / 3600_f32;
let rl_hours_str = format!(
"Rocket League Hours\nTotal Seconds: {}s\nTotal Hours: {:.1}hrs\nHours Past Two Weeks: {:.1}hrs\n", total_seconds, total_hours, hours_past_two
);
yellow_ln_bold!("Writing to hours.txt...");
file.write_all(rl_hours_str.as_bytes())?;
green_ln_bold!("The hours file was successfully created");
Ok(())
}
}
fn write_to_date(date_result: IoResult<File>, seconds: &u64) -> IoResult<()> {
if date_result.is_ok() {
let mut append_date_result = File::options()
.append(true)
.open("C:\\RLHoursFolder\\date.txt")?;
let today = Local::now().date_naive();
let today_str = format!("{} {}s\n", today, seconds);
yellow_ln_bold!("Appending to date.txt...");
append_date_result.write_all(today_str.as_bytes())?;
green_ln_bold!("Successful!\n");
Ok(())
} else {
let mut file = File::create("C:\\RLHoursFolder\\date.txt")?;
let today = Local::now().date_naive();
let today_str = format!("{} {}s\n", today, seconds);
yellow_ln_bold!("Appending to date.txt...");
file.write_all(today_str.as_bytes())?;
green_ln_bold!("The date file was successfully created");
Ok(())
}
}
fn check_for_process(name: &str) -> bool {
let sys = System::new_all();
let mut result = false;
for process in sys.processes_by_exact_name(name.as_ref()) {
if process.name() == name {
result = true;
break;
}
}
result
}