1use 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
14pub trait Todo {
16 fn uid(&self) -> &str;
18
19 fn completed(&self) -> Option<DateTime<Local>>;
21
22 fn description(&self) -> Option<&str>;
24
25 fn due(&self) -> Option<LooseDateTime>;
27
28 fn percent_complete(&self) -> Option<u8>;
30
31 fn priority(&self) -> Priority;
33
34 fn status(&self) -> TodoStatus;
36
37 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#[derive(Debug)]
79pub struct TodoDraft {
80 pub description: Option<String>,
82
83 pub due: Option<LooseDateTime>,
85
86 pub percent_complete: Option<u8>,
88
89 pub priority: Priority,
91
92 pub status: Option<TodoStatus>,
94
95 pub summary: String,
97}
98
99impl TodoDraft {
100 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#[derive(Debug, Default, Clone)]
129pub struct TodoPatch {
130 pub uid: String,
132
133 pub description: Option<Option<String>>,
135
136 pub due: Option<Option<LooseDateTime>>,
138
139 pub percent_complete: Option<Option<u8>>,
141
142 pub priority: Option<Priority>,
144
145 pub status: Option<TodoStatus>,
147
148 pub summary: Option<String>,
150}
151
152impl TodoPatch {
153 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 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
207#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
208pub enum TodoStatus {
209 #[default]
211 NeedsAction,
212
213 Completed,
215
216 InProcess,
218
219 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#[derive(Debug, Clone, Copy)]
283pub struct TodoConditions {
284 pub now: NaiveDateTime,
286
287 pub status: Option<TodoStatus>,
289
290 pub due: Option<Duration>,
292}
293
294impl TodoConditions {
295 pub fn due_before(&self) -> Option<NaiveDateTime> {
297 self.due.map(|a| self.now + a)
298 }
299}
300
301#[derive(Debug, Clone, Copy)]
303pub enum TodoSort {
304 Due(SortOrder),
306
307 Priority {
309 order: SortOrder,
311
312 none_first: bool,
314 },
315}