time_tracker/
lib.rs

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        // skipping filename
34        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    // TODO: remove repetition
49    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
66/// Logs current time along with the `mode` to `file`
67pub 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
121/// Returns current date & time in a readable format
122pub 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}