1use chrono::{Duration, Local, NaiveDateTime};
2use dirs::data_local_dir;
3use rev_lines::RevLines;
4use std::fs::create_dir;
5use std::io::BufReader;
6use std::path::PathBuf;
7use std::{
8 fs::{File, OpenOptions},
9 io::Write,
10 path::Path,
11};
12const TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
13pub struct Config {
14 pub data_file_path: PathBuf,
15}
16
17impl Default for Config {
18 fn default() -> Self {
19 let mut path = PathBuf::from(data_local_dir().unwrap());
20 path.push("time-tracker");
21 path.push("logs");
22 Self {
23 data_file_path: path,
24 }
25 }
26}
27pub struct Args {
28 pub mode: String,
29}
30
31impl Args {
32 pub fn build(mut args: impl Iterator<Item = String>) -> Result<Self, &'static str> {
33 args.next();
35
36 let mode = match args.next() {
37 Some(arg) => arg.to_lowercase(),
38 None => return Err("Didn't get mode (start/end)."),
39 };
40
41 Ok(Args { mode })
42 }
43}
44
45fn open_file(config: Config) -> File {
46 let path = Path::new(&config.data_file_path);
47
48 if path.exists() {
50 OpenOptions::new()
51 .read(true)
52 .append(true)
53 .open(&path)
54 .expect("Could not open file!")
55 } else {
56 create_dir(path.parent().unwrap()).expect("Couldn't create directory");
57 OpenOptions::new()
58 .create(true)
59 .read(true)
60 .append(true)
61 .open(&path)
62 .expect("Could not open file!")
63 }
64}
65
66pub fn write(mut file: &File, mode: &str) {
68 file.write_fmt(format_args!("{0} {1}\n", get_current_time(), mode))
69 .expect("Could not write to file");
70}
71
72pub fn log(args: Args, config: Config) -> Result<(), &'static str> {
73 if !["start", "end"].contains(&&*args.mode) {
74 return Err("Invalid mode! Please provide one of the following: `start` or `end`");
75 }
76
77 let mode = args.mode;
78 let file = open_file(config);
79
80 write(&file, &mode);
81 if mode == "end" {
82 let duration = calculate_time_difference(&file).unwrap();
83 println!("{}", format_duration(duration));
84 }
85 Ok(())
86}
87
88pub fn calculate_time_difference(file: &File) -> Option<chrono::Duration> {
89 let mut rev_lines = RevLines::new(BufReader::new(file)).unwrap();
90 let end_line = rev_lines.next()?;
91
92 let (date_time, mode) = parse_line(end_line).unwrap();
93 let end_time = parse_time(&date_time);
94 if mode != "end" {
95 panic!("End record wasn't logged properly");
96 }
97
98 for line in rev_lines {
99 let (date_time, mode) = parse_line(line).unwrap();
100 if mode == "start" {
101 let start_time = parse_time(&date_time);
102 return Some(end_time - start_time);
103 }
104 }
105 None
106}
107
108fn parse_time(date_time: &str) -> NaiveDateTime {
109 NaiveDateTime::parse_from_str(&date_time, TIME_FORMAT).expect("Could not parse time")
110}
111
112fn parse_line(line: String) -> Option<(String, String)> {
113 let mut parsed_line = line.split(' ');
114 let date = parsed_line.next()?;
115 let time = parsed_line.next()?;
116 let mode = parsed_line.next()?;
117
118 Some((format!("{} {}", date, time), mode.to_string()))
119}
120
121pub fn get_current_time() -> String {
123 let now = Local::now();
124 format!("{}", now.format(TIME_FORMAT))
125}
126
127fn format_duration(duration: Duration) -> String {
128 let seconds = duration.num_seconds() % 60;
129 let minutes = duration.num_minutes() % 60;
130 let hours = duration.num_hours();
131 format!("{}h {}m {}s", hours, minutes, seconds)
132}