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) -> Option<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_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#[derive(Debug)]
78pub struct TodoDraft {
79 pub uid: String,
81
82 pub description: Option<String>,
84
85 pub due: Option<LooseDateTime>,
87
88 pub priority: Priority,
90
91 pub summary: String,
93}
94
95impl TodoDraft {
96 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 pub fn path(&self) -> String {
115 format!("{}.ics", self.uid)
116 }
117}
118
119#[derive(Debug, Default, Clone)]
121pub struct TodoPatch {
122 pub uid: String,
124
125 pub description: Option<Option<String>>,
127
128 pub due: Option<Option<LooseDateTime>>,
130
131 pub percent_complete: Option<Option<u8>>,
133
134 pub priority: Option<Priority>,
136
137 pub status: Option<TodoStatus>,
139
140 pub summary: Option<String>,
142}
143
144impl TodoPatch {
145 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 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#[derive(Debug, Clone, Copy)]
199#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
200pub enum TodoStatus {
201 NeedsAction,
203
204 Completed,
206
207 InProcess,
209
210 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#[derive(Debug, Clone, Copy)]
274pub struct TodoConditions {
275 pub now: NaiveDateTime,
277
278 pub status: Option<TodoStatus>,
280
281 pub due: Option<Duration>,
283}
284
285impl TodoConditions {
286 pub fn due_before(&self) -> Option<NaiveDateTime> {
288 self.due.map(|a| self.now + a)
289 }
290}
291
292#[derive(Debug, Clone, Copy)]
294pub enum TodoSort {
295 Due(SortOrder),
297
298 Priority {
300 order: SortOrder,
302
303 none_first: bool,
305 },
306}