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