1use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Local, TimeZone, 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 self.get_priority()
69 .map(|p| Priority::from(p as u8))
70 .unwrap_or_default()
71 }
72
73 fn status(&self) -> TodoStatus {
74 self.get_status().map(Into::into).unwrap_or_default()
75 }
76
77 fn summary(&self) -> &str {
78 self.get_summary().unwrap_or_default()
79 }
80}
81
82#[derive(Debug)]
84pub struct TodoDraft {
85 pub description: Option<String>,
87
88 pub due: Option<LooseDateTime>,
90
91 pub percent_complete: Option<u8>,
93
94 pub priority: Option<Priority>,
96
97 pub status: TodoStatus,
99
100 pub summary: String,
102}
103
104impl TodoDraft {
105 pub(crate) fn default(config: &Config, now: &DateTime<Local>) -> Self {
107 Self {
108 description: None,
109 due: config.default_due.map(|d| d.resolve_since_datetime(now)),
110 percent_complete: None,
111 priority: Some(config.default_priority),
112 status: TodoStatus::default(),
113 summary: String::default(),
114 }
115 }
116
117 pub(crate) fn into_ics(
119 self,
120 config: &Config,
121 now: &DateTime<Local>,
122 uid: &str,
123 ) -> icalendar::Todo {
124 let mut todo = icalendar::Todo::with_uid(uid);
125
126 if let Some(description) = self.description {
127 Component::description(&mut todo, &description);
128 }
129
130 if let Some(due) = self.due {
131 icalendar::Todo::due(&mut todo, due);
132 } else if let Some(duration) = config.default_due {
133 icalendar::Todo::due(&mut todo, duration.resolve_since_datetime(now));
134 }
135
136 if let Some(percent) = self.percent_complete {
137 icalendar::Todo::percent_complete(&mut todo, percent.max(100));
138 }
139
140 if let Some(priority) = self.priority {
141 Component::priority(&mut todo, priority.into());
142 } else {
143 Component::priority(&mut todo, config.default_priority.into());
144 }
145
146 icalendar::Todo::status(&mut todo, self.status.into());
147
148 Component::summary(&mut todo, &self.summary);
149
150 todo
151 }
152}
153
154#[derive(Debug, Default, Clone)]
156pub struct TodoPatch {
157 pub description: Option<Option<String>>,
159
160 pub due: Option<Option<LooseDateTime>>,
162
163 pub percent_complete: Option<Option<u8>>,
165
166 pub priority: Option<Priority>,
168
169 pub status: Option<TodoStatus>,
171
172 pub summary: Option<String>,
174}
175
176impl TodoPatch {
177 pub fn is_empty(&self) -> bool {
179 self.description.is_none()
180 && self.due.is_none()
181 && self.percent_complete.is_none()
182 && self.priority.is_none()
183 && self.status.is_none()
184 && self.summary.is_none()
185 }
186
187 pub(crate) fn apply_to<'a, Tz: TimeZone>(
189 &self,
190 now: &DateTime<Tz>,
191 t: &'a mut icalendar::Todo,
192 ) -> &'a mut icalendar::Todo {
193 if let Some(description) = &self.description {
194 match description {
195 Some(desc) => t.description(desc),
196 None => t.remove_description(),
197 };
198 }
199
200 if let Some(due) = &self.due {
201 match due {
202 Some(d) => t.due(*d),
203 None => t.remove_due(),
204 };
205 }
206
207 if let Some(percent) = self.percent_complete {
208 t.percent_complete(percent.unwrap_or(0).max(100));
209 }
210
211 if let Some(priority) = self.priority {
212 t.priority(priority.into());
213 }
214
215 if let Some(status) = self.status {
216 t.status(status.into());
217
218 match status {
219 TodoStatus::Completed => t.completed(now.with_timezone(&Utc)),
220 _ if t.get_completed().is_some() => t.remove_completed(),
221 _ => t,
222 };
223 }
224
225 if let Some(summary) = &self.summary {
226 t.summary(summary);
227 }
228
229 t
230 }
231}
232
233#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
235#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
236pub enum TodoStatus {
237 #[default]
239 NeedsAction,
240
241 Completed,
243
244 InProcess,
246
247 Cancelled,
249}
250
251const STATUS_NEEDS_ACTION: &str = "NEEDS-ACTION";
252const STATUS_COMPLETED: &str = "COMPLETED";
253const STATUS_IN_PROCESS: &str = "IN-PROGRESS";
254const STATUS_CANCELLED: &str = "CANCELLED";
255
256impl AsRef<str> for TodoStatus {
257 fn as_ref(&self) -> &str {
258 match self {
259 TodoStatus::NeedsAction => STATUS_NEEDS_ACTION,
260 TodoStatus::Completed => STATUS_COMPLETED,
261 TodoStatus::InProcess => STATUS_IN_PROCESS,
262 TodoStatus::Cancelled => STATUS_CANCELLED,
263 }
264 }
265}
266
267impl Display for TodoStatus {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 write!(f, "{}", self.as_ref())
270 }
271}
272
273impl FromStr for TodoStatus {
274 type Err = ();
275
276 fn from_str(value: &str) -> Result<Self, Self::Err> {
277 match value {
278 STATUS_NEEDS_ACTION => Ok(TodoStatus::NeedsAction),
279 STATUS_COMPLETED => Ok(TodoStatus::Completed),
280 STATUS_IN_PROCESS => Ok(TodoStatus::InProcess),
281 STATUS_CANCELLED => Ok(TodoStatus::Cancelled),
282 _ => Err(()),
283 }
284 }
285}
286
287impl From<TodoStatus> for icalendar::TodoStatus {
288 fn from(item: TodoStatus) -> icalendar::TodoStatus {
289 match item {
290 TodoStatus::NeedsAction => icalendar::TodoStatus::NeedsAction,
291 TodoStatus::Completed => icalendar::TodoStatus::Completed,
292 TodoStatus::InProcess => icalendar::TodoStatus::InProcess,
293 TodoStatus::Cancelled => icalendar::TodoStatus::Cancelled,
294 }
295 }
296}
297
298impl From<icalendar::TodoStatus> for TodoStatus {
299 fn from(status: icalendar::TodoStatus) -> Self {
300 match status {
301 icalendar::TodoStatus::NeedsAction => TodoStatus::NeedsAction,
302 icalendar::TodoStatus::Completed => TodoStatus::Completed,
303 icalendar::TodoStatus::InProcess => TodoStatus::InProcess,
304 icalendar::TodoStatus::Cancelled => TodoStatus::Cancelled,
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy)]
311pub struct TodoConditions {
312 pub status: Option<TodoStatus>,
314
315 pub due: Option<DateTimeAnchor>,
317}
318
319#[derive(Debug, Clone, Copy)]
321pub(crate) struct ParsedTodoConditions {
322 pub status: Option<TodoStatus>,
324
325 pub due: Option<DateTime<Local>>,
327}
328
329impl ParsedTodoConditions {
330 pub fn parse(now: &DateTime<Local>, conds: &TodoConditions) -> Self {
331 let status = conds.status;
332 let due = conds.due.map(|a| a.resolve_at_end_of_day(now));
333 ParsedTodoConditions { status, due }
334 }
335}
336
337#[derive(Debug, Clone, Copy)]
339pub enum TodoSort {
340 Due(SortOrder),
342
343 Priority {
345 order: SortOrder,
347
348 none_first: Option<bool>,
350 },
351}
352
353#[derive(Debug, Clone, Copy)]
354pub(crate) enum ParsedTodoSort {
355 Due(SortOrder),
357
358 Priority {
360 order: SortOrder,
362
363 none_first: bool,
365 },
366}
367
368impl ParsedTodoSort {
369 pub fn parse(config: &Config, sort: TodoSort) -> Self {
370 match sort {
371 TodoSort::Due(order) => ParsedTodoSort::Due(order),
372 TodoSort::Priority { order, none_first } => ParsedTodoSort::Priority {
373 order,
374 none_first: none_first.unwrap_or(config.default_priority_none_fist),
375 },
376 }
377 }
378
379 pub fn parse_vec(config: &Config, sort: &[TodoSort]) -> Vec<Self> {
380 sort.iter()
381 .map(|s| ParsedTodoSort::parse(config, *s))
382 .collect()
383 }
384}