toodoux/
task.rs

1//! Tasks related code.
2
3use crate::{
4  config::Config, error::Error, filter::TaskDescriptionFilter, metadata::Metadata,
5  metadata::Priority,
6};
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json as json;
10use std::{cmp::Reverse, collections::HashMap, fmt, fs, str::FromStr};
11use unicase::UniCase;
12
13/// Create, edit, remove and list tasks.
14#[derive(Debug, Deserialize, Serialize)]
15pub struct TaskManager {
16  /// Next UID to use for the next task to create.
17  next_uid: UID,
18  /// List of known tasks.
19  tasks: HashMap<UID, Task>,
20}
21
22impl TaskManager {
23  /// Create a manager from a configuration.
24  pub fn new_from_config(config: &Config) -> Result<Self, Error> {
25    let path = config.tasks_path();
26
27    if path.is_file() {
28      Ok(json::from_reader(
29        fs::File::open(path).map_err(Error::CannotOpenFile)?,
30      )?)
31    } else {
32      let task_mgr = TaskManager {
33        next_uid: UID::default(),
34        tasks: HashMap::new(),
35      };
36      Ok(task_mgr)
37    }
38  }
39
40  /// Increment the next UID to use.
41  fn increment_uid(&mut self) {
42    let uid = self.next_uid.0 + 1;
43    self.next_uid = UID(uid);
44  }
45
46  /// Register a task and give it an [`UID`].
47  pub fn register_task(&mut self, task: Task) -> UID {
48    let uid = self.next_uid;
49
50    self.increment_uid();
51    self.tasks.insert(uid, task);
52
53    uid
54  }
55
56  pub fn save(&mut self, config: &Config) -> Result<(), Error> {
57    Ok(json::to_writer_pretty(
58      fs::File::create(config.tasks_path()).map_err(Error::CannotSave)?,
59      self,
60    )?)
61  }
62
63  pub fn tasks(&self) -> impl Iterator<Item = (&UID, &Task)> {
64    self.tasks.iter()
65  }
66
67  pub fn get(&self, uid: UID) -> Option<&Task> {
68    self.tasks.get(&uid)
69  }
70
71  pub fn get_mut(&mut self, uid: UID) -> Option<&mut Task> {
72    self.tasks.get_mut(&uid)
73  }
74
75  pub fn rename_project(
76    &mut self,
77    current_project: impl AsRef<str>,
78    new_project: impl AsRef<str>,
79    mut on_renamed: impl FnMut(UID),
80  ) {
81    let current_project = current_project.as_ref();
82    let new_project = new_project.as_ref();
83
84    for (uid, task) in &mut self.tasks {
85      match task.project() {
86        Some(project) if project == current_project => {
87          task.set_project(new_project);
88          on_renamed(*uid);
89        }
90
91        _ => (),
92      }
93    }
94  }
95
96  /// Get a listing of tasks that can be filtered with metadata and name filters.
97  pub fn filtered_task_listing(
98    &self,
99    metadata: Vec<Metadata>,
100    name_filter: TaskDescriptionFilter,
101    todo: bool,
102    start: bool,
103    done: bool,
104    cancelled: bool,
105    case_insensitive: bool,
106  ) -> Vec<(&UID, &Task)> {
107    let mut tasks: Vec<_> = self
108      .tasks()
109      .filter(|(_, task)| {
110        // filter the task depending on what is passed as argument
111        let status_filter = match task.status() {
112          Status::Ongoing => start,
113          Status::Todo => todo,
114          Status::Done => done,
115          Status::Cancelled => cancelled,
116        };
117
118        if metadata.is_empty() {
119          status_filter
120        } else {
121          status_filter && task.check_metadata(metadata.iter(), case_insensitive)
122        }
123      })
124      .filter(|(_, task)| {
125        if !name_filter.is_empty() {
126          let mut name_filter = name_filter.clone();
127
128          for word in task.name().split_ascii_whitespace() {
129            let word_found = name_filter.remove(word);
130
131            if word_found && name_filter.is_empty() {
132              return true;
133            }
134          }
135
136          false
137        } else {
138          true
139        }
140      })
141      .collect();
142
143    tasks.sort_by_key(|&(uid, task)| Reverse((task.priority(), task.age(), task.status(), uid)));
144
145    tasks
146  }
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize)]
150pub struct Task {
151  /// Name of the task.
152  name: String,
153  /// Event history.
154  history: Vec<Event>,
155}
156
157impl Task {
158  /// Create a new [`Task`] and populate automatically its history with creation date and status.
159  pub fn new(name: impl Into<String>) -> Self {
160    let date = Utc::now();
161
162    Task {
163      name: name.into(),
164      history: vec![
165        Event::Created(date),
166        Event::StatusChanged {
167          event_date: date,
168          status: Status::Todo,
169        },
170      ],
171    }
172  }
173
174  /// Get the name of the [`Task`].
175  pub fn name(&self) -> &str {
176    &self.name
177  }
178
179  /// Get the current status of the [`Task`].
180  pub fn status(&self) -> Status {
181    self
182      .history
183      .iter()
184      .filter_map(|event| match event {
185        Event::StatusChanged { status, .. } => Some(status),
186        _ => None,
187      })
188      .copied()
189      .last()
190      .unwrap_or(Status::Todo)
191  }
192
193  /// Get the creation date of the [`Task`].
194  pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
195    self.history.iter().find_map(|event| match event {
196      Event::Created(date) => Some(date),
197      _ => None,
198    })
199  }
200
201  /// Get the age of the [`Task`]; i.e. the duration since its creation date.
202  pub fn age(&self) -> Duration {
203    Utc::now().signed_duration_since(self.creation_date().copied().unwrap_or_else(Utc::now))
204  }
205
206  /// Change the name of the [`Task`].
207  pub fn change_name(&mut self, name: impl Into<String>) {
208    self.name = name.into()
209  }
210
211  /// Change the status of the [`Task`].
212  pub fn change_status(&mut self, status: Status) {
213    self.history.push(Event::StatusChanged {
214      event_date: Utc::now(),
215      status,
216    });
217  }
218
219  /// Add a new note to the [`Task`].
220  pub fn add_note(&mut self, content: impl Into<String>) {
221    self.history.push(Event::NoteAdded {
222      event_date: Utc::now(),
223      content: content.into(),
224    });
225  }
226
227  /// Replace the content of a note for a given [`Task`].
228  pub fn replace_note(&mut self, note_uid: UID, content: impl Into<String>) -> Result<(), Error> {
229    // ensure the note exists first
230    let mut count = 0;
231    let id: u32 = note_uid.into();
232    let previous_note = self.history.iter().find(|event| match event {
233      Event::NoteAdded { .. } => {
234        if id == count {
235          true
236        } else {
237          count += 1;
238          false
239        }
240      }
241
242      _ => false,
243    });
244
245    if previous_note.is_none() {
246      return Err(Error::UnknownNote(note_uid));
247    }
248
249    self.history.push(Event::NoteReplaced {
250      event_date: Utc::now(),
251      note_uid,
252      content: content.into(),
253    });
254
255    Ok(())
256  }
257
258  /// Iterate over the notes, if any.
259  pub fn notes(&self) -> Vec<Note> {
260    let mut notes = Vec::new();
261
262    for event in &self.history {
263      match event {
264        Event::NoteAdded {
265          event_date,
266          content,
267        } => {
268          let note = Note {
269            creation_date: *event_date,
270            last_modification_date: *event_date,
271            content: content.clone(),
272          };
273          notes.push(note);
274        }
275
276        Event::NoteReplaced {
277          event_date,
278          note_uid,
279          content,
280        } => {
281          if let Some(note) = notes.get_mut(usize::from(*note_uid)) {
282            note.last_modification_date = *event_date;
283            note.content = content.clone();
284          }
285        }
286
287        _ => (),
288      }
289    }
290
291    notes
292  }
293
294  /// Iterate over the whole history, if any.
295  pub fn history(&self) -> impl Iterator<Item = &Event> {
296    self.history.iter()
297  }
298
299  /// Compute the time spent on this task.
300  pub fn spent_time(&self) -> Duration {
301    let (spent, last_wip) =
302      self
303        .history
304        .iter()
305        .fold((Duration::zero(), None), |(spent, last_wip), event| {
306          match event {
307            Event::StatusChanged { event_date, status } => match (status, last_wip) {
308              // We go from any status to WIP status; return the spent time untouched and set the new “last_wip” with the
309              // time at which the status change occurred
310              (Status::Ongoing, _) => (spent, Some(*event_date)),
311              // We go to anything but WIP while the previous status was WIP; accumulate.
312              (_, Some(last_wip)) => (spent + (event_date.signed_duration_since(last_wip)), None),
313              // We go between inactive status, ignore
314              _ => (spent, last_wip),
315            },
316            _ => (spent, last_wip),
317          }
318        });
319
320    if let Some(last_wip) = last_wip {
321      // last status was WIP; accumulate moaaar
322      spent + Utc::now().signed_duration_since(last_wip)
323    } else {
324      spent
325    }
326  }
327
328  /// Mark this task as part of the input project.
329  ///
330  /// If a project was already present, this method overrides it. Passing an empty string puts that task into the
331  /// _orphaned_ project.
332  pub fn set_project(&mut self, project: impl Into<String>) {
333    self.history.push(Event::SetProject {
334      event_date: Utc::now(),
335      project: project.into(),
336    });
337  }
338
339  /// Set the priority of this task.
340  ///
341  /// If a priority was already set, this method overrides it. Passing [`None`] removes the priority.
342  pub fn set_priority(&mut self, priority: Priority) {
343    self.history.push(Event::SetPriority {
344      event_date: Utc::now(),
345      priority,
346    });
347  }
348
349  /// Add a tag to task.
350  pub fn add_tag(&mut self, tag: impl Into<String>) {
351    self.history.push(Event::AddTag {
352      event_date: Utc::now(),
353      tag: tag.into(),
354    });
355  }
356
357  /// Apply a list of metadata.
358  pub fn apply_metadata(&mut self, metadata: impl IntoIterator<Item = Metadata>) {
359    for md in metadata {
360      match md {
361        Metadata::Project(project) => self.set_project(project),
362        Metadata::Priority(priority) => self.set_priority(priority),
363        Metadata::Tag(tag) => self.add_tag(tag),
364      }
365    }
366  }
367
368  /// Check all metadata against this I have no idea how to express the end of this sentence so good luck.
369  pub fn check_metadata<'a>(
370    &self,
371    metadata: impl IntoIterator<Item = &'a Metadata>,
372    case_insensitive: bool,
373  ) -> bool {
374    if case_insensitive {
375      let own_project = self.project().map(UniCase::new);
376      let own_tags = self.tags().map(UniCase::new).collect::<Vec<_>>();
377      metadata.into_iter().all(|md| match md {
378        Metadata::Project(project) => own_project == Some(UniCase::new(project)),
379        Metadata::Priority(priority) => self.priority() == Some(*priority),
380        Metadata::Tag(tag) => own_tags.contains(&UniCase::new(tag)),
381      })
382    } else {
383      metadata.into_iter().all(|md| match md {
384        Metadata::Project(project) => self.project() == Some(project),
385        Metadata::Priority(priority) => self.priority() == Some(*priority),
386        Metadata::Tag(tag) => self.tags().any(|t| t == tag),
387      })
388    }
389  }
390
391  /// Get the current project.
392  pub fn project(&self) -> Option<&str> {
393    self
394      .history
395      .iter()
396      .filter_map(|event| match event {
397        Event::SetProject { project, .. } => Some(project.as_str()),
398        _ => None,
399      })
400      .next_back()
401  }
402
403  /// Get the current project.
404  pub fn priority(&self) -> Option<Priority> {
405    self
406      .history
407      .iter()
408      .filter_map(|event| match event {
409        Event::SetPriority { priority, .. } => Some(*priority),
410        _ => None,
411      })
412      .next_back()
413  }
414
415  /// Get the current tags of a task.
416  pub fn tags(&self) -> impl Iterator<Item = &str> {
417    self.history.iter().filter_map(|event| match event {
418      Event::AddTag { tag, .. } => Some(tag.as_str()),
419      _ => None,
420    })
421  }
422}
423
424/// Unique identifier.
425#[derive(
426  Clone, Copy, Debug, Deserialize, Hash, Eq, Ord, PartialEq, PartialOrd, Serialize, Default,
427)]
428pub struct UID(u32);
429
430impl UID {
431  pub fn val(self) -> u32 {
432    self.0
433  }
434
435  pub fn dec(self) -> Self {
436    Self(self.0.saturating_sub(1))
437  }
438}
439
440impl From<UID> for u32 {
441  fn from(uid: UID) -> Self {
442    uid.0
443  }
444}
445
446impl From<UID> for usize {
447  fn from(uid: UID) -> Self {
448    uid.0 as _
449  }
450}
451
452impl FromStr for UID {
453  type Err = <u32 as FromStr>::Err;
454
455  fn from_str(s: &str) -> Result<Self, Self::Err> {
456    u32::from_str(s).map(UID)
457  }
458}
459
460impl fmt::Display for UID {
461  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
462    self.0.fmt(f)
463  }
464}
465
466/// State of a task.
467#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
468pub enum Status {
469  /// An “ongoing” state.
470  ///
471  /// Users will typically have “ONGOING”, “WIP”, etc.
472  Ongoing,
473  /// A “todo” state.
474  ///
475  /// Users will typically have “TODO“, “PLANNED”, etc.
476  Todo,
477  /// A “done” state.
478  ///
479  /// Users will typically have "DONE".
480  Done,
481  /// A “cancelled” state.
482  ///
483  /// Users will typically have "CANCELLED", "WONTFIX", etc.
484  Cancelled,
485}
486
487/// Task event.
488///
489/// Such events occurred when a change is made to a task (created, edited, scheduled, state
490/// changed, etc.).
491#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
492pub enum Event {
493  /// Event generated when a task is created.
494  Created(DateTime<Utc>),
495
496  /// Event generated when the status of a task changes.
497  StatusChanged {
498    event_date: DateTime<Utc>,
499    status: Status,
500  },
501
502  /// Event generated when a note is added to a task.
503  NoteAdded {
504    event_date: DateTime<Utc>,
505    content: String,
506  },
507
508  /// Event generated when a note is replaced in a task.
509  NoteReplaced {
510    event_date: DateTime<Utc>,
511    note_uid: UID,
512    content: String,
513  },
514
515  /// Event generated when a project is set on a task.
516  SetProject {
517    event_date: DateTime<Utc>,
518    project: String,
519  },
520
521  /// Event generated when a priority is set on a task.
522  SetPriority {
523    event_date: DateTime<Utc>,
524    priority: Priority,
525  },
526
527  /// Event generated when a tag is added to a task.
528  AddTag {
529    event_date: DateTime<Utc>,
530    tag: String,
531  },
532}
533
534/// A note.
535#[derive(Debug, Clone, Eq, PartialEq)]
536pub struct Note {
537  pub creation_date: DateTime<Utc>,
538  pub last_modification_date: DateTime<Utc>,
539  pub content: String,
540}