Skip to main content

tatuin_core/
task.rs

1// SPDX-License-Identifier: MIT
2
3use crate::RichString;
4
5use super::{filter, project::Project as ProjectTrait, task_patch::DatePatchItem};
6use chrono::{DateTime, prelude::*};
7use colored::Colorize;
8use serde::{Deserialize, Serialize};
9use std::{
10    any::Any,
11    cmp::Ordering,
12    fmt::{self, Write},
13};
14
15pub type DateTimeUtc = DateTime<Utc>;
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
18pub enum State {
19    Unknown(char),
20    #[default]
21    Uncompleted,
22    Completed,
23    InProgress,
24}
25
26impl fmt::Display for State {
27    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28        match self {
29            State::Completed => write!(f, "✅"),
30            State::Uncompleted => write!(f, "  "),
31            State::InProgress => write!(f, "⏳"),
32            State::Unknown(x) => f.write_char(*x),
33        }
34    }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
38pub enum Priority {
39    Lowest,
40    Low,
41    #[default]
42    Normal,
43    Medium,
44    High,
45    Highest,
46}
47
48impl Priority {
49    pub fn values() -> Vec<Priority> {
50        vec![
51            Priority::Lowest,
52            Priority::Low,
53            Priority::Normal,
54            Priority::Medium,
55            Priority::High,
56            Priority::Highest,
57        ]
58    }
59}
60
61impl fmt::Display for Priority {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "{self:?}")
64    }
65}
66
67#[derive(Default, Debug, Clone)]
68pub struct PatchPolicy {
69    pub is_editable: bool,
70    pub is_removable: bool,
71    pub available_states: Vec<State>,
72    pub available_priorities: Vec<Priority>,
73    pub available_due_items: Vec<DatePatchItem>,
74    pub available_scheduled_items: Vec<DatePatchItem>,
75}
76
77#[allow(dead_code)]
78pub trait Task: Send + Sync {
79    fn id(&self) -> String;
80    fn name(&self) -> RichString;
81
82    fn description(&self) -> Option<RichString> {
83        None
84    }
85
86    fn priority(&self) -> Priority {
87        Priority::Normal
88    }
89
90    fn state(&self) -> State;
91    fn created_at(&self) -> Option<DateTimeUtc> {
92        None
93    }
94    fn updated_at(&self) -> Option<DateTimeUtc> {
95        None
96    }
97    fn completed_at(&self) -> Option<DateTimeUtc> {
98        None
99    }
100    fn due(&self) -> Option<DateTimeUtc> {
101        None
102    }
103    fn scheduled(&self) -> Option<DateTimeUtc> {
104        None
105    }
106
107    fn planned_date(&self) -> Option<DateTimeUtc> {
108        *planned_date(&self.scheduled(), &self.due())
109    }
110
111    fn place(&self) -> String {
112        String::new()
113    }
114
115    fn url(&self) -> String {
116        String::new()
117    }
118
119    fn labels(&self) -> Vec<String> {
120        Vec::new()
121    }
122
123    fn provider(&self) -> String;
124
125    fn project(&self) -> Option<Box<dyn ProjectTrait>>;
126
127    fn as_any(&self) -> &dyn Any;
128
129    fn clone_boxed(&self) -> Box<dyn Task>;
130
131    fn const_patch_policy(&self) -> PatchPolicy {
132        PatchPolicy {
133            is_editable: false,
134            is_removable: false,
135            available_states: vec![State::Uncompleted, State::Completed, State::InProgress],
136            available_priorities: Priority::values(),
137            available_due_items: DatePatchItem::values(),
138            available_scheduled_items: DatePatchItem::values(),
139        }
140    }
141
142    fn patch_policy(&self) -> PatchPolicy {
143        let mut pp = self.const_patch_policy();
144
145        let s = self.state();
146        pp.available_states.retain(|e| e != &s);
147        let p = self.priority();
148        pp.available_priorities.retain(|e| e != &p);
149        pp
150    }
151}
152
153pub fn datetime_to_str<Tz: TimeZone>(t: Option<DateTimeUtc>, tz: &Tz) -> String
154where
155    <Tz as TimeZone>::Offset: std::fmt::Display,
156{
157    if let Some(d) = t {
158        if d.time() == chrono::NaiveTime::default() {
159            return d.format("%Y-%m-%d").to_string();
160        }
161
162        return d.with_timezone(tz).format("%Y-%m-%d %H:%M:%S %Z").to_string();
163    }
164
165    String::from("-")
166}
167
168pub fn format(t: &dyn Task) -> String {
169    format!(
170        "- [{}] {} ({}) ({})",
171        t.state(),
172        t.name().display(),
173        format!("due: {}", datetime_to_str(t.due(), &Local::now().timezone())).blue(),
174        t.place().green()
175    )
176}
177
178pub fn due_group(due: &Option<DateTimeUtc>) -> filter::Due {
179    match due {
180        Some(d) => {
181            let now = chrono::Utc::now().date_naive();
182            match d.date_naive().cmp(&now) {
183                Ordering::Less => filter::Due::Overdue,
184                Ordering::Equal => filter::Due::Today,
185                Ordering::Greater => filter::Due::Future,
186            }
187        }
188        None => filter::Due::NoDate,
189    }
190}
191
192pub fn planned_date<'a>(scheduled: &'a Option<DateTimeUtc>, due: &'a Option<DateTimeUtc>) -> &'a Option<DateTimeUtc> {
193    if let Some(s) = scheduled
194        && (due.is_none() || s < due.as_ref().unwrap())
195    // scheduled date can't be more than due date
196    {
197        return scheduled;
198    }
199
200    due
201}
202
203#[cfg(test)]
204mod test {
205    use chrono::{NaiveDate, Utc};
206
207    use crate::task::DateTimeUtc;
208
209    use super::planned_date;
210
211    fn dt(year: u16, month: u8, day: u8) -> Option<DateTimeUtc> {
212        Some(DateTimeUtc::from_naive_utc_and_offset(
213            NaiveDate::from_ymd_opt(year as i32, month as u32, day as u32)
214                .unwrap()
215                .and_hms_opt(0, 0, 0)
216                .unwrap(),
217            Utc,
218        ))
219    }
220
221    #[test]
222    fn planned_date_test() {
223        assert_eq!(&None, planned_date(&None, &None));
224        assert_eq!(&dt(2026, 1, 1), planned_date(&None, &dt(2026, 1, 1)));
225        assert_eq!(&dt(2026, 1, 1), planned_date(&dt(2026, 1, 1), &None));
226        assert_eq!(&dt(2026, 1, 1), planned_date(&dt(2026, 1, 1), &dt(2026, 1, 2)));
227        assert_eq!(&dt(2026, 1, 1), planned_date(&dt(2026, 1, 2), &dt(2026, 1, 1)));
228    }
229}