1use color_eyre::{eyre::bail, Result};
2use serde::Deserialize;
3
4use std::{collections::HashSet, fmt::Display, path::PathBuf};
5use vault_data::VaultData;
6
7use filter::{filter, Filter};
8use tracing::error;
9use vault_parser::VaultParser;
10
11pub mod filter;
12pub mod parser;
13pub mod sorter;
14pub mod task;
15pub mod vault_data;
16mod vault_parser;
17
18#[derive(Clone, Debug, Deserialize)]
19pub struct TaskMarkerConfig {
20 pub done: char,
21 pub todo: char,
22 pub incomplete: char,
23 pub canceled: char,
24}
25
26impl Default for TaskMarkerConfig {
28 fn default() -> Self {
29 Self {
30 done: 'x',
31 todo: ' ',
32 incomplete: '/',
33 canceled: '-',
34 }
35 }
36}
37
38#[derive(Clone, Debug, Deserialize)]
39pub struct PrettySymbolsConfig {
40 pub task_done: String,
41 pub task_todo: String,
42 pub task_incomplete: String,
43 pub task_canceled: String,
44 pub due_date: String,
45 pub priority: String,
46 pub today_tag: String,
47}
48impl Default for PrettySymbolsConfig {
49 fn default() -> Self {
50 Self {
51 task_done: String::from("✅"),
52 task_todo: String::from("❌"),
53 task_incomplete: String::from("⏳"),
54 task_canceled: String::from("🚫"),
55 due_date: String::from("📅"),
56 priority: String::from("❗"),
57 today_tag: String::from("☀️"),
58 }
59 }
60}
61#[derive(Clone, Debug, Deserialize, Default)]
62pub struct TasksConfig {
63 #[serde(default)]
64 pub parse_dot_files: bool,
65 #[serde(default)]
66 pub file_tags_propagation: bool,
67 #[serde(default)]
68 pub ignored: Vec<PathBuf>,
69 #[serde(default)]
70 pub indent_length: usize,
71 #[serde(default)]
72 pub use_american_format: bool,
73 #[serde(default)]
74 pub show_relative_due_dates: bool,
75 #[serde(default)]
76 pub vault_path: PathBuf,
77 #[serde(default)]
78 pub explorer_default_search_string: String,
79 #[serde(default)]
80 pub filter_default_search_string: String,
81 #[serde(default)]
82 pub task_state_markers: TaskMarkerConfig,
83 #[serde(default)]
84 pub pretty_symbols: PrettySymbolsConfig,
85}
86
87pub struct TaskManager {
88 pub tasks: VaultData,
89 pub tags: HashSet<String>,
90 pub current_filter: Option<Filter>,
91}
92impl Default for TaskManager {
93 fn default() -> Self {
94 Self {
95 tasks: VaultData::Directory("Empty Vault".to_owned(), vec![]),
96 tags: HashSet::new(),
97 current_filter: None,
98 }
99 }
100}
101impl TaskManager {
102 pub fn load_from_config(config: &TasksConfig) -> Result<Self> {
108 let mut res = Self::default();
109 res.reload(config)?;
110 Ok(res)
111 }
112
113 pub fn reload(&mut self, config: &TasksConfig) -> Result<()> {
119 let vault_parser = VaultParser::new(config.clone());
120 let tasks = vault_parser.scan_vault()?;
121
122 Self::rewrite_vault_tasks(config, &tasks)
123 .unwrap_or_else(|e| error!("Failed to fix tasks: {e}"));
124
125 let mut tags = HashSet::new();
126 Self::collect_tags(&tasks, &mut tags);
127
128 self.tasks = tasks;
129 self.tags = tags;
130 Ok(())
131 }
132
133 pub fn collect_tags(tasks: &VaultData, tags: &mut HashSet<String>) {
135 match tasks {
136 VaultData::Directory(_, children) | VaultData::Header(_, _, children) => {
137 children.iter().for_each(|c| Self::collect_tags(c, tags));
138 }
139 VaultData::Task(task) => {
140 task.tags.clone().unwrap_or_default().iter().for_each(|t| {
141 tags.insert(t.clone());
142 });
143 task.subtasks
144 .iter()
145 .for_each(|task| Self::collect_tags(&VaultData::Task(task.clone()), tags));
146 }
147 }
148 }
149 pub fn get_path_layer_entries(&self, path: &[String]) -> Result<Vec<VaultData>> {
155 Ok(self
156 .get_explorer_entries(path)?
157 .iter()
158 .map(|vd| match vd {
159 VaultData::Directory(name, _) => VaultData::Directory(name.clone(), vec![]),
160 VaultData::Header(level, name, _) => {
161 VaultData::Header(*level, name.clone(), vec![])
162 }
163 VaultData::Task(t) => {
164 let mut t = t.clone();
165 t.subtasks = vec![];
166 VaultData::Task(t)
167 }
168 })
169 .collect::<Vec<VaultData>>())
170 }
171
172 fn rewrite_vault_tasks(config: &TasksConfig, tasks: &VaultData) -> Result<()> {
174 fn explore_tasks_rec(
175 config: &TasksConfig,
176 filename: &mut PathBuf,
177 file_entry: &VaultData,
178 ) -> Result<()> {
179 match file_entry {
180 VaultData::Header(_, _, children) => {
181 children
182 .iter()
183 .try_for_each(|c| explore_tasks_rec(config, filename, c))?;
184 }
185 VaultData::Task(task) => {
186 task.fix_task_attributes(config, filename)?;
187 task.subtasks
188 .iter()
189 .try_for_each(|t| t.fix_task_attributes(config, filename))?;
190 }
191 VaultData::Directory(dir_name, children) => {
192 let mut filename = filename.clone();
193 filename.push(dir_name);
194 children
195 .iter()
196 .try_for_each(|c| explore_tasks_rec(config, &mut filename.clone(), c))?;
197 }
198 }
199 Ok(())
200 }
201 explore_tasks_rec(config, &mut PathBuf::new(), tasks)
202 }
203
204 pub fn get_explorer_entries(&self, selected_header_path: &[String]) -> Result<Vec<VaultData>> {
210 fn aux(
211 file_entry: Vec<VaultData>,
212 selected_header_path: &[String],
213 path_index: usize,
214 ) -> Result<Vec<VaultData>> {
215 if path_index == selected_header_path.len() {
216 Ok(file_entry)
217 } else {
218 for entry in file_entry {
219 match entry {
220 VaultData::Directory(name, children)
221 | VaultData::Header(_, name, children) => {
222 if name == selected_header_path[path_index] {
223 return aux(children, selected_header_path, path_index + 1);
224 }
225 }
226 VaultData::Task(task) => {
227 if task.name == selected_header_path[path_index] {
228 return aux(
229 task.subtasks
230 .iter()
231 .map(|t| VaultData::Task(t.clone()))
232 .collect(),
233 selected_header_path,
234 path_index + 1,
235 );
236 }
237 }
238 }
239 }
240 bail!("Couldn't find corresponding entry");
241 }
242 }
243
244 let filtered_tasks = if let Some(task_filter) = &self.current_filter {
245 filter(&self.tasks, task_filter)
246 } else {
247 Some(self.tasks.clone())
248 };
249
250 match filtered_tasks {
251 Some(VaultData::Directory(_, entries)) => aux(entries, selected_header_path, 0),
252 None => bail!("Empty Vault"),
253 _ => {
254 error!("First layer of VaultData was not a Directory");
255 bail!("First layer of VaultData was not a Directory")
256 }
257 }
258 }
259
260 pub fn get_vault_data_from_path(
269 &self,
270 selected_header_path: &[String],
271 task_preview_offset: usize,
272 ) -> Result<Vec<VaultData>> {
273 fn aux(
274 file_entry: VaultData,
275 selected_header_path: &[String],
276 path_index: usize,
277 task_preview_offset: usize,
278 ) -> Result<Vec<VaultData>> {
279 if path_index == selected_header_path.len() {
280 Ok(vec![file_entry])
281 } else {
282 match file_entry {
283 VaultData::Directory(name, children) | VaultData::Header(_, name, children) => {
284 if name == selected_header_path[path_index] {
285 let mut res = vec![];
286 for child in children {
287 if let Ok(mut found) = aux(
288 child,
289 selected_header_path,
290 path_index + 1,
291 task_preview_offset,
292 ) {
293 res.append(&mut found);
294 }
295 }
296 Ok(res)
297 } else {
298 bail!("Couldn't find corresponding entry");
299 }
300 }
301 VaultData::Task(task) => {
302 if task.name == selected_header_path[path_index] {
303 let mut res = vec![];
304
305 if path_index + task_preview_offset == selected_header_path.len() {
306 res.push(VaultData::Task(task));
307 } else {
308 for child in task.subtasks {
309 if let Ok(mut found) = aux(
310 VaultData::Task(child),
311 selected_header_path,
312 path_index + 1,
313 task_preview_offset,
314 ) {
315 res.append(&mut found);
316 }
317 }
318 }
319 Ok(res)
320 } else {
321 bail!("Couldn't find corresponding entry");
322 }
323 }
324 }
325 }
326 }
327
328 let filtered_tasks = if let Some(task_filter) = &self.current_filter {
329 filter(&self.tasks, task_filter)
330 } else {
331 Some(self.tasks.clone())
332 };
333 match filtered_tasks {
334 Some(VaultData::Directory(_, entries)) => {
335 for entry in entries {
336 if let Ok(res) = aux(entry, selected_header_path, 0, task_preview_offset) {
337 return Ok(res);
338 }
339 }
340 error!("Vault was not empty but the entry was not found");
341 bail!("Vault was not empty but the entry was not found");
342 }
343 None => bail!("Empty Vault"),
344 _ => {
345 error!("First layer of VaultData was not a Directory");
346 bail!("Empty Vault")
347 }
348 }
349 }
350
351 #[must_use]
354 pub fn can_enter(&self, selected_header_path: &[String]) -> bool {
355 fn aux(file_entry: VaultData, selected_header_path: &[String], path_index: usize) -> bool {
356 if path_index == selected_header_path.len() {
357 true
358 } else {
359 match file_entry {
360 VaultData::Directory(name, children) | VaultData::Header(_, name, children) => {
361 if name == selected_header_path[path_index] {
362 return children
363 .iter()
364 .any(|c| aux(c.clone(), selected_header_path, path_index + 1));
365 }
366 false
367 }
368 VaultData::Task(task) => {
369 if task.name == selected_header_path[path_index] {
370 return task.subtasks.iter().any(|t| {
371 aux(
372 VaultData::Task(t.clone()),
373 selected_header_path,
374 path_index + 1,
375 )
376 });
377 }
378 false
379 }
380 }
381 }
382 }
383
384 let filtered_tasks = if let Some(task_filter) = &self.current_filter {
385 filter(&self.tasks, task_filter)
386 } else {
387 return false;
388 };
389 let Some(VaultData::Directory(_, entries)) = filtered_tasks else {
390 return false;
391 };
392 entries
393 .iter()
394 .any(|e| aux(e.clone(), selected_header_path, 0))
395 }
396}
397impl Display for TaskManager {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 write!(f, "{}", self.tasks)?;
400 Ok(())
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use std::collections::HashSet;
407
408 use super::TaskManager;
409
410 use crate::{task::Task, vault_data::VaultData};
411
412 #[test]
413 fn test_get_vault_data() {
414 let expected_tasks = vec![
415 VaultData::Task(Task {
416 name: "test".to_string(),
417 line_number: 8,
418 description: Some("test\ndesc".to_string()),
419 ..Default::default()
420 }),
421 VaultData::Task(Task {
422 name: "test".to_string(),
423 line_number: 8,
424 description: Some("test\ndesc".to_string()),
425 ..Default::default()
426 }),
427 VaultData::Task(Task {
428 name: "test".to_string(),
429 line_number: 8,
430 description: Some("test\ndesc".to_string()),
431 ..Default::default()
432 }),
433 ];
434 let expected_header = VaultData::Header(3, "3".to_string(), expected_tasks.clone());
435 let input = VaultData::Directory(
436 "test".to_owned(),
437 vec![VaultData::Header(
438 0,
439 "Test".to_string(),
440 vec![
441 VaultData::Header(
442 1,
443 "1".to_string(),
444 vec![VaultData::Header(
445 2,
446 "2".to_string(),
447 vec![expected_header.clone()],
448 )],
449 ),
450 VaultData::Header(
451 1,
452 "1.2".to_string(),
453 vec![
454 VaultData::Header(3, "3".to_string(), vec![]),
455 VaultData::Header(
456 2,
457 "4".to_string(),
458 vec![VaultData::Task(Task {
459 name: "test".to_string(),
460 line_number: 8,
461 description: Some("test\ndesc".to_string()),
462 ..Default::default()
463 })],
464 ),
465 ],
466 ),
467 ],
468 )],
469 );
470
471 let task_mgr = TaskManager {
472 tasks: input,
473 tags: HashSet::new(),
474 ..Default::default()
475 };
476
477 let path = vec![String::from("Test"), String::from("1"), String::from("2")];
478 let res = task_mgr.get_vault_data_from_path(&path, 0).unwrap();
479 assert_eq!(vec![expected_header], res);
480
481 let path = vec![
482 String::from("Test"),
483 String::from("1"),
484 String::from("2"),
485 String::from("3"),
486 ];
487 let res = task_mgr.get_vault_data_from_path(&path, 0).unwrap();
488 assert_eq!(expected_tasks, res);
489 }
490}