1use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Local, Utc};
8use icalendar::Component;
9
10use crate::{Config, DateTimeAnchor, LooseDateTime, Priority, SortOrder};
11
12pub trait Todo {
14 fn short_id(&self) -> Option<NonZeroU32> {
18 None
19 }
20
21 fn uid(&self) -> &str;
23
24 fn completed(&self) -> Option<DateTime<Local>>;
26
27 fn description(&self) -> Option<&str>;
29
30 fn due(&self) -> Option<LooseDateTime>;
32
33 fn percent_complete(&self) -> Option<u8>;
35
36 fn priority(&self) -> Priority;
38
39 fn status(&self) -> TodoStatus;
41
42 fn summary(&self) -> &str;
44}
45
46impl Todo for icalendar::Todo {
47 fn uid(&self) -> &str {
48 self.get_uid().unwrap_or_default()
49 }
50
51 fn completed(&self) -> Option<DateTime<Local>> {
52 self.get_completed().map(|dt| dt.with_timezone(&Local))
53 }
54
55 fn description(&self) -> Option<&str> {
56 self.get_description()
57 }
58
59 fn due(&self) -> Option<LooseDateTime> {
60 self.get_due().map(Into::into)
61 }
62
63 fn percent_complete(&self) -> Option<u8> {
64 self.get_percent_complete()
65 }
66
67 fn priority(&self) -> Priority {
68 match self.get_priority() {
69 Some(p) => Priority::from(u8::try_from(p.min(9)).unwrap_or_default()),
70 _ => Priority::default(),
71 }
72 }
73
74 fn status(&self) -> TodoStatus {
75 self.get_status().map(Into::into).unwrap_or_default()
76 }
77
78 fn summary(&self) -> &str {
79 self.get_summary().unwrap_or_default()
80 }
81}
82
83#[derive(Debug)]
85pub struct TodoDraft {
86 pub description: Option<String>,
88
89 pub due: Option<LooseDateTime>,
91
92 pub percent_complete: Option<u8>,
94
95 pub priority: Option<Priority>,
97
98 pub status: TodoStatus,
100
101 pub summary: String,
103}
104
105impl TodoDraft {
106 pub(crate) fn default(config: &Config, now: &DateTime<Local>) -> Self {
108 Self {
109 description: None,
110 due: config.default_due.map(|d| d.resolve_since_datetime(now)),
111 percent_complete: None,
112 priority: Some(config.default_priority),
113 status: TodoStatus::default(),
114 summary: String::default(),
115 }
116 }
117
118 pub(crate) fn resolve<'a>(
120 &'a self,
121 config: &Config,
122 now: &'a DateTime<Local>,
123 ) -> ResolvedTodoDraft<'a> {
124 let due = self
125 .due
126 .or_else(|| config.default_due.map(|d| d.resolve_since_datetime(now)));
127
128 let percent_complete = self.percent_complete.map(|a| a.max(100));
129
130 let priority = self.priority.or(Some(config.default_priority));
131
132 ResolvedTodoDraft {
133 description: self.description.as_deref(),
134 due,
135 percent_complete,
136 priority,
137 status: self.status,
138 summary: &self.summary,
139
140 now,
141 }
142 }
143}
144
145#[derive(Debug, Clone, Copy)]
146pub struct ResolvedTodoDraft<'a> {
147 pub description: Option<&'a str>,
148 pub due: Option<LooseDateTime>,
149 pub percent_complete: Option<u8>,
150 pub priority: Option<Priority>,
151 pub status: TodoStatus,
152 pub summary: &'a str,
153
154 pub now: &'a DateTime<Local>,
155}
156
157impl ResolvedTodoDraft<'_> {
158 pub(crate) fn into_ics(self, uid: &str) -> icalendar::Todo {
160 let mut todo = icalendar::Todo::with_uid(uid);
161
162 if let Some(description) = self.description {
163 Component::description(&mut todo, description);
164 }
165
166 if let Some(due) = self.due {
167 icalendar::Todo::due(&mut todo, due);
168 }
169
170 if let Some(percent) = self.percent_complete {
171 icalendar::Todo::percent_complete(&mut todo, percent);
172 }
173
174 if let Some(priority) = self.priority {
175 Component::priority(&mut todo, priority.into());
176 }
177
178 icalendar::Todo::status(&mut todo, self.status.into());
179
180 Component::summary(&mut todo, self.summary);
181
182 Component::created(&mut todo, self.now.with_timezone(&Utc));
184 todo
185 }
186}
187
188#[derive(Debug, Default, Clone)]
190pub struct TodoPatch {
191 pub description: Option<Option<String>>,
193
194 pub due: Option<Option<LooseDateTime>>,
196
197 pub percent_complete: Option<Option<u8>>,
199
200 pub priority: Option<Priority>,
202
203 pub status: Option<TodoStatus>,
205
206 pub summary: Option<String>,
208}
209
210impl TodoPatch {
211 #[must_use]
213 pub fn is_empty(&self) -> bool {
214 self.description.is_none()
215 && self.due.is_none()
216 && self.percent_complete.is_none()
217 && self.priority.is_none()
218 && self.status.is_none()
219 && self.summary.is_none()
220 }
221
222 pub(crate) fn resolve<'a>(&'a self, now: &'a DateTime<Local>) -> ResolvedTodoPatch<'a> {
223 let percent_complete = match self.percent_complete {
224 Some(Some(v)) => Some(Some(v.min(100))),
225 _ => self.percent_complete,
226 };
227
228 ResolvedTodoPatch {
229 description: self.description.as_ref().map(|opt| opt.as_deref()),
230 due: self.due,
231 percent_complete,
232 priority: self.priority,
233 status: self.status,
234 summary: self.summary.as_deref(),
235 now,
236 }
237 }
238}
239
240#[derive(Debug, Clone, Copy)]
241pub struct ResolvedTodoPatch<'a> {
242 pub description: Option<Option<&'a str>>,
243 pub due: Option<Option<LooseDateTime>>,
244 pub percent_complete: Option<Option<u8>>,
245 pub priority: Option<Priority>,
246 pub status: Option<TodoStatus>,
247 pub summary: Option<&'a str>,
248
249 pub now: &'a DateTime<Local>,
250}
251
252impl ResolvedTodoPatch<'_> {
253 pub fn apply_to<'b>(&self, t: &'b mut icalendar::Todo) -> &'b mut icalendar::Todo {
255 match self.description {
256 Some(Some(desc)) => t.description(desc),
257 Some(None) => t.remove_description(),
258 None => t,
259 };
260
261 match self.due {
262 Some(Some(due)) => t.due(due),
263 Some(None) => t.remove_due(),
264 None => t,
265 };
266
267 match self.percent_complete {
268 Some(Some(v)) => t.percent_complete(v),
269 Some(None) => t.remove_percent_complete(),
270 None => t,
271 };
272
273 if let Some(priority) = self.priority {
274 t.priority(priority.into());
275 }
276
277 if let Some(status) = self.status {
278 t.status(status.into());
279
280 match status {
281 TodoStatus::Completed => t.completed(self.now.with_timezone(&Utc)),
282 _ if t.get_completed().is_some() => t.remove_completed(),
283 _ => t,
284 };
285 }
286
287 if let Some(summary) = &self.summary {
288 t.summary(summary);
289 }
290
291 if t.get_created().is_none() {
293 Component::created(t, self.now.with_timezone(&Utc));
294 }
295 t
296 }
297}
298
299#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
301#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
302pub enum TodoStatus {
303 #[default]
305 NeedsAction,
306
307 Completed,
309
310 InProcess,
312
313 Cancelled,
315}
316
317const STATUS_NEEDS_ACTION: &str = "NEEDS-ACTION";
318const STATUS_COMPLETED: &str = "COMPLETED";
319const STATUS_IN_PROCESS: &str = "IN-PROGRESS";
320const STATUS_CANCELLED: &str = "CANCELLED";
321
322impl AsRef<str> for TodoStatus {
323 fn as_ref(&self) -> &str {
324 match self {
325 TodoStatus::NeedsAction => STATUS_NEEDS_ACTION,
326 TodoStatus::Completed => STATUS_COMPLETED,
327 TodoStatus::InProcess => STATUS_IN_PROCESS,
328 TodoStatus::Cancelled => STATUS_CANCELLED,
329 }
330 }
331}
332
333impl Display for TodoStatus {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 self.as_ref().fmt(f)
336 }
337}
338
339impl FromStr for TodoStatus {
340 type Err = ();
341
342 fn from_str(value: &str) -> Result<Self, Self::Err> {
343 match value {
344 STATUS_NEEDS_ACTION => Ok(TodoStatus::NeedsAction),
345 STATUS_COMPLETED => Ok(TodoStatus::Completed),
346 STATUS_IN_PROCESS => Ok(TodoStatus::InProcess),
347 STATUS_CANCELLED => Ok(TodoStatus::Cancelled),
348 _ => Err(()),
349 }
350 }
351}
352
353impl From<TodoStatus> for icalendar::TodoStatus {
354 fn from(item: TodoStatus) -> icalendar::TodoStatus {
355 match item {
356 TodoStatus::NeedsAction => icalendar::TodoStatus::NeedsAction,
357 TodoStatus::Completed => icalendar::TodoStatus::Completed,
358 TodoStatus::InProcess => icalendar::TodoStatus::InProcess,
359 TodoStatus::Cancelled => icalendar::TodoStatus::Cancelled,
360 }
361 }
362}
363
364impl From<icalendar::TodoStatus> for TodoStatus {
365 fn from(status: icalendar::TodoStatus) -> Self {
366 match status {
367 icalendar::TodoStatus::NeedsAction => TodoStatus::NeedsAction,
368 icalendar::TodoStatus::Completed => TodoStatus::Completed,
369 icalendar::TodoStatus::InProcess => TodoStatus::InProcess,
370 icalendar::TodoStatus::Cancelled => TodoStatus::Cancelled,
371 }
372 }
373}
374
375#[derive(Debug, Clone, Copy)]
377pub struct TodoConditions {
378 pub status: Option<TodoStatus>,
380
381 pub due: Option<DateTimeAnchor>,
383}
384
385impl TodoConditions {
386 pub(crate) fn resolve(&self, now: &DateTime<Local>) -> ResolvedTodoConditions {
387 ResolvedTodoConditions {
388 status: self.status,
389 due: self.due.map(|a| a.resolve_at_end_of_day(now)),
390 }
391 }
392}
393
394#[derive(Debug, Clone, Copy)]
395pub struct ResolvedTodoConditions {
396 pub status: Option<TodoStatus>,
397 pub due: Option<DateTime<Local>>,
398}
399
400#[derive(Debug, Clone, Copy)]
402pub enum TodoSort {
403 Due(SortOrder),
405
406 Priority {
408 order: SortOrder,
410
411 none_first: Option<bool>,
413 },
414}
415
416impl TodoSort {
417 pub(crate) fn resolve(self, config: &Config) -> ResolvedTodoSort {
418 match self {
419 TodoSort::Due(order) => ResolvedTodoSort::Due(order),
420 TodoSort::Priority { order, none_first } => ResolvedTodoSort::Priority {
421 order,
422 none_first: none_first.unwrap_or(config.default_priority_none_fist),
423 },
424 }
425 }
426
427 pub(crate) fn resolve_vec(sort: &[TodoSort], config: &Config) -> Vec<ResolvedTodoSort> {
428 sort.iter().map(|s| s.resolve(config)).collect()
429 }
430}
431
432#[derive(Debug, Clone, Copy)]
433pub enum ResolvedTodoSort {
434 Due(SortOrder),
435 Priority { order: SortOrder, none_first: bool },
436}