1use crate::{
6 Event, EventConditions, Pager, Priority, Todo, TodoConditions, TodoDraft, TodoSort,
7 event::ParsedEventConditions,
8 localdb::LocalDb,
9 short_id::ShortIds,
10 todo::{ParsedTodoConditions, ParsedTodoSort, TodoPatch},
11};
12use chrono::{DateTime, Duration, Local};
13use icalendar::{Calendar, CalendarComponent, Component};
14use std::{
15 error::Error,
16 path::{Path, PathBuf},
17};
18use tokio::fs;
19use uuid::Uuid;
20
21#[derive(Debug, Clone)]
23pub struct Aim {
24 now: DateTime<Local>,
25 config: Config,
26 db: LocalDb,
27 short_ids: ShortIds,
28 calendar_path: PathBuf,
29}
30
31impl Aim {
32 pub async fn new(config: Config) -> Result<Self, Box<dyn Error>> {
34 let now = Local::now();
35
36 if let Some(parent) = &config.state_dir {
37 log::info!("Ensuring state directory exists: {}", parent.display());
38 fs::create_dir_all(parent).await?;
39 }
40
41 let db = LocalDb::open(&config.state_dir)
42 .await
43 .map_err(|e| format!("Failed to initialize db: {e}"))?;
44
45 let short_ids = ShortIds::new(db.clone());
46 let calendar_path = config.calendar_path.clone();
47 let that = Self {
48 now,
49 config,
50 db,
51 short_ids,
52 calendar_path,
53 };
54 that.add_calendar(&that.calendar_path)
55 .await
56 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
57
58 Ok(that)
59 }
60
61 pub fn now(&self) -> DateTime<Local> {
63 self.now
64 }
65
66 pub fn refresh_now(&mut self) {
68 self.now = Local::now();
69 }
70
71 pub async fn list_events(
73 &self,
74 conds: &EventConditions,
75 pager: &Pager,
76 ) -> Result<Vec<impl Event>, Box<dyn Error>> {
77 let conds = ParsedEventConditions::parse(&self.now, conds);
78 let events = self.db.events.list(&conds, pager).await?;
79 let events = self.short_ids.events(events).await?;
80 Ok(events)
81 }
82
83 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
85 let conds = ParsedEventConditions::parse(&self.now, conds);
86 self.db.events.count(&conds).await
87 }
88
89 pub fn default_todo_draft(&self) -> TodoDraft {
91 TodoDraft::default(&self.config)
92 }
93
94 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo, Box<dyn Error>> {
96 let uid = self.generate_uid().await?;
97 let todo = draft.into_todo(&self.config, self.now, &uid);
98 let path = self.get_path(&uid);
99
100 let calendar = Calendar::new().push(todo.clone()).done();
101 fs::write(&path, calendar.to_string())
102 .await
103 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
104
105 self.db.upsert_todo(&path, &todo).await?;
106
107 let todo = self.short_ids.todo(todo).await?;
108 Ok(todo)
109 }
110
111 pub async fn update_todo(
113 &self,
114 id: &Id,
115 patch: TodoPatch,
116 ) -> Result<impl Todo, Box<dyn Error>> {
117 let uid = self.short_ids.get_uid(id).await?;
118 let todo = match self.db.todos.get(&uid).await? {
119 Some(todo) => todo,
120 None => return Err("Todo not found".into()),
121 };
122
123 let path: PathBuf = todo.path().into();
124 let mut calendar = parse_ics(&path).await?;
125 let t = calendar
126 .components
127 .iter_mut()
128 .filter_map(|a| match a {
129 CalendarComponent::Todo(a) => Some(a),
130 _ => None,
131 })
132 .find(|a| a.get_uid() == Some(todo.uid()))
133 .ok_or("Todo not found in calendar")?;
134
135 patch.apply_to(t);
136 let todo = t.clone();
137 fs::write(&path, calendar.done().to_string())
138 .await
139 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
140
141 self.db.upsert_todo(&path, &todo).await?;
142
143 let todo = self.short_ids.todo(todo).await?;
144 Ok(todo)
145 }
146
147 pub async fn get_todo(&self, id: &Id) -> Result<Option<impl Todo>, Box<dyn Error>> {
149 let uid = self.short_ids.get_uid(id).await?;
150 match self.db.todos.get(&uid).await {
151 Ok(Some(todo)) => {
152 let todo = self.short_ids.todo(todo).await?;
153 Ok(Some(todo))
154 }
155 Ok(None) => Ok(None),
156 Err(e) => Err(e.into()),
157 }
158 }
159
160 pub async fn list_todos(
162 &self,
163 conds: &TodoConditions,
164 sort: &[TodoSort],
165 pager: &Pager,
166 ) -> Result<Vec<impl Todo>, Box<dyn Error>> {
167 let conds = ParsedTodoConditions::parse(&self.now, conds);
168
169 let sort: Vec<_> = sort
170 .iter()
171 .map(|s| ParsedTodoSort::parse(&self.config, *s))
172 .collect();
173
174 let todos = self.db.todos.list(&conds, &sort, pager).await?;
175 let todos = self.short_ids.todos(todos).await?;
176 Ok(todos)
177 }
178
179 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
181 let conds = ParsedTodoConditions::parse(&self.now, conds);
182 self.db.todos.count(&conds).await
183 }
184
185 pub async fn close(self) -> Result<(), Box<dyn Error>> {
187 self.db.close().await?;
188 Ok(())
189 }
190
191 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
192 let mut reader = fs::read_dir(calendar_path)
193 .await
194 .map_err(|e| format!("Failed to read directory: {e}"))?;
195
196 let mut handles = vec![];
197 let mut count_ics = 0;
198
199 while let Some(entry) = reader.next_entry().await? {
200 let path = entry.path();
201 match path.extension() {
202 Some(ext) if ext == "ics" => {
203 count_ics += 1;
204 let that = self.clone();
205 handles.push(tokio::spawn(async move {
206 if let Err(e) = that.add_ics(&path).await {
207 log::error!("Failed to process file {}: {}", path.display(), e);
208 }
209 }));
210 }
211 _ => {}
212 }
213 }
214
215 for handle in handles {
216 handle.await?;
217 }
218
219 log::debug!("Total .ics files processed: {count_ics}");
220 Ok(())
221 }
222
223 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
224 log::debug!("Parsing file: {}", path.display());
225 let calendar = parse_ics(path).await?;
226 log::debug!(
227 "Found {} components in {}.",
228 calendar.components.len(),
229 path.display()
230 );
231
232 for component in calendar.components {
233 log::debug!("Processing component: {component:?}");
234 match component {
235 CalendarComponent::Event(event) => self.db.upsert_event(path, &event).await?,
236 CalendarComponent::Todo(todo) => self.db.upsert_todo(path, &todo).await?,
237 _ => log::warn!("Ignoring unsupported component type: {component:?}"),
238 }
239 }
240
241 Ok(())
242 }
243
244 async fn generate_uid(&self) -> Result<String, Box<dyn Error>> {
245 for _ in 0..16 {
246 let uid = Uuid::new_v4().to_string(); if self.db.todos.get(&uid).await?.is_some()
248 || fs::try_exists(&self.get_path(&uid)).await?
249 {
250 continue;
251 }
252 return Ok(uid);
253 }
254
255 Err("Failed to generate a unique UID after multiple attempts".into())
256 }
257
258 fn get_path(&self, uid: &str) -> PathBuf {
259 self.calendar_path.join(format!("{uid}.ics"))
260 }
261}
262
263#[derive(Debug, Clone)]
265pub struct Config {
266 pub calendar_path: PathBuf,
268
269 pub state_dir: Option<PathBuf>,
271
272 pub default_due: Option<Duration>,
274
275 pub default_priority: Priority,
277
278 pub default_priority_none_fist: bool,
280}
281
282async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
283 fs::read_to_string(path)
284 .await
285 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
286 .parse()
287 .map_err(|e| format!("Failed to parse calendar: {e}").into())
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
292pub enum Id {
293 Uid(String),
295 ShortIdOrUid(String),
297}