1use color_eyre::{Result, eyre::bail};
2use core::fmt;
3use std::fmt::Write;
4use std::{
5 cmp::Ordering,
6 fmt::Display,
7 fs::{File, read_to_string},
8 path::PathBuf,
9};
10use tracing::{debug, info};
11
12use crate::TasksConfig;
13use crate::config::{CoreConfig, PrettySymbolsConfig};
14
15use super::date::Date;
16
17#[derive(Debug, Hash, Eq, PartialEq, Clone)]
21pub enum State {
22 ToDo,
23 Done,
24 Incomplete,
25 Canceled,
26}
27
28impl Ord for State {
29 fn cmp(&self, other: &Self) -> Ordering {
30 match (self, other) {
31 (State::ToDo, State::ToDo)
32 | (State::Done, State::Done)
33 | (State::Canceled, State::Canceled)
34 | (State::Incomplete, State::Incomplete) => Ordering::Equal,
35 (State::Canceled | State::Done, State::ToDo)
36 | (State::ToDo | State::Done | State::Canceled, State::Incomplete)
37 | (State::Done, State::Canceled) => Ordering::Greater,
38 (State::ToDo, State::Done | State::Canceled)
39 | (State::Incomplete, State::ToDo | State::Done | State::Canceled)
40 | (State::Canceled, State::Done) => Ordering::Less,
41 }
42 }
43}
44
45impl PartialOrd for State {
46 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
47 Some(self.cmp(other))
48 }
49}
50
51impl State {
52 pub fn display(&self, state_symbols: &PrettySymbolsConfig) -> String {
53 match self {
54 Self::Done => state_symbols.task_done.clone(),
55 Self::ToDo => state_symbols.task_todo.clone(),
56 Self::Incomplete => state_symbols.task_incomplete.clone(),
57 Self::Canceled => state_symbols.task_canceled.clone(),
58 }
59 }
60}
61impl Display for State {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 let default_symbols = TasksConfig::default().pretty_symbols.clone();
64 write!(f, "{}", self.display(&default_symbols))?;
65 Ok(())
66 }
67}
68
69#[derive(Debug, Hash, Eq, PartialEq, Clone)]
70pub struct Task {
71 pub subtasks: Vec<Task>,
72 pub description: Option<String>,
73 pub due_date: Option<Date>,
75 pub path: PathBuf,
76 pub line_number: Option<usize>,
80 pub name: String,
81 pub priority: usize,
82 pub completion: Option<usize>,
83 pub state: State,
84 pub tags: Option<Vec<String>>,
85 pub is_today: bool,
86}
87
88impl Default for Task {
89 fn default() -> Self {
90 Self {
91 due_date: None,
92 name: String::new(),
93 priority: 0,
94 state: State::ToDo,
95 tags: None,
96 description: None,
97 line_number: None,
98 subtasks: vec![],
99 path: PathBuf::new(),
100 is_today: false,
101 completion: None,
102 }
103 }
104}
105
106impl fmt::Display for Task {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 let default_symbols = PrettySymbolsConfig::default(); let state = self.state.to_string();
110 let title = format!("{state} {}", self.name);
111 writeln!(f, "{title}")?;
112
113 let mut data_line = String::new();
114 let is_today = if self.is_today {
115 format!("{} ", default_symbols.today_tag)
116 } else {
117 String::new()
118 };
119 data_line.push_str(&is_today);
120 if let Some(date) = &self.due_date {
121 write!(
122 data_line,
123 "{} {} ({}) ",
124 default_symbols.due_date,
125 date,
126 date.get_relative_str()
127 )?;
128 }
129
130 if self.priority > 0 {
131 write!(data_line, "{}{} ", default_symbols.priority, self.priority)?;
132 }
133 if let Some(bar) = self.completion_bar_to_string(5, &default_symbols) {
134 data_line.push_str(&bar);
135 }
136 if !data_line.is_empty() {
137 writeln!(f, "{data_line}")?;
138 }
139 let mut tag_line = String::new();
140 if self.tags.is_some() {
141 tag_line.push_str(
142 &self
143 .tags
144 .clone()
145 .unwrap()
146 .iter()
147 .map(|t| format!("#{t}"))
148 .collect::<Vec<String>>()
149 .join(" "),
150 );
151 }
152 if !tag_line.is_empty() {
153 writeln!(f, "{tag_line}")?;
154 }
155 if let Some(description) = self.description.clone() {
156 for l in description.lines() {
157 writeln!(f, "{l}")?;
158 }
159 }
160 Ok(())
161 }
162}
163impl Task {
164 #[must_use]
165 pub fn priority_to_string(&self, symbols: &PrettySymbolsConfig) -> String {
166 if self.priority > 0 {
167 format!("{}{}", symbols.priority, self.priority)
168 } else {
169 String::new()
170 }
171 }
172 #[must_use]
173 pub fn due_date_to_string(
174 &self,
175 symbols: &PrettySymbolsConfig,
176 core_config: &CoreConfig,
177 ) -> String {
178 match &self.due_date {
179 None => String::new(),
180 Some(date) => {
181 let this = &date;
182 let due_date_symbol: &str = &symbols.due_date;
183 format!(
184 "{due_date_symbol} {}",
185 this.to_string_format(core_config.use_american_format)
186 )
187 }
188 }
189 }
190 #[must_use]
191 pub fn is_today_to_string(&self, symbols: &PrettySymbolsConfig) -> String {
192 if self.is_today {
193 symbols.today_tag.to_string()
194 } else {
195 String::new()
196 }
197 }
198 #[must_use]
199 pub fn state_to_string(&self, symbols: &PrettySymbolsConfig) -> String {
200 self.state.display(symbols)
201 }
202 #[must_use]
203 pub fn completion_bar_to_string(
204 &self,
205 length: usize,
206 symbols: &PrettySymbolsConfig,
207 ) -> Option<String> {
208 let percentage = self.completion?;
209 let progress_bar = (1..=length)
210 .map(|c| {
211 if c * (100 / length) <= percentage {
212 symbols.progress_bar_true.clone()
213 } else {
214 symbols.progress_bar_false.clone()
215 }
216 })
217 .collect::<String>();
218
219 Some(format!("[{progress_bar} {percentage}%]"))
220 }
221 pub fn get_fixed_attributes(&self, config: &TasksConfig, indent_length: usize) -> String {
222 let indent = " ".repeat(indent_length);
223
224 let state_str = match self.state {
225 State::Done => config.task_state_markers.done,
226 State::ToDo => config.task_state_markers.todo,
227 State::Incomplete => config.task_state_markers.incomplete,
228 State::Canceled => config.task_state_markers.canceled,
229 };
230
231 let priority = if self.priority > 0 {
232 format!("p{}", self.priority)
233 } else {
234 String::new()
235 };
236
237 let completion = match self.completion {
238 Some(c) => format!("c{c}"),
239 None => String::new(),
240 };
241
242 let due_date = if let Some(due_date) = &self.due_date {
243 due_date.to_string_format(config.core.use_american_format)
244 } else {
245 String::new()
246 };
247
248 let tags_str = self.tags.as_ref().map_or_else(String::new, |tags| {
249 tags.clone()
250 .iter()
251 .map(|t| format!("#{t}"))
252 .collect::<Vec<String>>()
253 .join(" ")
254 });
255
256 let today_tag = if self.is_today {
257 String::from("@today")
258 } else {
259 String::new()
260 };
261
262 let res = format!(
263 "{}- [{}] {}",
264 indent,
265 state_str,
266 [
267 self.name.clone(),
268 due_date,
269 completion,
270 priority,
271 tags_str,
272 today_tag
273 ]
274 .into_iter()
275 .filter(|s| !String::is_empty(s))
276 .collect::<Vec<String>>()
277 .join(" ")
278 );
279 res.trim_end().to_string()
280 }
281
282 pub fn fix_task_attributes(&self, config: &TasksConfig) -> Result<()> {
286 if !self.path.exists() {
287 File::create(self.path.clone())?;
289 }
290 if !self.path.is_file() {
291 bail!(
292 "Tried to fix tasks attributes but {:?} is not a file",
293 self.path
294 );
295 }
296 let content = read_to_string(self.path.clone())?;
297 let mut lines = content
298 .replace('\r', "")
299 .split('\n')
300 .map(str::to_owned)
301 .collect::<Vec<String>>();
302 if let Some(line_number) = self.line_number {
303 if lines.len() < line_number - 1 {
304 bail!(
305 "Task's line number {} was greater than length of file {:?}",
306 line_number,
307 self.path
308 );
309 }
310
311 let indent_length = lines[line_number - 1]
312 .chars()
313 .take_while(|c| c.is_whitespace())
314 .count();
315
316 let fixed_line = self.get_fixed_attributes(config, indent_length);
317
318 if lines[line_number - 1] != fixed_line {
319 debug!(
320 "\nReplacing\n{}\nWith\n{}\n",
321 lines[line_number - 1],
322 self.get_fixed_attributes(config, indent_length,)
323 );
324 lines[line_number - 1] = fixed_line;
325
326 info!("Wrote to {1:?} at line {}", line_number, self.path);
327 }
328 } else {
329 debug!("Creating a new task at end of file {}", self.path.display());
330 let fixed_line = self.get_fixed_attributes(config, 0);
331 lines.push(fixed_line);
332 lines.push(String::new()); }
334 let mut file = File::create(self.path.clone())?;
335 std::io::Write::write_all(&mut file, lines.join("\n").as_bytes())?;
336
337 Ok(())
338 }
339}
340
341#[cfg(test)]
342mod tests_tasks {
343 use chrono::NaiveDate;
344 use pretty_assertions::assert_eq;
345
346 use crate::{
347 TasksConfig,
348 task::{Date, State, Task},
349 };
350
351 #[test]
352 fn test_fix_attributes() {
353 let config = TasksConfig {
354 ..Default::default()
355 };
356 let task = Task {
357 due_date: Some(Date::Day(NaiveDate::from_ymd_opt(2021, 12, 3).unwrap())),
358 name: String::from("Test Task"),
359 priority: 1,
360 state: State::ToDo,
361 tags: Some(vec![String::from("tag1"), String::from("tag2")]),
362 description: Some(String::from("This is a test task.")),
363 line_number: Some(2),
364 ..Default::default()
365 };
366 let res = task.get_fixed_attributes(&config, 0);
367 assert_eq!(res, "- [ ] Test Task 2021-12-03 p1 #tag1 #tag2");
368 }
369
370 #[test]
371 fn test_fix_attributes_with_no_date() {
372 let config = TasksConfig {
373 ..Default::default()
374 };
375 let task = Task {
376 due_date: None,
377 name: String::from("Test Task with No Date"),
378 priority: 2,
379 state: State::Done,
380 tags: Some(vec![String::from("tag3")]),
381 description: None,
382 line_number: Some(3),
383 ..Default::default()
384 };
385
386 let res = task.get_fixed_attributes(&config, 0);
387 assert_eq!(res, "- [x] Test Task with No Date p2 #tag3");
388 }
389 #[test]
390 fn test_fix_attributes_with_today_tag() {
391 let config = TasksConfig {
392 ..Default::default()
393 };
394 let task = Task {
395 due_date: None,
396 name: String::from("Test Task with Today tag"),
397 priority: 2,
398 state: State::Done,
399 tags: Some(vec![String::from("tag3")]),
400 description: None,
401 line_number: Some(3),
402 is_today: true,
403 ..Default::default()
404 };
405
406 let res = task.get_fixed_attributes(&config, 0);
407 assert_eq!(res, "- [x] Test Task with Today tag p2 #tag3 @today");
408 }
409}
410#[cfg(test)]
411mod tests_due_date {
412
413 use crate::task::Date;
414 use chrono::{Duration, Local};
415
416 #[test]
417 fn test_day_today() {
418 let today = Local::now().naive_local().date();
419 let due = Date::Day(today);
420 assert_eq!(due.get_relative_str(), ("today".to_string()));
421 }
422
423 #[test]
424 fn test_day_tomorrow() {
425 let date = Local::now().naive_local().date() + Duration::days(1);
426 let due = Date::Day(date);
427 assert_eq!(due.get_relative_str(), ("tomorrow".to_string()));
428 }
429
430 #[test]
431 fn test_day_yesterday() {
432 let date = Local::now().naive_local().date() - Duration::days(1);
433 let due = Date::Day(date);
434 assert_eq!(due.get_relative_str(), ("yesterday".to_string()));
435 }
436
437 #[test]
438 fn test_day_in_three_days() {
439 let date = Local::now().naive_local().date() + Duration::days(3);
440 let due = Date::Day(date);
441 assert_eq!(due.get_relative_str(), ("in 3 days".to_string()));
442 }
443
444 #[test]
445 fn test_day_two_weeks_ago() {
446 let date = Local::now().naive_local().date() - Duration::weeks(2);
447 let due = Date::Day(date);
448 assert_eq!(due.get_relative_str(), ("2 weeks ago".to_string()));
449 }
450
451 #[test]
452 fn test_day_in_two_months() {
453 let date = Local::now().naive_local().date() + Duration::weeks(9); let due = Date::Day(date);
455 assert_eq!(due.get_relative_str(), ("in 2 months".to_string()));
456 }
457
458 #[test]
459 fn test_day_three_years_ago() {
460 let date = Local::now().naive_local().date() - Duration::weeks(4 * 12 * 3);
461 let due = Date::Day(date);
462 assert_eq!(due.get_relative_str(), ("3 years ago".to_string()));
463 }
464
465 #[test]
466 fn test_daytime_now() {
467 let now = Local::now().naive_local();
468 let due = Date::DayTime(now);
469 assert_eq!(due.get_relative_str(), ("today".to_string()));
470 }
471
472 #[test]
473 fn test_daytime_in_30_minutes() {
474 let dt = Local::now().naive_local() + Duration::minutes(30);
475 let due = Date::DayTime(dt);
476 assert_eq!(due.get_relative_str(), ("in 30 minutes".to_string()));
477 }
478
479 #[test]
480 fn test_daytime_three_hours_ago() {
481 let dt = Local::now().naive_local() - Duration::hours(3);
482 let due = Date::DayTime(dt);
483 assert_eq!(due.get_relative_str(), ("3 hours ago".to_string()));
484 }
485
486 #[test]
487 fn test_daytime_tomorrow_same_time() {
488 let dt = Local::now().naive_local() + Duration::hours(24);
489 let due = Date::DayTime(dt);
490 assert_eq!(due.get_relative_str(), ("tomorrow".to_string()));
491 }
492 #[test]
493 fn test_daytime_tomorrow_hours() {
494 let dt = Local::now().naive_local() + Duration::hours(25);
495 let due = Date::DayTime(dt);
496 assert_eq!(due.get_relative_str(), ("in 25 hours".to_string()));
497 }
498
499 #[test]
500 fn test_daytime_yesterday_same_time() {
501 let dt = Local::now().naive_local() - Duration::hours(24);
502 let due = Date::DayTime(dt);
503 assert_eq!(due.get_relative_str(), ("yesterday".to_string()));
504 }
505
506 #[test]
507 fn test_daytime_in_1_minute() {
508 let dt = Local::now().naive_local() + Duration::minutes(1);
509 let due = Date::DayTime(dt);
510 assert_eq!(due.get_relative_str(), ("in 1 minutes".to_string()));
511 }
512
513 #[test]
514 fn test_daytime_45_minutes_ago() {
515 let dt = Local::now().naive_local() - Duration::minutes(45);
516 let due = Date::DayTime(dt);
517 assert_eq!(due.get_relative_str(), ("45 minutes ago".to_string()));
518 }
519
520 #[test]
521 fn test_daytime_in_3_hours() {
522 let dt = Local::now().naive_local() + Duration::hours(3);
523 let due = Date::DayTime(dt);
524 assert_eq!(due.get_relative_str(), ("in 3 hours".to_string()));
525 }
526
527 #[test]
528 fn test_daytime_12_hours_ago() {
529 let dt = Local::now().naive_local() - Duration::hours(12);
530 let due = Date::DayTime(dt);
531 assert_eq!(due.get_relative_str(), ("12 hours ago".to_string()));
532 }
533
534 #[test]
535 fn test_daytime_in_1_day_exact() {
536 let dt = Local::now().naive_local() + Duration::days(1);
537 let due = Date::DayTime(dt);
538 assert_eq!(due.get_relative_str(), ("tomorrow".to_string()));
539 }
540
541 #[test]
542 fn test_daytime_exactly_yesterday() {
543 let dt = Local::now().naive_local() - Duration::days(1);
544 let due = Date::DayTime(dt);
545 assert_eq!(due.get_relative_str(), ("yesterday".to_string()));
546 }
547
548 #[test]
549 fn test_daytime_in_2_days() {
550 let dt = Local::now().naive_local() + Duration::days(2);
551 let due = Date::DayTime(dt);
552 assert_eq!(due.get_relative_str(), ("in 2 days".to_string()));
553 }
554
555 #[test]
556 fn test_daytime_10_days_ago() {
557 let dt = Local::now().naive_local() - Duration::days(10);
558 let due = Date::DayTime(dt);
559 assert_eq!(due.get_relative_str(), ("10 days ago".to_string()));
560 }
561
562 #[test]
563 fn test_daytime_in_3_weeks() {
564 let dt = Local::now().naive_local() + Duration::weeks(3);
565 let due = Date::DayTime(dt);
566 assert_eq!(due.get_relative_str(), ("in 3 weeks".to_string()));
567 }
568
569 #[test]
570 fn test_daytime_2_months_ago() {
571 let dt = Local::now().naive_local() - Duration::weeks(9); let due = Date::DayTime(dt);
573 assert_eq!(due.get_relative_str(), ("2 months ago".to_string()));
574 }
575}