aimcal_core/
todo.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use crate::{LooseDateTime, Priority, SortOrder};
6use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc};
7use icalendar::Component;
8use std::{fmt::Display, str::FromStr};
9
10const KEY_COMPLETED: &str = "COMPLETED";
11const KEY_DESCRIPTION: &str = "DESCRIPTION";
12const KEY_DUE: &str = "DUE";
13
14/// Trait representing a todo item.
15pub trait Todo {
16    /// Returns the unique identifier for the todo item.
17    fn uid(&self) -> &str;
18
19    /// Returns the description of the todo item.
20    fn completed(&self) -> Option<DateTime<Local>>;
21
22    /// Returns the description of the todo item, if available.
23    fn description(&self) -> Option<&str>;
24
25    /// Returns the due date and time of the todo item, if available.
26    fn due(&self) -> Option<LooseDateTime>;
27
28    /// The percent complete, from 0 to 100.
29    fn percent_complete(&self) -> Option<u8>;
30
31    /// The priority from 1 to 9, where 1 is the highest priority.
32    fn priority(&self) -> Priority;
33
34    /// Returns the status of the todo item.
35    fn status(&self) -> TodoStatus;
36
37    /// Returns the summary of the todo item.
38    fn summary(&self) -> &str;
39}
40
41impl Todo for icalendar::Todo {
42    fn uid(&self) -> &str {
43        self.get_uid().unwrap_or("")
44    }
45
46    fn completed(&self) -> Option<DateTime<Local>> {
47        self.get_completed().map(|dt| dt.with_timezone(&Local))
48    }
49
50    fn description(&self) -> Option<&str> {
51        self.get_description()
52    }
53
54    fn due(&self) -> Option<LooseDateTime> {
55        self.get_due().map(Into::into)
56    }
57
58    fn percent_complete(&self) -> Option<u8> {
59        self.get_percent_complete()
60    }
61
62    fn priority(&self) -> Priority {
63        self.get_priority()
64            .map(|p| Priority::from(p as u8))
65            .unwrap_or_default()
66    }
67
68    fn status(&self) -> TodoStatus {
69        self.get_status().map(Into::into).unwrap_or_default()
70    }
71
72    fn summary(&self) -> &str {
73        self.get_summary().unwrap_or("")
74    }
75}
76
77/// Darft for a todo item, used for creating new todos.
78#[derive(Debug)]
79pub struct TodoDraft {
80    /// The description of the todo item, if available.
81    pub description: Option<String>,
82
83    /// The due date and time of the todo item, if available.
84    pub due: Option<LooseDateTime>,
85
86    /// The percent complete, from 0 to 100.
87    pub percent_complete: Option<u8>,
88
89    /// The priority of the todo item.
90    pub priority: Priority,
91
92    /// The status of the todo item, if available.
93    pub status: Option<TodoStatus>,
94
95    /// The summary of the todo item.
96    pub summary: String,
97}
98
99impl TodoDraft {
100    /// Converts the draft into a icalendar Todo component.
101    pub(crate) fn into_todo(self, uid: &str) -> icalendar::Todo {
102        let mut todo = icalendar::Todo::with_uid(uid);
103
104        if let Some(description) = self.description {
105            Component::description(&mut todo, &description);
106        }
107
108        if let Some(due) = self.due {
109            icalendar::Todo::due(&mut todo, due);
110        }
111
112        if let Some(percent) = self.percent_complete {
113            icalendar::Todo::percent_complete(&mut todo, percent);
114        }
115
116        Component::priority(&mut todo, self.priority.into());
117
118        let status = self.status.unwrap_or(TodoStatus::NeedsAction).into();
119        icalendar::Todo::status(&mut todo, status);
120
121        Component::summary(&mut todo, &self.summary);
122
123        todo
124    }
125}
126
127/// Patch for a todo item, allowing partial updates.
128#[derive(Debug, Default, Clone)]
129pub struct TodoPatch {
130    /// The unique identifier for the todo item.
131    pub uid: String,
132
133    /// The description of the todo item, if available.
134    pub description: Option<Option<String>>,
135
136    /// The due date and time of the todo item, if available.
137    pub due: Option<Option<LooseDateTime>>,
138
139    /// The percent complete, from 0 to 100.
140    pub percent_complete: Option<Option<u8>>,
141
142    /// The priority of the todo item, from 1 to 9, where 1 is the highest priority.
143    pub priority: Option<Priority>,
144
145    /// The status of the todo item, if available.
146    pub status: Option<TodoStatus>,
147
148    /// The summary of the todo item, if available.
149    pub summary: Option<String>,
150}
151
152impl TodoPatch {
153    /// Is this patch empty, meaning no fields are set
154    pub fn is_empty(&self) -> bool {
155        self.description.is_none()
156            && self.due.is_none()
157            && self.percent_complete.is_none()
158            && self.priority.is_none()
159            && self.status.is_none()
160            && self.summary.is_none()
161    }
162
163    /// Applies the patch to a mutable todo item, modifying it in place.
164    pub(crate) fn apply_to<'a>(&self, t: &'a mut icalendar::Todo) -> &'a mut icalendar::Todo {
165        if let Some(description) = &self.description {
166            match description {
167                Some(desc) => t.description(desc),
168                None => t.remove_property(KEY_DESCRIPTION),
169            };
170        }
171
172        if let Some(due) = &self.due {
173            match due {
174                Some(d) => t.due(*d),
175                None => t.remove_property(KEY_DUE),
176            };
177        }
178
179        if let Some(percent) = self.percent_complete {
180            t.percent_complete(percent.unwrap_or(0));
181        }
182
183        if let Some(priority) = self.priority {
184            t.priority(priority.into());
185        }
186
187        if let Some(status) = self.status {
188            t.status(status.into());
189
190            match status {
191                TodoStatus::Completed => t.completed(Utc::now()),
192                _ if t.get_completed().is_some() => t.remove_property(KEY_COMPLETED),
193                _ => t,
194            };
195        }
196
197        if let Some(summary) = &self.summary {
198            t.summary(summary);
199        }
200
201        t
202    }
203}
204
205/// The status of a todo item, which can be one of several predefined states.
206#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
207#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
208pub enum TodoStatus {
209    /// The todo item needs action.
210    #[default]
211    NeedsAction,
212
213    /// The todo item has been completed.
214    Completed,
215
216    /// The todo item is currently in process.
217    InProcess,
218
219    /// The todo item has been cancelled.
220    Cancelled,
221}
222
223const STATUS_NEEDS_ACTION: &str = "NEEDS-ACTION";
224const STATUS_COMPLETED: &str = "COMPLETED";
225const STATUS_IN_PROCESS: &str = "IN-PROGRESS";
226const STATUS_CANCELLED: &str = "CANCELLED";
227
228impl AsRef<str> for TodoStatus {
229    fn as_ref(&self) -> &str {
230        match self {
231            TodoStatus::NeedsAction => STATUS_NEEDS_ACTION,
232            TodoStatus::Completed => STATUS_COMPLETED,
233            TodoStatus::InProcess => STATUS_IN_PROCESS,
234            TodoStatus::Cancelled => STATUS_CANCELLED,
235        }
236    }
237}
238
239impl Display for TodoStatus {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        write!(f, "{}", self.as_ref())
242    }
243}
244
245impl FromStr for TodoStatus {
246    type Err = ();
247
248    fn from_str(value: &str) -> Result<Self, Self::Err> {
249        match value {
250            STATUS_NEEDS_ACTION => Ok(TodoStatus::NeedsAction),
251            STATUS_COMPLETED => Ok(TodoStatus::Completed),
252            STATUS_IN_PROCESS => Ok(TodoStatus::InProcess),
253            STATUS_CANCELLED => Ok(TodoStatus::Cancelled),
254            _ => Err(()),
255        }
256    }
257}
258
259impl From<TodoStatus> for icalendar::TodoStatus {
260    fn from(item: TodoStatus) -> icalendar::TodoStatus {
261        match item {
262            TodoStatus::NeedsAction => icalendar::TodoStatus::NeedsAction,
263            TodoStatus::Completed => icalendar::TodoStatus::Completed,
264            TodoStatus::InProcess => icalendar::TodoStatus::InProcess,
265            TodoStatus::Cancelled => icalendar::TodoStatus::Cancelled,
266        }
267    }
268}
269
270impl From<icalendar::TodoStatus> for TodoStatus {
271    fn from(status: icalendar::TodoStatus) -> Self {
272        match status {
273            icalendar::TodoStatus::NeedsAction => TodoStatus::NeedsAction,
274            icalendar::TodoStatus::Completed => TodoStatus::Completed,
275            icalendar::TodoStatus::InProcess => TodoStatus::InProcess,
276            icalendar::TodoStatus::Cancelled => TodoStatus::Cancelled,
277        }
278    }
279}
280
281/// Conditions for filtering todo items, such as current time, status, and due date.
282#[derive(Debug, Clone, Copy)]
283pub struct TodoConditions {
284    /// The current time, used for filtering todos.
285    pub now: NaiveDateTime,
286
287    /// The status of the todo item to filter by, if any.
288    pub status: Option<TodoStatus>,
289
290    /// The priority of the todo item to filter by, if any.
291    pub due: Option<Duration>,
292}
293
294impl TodoConditions {
295    /// The due datetime.
296    pub fn due_before(&self) -> Option<NaiveDateTime> {
297        self.due.map(|a| self.now + a)
298    }
299}
300
301/// The default sort key for todo items, which is by due date.
302#[derive(Debug, Clone, Copy)]
303pub enum TodoSort {
304    /// Sort by the due date and time of the todo item.
305    Due(SortOrder),
306
307    /// Sort by the priority of the todo item.
308    Priority {
309        /// Sort order, either ascending or descending.
310        order: SortOrder,
311
312        /// Put items with no priority first or last.
313        none_first: bool,
314    },
315}