1use crate::{Config, LooseDateTime, Priority, SortOrder};
6use chrono::{DateTime, Duration, Local, Utc};
7use icalendar::Component;
8use std::{fmt::Display, num::NonZeroU32, str::FromStr};
9
10pub trait Todo {
12 fn short_id(&self) -> Option<NonZeroU32> {
16 None
17 }
18
19 fn uid(&self) -> &str;
21
22 fn completed(&self) -> Option<DateTime<Local>>;
24
25 fn description(&self) -> Option<&str>;
27
28 fn due(&self) -> Option<LooseDateTime>;
30
31 fn percent_complete(&self) -> Option<u8>;
33
34 fn priority(&self) -> Priority;
36
37 fn status(&self) -> TodoStatus;
39
40 fn summary(&self) -> &str;
42}
43
44impl Todo for icalendar::Todo {
45 fn uid(&self) -> &str {
46 self.get_uid().unwrap_or("")
47 }
48
49 fn completed(&self) -> Option<DateTime<Local>> {
50 self.get_completed().map(|dt| dt.with_timezone(&Local))
51 }
52
53 fn description(&self) -> Option<&str> {
54 self.get_description()
55 }
56
57 fn due(&self) -> Option<LooseDateTime> {
58 self.get_due().map(Into::into)
59 }
60
61 fn percent_complete(&self) -> Option<u8> {
62 self.get_percent_complete()
63 }
64
65 fn priority(&self) -> Priority {
66 self.get_priority()
67 .map(|p| Priority::from(p as u8))
68 .unwrap_or_default()
69 }
70
71 fn status(&self) -> TodoStatus {
72 self.get_status().map(Into::into).unwrap_or_default()
73 }
74
75 fn summary(&self) -> &str {
76 self.get_summary().unwrap_or("")
77 }
78}
79
80#[derive(Debug)]
82pub struct TodoDraft {
83 pub description: Option<String>,
85
86 pub due: Option<LooseDateTime>,
88
89 pub percent_complete: Option<u8>,
91
92 pub priority: Option<Priority>,
94
95 pub status: Option<TodoStatus>,
97
98 pub summary: String,
100}
101
102impl TodoDraft {
103 pub(crate) fn default(config: &Config) -> Self {
105 Self {
106 description: None,
107 due: config
108 .default_due
109 .map(|d| LooseDateTime::Local(Local::now() + d)),
110 percent_complete: None,
111 priority: Some(config.default_priority),
112 status: Some(TodoStatus::NeedsAction),
113 summary: "".to_string(),
114 }
115 }
116
117 pub(crate) fn into_todo(
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, LooseDateTime::Local(now + duration));
134 }
135
136 if let Some(percent) = self.percent_complete {
137 icalendar::Todo::percent_complete(&mut todo, percent);
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 let status = self.status.unwrap_or(TodoStatus::NeedsAction).into();
147 icalendar::Todo::status(&mut todo, status);
148
149 Component::summary(&mut todo, &self.summary);
150
151 todo
152 }
153}
154
155#[derive(Debug, Default, Clone)]
157pub struct TodoPatch {
158 pub description: Option<Option<String>>,
160
161 pub due: Option<Option<LooseDateTime>>,
163
164 pub percent_complete: Option<Option<u8>>,
166
167 pub priority: Option<Priority>,
169
170 pub status: Option<TodoStatus>,
172
173 pub summary: Option<String>,
175}
176
177impl TodoPatch {
178 pub fn is_empty(&self) -> bool {
180 self.description.is_none()
181 && self.due.is_none()
182 && self.percent_complete.is_none()
183 && self.priority.is_none()
184 && self.status.is_none()
185 && self.summary.is_none()
186 }
187
188 pub(crate) fn apply_to<'a>(&self, t: &'a mut icalendar::Todo) -> &'a mut icalendar::Todo {
190 if let Some(description) = &self.description {
191 match description {
192 Some(desc) => t.description(desc),
193 None => t.remove_description(),
194 };
195 }
196
197 if let Some(due) = &self.due {
198 match due {
199 Some(d) => t.due(*d),
200 None => t.remove_due(),
201 };
202 }
203
204 if let Some(percent) = self.percent_complete {
205 t.percent_complete(percent.unwrap_or(0));
206 }
207
208 if let Some(priority) = self.priority {
209 t.priority(priority.into());
210 }
211
212 if let Some(status) = self.status {
213 t.status(status.into());
214
215 match status {
216 TodoStatus::Completed => t.completed(Utc::now()),
217 _ if t.get_completed().is_some() => t.remove_completed(),
218 _ => t,
219 };
220 }
221
222 if let Some(summary) = &self.summary {
223 t.summary(summary);
224 }
225
226 t
227 }
228}
229
230#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
232#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
233pub enum TodoStatus {
234 #[default]
236 NeedsAction,
237
238 Completed,
240
241 InProcess,
243
244 Cancelled,
246}
247
248const STATUS_NEEDS_ACTION: &str = "NEEDS-ACTION";
249const STATUS_COMPLETED: &str = "COMPLETED";
250const STATUS_IN_PROCESS: &str = "IN-PROGRESS";
251const STATUS_CANCELLED: &str = "CANCELLED";
252
253impl AsRef<str> for TodoStatus {
254 fn as_ref(&self) -> &str {
255 match self {
256 TodoStatus::NeedsAction => STATUS_NEEDS_ACTION,
257 TodoStatus::Completed => STATUS_COMPLETED,
258 TodoStatus::InProcess => STATUS_IN_PROCESS,
259 TodoStatus::Cancelled => STATUS_CANCELLED,
260 }
261 }
262}
263
264impl Display for TodoStatus {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 write!(f, "{}", self.as_ref())
267 }
268}
269
270impl FromStr for TodoStatus {
271 type Err = ();
272
273 fn from_str(value: &str) -> Result<Self, Self::Err> {
274 match value {
275 STATUS_NEEDS_ACTION => Ok(TodoStatus::NeedsAction),
276 STATUS_COMPLETED => Ok(TodoStatus::Completed),
277 STATUS_IN_PROCESS => Ok(TodoStatus::InProcess),
278 STATUS_CANCELLED => Ok(TodoStatus::Cancelled),
279 _ => Err(()),
280 }
281 }
282}
283
284impl From<TodoStatus> for icalendar::TodoStatus {
285 fn from(item: TodoStatus) -> icalendar::TodoStatus {
286 match item {
287 TodoStatus::NeedsAction => icalendar::TodoStatus::NeedsAction,
288 TodoStatus::Completed => icalendar::TodoStatus::Completed,
289 TodoStatus::InProcess => icalendar::TodoStatus::InProcess,
290 TodoStatus::Cancelled => icalendar::TodoStatus::Cancelled,
291 }
292 }
293}
294
295impl From<icalendar::TodoStatus> for TodoStatus {
296 fn from(status: icalendar::TodoStatus) -> Self {
297 match status {
298 icalendar::TodoStatus::NeedsAction => TodoStatus::NeedsAction,
299 icalendar::TodoStatus::Completed => TodoStatus::Completed,
300 icalendar::TodoStatus::InProcess => TodoStatus::InProcess,
301 icalendar::TodoStatus::Cancelled => TodoStatus::Cancelled,
302 }
303 }
304}
305
306#[derive(Debug, Clone, Copy)]
308pub struct TodoConditions {
309 pub status: Option<TodoStatus>,
311
312 pub due: Option<Duration>,
314}
315
316#[derive(Debug, Clone, Copy)]
318pub(crate) struct ParsedTodoConditions {
319 pub status: Option<TodoStatus>,
321
322 pub due: Option<DateTime<Local>>,
324}
325
326impl ParsedTodoConditions {
327 pub fn parse(now: &DateTime<Local>, conds: &TodoConditions) -> Self {
328 let status = conds.status;
329 let due = conds.due.map(|d| *now + d);
330 ParsedTodoConditions { status, due }
331 }
332}
333
334#[derive(Debug, Clone, Copy)]
336pub enum TodoSort {
337 Due(SortOrder),
339
340 Priority {
342 order: SortOrder,
344
345 none_first: Option<bool>,
347 },
348}
349
350#[derive(Debug, Clone, Copy)]
351pub(crate) enum ParsedTodoSort {
352 Due(SortOrder),
354
355 Priority {
357 order: SortOrder,
359
360 none_first: bool,
362 },
363}
364
365impl ParsedTodoSort {
366 pub fn parse(config: &Config, sort: TodoSort) -> Self {
367 match sort {
368 TodoSort::Due(order) => ParsedTodoSort::Due(order),
369 TodoSort::Priority { order, none_first } => ParsedTodoSort::Priority {
370 order,
371 none_first: none_first.unwrap_or(config.default_priority_none_fist),
372 },
373 }
374 }
375}