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 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
110 .default_due
111 .map(|d| LooseDateTime::Local(d.datetime(now))),
112 percent_complete: None,
113 priority: Some(config.default_priority),
114 status: TodoStatus::default(),
115 summary: "".to_string(),
116 }
117 }
118
119 pub(crate) fn into_ics(
121 self,
122 config: &Config,
123 now: DateTime<Local>,
124 uid: &str,
125 ) -> icalendar::Todo {
126 let mut todo = icalendar::Todo::with_uid(uid);
127
128 if let Some(description) = self.description {
129 Component::description(&mut todo, &description);
130 }
131
132 if let Some(due) = self.due {
133 icalendar::Todo::due(&mut todo, due);
134 } else if let Some(duration) = config.default_due {
135 icalendar::Todo::due(&mut todo, LooseDateTime::Local(duration.datetime(now)));
136 }
137
138 if let Some(percent) = self.percent_complete {
139 icalendar::Todo::percent_complete(&mut todo, percent.max(100));
140 }
141
142 if let Some(priority) = self.priority {
143 Component::priority(&mut todo, priority.into());
144 } else {
145 Component::priority(&mut todo, config.default_priority.into());
146 }
147
148 icalendar::Todo::status(&mut todo, self.status.into());
149
150 Component::summary(&mut todo, &self.summary);
151
152 todo
153 }
154}
155
156#[derive(Debug, Default, Clone)]
158pub struct TodoPatch {
159 pub description: Option<Option<String>>,
161
162 pub due: Option<Option<LooseDateTime>>,
164
165 pub percent_complete: Option<Option<u8>>,
167
168 pub priority: Option<Priority>,
170
171 pub status: Option<TodoStatus>,
173
174 pub summary: Option<String>,
176}
177
178impl TodoPatch {
179 pub fn is_empty(&self) -> bool {
181 self.description.is_none()
182 && self.due.is_none()
183 && self.percent_complete.is_none()
184 && self.priority.is_none()
185 && self.status.is_none()
186 && self.summary.is_none()
187 }
188
189 pub(crate) fn apply_to<'a>(&self, t: &'a mut icalendar::Todo) -> &'a mut icalendar::Todo {
191 if let Some(description) = &self.description {
192 match description {
193 Some(desc) => t.description(desc),
194 None => t.remove_description(),
195 };
196 }
197
198 if let Some(due) = &self.due {
199 match due {
200 Some(d) => t.due(*d),
201 None => t.remove_due(),
202 };
203 }
204
205 if let Some(percent) = self.percent_complete {
206 t.percent_complete(percent.unwrap_or(0).max(100));
207 }
208
209 if let Some(priority) = self.priority {
210 t.priority(priority.into());
211 }
212
213 if let Some(status) = self.status {
214 t.status(status.into());
215
216 match status {
217 TodoStatus::Completed => t.completed(Utc::now()),
218 _ if t.get_completed().is_some() => t.remove_completed(),
219 _ => t,
220 };
221 }
222
223 if let Some(summary) = &self.summary {
224 t.summary(summary);
225 }
226
227 t
228 }
229}
230
231#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
233#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
234pub enum TodoStatus {
235 #[default]
237 NeedsAction,
238
239 Completed,
241
242 InProcess,
244
245 Cancelled,
247}
248
249const STATUS_NEEDS_ACTION: &str = "NEEDS-ACTION";
250const STATUS_COMPLETED: &str = "COMPLETED";
251const STATUS_IN_PROCESS: &str = "IN-PROGRESS";
252const STATUS_CANCELLED: &str = "CANCELLED";
253
254impl AsRef<str> for TodoStatus {
255 fn as_ref(&self) -> &str {
256 match self {
257 TodoStatus::NeedsAction => STATUS_NEEDS_ACTION,
258 TodoStatus::Completed => STATUS_COMPLETED,
259 TodoStatus::InProcess => STATUS_IN_PROCESS,
260 TodoStatus::Cancelled => STATUS_CANCELLED,
261 }
262 }
263}
264
265impl Display for TodoStatus {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(f, "{}", self.as_ref())
268 }
269}
270
271impl FromStr for TodoStatus {
272 type Err = ();
273
274 fn from_str(value: &str) -> Result<Self, Self::Err> {
275 match value {
276 STATUS_NEEDS_ACTION => Ok(TodoStatus::NeedsAction),
277 STATUS_COMPLETED => Ok(TodoStatus::Completed),
278 STATUS_IN_PROCESS => Ok(TodoStatus::InProcess),
279 STATUS_CANCELLED => Ok(TodoStatus::Cancelled),
280 _ => Err(()),
281 }
282 }
283}
284
285impl From<TodoStatus> for icalendar::TodoStatus {
286 fn from(item: TodoStatus) -> icalendar::TodoStatus {
287 match item {
288 TodoStatus::NeedsAction => icalendar::TodoStatus::NeedsAction,
289 TodoStatus::Completed => icalendar::TodoStatus::Completed,
290 TodoStatus::InProcess => icalendar::TodoStatus::InProcess,
291 TodoStatus::Cancelled => icalendar::TodoStatus::Cancelled,
292 }
293 }
294}
295
296impl From<icalendar::TodoStatus> for TodoStatus {
297 fn from(status: icalendar::TodoStatus) -> Self {
298 match status {
299 icalendar::TodoStatus::NeedsAction => TodoStatus::NeedsAction,
300 icalendar::TodoStatus::Completed => TodoStatus::Completed,
301 icalendar::TodoStatus::InProcess => TodoStatus::InProcess,
302 icalendar::TodoStatus::Cancelled => TodoStatus::Cancelled,
303 }
304 }
305}
306
307#[derive(Debug, Clone, Copy)]
309pub struct TodoConditions {
310 pub status: Option<TodoStatus>,
312
313 pub due: Option<DateTimeAnchor>,
315}
316
317#[derive(Debug, Clone, Copy)]
319pub(crate) struct ParsedTodoConditions {
320 pub status: Option<TodoStatus>,
322
323 pub due: Option<DateTime<Local>>,
325}
326
327impl ParsedTodoConditions {
328 pub fn parse(now: &DateTime<Local>, conds: &TodoConditions) -> Self {
329 let status = conds.status;
330 let due = conds.due.map(|a| a.parse_as_end_of_day(now));
331 ParsedTodoConditions { status, due }
332 }
333}
334
335#[derive(Debug, Clone, Copy)]
337pub enum TodoSort {
338 Due(SortOrder),
340
341 Priority {
343 order: SortOrder,
345
346 none_first: Option<bool>,
348 },
349}
350
351#[derive(Debug, Clone, Copy)]
352pub(crate) enum ParsedTodoSort {
353 Due(SortOrder),
355
356 Priority {
358 order: SortOrder,
360
361 none_first: bool,
363 },
364}
365
366impl ParsedTodoSort {
367 pub fn parse(config: &Config, sort: TodoSort) -> Self {
368 match sort {
369 TodoSort::Due(order) => ParsedTodoSort::Due(order),
370 TodoSort::Priority { order, none_first } => ParsedTodoSort::Priority {
371 order,
372 none_first: none_first.unwrap_or(config.default_priority_none_fist),
373 },
374 }
375 }
376
377 pub fn parse_vec(config: &Config, sort: &[TodoSort]) -> Vec<Self> {
378 sort.iter()
379 .map(|s| ParsedTodoSort::parse(config, *s))
380 .collect()
381 }
382}