aimcal_core/
todo.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use crate::{DatePerhapsTime, Priority, SortOrder};
6use chrono::{DateTime, Duration, FixedOffset, 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    /// Returns the description of the todo item.
19    fn completed(&self) -> Option<DateTime<FixedOffset>>;
20    /// Returns the description of the todo item, if available.
21    fn description(&self) -> Option<&str>;
22    /// Returns the due date and time of the todo item, if available.
23    fn due(&self) -> Option<DatePerhapsTime>;
24    /// The percent complete, from 0 to 100.
25    fn percent(&self) -> Option<u8>;
26    /// The priority from 1 to 9, where 1 is the highest priority.
27    fn priority(&self) -> Priority;
28    /// Returns the status of the todo item, if available.
29    fn status(&self) -> Option<TodoStatus>;
30    /// Returns the summary of the todo item.
31    fn summary(&self) -> &str;
32}
33
34/// Patch for a todo item, allowing partial updates.
35#[derive(Debug, Default, Clone)]
36pub struct TodoPatch {
37    /// The unique identifier for the todo item.
38    pub uid: String,
39
40    /// The completion date and time of the todo item, if available.
41    pub completed: Option<Option<DateTime<FixedOffset>>>,
42
43    /// The description of the todo item, if available.
44    pub description: Option<Option<String>>,
45
46    /// The due date and time of the todo item, if available.
47    pub due: Option<Option<DatePerhapsTime>>,
48
49    /// The percent complete, from 0 to 100.
50    pub percent: Option<Option<u8>>,
51
52    /// The priority of the todo item, from 1 to 9, where 1 is the highest priority.
53    pub priority: Option<Priority>,
54
55    /// The status of the todo item, if available.
56    pub status: Option<TodoStatus>,
57
58    /// The summary of the todo item, if available.
59    pub summary: Option<String>,
60}
61
62impl TodoPatch {
63    /// Is this patch empty, meaning no fields are set
64    pub fn is_empty(&self) -> bool {
65        self.completed.is_none()
66            && self.description.is_none()
67            && self.due.is_none()
68            && self.percent.is_none()
69            && self.priority.is_none()
70            && self.status.is_none()
71            && self.summary.is_none()
72    }
73
74    /// Applies the patch to a mutable todo item, modifying it in place.
75    pub fn apply_to<'a>(&self, t: &'a mut icalendar::Todo) -> &'a mut icalendar::Todo {
76        if let Some(completed) = self.completed {
77            match completed {
78                Some(dt) => t.completed(dt.with_timezone(&Utc)),
79                None => t.remove_property(KEY_COMPLETED),
80            };
81        }
82
83        if let Some(description) = &self.description {
84            match description {
85                Some(desc) => t.description(desc),
86                None => t.remove_property(KEY_DESCRIPTION),
87            };
88        }
89
90        if let Some(due) = &self.due {
91            match due {
92                Some(d) => t.due(*d),
93                None => t.remove_property(KEY_DUE),
94            };
95        }
96
97        if let Some(percent) = self.percent {
98            t.percent_complete(percent.unwrap_or(0));
99        }
100
101        if let Some(priority) = self.priority {
102            t.priority(priority.into());
103        }
104
105        if let Some(status) = self.status {
106            t.status(status.into());
107        }
108
109        if let Some(summary) = &self.summary {
110            t.summary(summary);
111        }
112
113        t
114    }
115}
116
117/// The status of a todo item, which can be one of several predefined states.
118#[derive(Debug, Clone, Copy)]
119pub enum TodoStatus {
120    /// The todo item needs action.
121    NeedsAction,
122    /// The todo item has been completed.
123    Completed,
124    /// The todo item is currently in process.
125    InProcess,
126    /// The todo item has been cancelled.
127    Cancelled,
128}
129
130const STATUS_NEEDS_ACTION: &str = "NEEDS-ACTION";
131const STATUS_COMPLETED: &str = "COMPLETED";
132const STATUS_IN_PROCESS: &str = "IN-PROGRESS";
133const STATUS_CANCELLED: &str = "CANCELLED";
134
135impl AsRef<str> for TodoStatus {
136    fn as_ref(&self) -> &str {
137        match self {
138            TodoStatus::NeedsAction => STATUS_NEEDS_ACTION,
139            TodoStatus::Completed => STATUS_COMPLETED,
140            TodoStatus::InProcess => STATUS_IN_PROCESS,
141            TodoStatus::Cancelled => STATUS_CANCELLED,
142        }
143    }
144}
145
146impl Display for TodoStatus {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(f, "{}", self.as_ref())
149    }
150}
151
152impl FromStr for TodoStatus {
153    type Err = ();
154
155    fn from_str(value: &str) -> Result<Self, Self::Err> {
156        match value {
157            STATUS_NEEDS_ACTION => Ok(TodoStatus::NeedsAction),
158            STATUS_COMPLETED => Ok(TodoStatus::Completed),
159            STATUS_IN_PROCESS => Ok(TodoStatus::InProcess),
160            STATUS_CANCELLED => Ok(TodoStatus::Cancelled),
161            _ => Err(()),
162        }
163    }
164}
165
166impl From<TodoStatus> for icalendar::TodoStatus {
167    fn from(item: TodoStatus) -> icalendar::TodoStatus {
168        match item {
169            TodoStatus::NeedsAction => icalendar::TodoStatus::NeedsAction,
170            TodoStatus::Completed => icalendar::TodoStatus::Completed,
171            TodoStatus::InProcess => icalendar::TodoStatus::InProcess,
172            TodoStatus::Cancelled => icalendar::TodoStatus::Cancelled,
173        }
174    }
175}
176
177impl From<&icalendar::TodoStatus> for TodoStatus {
178    fn from(status: &icalendar::TodoStatus) -> Self {
179        match status {
180            icalendar::TodoStatus::NeedsAction => TodoStatus::NeedsAction,
181            icalendar::TodoStatus::Completed => TodoStatus::Completed,
182            icalendar::TodoStatus::InProcess => TodoStatus::InProcess,
183            icalendar::TodoStatus::Cancelled => TodoStatus::Cancelled,
184        }
185    }
186}
187
188/// Conditions for filtering todo items, such as current time, status, and due date.
189#[derive(Debug, Clone, Copy)]
190pub struct TodoConditions {
191    /// The current time, used for filtering todos.
192    pub now: NaiveDateTime,
193
194    /// The status of the todo item to filter by, if any.
195    pub status: Option<TodoStatus>,
196
197    /// The priority of the todo item to filter by, if any.
198    pub due: Option<Duration>,
199}
200
201impl TodoConditions {
202    /// The due datetime.
203    pub fn due_before(&self) -> Option<NaiveDateTime> {
204        self.due.map(|a| self.now + a)
205    }
206}
207
208/// The default sort key for todo items, which is by due date.
209#[derive(Debug, Clone, Copy)]
210pub enum TodoSort {
211    /// Sort by the due date and time of the todo item.
212    Due(SortOrder),
213
214    /// Sort by the priority of the todo item.
215    Priority {
216        /// Sort order, either ascending or descending.
217        order: SortOrder,
218
219        /// Put items with no priority first or last.
220        none_first: bool,
221    },
222}