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