1use std::error::Error;
2
3use chrono::Local;
4
5use crate::cli::{
6 Opts, Subcommand, TimingCommand, DEFAULT_EMPTY_STATUS_MSG, DEFAULT_STATUS_FORMAT,
7};
8use crate::persistence::{load_or_create_log, save_log, FileHandler};
9use crate::pretty_string::PrettyString;
10use crate::report_creator::ReportCreator;
11use crate::time_log::TimeLog;
12use colored::Colorize;
13use std::fmt::Display;
14use std::fmt::Formatter;
15
16pub mod cli;
17pub mod persistence;
18mod pretty_string;
19mod report_creator;
20mod time_log;
21
22pub fn run_app(o: Opts, fh: &mut dyn FileHandler) -> Result<(), TrackieError> {
23 let mut modified = false;
24 let mut log = load_or_create_log(fh)?;
25 let report_creator = ReportCreator::new(&log);
26
27 match o.sub_cmd {
28 Subcommand::Start(p) => {
29 modified = true;
30 start_tracking(&mut log, p)?;
31 }
32 Subcommand::Stop(_) => {
33 modified = true;
34 let pending = log.stop_pending()?;
35 let dur = pending.get_pending_duration();
36 println!(
37 "Tracked {} on project {}",
38 dur.to_pretty_string().bold(),
39 pending.project_name.italic()
40 );
41 }
42 Subcommand::Report(o) => {
43 let report = report_creator.report_days(Local::today(), o.days, o.include_empty_days);
44 match o.json {
45 true => println!("{}", serde_json::to_string_pretty(&report)?),
46 false => println!("{}", report),
47 };
48 }
49 Subcommand::Status(s) => match &log.pending {
50 None => {
51 let msg = s
52 .fallback
53 .unwrap_or_else(|| DEFAULT_EMPTY_STATUS_MSG.to_string());
54
55 return Err(TrackieError {
56 msg,
57 print_as_error: false,
58 });
59 }
60 Some(p) => {
61 let format = s
62 .format
63 .unwrap_or_else(|| DEFAULT_STATUS_FORMAT.to_string());
64
65 let output = format
66 .replace("%p", p.project_name.as_str())
67 .replace("%d", p.start.format("%F").to_string().as_str())
68 .replace("%t", p.start.format("%R").to_string().as_str())
69 .replace("%D", p.get_pending_duration().to_pretty_string().as_str());
70
71 println!("{}", output);
72 }
73 },
74 Subcommand::Resume(_) => match (&log.pending, log.get_latest_entry()) {
75 (None, Some(s)) => {
76 modified = true;
77 let name = s.project_name.clone();
78 start_tracking(&mut log, TimingCommand { project_name: name })?;
79 }
80 (Some(p), _) => {
81 return Err(TrackieError::new(
82 format!("Already tracking time for project {}", p.project_name).as_str(),
83 ))
84 }
85 (_, None) => {
86 return Err(TrackieError::new(
87 "Unable to find latest time log. Maybe no time was ever tracked?",
88 ));
89 }
90 },
91 }
92
93 if modified {
94 save_log(fh, &log)?;
95 }
96
97 Ok(())
98}
99
100fn start_tracking(log: &mut TimeLog, p: TimingCommand) -> Result<(), Box<dyn Error>> {
101 if let Some(warn) = log.start_log(&p.project_name)? {
102 println!("{} {}", "WARN:".yellow(), warn);
103 }
104 println!(
105 "Tracking time for project {}",
106 p.project_name.as_str().italic()
107 );
108 Ok(())
109}
110
111#[derive(Debug)]
112pub struct TrackieError {
113 msg: String,
114 pub print_as_error: bool,
115}
116
117impl TrackieError {
118 fn new(msg: &str) -> TrackieError {
119 TrackieError {
120 msg: msg.to_string(),
121 print_as_error: true,
122 }
123 }
124}
125
126impl Display for TrackieError {
127 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
128 write!(f, "{}", self.msg)
129 }
130}
131
132impl From<Box<dyn Error>> for TrackieError {
133 fn from(i: Box<dyn Error>) -> Self {
134 TrackieError::new(i.to_string().as_str())
135 }
136}
137
138impl From<serde_json::Error> for TrackieError {
139 fn from(e: serde_json::Error) -> Self {
140 TrackieError::new(e.to_string().as_str())
141 }
142}
143
144impl Error for TrackieError {}
145
146#[cfg(test)]
147mod tests {
148 use crate::cli::{
149 EmptyCommand, Opts, StatusCommand, Subcommand, TimingCommand, DEFAULT_EMPTY_STATUS_MSG,
150 };
151 use crate::persistence::FileHandler;
152 use crate::run_app;
153 use std::error::Error;
154
155 #[test]
156 fn status_on_empty_fallback() {
157 let mut handler = TestFileHandler::default();
158 let e = run_app(
159 Opts {
160 sub_cmd: Subcommand::Status(StatusCommand {
161 format: None,
162 fallback: Some("Foo".to_string()),
163 }),
164 },
165 &mut handler,
166 );
167 assert!(e.is_err());
168 assert_eq!(e.unwrap_err().msg, "Foo");
169 }
170
171 #[test]
172 fn status_on_empty_no_fallback() {
173 let mut handler = TestFileHandler::default();
174 let e = run_app(
175 Opts {
176 sub_cmd: Subcommand::Status(StatusCommand {
177 format: None,
178 fallback: None,
179 }),
180 },
181 &mut handler,
182 );
183 assert!(e.is_err());
184 assert_eq!(e.unwrap_err().msg, DEFAULT_EMPTY_STATUS_MSG);
185 }
186
187 #[test]
188 fn start_tracking() -> Result<(), Box<dyn Error>> {
189 let mut handler = TestFileHandler::default();
190 run_app(
191 Opts {
192 sub_cmd: Subcommand::Start(TimingCommand {
193 project_name: "Foo".to_string(),
194 }),
195 },
196 &mut handler,
197 )?;
198
199 let content = handler.content.unwrap();
200 assert!(content.contains("Foo"));
201 Ok(())
202 }
203
204 #[test]
205 fn resume_after_stop() -> Result<(), Box<dyn Error>> {
206 let mut handler = TestFileHandler::default();
207
208 let x = run_app(
209 Opts {
210 sub_cmd: Subcommand::Resume(EmptyCommand {}),
211 },
212 &mut handler,
213 );
214 assert!(x.is_err());
215
216 run_app(
217 Opts {
218 sub_cmd: Subcommand::Start(TimingCommand {
219 project_name: "Foo".to_string(),
220 }),
221 },
222 &mut handler,
223 )?;
224 run_app(
225 Opts {
226 sub_cmd: Subcommand::Stop(EmptyCommand {}),
227 },
228 &mut handler,
229 )?;
230
231 let status = run_app(
232 Opts {
233 sub_cmd: Subcommand::Status(StatusCommand {
234 fallback: None,
235 format: None,
236 }),
237 },
238 &mut handler,
239 );
240 assert!(status.is_err());
241
242 run_app(
243 Opts {
244 sub_cmd: Subcommand::Resume(EmptyCommand {}),
245 },
246 &mut handler,
247 )?;
248
249 let status = run_app(
250 Opts {
251 sub_cmd: Subcommand::Status(StatusCommand {
252 fallback: None,
253 format: None,
254 }),
255 },
256 &mut handler,
257 );
258 assert!(status.is_ok());
259
260 Ok(())
261 }
262
263 #[test]
264 fn status_after_start_tracking() -> Result<(), Box<dyn Error>> {
265 let mut handler = TestFileHandler::default();
266 run_app(
267 Opts {
268 sub_cmd: Subcommand::Start(TimingCommand {
269 project_name: "Foo".to_string(),
270 }),
271 },
272 &mut handler,
273 )?;
274
275 run_app(
276 Opts {
277 sub_cmd: Subcommand::Status(StatusCommand {
278 format: None,
279 fallback: None,
280 }),
281 },
282 &mut handler,
283 )?;
284 Ok(())
285 }
286
287 #[test]
288 fn stop_tracking() -> Result<(), Box<dyn Error>> {
289 let mut handler = TestFileHandler::default();
290 run_app(
291 Opts {
292 sub_cmd: Subcommand::Start(TimingCommand {
293 project_name: "Foo".to_string(),
294 }),
295 },
296 &mut handler,
297 )?;
298
299 run_app(
300 Opts {
301 sub_cmd: Subcommand::Stop(EmptyCommand {}),
302 },
303 &mut handler,
304 )?;
305
306 let status = run_app(
307 Opts {
308 sub_cmd: Subcommand::Status(StatusCommand {
309 format: None,
310 fallback: None,
311 }),
312 },
313 &mut handler,
314 );
315
316 assert!(status.is_err());
317
318 let second_stop = run_app(
319 Opts {
320 sub_cmd: Subcommand::Stop(EmptyCommand {}),
321 },
322 &mut handler,
323 );
324
325 assert!(second_stop.is_err());
326 Ok(())
327 }
328
329 struct TestFileHandler {
330 content: Option<String>,
331 }
332
333 impl Default for TestFileHandler {
334 fn default() -> Self {
335 Self { content: None }
336 }
337 }
338
339 impl FileHandler for TestFileHandler {
340 fn read_file(&self) -> Result<Option<String>, Box<dyn Error>> {
341 Ok(self.content.clone())
342 }
343
344 fn write_file(&mut self, content: &str) -> Result<(), Box<dyn Error>> {
345 self.content = Some(content.to_string());
346 Ok(())
347 }
348 }
349}