1use 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#[derive(Debug, Deserialize, Serialize)]
15pub struct TaskManager {
16 next_uid: UID,
18 tasks: HashMap<UID, Task>,
20}
21
22impl TaskManager {
23 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 fn increment_uid(&mut self) {
42 let uid = self.next_uid.0 + 1;
43 self.next_uid = UID(uid);
44 }
45
46 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 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 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: String,
153 history: Vec<Event>,
155}
156
157impl Task {
158 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 pub fn name(&self) -> &str {
176 &self.name
177 }
178
179 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 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 pub fn age(&self) -> Duration {
203 Utc::now().signed_duration_since(self.creation_date().copied().unwrap_or_else(Utc::now))
204 }
205
206 pub fn change_name(&mut self, name: impl Into<String>) {
208 self.name = name.into()
209 }
210
211 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 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 pub fn replace_note(&mut self, note_uid: UID, content: impl Into<String>) -> Result<(), Error> {
229 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 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 pub fn history(&self) -> impl Iterator<Item = &Event> {
296 self.history.iter()
297 }
298
299 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 (Status::Ongoing, _) => (spent, Some(*event_date)),
311 (_, Some(last_wip)) => (spent + (event_date.signed_duration_since(last_wip)), None),
313 _ => (spent, last_wip),
315 },
316 _ => (spent, last_wip),
317 }
318 });
319
320 if let Some(last_wip) = last_wip {
321 spent + Utc::now().signed_duration_since(last_wip)
323 } else {
324 spent
325 }
326 }
327
328 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 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 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 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 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 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 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 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#[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#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
468pub enum Status {
469 Ongoing,
473 Todo,
477 Done,
481 Cancelled,
485}
486
487#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
492pub enum Event {
493 Created(DateTime<Utc>),
495
496 StatusChanged {
498 event_date: DateTime<Utc>,
499 status: Status,
500 },
501
502 NoteAdded {
504 event_date: DateTime<Utc>,
505 content: String,
506 },
507
508 NoteReplaced {
510 event_date: DateTime<Utc>,
511 note_uid: UID,
512 content: String,
513 },
514
515 SetProject {
517 event_date: DateTime<Utc>,
518 project: String,
519 },
520
521 SetPriority {
523 event_date: DateTime<Utc>,
524 priority: Priority,
525 },
526
527 AddTag {
529 event_date: DateTime<Utc>,
530 tag: String,
531 },
532}
533
534#[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}