timetrack/config/
mod.rs

1use crate::watcher;
2use crate::TimeTracker;
3use directories::BaseDirs;
4use directories::ProjectDirs;
5use serde_derive::{Deserialize, Serialize};
6use std::fmt;
7use std::fmt::Display;
8use std::fmt::Formatter;
9use std::fs;
10use std::fs::OpenOptions;
11use std::io::Read;
12use std::io::Write;
13use std::path::Path;
14use std::path::PathBuf;
15use std::sync::mpsc::channel;
16use toml;
17
18pub struct Configuration {
19    user_config_path: PathBuf, // this file should not be read outside this module
20    pub track_paths: Vec<PathBuf>,
21    pub raw_data_path: PathBuf,
22    pub processed_data_path: PathBuf,
23}
24
25impl Display for Configuration {
26    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
27        write!(f,
28// Caution: The indent level below matters
29"TimeTrack Configuration
30    User configuration: {:?}
31    Tracking paths: {:?}
32    Raw data: {:?}
33    Processed data: {:?}",
34            self.user_config_path,
35            self.track_paths,
36            self.raw_data_path,
37            self.processed_data_path
38        )
39    }
40}
41
42impl Configuration {
43    /// Used for creating mock configuration files to test other modules
44    pub fn new_mock_config(
45        track_paths: Vec<PathBuf>,
46        raw_data_path: PathBuf,
47        processed_data_path: PathBuf,
48    ) -> Self {
49        Configuration {
50            user_config_path: PathBuf::new(), // this is a private field so for mocking purposes doesn't matter
51            track_paths,
52            raw_data_path,
53            processed_data_path,
54        }
55    }
56}
57
58#[derive(Deserialize, Serialize)]
59struct UserConfig {
60    track_paths: Vec<PathBuf>,
61}
62
63pub fn get_config() -> Configuration {
64    let project_dir = ProjectDirs::from("rust", "cargo", "timetrack")
65        .expect("Failed to read project directories");
66
67    let raw_data_path = get_data_file_path(&project_dir, ".timetrack_raw");
68    let processed_data_path = get_data_file_path(&project_dir, ".timetrack_processed");
69    let user_config_path = project_dir.config_dir().join("timetrack_config");
70    let user_config = read_user_config(&user_config_path);
71
72    Configuration {
73        user_config_path,
74        // TODO how to handle two track paths where one is a subdirectory of another
75        track_paths: user_config.track_paths,
76        raw_data_path,
77        processed_data_path,
78    }
79}
80
81impl<'a> TimeTracker<'a> {
82    pub fn print_config(&self) {
83        println!("{}", self.config);
84        println!("Starting self test..");
85        let (tx, _rx) = channel();
86        for track_path in &self.config.track_paths {
87            match watcher::get_watcher(track_path, tx.clone()) {
88                Ok(_) => {
89                    println!(
90                        "Successfully added watcher for path {}",
91                        track_path.to_string_lossy()
92                    );
93                }
94                Err(err) => {
95                    println!(
96                        "Error {} adding watcher for path {:?}",
97                        err,
98                        track_path.to_string_lossy()
99                    );
100                }
101            };
102        }
103        println!("Completed self test.");
104    }
105}
106
107fn get_data_file_path(project_dirs: &ProjectDirs, filename: &str) -> PathBuf {
108    let data_directory = project_dirs.data_local_dir();
109    let data_file_path = data_directory.join(filename);
110
111    fs::create_dir_all(&data_directory).expect("Failed to create data directory");
112    OpenOptions::new()
113        .create(true)
114        .read(true)
115        .write(true)
116        .open(&data_file_path)
117        .expect("Failed to create data file");
118
119    data_file_path
120}
121
122fn read_user_config(user_config_path: &PathBuf) -> UserConfig {
123    if !user_config_path.exists() {
124        init_config_file(&user_config_path);
125    }
126
127    let mut f = OpenOptions::new()
128        .read(true)
129        .open(&user_config_path)
130        .expect("Failed to open config file");
131
132    let mut contents = String::new();
133    f.read_to_string(&mut contents)
134        .expect("something went wrong reading the file");
135
136    toml::from_str(&contents).expect("Failed to parse config file as TOML")
137}
138
139fn init_config_file(config_file_path: impl AsRef<Path>) {
140    let config_dir = config_file_path.as_ref().parent().unwrap();
141
142    fs::create_dir_all(&config_dir).expect("Failed to create config directory");
143    let mut f = OpenOptions::new()
144        .create(true)
145        .read(true)
146        .write(true)
147        .open(&config_file_path)
148        .expect("Failed to create config file");
149
150    let home_dir = BaseDirs::new()
151        .expect("Unable to find home directory")
152        .home_dir()
153        .to_owned();
154    let default_config = UserConfig {
155        track_paths: vec![home_dir],
156    };
157
158    write!(
159        &mut f,
160        "{}",
161        toml::to_string(&default_config).expect("Failed to convert default user config to TOML")
162    )
163    .expect("Failed to initialize configuration file");
164}